diff --git a/README.md b/README.md
index 48d2b770..db467c04 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@
- A fast and lightweight AI gateway written in Go, providing a unified OpenAI-compatible API for OpenAI, Anthropic, Gemini, xAI, Groq, OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama, and more.
+ A fast and lightweight AI gateway written in Go, providing a unified OpenAI-compatible API for OpenAI, Anthropic, Gemini, DeepSeek, xAI, Groq, OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama, and more.
@@ -51,6 +51,7 @@ docker run --rm -p 8080:8080 \
-e OPENAI_API_KEY="your-openai-key" \
-e ANTHROPIC_API_KEY="your-anthropic-key" \
-e GEMINI_API_KEY="your-gemini-key" \
+ -e DEEPSEEK_API_KEY="your-deepseek-key" \
-e GROQ_API_KEY="your-groq-key" \
-e OPENROUTER_API_KEY="your-openrouter-key" \
-e ZAI_API_KEY="your-zai-key" \
@@ -90,6 +91,7 @@ Example model identifiers are illustrative and subject to change; consult provid
| OpenAI | `OPENAI_API_KEY` | `gpt-5.5` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Google Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
+| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek-v4-pro` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Groq | `GROQ_API_KEY` | `llama-3.3-70b-versatile` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| OpenRouter | `OPENROUTER_API_KEY` | `google/gemini-2.5-flash` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Z.ai | `ZAI_API_KEY` (`ZAI_BASE_URL` optional) | `glm-5.1` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
@@ -105,7 +107,9 @@ For Z.ai's GLM Coding Plan, set `ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v
Configured model lists are available for every provider with
`_MODELS`, for example
`OPENROUTER_MODELS=openai/gpt-oss-120b,anthropic/claude-sonnet-4` or
-`ORACLE_MODELS=openai.gpt-oss-120b,xai.grok-3`. By default,
+`ORACLE_MODELS=openai.gpt-oss-120b,xai.grok-3`. DeepSeek defaults to
+`https://api.deepseek.com`; set `DEEPSEEK_BASE_URL` only when using a compatible
+proxy or alternate DeepSeek endpoint. By default,
`CONFIGURED_PROVIDER_MODELS_MODE=fallback` uses those lists only when upstream
`/models` is unavailable or empty. Set `CONFIGURED_PROVIDER_MODELS_MODE=allowlist`
to expose only configured models for providers that define a list, skipping
@@ -288,7 +292,7 @@ See [DEVELOPMENT.md](docs/DEVELOPMENT.md) for testing, linting, and pre-commit s
### Must Have
- [ ] Intelligent routing
-- [ ] Broader provider support: Oracle model configuration via environment variables, plus Cohere, Command A, Operational, and DeepSeek V3
+- [ ] Broader provider support: Cohere, Command A, and Operational
- [ ] Budget management with limits per `user_path` and/or API key
- [ ] Editable model pricing for accurate cost tracking and budgeting
- [ ] Full support for the OpenAI `/responses` and `/conversations` lifecycle
diff --git a/cmd/gomodel/docs/docs.go b/cmd/gomodel/docs/docs.go
index d4e2e3fa..8f2d34a9 100644
--- a/cmd/gomodel/docs/docs.go
+++ b/cmd/gomodel/docs/docs.go
@@ -4355,7 +4355,7 @@ var SwaggerInfo = &swag.Spec{
BasePath: "/",
Schemes: []string{"http"},
Title: "GoModel API",
- Description: "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, Z.ai, xAI, Oracle, Ollama). Drop-in OpenAI-compatible API.",
+ Description: "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, DeepSeek, Z.ai, xAI, MiniMax, Oracle, Ollama). Drop-in OpenAI-compatible API.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
diff --git a/cmd/gomodel/main.go b/cmd/gomodel/main.go
index cc911cc8..18a9333e 100644
--- a/cmd/gomodel/main.go
+++ b/cmd/gomodel/main.go
@@ -18,6 +18,7 @@ import (
"gomodel/internal/providers"
"gomodel/internal/providers/anthropic"
"gomodel/internal/providers/azure"
+ "gomodel/internal/providers/deepseek"
"gomodel/internal/providers/gemini"
"gomodel/internal/providers/groq"
"gomodel/internal/providers/minimax"
@@ -73,7 +74,7 @@ func startApplication(application lifecycleApp, addr string) error {
// @title GoModel API
// @version 1.0
-// @description High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, Z.ai, xAI, MiniMax, Oracle, Ollama). Drop-in OpenAI-compatible API.
+// @description High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, DeepSeek, Z.ai, xAI, MiniMax, Oracle, Ollama). Drop-in OpenAI-compatible API.
// @BasePath /
// @schemes http
// @securityDefinitions.apikey BearerAuth
@@ -119,6 +120,7 @@ func main() {
factory.Add(azure.Registration)
factory.Add(oracle.Registration)
factory.Add(anthropic.Registration)
+ factory.Add(deepseek.Registration)
factory.Add(gemini.Registration)
factory.Add(groq.Registration)
factory.Add(minimax.Registration)
diff --git a/config/config.example.yaml b/config/config.example.yaml
index bb3ff27c..359bf7d2 100644
--- a/config/config.example.yaml
+++ b/config/config.example.yaml
@@ -264,10 +264,11 @@ providers:
# - openai.gpt-oss-120b
# - xai.grok-3
- # Example: DeepSeek (OpenAI-compatible)
+ # Example: DeepSeek. GoModel translates /v1/responses requests to DeepSeek
+ # chat completions because DeepSeek does not expose a native Responses API.
# deepseek:
- # type: "openai"
- # base_url: "https://api.deepseek.com/v1"
+ # type: "deepseek"
+ # base_url: "https://api.deepseek.com"
# api_key: "${DEEPSEEK_API_KEY}"
# Example: local Ollama server with explicit per-model metadata.
diff --git a/config/config_test.go b/config/config_test.go
index dfaf772f..1b8a942f 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -18,6 +18,7 @@ func clearProviderEnvVars(t *testing.T) {
"OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODELS",
"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "ANTHROPIC_MODELS",
"GEMINI_API_KEY", "GEMINI_BASE_URL", "GEMINI_MODELS",
+ "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL", "DEEPSEEK_MODELS",
"XAI_API_KEY", "XAI_BASE_URL", "XAI_MODELS",
"GROQ_API_KEY", "GROQ_BASE_URL", "GROQ_MODELS",
"OPENROUTER_API_KEY", "OPENROUTER_BASE_URL", "OPENROUTER_MODELS", "OPENROUTER_SITE_URL", "OPENROUTER_APP_NAME",
diff --git a/docs/about/roadmap.mdx b/docs/about/roadmap.mdx
index b279c59a..6247660f 100644
--- a/docs/about/roadmap.mdx
+++ b/docs/about/roadmap.mdx
@@ -7,7 +7,7 @@ icon: "list-todo"
### Must Have
- [ ] Intelligent routing
-- [ ] Broader provider support: Oracle model configuration via environment variables, plus Cohere, Command A, Operational, and DeepSeek V3
+- [ ] Broader provider support: Cohere, Command A, and Operational
- [ ] Budget management with limits per `user_path` and/or API key
- [ ] Editable model pricing for accurate cost tracking and budgeting
- [ ] Full support for the OpenAI `/responses` and `/conversations` lifecycle
diff --git a/docs/adr/0001-explicit-provider-registration.md b/docs/adr/0001-explicit-provider-registration.md
index 80776bea..2fc5a3d8 100644
--- a/docs/adr/0001-explicit-provider-registration.md
+++ b/docs/adr/0001-explicit-provider-registration.md
@@ -2,7 +2,7 @@
## Context
-GoModel supports multiple LLM providers, including OpenAI, Anthropic, Gemini, xAI, Groq, OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama, and custom OpenAI-compatible endpoints. Each provider must be registered with the factory before use.
+GoModel supports multiple LLM providers, including OpenAI, Anthropic, Gemini, DeepSeek, xAI, Groq, OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama, and custom OpenAI-compatible endpoints. Each provider must be registered with the factory before use.
## Decision
diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx
index fb03a177..76edd057 100644
--- a/docs/advanced/configuration.mdx
+++ b/docs/advanced/configuration.mdx
@@ -181,6 +181,7 @@ Set these to automatically register providers. No YAML configuration required.
| `OPENAI_API_KEY` | OpenAI |
| `ANTHROPIC_API_KEY` | Anthropic |
| `GEMINI_API_KEY` | Google Gemini |
+| `DEEPSEEK_API_KEY` | DeepSeek |
| `OPENROUTER_API_KEY` | OpenRouter |
| `ZAI_API_KEY` | Z.ai |
| `XAI_API_KEY` | xAI (Grok) |
@@ -190,7 +191,7 @@ Set these to automatically register providers. No YAML configuration required.
| `OLLAMA_BASE_URL` | Ollama (no API key needed) |
| `VLLM_BASE_URL` | vLLM (no API key needed unless upstream requires) |
-Most providers can use a custom base URL via `_BASE_URL` (for example `OPENAI_BASE_URL`). OpenRouter defaults to `https://openrouter.ai/api/v1` and can be overridden with `OPENROUTER_BASE_URL`. Z.ai defaults to `https://api.z.ai/api/paas/v4`; set `ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v4` for the GLM Coding Plan endpoint. vLLM defaults to `http://localhost:8000/v1` when `VLLM_API_KEY` is set, but keyless deployments should set `VLLM_BASE_URL` explicitly to register the provider. Azure uses `AZURE_BASE_URL` for its deployment base URL and accepts an optional `AZURE_API_VERSION` override; otherwise it defaults to `2024-10-21`. Oracle requires `ORACLE_BASE_URL` because its OpenAI-compatible endpoint is region-specific.
+Most providers can use a custom base URL via `_BASE_URL` (for example `OPENAI_BASE_URL`). DeepSeek defaults to `https://api.deepseek.com`; set `DEEPSEEK_BASE_URL` only for a compatible proxy or alternate DeepSeek endpoint. OpenRouter defaults to `https://openrouter.ai/api/v1` and can be overridden with `OPENROUTER_BASE_URL`. Z.ai defaults to `https://api.z.ai/api/paas/v4`; set `ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v4` for the GLM Coding Plan endpoint. vLLM defaults to `http://localhost:8000/v1` when `VLLM_API_KEY` is set, but keyless deployments should set `VLLM_BASE_URL` explicitly to register the provider. Azure uses `AZURE_BASE_URL` for its deployment base URL and accepts an optional `AZURE_API_VERSION` override; otherwise it defaults to `2024-10-21`. Oracle requires `ORACLE_BASE_URL` because its OpenAI-compatible endpoint is region-specific.
Every provider type also accepts a comma-separated configured model list via
`_MODELS`, for example `OPENROUTER_MODELS`, `ORACLE_MODELS`,
@@ -311,6 +312,7 @@ The simplest way to add providers. GoModel checks for well-known API key environ
export OPENAI_API_KEY="sk-..." # Registers "openai" provider
export ANTHROPIC_API_KEY="sk-ant-..." # Registers "anthropic" provider
export GEMINI_API_KEY="..." # Registers "gemini" provider
+export DEEPSEEK_API_KEY="..." # Registers "deepseek" provider
export XAI_API_KEY="..." # Registers "xai" provider
export GROQ_API_KEY="gsk_..." # Registers "groq" provider
export OPENROUTER_API_KEY="sk-or-..." # Registers "openrouter" provider
@@ -381,6 +383,12 @@ providers:
- openai.gpt-oss-120b
- xai.grok-3
+ # Add DeepSeek. GoModel translates /v1/responses to DeepSeek chat completions.
+ deepseek:
+ type: deepseek
+ base_url: "https://api.deepseek.com"
+ api_key: "..."
+
# Add a vLLM OpenAI-compatible server
vllm:
type: vllm
diff --git a/docs/docs.json b/docs/docs.json
index c681c215..974fae70 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -86,6 +86,7 @@
"pages": [
"guides/openclaw",
"guides/oracle",
+ "guides/deepseek",
"guides/vllm",
"guides/multiple-ollama",
"guides/claude-code",
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 377918de..9470866d 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -7,9 +7,9 @@ icon: "rocket"
## Run GoModel in 30 Seconds
GoModel is an OpenAI-compatible AI gateway. You can connect one endpoint and
-route traffic across OpenAI, Anthropic, Gemini, xAI, Groq, OpenRouter, Z.ai,
-Azure OpenAI, Oracle, Ollama, and more while keeping auth, audit logs, and admin
-visibility in one place.
+route traffic across OpenAI, Anthropic, Gemini, DeepSeek, xAI, Groq, OpenRouter,
+Z.ai, Azure OpenAI, Oracle, Ollama, and more while keeping auth, audit logs, and
+admin visibility in one place.
### 1. Start GoModel
@@ -26,9 +26,10 @@ docker run --rm -p 8080:8080 \
Set at least one provider credential or base URL
- (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`,
- `GROQ_API_KEY`, `OPENROUTER_API_KEY`, `ZAI_API_KEY`, `AZURE_API_KEY` +
- `AZURE_BASE_URL`, `ORACLE_API_KEY` + `ORACLE_BASE_URL`,
+ (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`,
+ `DEEPSEEK_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY`,
+ `ZAI_API_KEY`, `AZURE_API_KEY` + `AZURE_BASE_URL`,
+ `ORACLE_API_KEY` + `ORACLE_BASE_URL`,
`OLLAMA_BASE_URL`) or GoModel will have no models to route. GoModel also
works well with additional OpenAI-compatible providers out of the box.
diff --git a/docs/guides/codex.mdx b/docs/guides/codex.mdx
index 071dbffa..e79e0c3d 100644
--- a/docs/guides/codex.mdx
+++ b/docs/guides/codex.mdx
@@ -9,7 +9,7 @@ Responses API.
Flow:
-`Codex -> GoModel -> OpenAI`
+`Codex -> GoModel -> upstream model provider`
## Before you start
@@ -97,6 +97,42 @@ The validated result was:
ok
```
+## DeepSeek V4
+
+Codex sends `POST /v1/responses`. DeepSeek exposes chat completions instead of
+a native Responses API, so configure the first-class DeepSeek provider and let
+GoModel translate the request.
+
+```yaml
+providers:
+ deepseek:
+ type: deepseek
+ base_url: "https://api.deepseek.com"
+ api_key: "${DEEPSEEK_API_KEY}"
+```
+
+If you previously configured DeepSeek as `type: openai`, change it to
+`type: deepseek` for Codex. The generic OpenAI provider forwards `/responses`
+upstream, while the DeepSeek provider translates `/responses` to
+`/chat/completions`.
+
+See the [DeepSeek guide](/guides/deepseek) for the full reasoning effort
+mapping table (DeepSeek V4 only accepts `high` and `max`, so GoModel maps
+`low` and `medium` up to `high`).
+
+Then use the DeepSeek model name in Codex:
+
+```toml
+model_provider = "gomodel"
+model = "deepseek-v4-pro"
+
+[model_providers.gomodel]
+name = "GoModel"
+base_url = "http://localhost:8080/v1"
+env_key = "OPENAI_API_KEY"
+wire_api = "responses"
+```
+
## 5. Check the traffic in GoModel
Open the GoModel dashboard audit logs:
diff --git a/docs/guides/deepseek.mdx b/docs/guides/deepseek.mdx
new file mode 100644
index 00000000..b543427d
--- /dev/null
+++ b/docs/guides/deepseek.mdx
@@ -0,0 +1,67 @@
+---
+title: "GoModel & DeepSeek"
+description: "Configure DeepSeek V4 in GoModel and understand how reasoning effort is mapped to DeepSeek's reasoning_effort field."
+icon: "brain"
+---
+
+GoModel routes to DeepSeek through DeepSeek's chat completions API. DeepSeek
+does not expose a native Responses API, so GoModel translates `/v1/responses`
+requests to `/chat/completions` automatically.
+
+## 1. Configure DeepSeek
+
+Env-only is enough:
+
+```bash
+export DEEPSEEK_API_KEY="..."
+```
+
+Or in `config.yaml` (not recommended):
+
+```yaml
+providers:
+ deepseek:
+ type: deepseek
+ base_url: "https://api.deepseek.com"
+ api_key: "${DEEPSEEK_API_KEY}"
+```
+
+
+ If you previously configured DeepSeek as `type: openai`, switch it to `type:
+ deepseek` so GoModel translates `/v1/responses` and remaps reasoning effort.
+ The generic OpenAI provider does neither.
+
+
+## 2. Reasoning effort mapping
+
+DeepSeek V4 reasoning models accept `reasoning_effort` as a top-level string
+with two levels: `high` and `max`. GoModel accepts the standard OpenAI-style
+levels and remaps them:
+
+| Client sends | DeepSeek receives |
+| --------------- | ----------------- |
+| `low` | `high` |
+| `medium` | `high` |
+| `high` | `high` |
+| `xhigh` / `max` | `max` |
+| anything else | passed through |
+
+`low` and `medium` are mapped up to `high` because DeepSeek's reasoning models
+do not accept lower levels. If you want to avoid reasoning entirely, omit the
+`reasoning` field instead of passing `low`.
+
+GoModel rewrites the request body so DeepSeek sees a top-level
+`reasoning_effort: "..."` instead of OpenAI's nested
+`"reasoning": {"effort": "..."}` shape. No client change is required.
+
+## Current support
+
+Integrated:
+
+- chat completions and streaming
+- Responses API and streaming (translated to chat completions)
+- model listing through `/models`
+
+Not supported by DeepSeek:
+
+- embeddings (returns an `invalid_request_error`)
diff --git a/docs/openapi.json b/docs/openapi.json
index 023f0a92..4ffb4ed3 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
- "description": "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, Z.ai, xAI, MiniMax, Oracle, Ollama). Drop-in OpenAI-compatible API.",
+ "description": "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, OpenRouter, DeepSeek, Z.ai, xAI, MiniMax, Oracle, Ollama). Drop-in OpenAI-compatible API.",
"title": "GoModel API",
"contact": {},
"version": "1.0"
diff --git a/helm/Chart.yaml b/helm/Chart.yaml
index 516cd401..f63e5ed4 100644
--- a/helm/Chart.yaml
+++ b/helm/Chart.yaml
@@ -1,6 +1,6 @@
apiVersion: v2
name: gomodel
-description: High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, Z.ai, xAI)
+description: High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, DeepSeek, Groq, Z.ai, xAI)
type: application
version: 0.1.0
appVersion: "1.0.0"
diff --git a/helm/README.md b/helm/README.md
index 40e6d7e2..7f2e0752 100644
--- a/helm/README.md
+++ b/helm/README.md
@@ -1,6 +1,6 @@
# GoModel Helm Chart
-High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, Z.ai, xAI, Oracle).
+High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, DeepSeek, Groq, Z.ai, xAI, Oracle).
## Prerequisites
diff --git a/internal/providers/config_test.go b/internal/providers/config_test.go
index c4980f8c..37f2736f 100644
--- a/internal/providers/config_test.go
+++ b/internal/providers/config_test.go
@@ -27,6 +27,9 @@ var testDiscoveryConfigs = map[string]DiscoveryConfig{
"gemini": {
DefaultBaseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
},
+ "deepseek": {
+ DefaultBaseURL: "https://api.deepseek.com",
+ },
"xai": {
DefaultBaseURL: "https://api.x.ai/v1",
},
@@ -402,6 +405,26 @@ func TestApplyProviderEnvVars_DiscoversOpenRouterFromAPIKey(t *testing.T) {
}
}
+func TestApplyProviderEnvVars_DiscoversDeepSeekFromAPIKey(t *testing.T) {
+ t.Setenv("DEEPSEEK_API_KEY", "deepseek-key")
+
+ got := applyProviderEnvVars(map[string]config.RawProviderConfig{}, testDiscoveryConfigs)
+
+ p, exists := got["deepseek"]
+ if !exists {
+ t.Fatal("expected deepseek to be discovered from env var")
+ }
+ if p.APIKey != "deepseek-key" {
+ t.Errorf("APIKey = %q, want deepseek-key", p.APIKey)
+ }
+ if p.Type != "deepseek" {
+ t.Errorf("Type = %q, want deepseek", p.Type)
+ }
+ if p.BaseURL != testDiscoveryConfigs["deepseek"].DefaultBaseURL {
+ t.Errorf("BaseURL = %q, want %q", p.BaseURL, testDiscoveryConfigs["deepseek"].DefaultBaseURL)
+ }
+}
+
func TestApplyProviderEnvVars_DiscoversZAIFromAPIKey(t *testing.T) {
t.Setenv("ZAI_API_KEY", "zai-key")
diff --git a/internal/providers/deepseek/deepseek.go b/internal/providers/deepseek/deepseek.go
new file mode 100644
index 00000000..1b512012
--- /dev/null
+++ b/internal/providers/deepseek/deepseek.go
@@ -0,0 +1,182 @@
+// Package deepseek provides DeepSeek API integration for the LLM gateway.
+package deepseek
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+
+ "gomodel/internal/core"
+ "gomodel/internal/llmclient"
+ "gomodel/internal/providers"
+)
+
+const defaultBaseURL = "https://api.deepseek.com"
+
+// Registration provides factory registration for the DeepSeek provider.
+var Registration = providers.Registration{
+ Type: "deepseek",
+ New: New,
+ Discovery: providers.DiscoveryConfig{
+ DefaultBaseURL: defaultBaseURL,
+ },
+}
+
+// Provider implements the core.Provider interface for DeepSeek.
+type Provider struct {
+ client *llmclient.Client
+ apiKey string
+}
+
+var _ core.Provider = (*Provider)(nil)
+
+// New creates a new DeepSeek provider.
+func New(cfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider {
+ p := &Provider{apiKey: cfg.APIKey}
+ clientCfg := llmclient.Config{
+ ProviderName: "deepseek",
+ BaseURL: providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL),
+ Retry: opts.Resilience.Retry,
+ Hooks: opts.Hooks,
+ CircuitBreaker: opts.Resilience.CircuitBreaker,
+ }
+ p.client = llmclient.New(clientCfg, p.setHeaders)
+ return p
+}
+
+// NewWithHTTPClient creates a new DeepSeek provider with a custom HTTP client.
+// If httpClient is nil, http.DefaultClient is used.
+func NewWithHTTPClient(apiKey string, baseURL string, httpClient *http.Client, hooks llmclient.Hooks) *Provider {
+ if httpClient == nil {
+ httpClient = http.DefaultClient
+ }
+ p := &Provider{apiKey: apiKey}
+ cfg := llmclient.DefaultConfig("deepseek", providers.ResolveBaseURL(baseURL, defaultBaseURL))
+ cfg.Hooks = hooks
+ p.client = llmclient.NewWithHTTPClient(httpClient, cfg, p.setHeaders)
+ return p
+}
+
+// SetBaseURL allows configuring a custom base URL for the provider.
+func (p *Provider) SetBaseURL(url string) {
+ p.client.SetBaseURL(url)
+}
+
+func (p *Provider) setHeaders(req *http.Request) {
+ req.Header.Set("Authorization", "Bearer "+p.apiKey)
+ if requestID := core.GetRequestID(req.Context()); requestID != "" {
+ req.Header.Set("X-Request-Id", requestID)
+ }
+}
+
+// adaptChatRequest rewrites GoModel's common reasoning shape into DeepSeek's
+// OpenAI-compatible chat extension. DeepSeek accepts reasoning_effort as a
+// top-level string, not "reasoning": {"effort": "..."}.
+func adaptChatRequest(req *core.ChatRequest) (any, error) {
+ if req == nil || req.Reasoning == nil || strings.TrimSpace(req.Reasoning.Effort) == "" {
+ return req, nil
+ }
+
+ body, err := json.Marshal(req)
+ if err != nil {
+ return nil, core.NewInvalidRequestError("failed to marshal deepseek request: "+err.Error(), err)
+ }
+
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(body, &raw); err != nil {
+ return nil, core.NewInvalidRequestError("failed to decode deepseek request payload: "+err.Error(), err)
+ }
+
+ effort, _ := json.Marshal(normalizeReasoningEffort(req.Reasoning.Effort))
+ raw["reasoning_effort"] = effort
+ delete(raw, "reasoning")
+ return raw, nil
+}
+
+// normalizeReasoningEffort maps GoModel's OpenAI-style effort levels to the two
+// levels DeepSeek V4 accepts ("high" and "max"). "low" and "medium" are mapped
+// up to "high" because DeepSeek does not support lower levels; clients that
+// want to disable reasoning should omit the field entirely. See
+// docs/guides/deepseek.mdx for the user-facing table.
+func normalizeReasoningEffort(effort string) string {
+ normalized := strings.ToLower(strings.TrimSpace(effort))
+ switch normalized {
+ case "low", "medium":
+ return "high"
+ case "xhigh", "max":
+ return "max"
+ default:
+ return normalized
+ }
+}
+
+// ChatCompletion sends a chat completion request to DeepSeek.
+func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*core.ChatResponse, error) {
+ if req == nil {
+ return nil, core.NewInvalidRequestError("chat request is required", nil)
+ }
+ body, err := adaptChatRequest(req)
+ if err != nil {
+ return nil, err
+ }
+ var resp core.ChatResponse
+ err = p.client.Do(ctx, llmclient.Request{
+ Method: http.MethodPost,
+ Endpoint: "/chat/completions",
+ Body: body,
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ if resp.Model == "" {
+ resp.Model = req.Model
+ }
+ return &resp, nil
+}
+
+// StreamChatCompletion returns a raw response body for streaming.
+func (p *Provider) StreamChatCompletion(ctx context.Context, req *core.ChatRequest) (io.ReadCloser, error) {
+ if req == nil {
+ return nil, core.NewInvalidRequestError("chat request is required", nil)
+ }
+ streamReq := req.WithStreaming()
+ body, err := adaptChatRequest(streamReq)
+ if err != nil {
+ return nil, err
+ }
+ return p.client.DoStream(ctx, llmclient.Request{
+ Method: http.MethodPost,
+ Endpoint: "/chat/completions",
+ Body: body,
+ })
+}
+
+// ListModels retrieves the list of available models from DeepSeek.
+func (p *Provider) ListModels(ctx context.Context) (*core.ModelsResponse, error) {
+ var resp core.ModelsResponse
+ err := p.client.Do(ctx, llmclient.Request{
+ Method: http.MethodGet,
+ Endpoint: "/models",
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
+
+// Responses sends a Responses API request to DeepSeek using chat-completions translation.
+func (p *Provider) Responses(ctx context.Context, req *core.ResponsesRequest) (*core.ResponsesResponse, error) {
+ return providers.ResponsesViaChat(ctx, p, req)
+}
+
+// StreamResponses streams a Responses API request to DeepSeek using chat-completions translation.
+func (p *Provider) StreamResponses(ctx context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) {
+ return providers.StreamResponsesViaChat(ctx, p, req, "deepseek")
+}
+
+// Embeddings returns an error because DeepSeek does not expose an embeddings endpoint.
+func (p *Provider) Embeddings(_ context.Context, _ *core.EmbeddingRequest) (*core.EmbeddingResponse, error) {
+ return nil, core.NewInvalidRequestError("deepseek does not support embeddings", nil)
+}
diff --git a/internal/providers/deepseek/deepseek_test.go b/internal/providers/deepseek/deepseek_test.go
new file mode 100644
index 00000000..c97dd16e
--- /dev/null
+++ b/internal/providers/deepseek/deepseek_test.go
@@ -0,0 +1,237 @@
+package deepseek
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "gomodel/internal/core"
+ "gomodel/internal/llmclient"
+)
+
+func TestChatCompletion_UsesBearerAuthAndChatEndpoint(t *testing.T) {
+ var gotPath string
+ var gotAuth string
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ gotPath = r.URL.Path
+ gotAuth = r.Header.Get("Authorization")
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{
+ "id":"chatcmpl-deepseek",
+ "created":1677652288,
+ "model":"deepseek-v4-pro",
+ "choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":"stop"}]
+ }`))
+ }))
+ defer server.Close()
+
+ provider := NewWithHTTPClient("deepseek-key", server.URL, server.Client(), llmclient.Hooks{})
+
+ resp, err := provider.ChatCompletion(context.Background(), &core.ChatRequest{
+ Model: "deepseek-v4-pro",
+ Messages: []core.Message{
+ {Role: "user", Content: "hi"},
+ },
+ })
+ if err != nil {
+ t.Fatalf("ChatCompletion() error = %v", err)
+ }
+ if resp.Model != "deepseek-v4-pro" {
+ t.Fatalf("resp.Model = %q, want deepseek-v4-pro", resp.Model)
+ }
+ if gotPath != "/chat/completions" {
+ t.Fatalf("path = %q, want /chat/completions", gotPath)
+ }
+ if gotAuth != "Bearer deepseek-key" {
+ t.Fatalf("authorization = %q, want Bearer deepseek-key", gotAuth)
+ }
+}
+
+func TestChatCompletion_MapsReasoningToDeepSeekReasoningEffort(t *testing.T) {
+ var gotBody map[string]any
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
+ http.Error(w, "decode error", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{
+ "id":"chatcmpl-deepseek",
+ "created":1677652288,
+ "model":"deepseek-v4-pro",
+ "choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]
+ }`))
+ }))
+ defer server.Close()
+
+ provider := NewWithHTTPClient("deepseek-key", server.URL, server.Client(), llmclient.Hooks{})
+
+ _, err := provider.ChatCompletion(context.Background(), &core.ChatRequest{
+ Model: "deepseek-v4-pro",
+ Messages: []core.Message{{Role: "user", Content: "hi"}},
+ Reasoning: &core.Reasoning{Effort: "medium"},
+ })
+ if err != nil {
+ t.Fatalf("ChatCompletion() error = %v", err)
+ }
+ if gotBody["reasoning"] != nil {
+ t.Fatalf("request body should not include nested reasoning, got %#v", gotBody["reasoning"])
+ }
+ if gotBody["reasoning_effort"] != "high" {
+ t.Fatalf("reasoning_effort = %#v, want high", gotBody["reasoning_effort"])
+ }
+}
+
+func TestResponses_TranslatesToChatCompletions(t *testing.T) {
+ var gotPath string
+ var gotBody map[string]any
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ gotPath = r.URL.Path
+ if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
+ http.Error(w, "decode error", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{
+ "id":"chatcmpl-deepseek",
+ "created":1677652288,
+ "model":"deepseek-v4-pro",
+ "choices":[{"index":0,"message":{"role":"assistant","content":"translated"},"finish_reason":"stop"}],
+ "usage":{"prompt_tokens":3,"completion_tokens":2,"total_tokens":5}
+ }`))
+ }))
+ defer server.Close()
+
+ provider := NewWithHTTPClient("deepseek-key", server.URL, server.Client(), llmclient.Hooks{})
+ maxOutputTokens := 64
+
+ resp, err := provider.Responses(context.Background(), &core.ResponsesRequest{
+ Model: "deepseek-v4-pro",
+ Input: "Reply with exactly ok",
+ MaxOutputTokens: &maxOutputTokens,
+ Reasoning: &core.Reasoning{Effort: "xhigh"},
+ })
+ if err != nil {
+ t.Fatalf("Responses() error = %v", err)
+ }
+ if gotPath != "/chat/completions" {
+ t.Fatalf("path = %q, want /chat/completions", gotPath)
+ }
+ if gotBody["max_output_tokens"] != nil {
+ t.Fatalf("request body should not include max_output_tokens, got %#v", gotBody["max_output_tokens"])
+ }
+ if gotBody["max_tokens"] != float64(64) {
+ t.Fatalf("max_tokens = %#v, want 64", gotBody["max_tokens"])
+ }
+ if gotBody["reasoning_effort"] != "max" {
+ t.Fatalf("reasoning_effort = %#v, want max", gotBody["reasoning_effort"])
+ }
+ messages, ok := gotBody["messages"].([]any)
+ if !ok || len(messages) != 1 {
+ t.Fatalf("messages = %#v, want one chat message", gotBody["messages"])
+ }
+ message, _ := messages[0].(map[string]any)
+ if message["role"] != "user" || message["content"] != "Reply with exactly ok" {
+ t.Fatalf("message = %#v, want converted user message", message)
+ }
+ if resp.Object != "response" || resp.Status != "completed" {
+ t.Fatalf("response metadata = object %q status %q, want response/completed", resp.Object, resp.Status)
+ }
+ if len(resp.Output) != 1 || len(resp.Output[0].Content) != 1 || resp.Output[0].Content[0].Text != "translated" {
+ t.Fatalf("unexpected responses output: %+v", resp.Output)
+ }
+ if resp.Usage == nil || resp.Usage.TotalTokens != 5 {
+ t.Fatalf("usage = %+v, want total_tokens=5", resp.Usage)
+ }
+}
+
+func TestStreamResponses_TranslatesToChatCompletions(t *testing.T) {
+ var gotPath string
+ var gotBody map[string]any
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ gotPath = r.URL.Path
+ if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
+ http.Error(w, "decode error", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "text/event-stream")
+ _, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-deepseek\",\"object\":\"chat.completion.chunk\",\"created\":1677652288,\"model\":\"deepseek-v4-pro\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"},\"finish_reason\":null}]}\n\n"))
+ _, _ = w.Write([]byte("data: [DONE]\n\n"))
+ }))
+ defer server.Close()
+
+ provider := NewWithHTTPClient("deepseek-key", server.URL, server.Client(), llmclient.Hooks{})
+
+ stream, err := provider.StreamResponses(context.Background(), &core.ResponsesRequest{
+ Model: "deepseek-v4-pro",
+ Input: "hi",
+ })
+ if err != nil {
+ t.Fatalf("StreamResponses() error = %v", err)
+ }
+ defer stream.Close()
+
+ body, err := io.ReadAll(stream)
+ if err != nil {
+ t.Fatalf("ReadAll() error = %v", err)
+ }
+ if gotPath != "/chat/completions" {
+ t.Fatalf("path = %q, want /chat/completions", gotPath)
+ }
+ if gotBody["stream"] != true {
+ t.Fatalf("stream = %#v, want true", gotBody["stream"])
+ }
+ raw := string(body)
+ if !strings.Contains(raw, "response.output_text.delta") || !strings.Contains(raw, "data: [DONE]") {
+ t.Fatalf("converted stream missing responses events or done marker: %s", raw)
+ }
+}
+
+func TestNormalizeReasoningEffort(t *testing.T) {
+ tests := map[string]string{
+ "low": "high",
+ "medium": "high",
+ "high": "high",
+ "xhigh": "max",
+ "max": "max",
+ "custom": "custom",
+ }
+ for input, expected := range tests {
+ t.Run(input, func(t *testing.T) {
+ if got := normalizeReasoningEffort(input); got != expected {
+ t.Fatalf("normalizeReasoningEffort(%q) = %q, want %q", input, got, expected)
+ }
+ })
+ }
+}
+
+func TestProvider_DoesNotExposeOptionalNativeInterfaces(t *testing.T) {
+ provider := NewWithHTTPClient("deepseek-key", "", nil, llmclient.Hooks{})
+
+ if _, ok := any(provider).(core.NativeBatchProvider); ok {
+ t.Fatal("deepseek provider should not implement native batch provider")
+ }
+ if _, ok := any(provider).(core.NativeFileProvider); ok {
+ t.Fatal("deepseek provider should not implement native file provider")
+ }
+ if _, ok := any(provider).(core.NativeResponseLifecycleProvider); ok {
+ t.Fatal("deepseek provider should not implement native response lifecycle provider")
+ }
+}
+
+func TestEmbeddings_ReturnsUnsupported(t *testing.T) {
+ provider := NewWithHTTPClient("deepseek-key", "", nil, llmclient.Hooks{})
+
+ _, err := provider.Embeddings(context.Background(), &core.EmbeddingRequest{Model: "embedding-model", Input: "hi"})
+ if err == nil {
+ t.Fatal("expected unsupported embeddings error, got nil")
+ }
+}