From 12780955a47a87947aee9005be8a62decd170c7e Mon Sep 17 00:00:00 2001 From: Sayt-0 Date: Wed, 1 Jul 2026 13:02:18 +0200 Subject: [PATCH] feat: add DeepSeek as a supported model provider Add deepseek as a built-in OpenAI-compatible alias provider, following the groq/baseten pattern. Registers the alias (base URL https://api.deepseek.com/v1, token DEEPSEEK_API_KEY), auto-detection in cloudProviders, default model deepseek-chat, schema and docs updates, an example config, and unit tests. DeepSeek is present in models.dev, so it is a first-class catalog provider and is intentionally not added to modelsDevAbsentProviders. Its first-party API has a fixed model lineup (deepseek-chat, deepseek-reasoner), so it is left out of shouldMergeConsecutiveMessages like the other first-party APIs (mistral, xai). Also adds an end-to-end test that drives a real HTTP request through the full stack against a local OpenAI-compatible server, plus a DEEPSEEK_API_KEY-gated live API test. Closes #3347 --- agent-schema.json | 2 +- docs/_data/nav.yml | 2 + docs/concepts/models/index.md | 1 + docs/configuration/models/index.md | 6 +- docs/providers/deepseek/index.md | 96 ++++++++++ docs/providers/overview/index.md | 1 + examples/README.md | 1 + examples/deepseek.yaml | 17 ++ pkg/config/auto.go | 2 + pkg/config/auto_test.go | 37 +++- pkg/model/provider/aliases.go | 5 + pkg/model/provider/aliases_test.go | 14 ++ pkg/model/provider/deepseek_test.go | 176 ++++++++++++++++++ .../provider/openai/repro_issue3344_test.go | 1 + 14 files changed, 352 insertions(+), 9 deletions(-) create mode 100644 docs/providers/deepseek/index.md create mode 100644 examples/deepseek.yaml create mode 100644 pkg/model/provider/deepseek_test.go diff --git a/agent-schema.json b/agent-schema.json index 250a7ef45..b992f44d0 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -182,7 +182,7 @@ "properties": { "provider": { "type": "string", - "description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, etc.).", + "description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, deepseek, etc.).", "examples": [ "openai", "anthropic", diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml index 9d092b620..e0d731251 100644 --- a/docs/_data/nav.yml +++ b/docs/_data/nav.yml @@ -151,6 +151,8 @@ url: /providers/ovhcloud/ - title: Groq url: /providers/groq/ + - title: DeepSeek + url: /providers/deepseek/ - title: MiniMax url: /providers/minimax/ - title: OpenRouter diff --git a/docs/concepts/models/index.md b/docs/concepts/models/index.md index 81b9aeaf1..cf6a73eda 100644 --- a/docs/concepts/models/index.md +++ b/docs/concepts/models/index.md @@ -84,6 +84,7 @@ for details. | Baseten | `baseten` | DeepSeek, Kimi, GLM, Llama models | `BASETEN_API_KEY` | | OVHcloud | `ovhcloud` | Qwen, Llama, Mistral, DeepSeek (EU-hosted) | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` | | Groq | `groq` | Llama, Qwen, GPT-OSS (fast inference) | `GROQ_API_KEY` | +| DeepSeek | `deepseek` | DeepSeek-V3 chat and R1 reasoner | `DEEPSEEK_API_KEY` | | Requesty | `requesty` | Multi-provider gateway | `REQUESTY_API_KEY` | | OpenRouter | `openrouter` | Multi-provider gateway | `OPENROUTER_API_KEY` | | Azure OpenAI | `azure` | gpt-4o, gpt-5 on Azure | `AZURE_API_KEY` + `base_url` | diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index 1b190f809..5cff81760 100644 --- a/docs/configuration/models/index.md +++ b/docs/configuration/models/index.md @@ -17,7 +17,7 @@ models: first_available: [list] # Optional: candidate model refs, tried in order by available credentials. # Mutually exclusive with other model settings. provider: string # Required unless using first_available. One of: openai, anthropic, google, amazon-bedrock, - # dmr, mistral, xai, nebius, minimax, baseten, ovhcloud, groq, requesty, openrouter, + # dmr, mistral, xai, nebius, minimax, baseten, ovhcloud, groq, deepseek, requesty, openrouter, # azure, ollama, github-copilot, or a named provider defined # under the top-level `providers:` section. model: string # Required: model identifier @@ -48,7 +48,7 @@ models: | Property | Type | Required | Description | | --------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- | | `first_available` | array | ✗ | Candidate model references tried in order; selects the first whose credentials are configured. Mutually exclusive with other model settings. | -| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, or any [named provider]({{ '/providers/custom/' | relative_url }}). | +| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, or any [named provider]({{ '/providers/custom/' | relative_url }}). | | `model` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Model name (e.g., `gpt-4o`, `claude-sonnet-4-5`, `gemini-3.5-flash`) | | `temperature` | float | ✗ | Sampling randomness. Range is provider-dependent — typically `0.0–2.0` (Anthropic caps at `1.0`). `0.0` is deterministic. | | `max_tokens` | int | ✗ | Maximum response length in tokens | @@ -404,7 +404,7 @@ See the [Anthropic provider page]({{ '/providers/anthropic/#thinking-display' | ## Custom HTTP Headers For OpenAI-compatible providers (`openai`, `github-copilot`, `mistral`, `xai`, -`nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API), +`nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API), `provider_opts.http_headers` adds arbitrary HTTP headers to every outgoing request: diff --git a/docs/providers/deepseek/index.md b/docs/providers/deepseek/index.md new file mode 100644 index 000000000..8aeb2251b --- /dev/null +++ b/docs/providers/deepseek/index.md @@ -0,0 +1,96 @@ +--- +title: "DeepSeek" +description: "Use DeepSeek models with docker-agent." +permalink: /providers/deepseek/ +--- + +# DeepSeek + +_Use DeepSeek models with docker-agent._ + +## Overview + +[DeepSeek](https://www.deepseek.com/) serves its frontier chat and reasoning +models through an OpenAI-compatible API, with strong price/performance on coding +and reasoning tasks. docker-agent includes built-in support for DeepSeek as an +alias provider. + +## Setup + +1. Create an API key from the [DeepSeek Platform](https://platform.deepseek.com/api_keys). +2. Set the environment variable: + + ```bash + export DEEPSEEK_API_KEY=your-api-key + ``` + +## Usage + +### Inline Syntax + +The simplest way to use DeepSeek: + +```yaml +agents: + root: + model: deepseek/deepseek-chat + description: Assistant using DeepSeek + instruction: You are a helpful assistant. +``` + +### Named Model + +For more control over parameters: + +```yaml +models: + deepseek_model: + provider: deepseek + model: deepseek-chat + temperature: 0.7 + max_tokens: 8192 + +agents: + root: + model: deepseek_model + description: Assistant using DeepSeek + instruction: You are a helpful assistant. +``` + +## Available Models + +DeepSeek exposes a small, vendor-controlled model lineup. Check the +[DeepSeek models documentation](https://api-docs.deepseek.com/quick_start/pricing) +for current model IDs, context limits, and pricing. + +| Model | Description | +| --- | --- | +| `deepseek-chat` | DeepSeek-V3, general-purpose chat and tool calling | +| `deepseek-reasoner` | DeepSeek-R1, extended-reasoning model | + +> Model IDs are case-sensitive and must be passed exactly as the catalogue lists +> them. + +## How It Works + +DeepSeek is implemented as a built-in alias in docker-agent: + +- **API Type:** OpenAI-compatible (`openai_chatcompletions`) +- **Base URL:** `https://api.deepseek.com/v1` +- **Token Variable:** `DEEPSEEK_API_KEY` + +## Example: Code Assistant + +```yaml +agents: + coder: + model: deepseek/deepseek-chat + description: Code assistant using DeepSeek-V3 + instruction: | + You are an expert programmer. + Write clean, well-documented code and follow language best practices. + toolsets: + - type: filesystem + - type: shell + - type: think +``` diff --git a/docs/providers/overview/index.md b/docs/providers/overview/index.md index fbf2fdf9c..eb8e13084 100644 --- a/docs/providers/overview/index.md +++ b/docs/providers/overview/index.md @@ -68,6 +68,7 @@ docker-agent also includes built-in aliases for these providers: | Baseten | `baseten` | `BASETEN_API_KEY` | | OVHcloud | `ovhcloud` | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` | | Groq | `groq` | `GROQ_API_KEY` | +| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | | Requesty | `requesty` | `REQUESTY_API_KEY` | | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | | Azure OpenAI | `azure` | `AZURE_API_KEY` + `base_url` | diff --git a/examples/README.md b/examples/README.md index a58c4ce43..1298d3b3f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -196,6 +196,7 @@ remote MCP endpoints. | [`baseten.yaml`](baseten.yaml) | Baseten cloud provider. | | [`ovhcloud.yaml`](ovhcloud.yaml) | OVHcloud AI Endpoints provider. | | [`groq.yaml`](groq.yaml) | Groq fast-inference provider. | +| [`deepseek.yaml`](deepseek.yaml) | DeepSeek chat and reasoning provider. | | [`grok.yaml`](grok.yaml) | xAI Grok model. | | [`github-copilot.yaml`](github-copilot.yaml) | GitHub Copilot models via OAuth device-flow. | | [`fallback_models.yaml`](fallback_models.yaml) | Automatic fallback to a secondary model when the primary fails. | diff --git a/examples/deepseek.yaml b/examples/deepseek.yaml new file mode 100644 index 000000000..2b3d55afb --- /dev/null +++ b/examples/deepseek.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=../agent-schema.json + +models: + deepseek_model: + provider: deepseek + model: deepseek-chat + +agents: + root: + model: deepseek_model + description: Assistant using DeepSeek + instruction: | + You are a helpful assistant. + toolsets: + - type: filesystem + - type: shell + - type: think diff --git a/pkg/config/auto.go b/pkg/config/auto.go index 4b515860a..a6179e695 100644 --- a/pkg/config/auto.go +++ b/pkg/config/auto.go @@ -47,6 +47,7 @@ var cloudProviders = []providerConfig{ {"baseten", []string{"BASETEN_API_KEY"}, "BASETEN_API_KEY"}, {"ovhcloud", []string{"OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, "OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, {"groq", []string{"GROQ_API_KEY"}, "GROQ_API_KEY"}, + {"deepseek", []string{"DEEPSEEK_API_KEY"}, "DEEPSEEK_API_KEY"}, {"amazon-bedrock", []string{ "AWS_BEARER_TOKEN_BEDROCK", "AWS_ACCESS_KEY_ID", @@ -113,6 +114,7 @@ var DefaultModels = map[string]string{ "baseten": "deepseek-ai/DeepSeek-V3.1", "ovhcloud": "Qwen3.5-397B-A17B", "groq": "llama-3.3-70b-versatile", + "deepseek": "deepseek-chat", "amazon-bedrock": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", "opencode-go": "deepseek-v4-flash", "opencode-zen": "deepseek-v4-flash-free", diff --git a/pkg/config/auto_test.go b/pkg/config/auto_test.go index 3fd6c51ac..83ad2dfe2 100644 --- a/pkg/config/auto_test.go +++ b/pkg/config/auto_test.go @@ -76,6 +76,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) { }, expectedProvider: "groq", }, + { + name: "deepseek api key present", + envVars: map[string]string{ + "DEEPSEEK_API_KEY": "test-key", + }, + expectedProvider: "deepseek", + }, { name: "no api keys - defaults to dmr", envVars: map[string]string{}, @@ -265,6 +272,15 @@ func TestAutoModelConfig(t *testing.T) { expectedModel: "llama-3.3-70b-versatile", expectedMaxTokens: 32000, }, + { + name: "deepseek provider", + envVars: map[string]string{ + "DEEPSEEK_API_KEY": "test-key", + }, + expectedProvider: "deepseek", + expectedModel: "deepseek-chat", + expectedMaxTokens: 32000, + }, { name: "dmr provider (no api keys)", envVars: map[string]string{}, @@ -347,7 +363,7 @@ func TestDefaultModels(t *testing.T) { t.Parallel() // Test that DefaultModels map has all expected providers - expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "amazon-bedrock", "opencode-zen", "opencode-go"} + expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "amazon-bedrock", "opencode-zen", "opencode-go"} for _, provider := range expectedProviders { t.Run(provider, func(t *testing.T) { @@ -367,6 +383,7 @@ func TestDefaultModels(t *testing.T) { assert.Equal(t, "deepseek-ai/DeepSeek-V3.1", DefaultModels["baseten"]) assert.Equal(t, "Qwen3.5-397B-A17B", DefaultModels["ovhcloud"]) assert.Equal(t, "llama-3.3-70b-versatile", DefaultModels["groq"]) + assert.Equal(t, "deepseek-chat", DefaultModels["deepseek"]) assert.Equal(t, "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DefaultModels["amazon-bedrock"]) assert.Equal(t, "deepseek-v4-flash", DefaultModels["opencode-go"]) assert.Equal(t, "deepseek-v4-flash-free", DefaultModels["opencode-zen"]) @@ -376,7 +393,7 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) { t.Parallel() // Verify that AutoModelConfig always returns a model from DefaultModels - providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "opencode-zen"} + providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "opencode-zen"} for _, provider := range providers { t.Run(provider, func(t *testing.T) { @@ -402,6 +419,8 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) { envVars["OVH_AI_ENDPOINTS_ACCESS_TOKEN"] = "test-token" case "groq": envVars["GROQ_API_KEY"] = "test-key" + case "deepseek": + envVars["DEEPSEEK_API_KEY"] = "test-key" case "opencode-zen": envVars["OPENCODE_API_KEY"] = "test-key" } @@ -523,14 +542,22 @@ func TestAvailableProviders_PrecedenceOrder(t *testing.T) { providers = AvailableProviders(t.Context(), "", env) assert.Equal(t, "ovhcloud", providers[0]) - // groq wins over amazon-bedrock + // groq wins over deepseek env = environment.NewMapEnvProvider(map[string]string{ - "GROQ_API_KEY": "test-key", - "AWS_ACCESS_KEY_ID": "test-key", + "GROQ_API_KEY": "test-key", + "DEEPSEEK_API_KEY": "test-key", }) providers = AvailableProviders(t.Context(), "", env) assert.Equal(t, "groq", providers[0]) + // deepseek wins over amazon-bedrock + env = environment.NewMapEnvProvider(map[string]string{ + "DEEPSEEK_API_KEY": "test-key", + "AWS_ACCESS_KEY_ID": "test-key", + }) + providers = AvailableProviders(t.Context(), "", env) + assert.Equal(t, "deepseek", providers[0]) + // Only OPENCODE_API_KEY set - opencode-zen should win (higher priority than opencode-go) env = environment.NewMapEnvProvider(map[string]string{ "OPENCODE_API_KEY": "test-key", diff --git a/pkg/model/provider/aliases.go b/pkg/model/provider/aliases.go index 93166550f..ae3eb6642 100644 --- a/pkg/model/provider/aliases.go +++ b/pkg/model/provider/aliases.go @@ -83,6 +83,11 @@ var Aliases = map[string]Alias{ BaseURL: "https://api.groq.com/openai/v1", TokenEnvVar: "GROQ_API_KEY", }, + "deepseek": { + APIType: "openai", + BaseURL: "https://api.deepseek.com/v1", + TokenEnvVar: "DEEPSEEK_API_KEY", + }, "github-copilot": { APIType: "openai", BaseURL: "https://api.githubcopilot.com", diff --git a/pkg/model/provider/aliases_test.go b/pkg/model/provider/aliases_test.go index 6762081a0..fd8755280 100644 --- a/pkg/model/provider/aliases_test.go +++ b/pkg/model/provider/aliases_test.go @@ -85,6 +85,20 @@ func TestGroqAlias(t *testing.T) { assert.True(t, IsCatalogProvider("groq")) } +func TestDeepSeekAlias(t *testing.T) { + t.Parallel() + + alias, ok := LookupAlias("deepseek") + require.True(t, ok) + assert.Equal(t, Alias{ + APIType: "openai", + BaseURL: "https://api.deepseek.com/v1", + TokenEnvVar: "DEEPSEEK_API_KEY", + }, alias) + assert.True(t, IsKnownProvider("deepseek")) + assert.True(t, IsCatalogProvider("deepseek")) +} + func TestEachAlias(t *testing.T) { t.Parallel() diff --git a/pkg/model/provider/deepseek_test.go b/pkg/model/provider/deepseek_test.go new file mode 100644 index 000000000..c2976994f --- /dev/null +++ b/pkg/model/provider/deepseek_test.go @@ -0,0 +1,176 @@ +//go:build !js && !docker_agent_no_openai + +package provider + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/docker/docker-agent/pkg/chat" + "github.com/docker/docker-agent/pkg/config/latest" + "github.com/docker/docker-agent/pkg/environment" + "github.com/docker/docker-agent/pkg/tools" +) + +// TestDeepSeekProvider_EndToEndRequest drives a real request through the full +// stack (alias resolution -> OpenAI chat-completions client -> HTTP -> SSE +// parsing) against a local server emulating DeepSeek's OpenAI-compatible API. +// +// It proves the deepseek alias is wired correctly without a live key: +// - the request is authenticated with DEEPSEEK_API_KEY (alias TokenEnvVar), +// - it is routed to the chat-completions endpoint (alias APIType "openai"), +// - the configured model is sent verbatim, and +// - the streamed content is reassembled correctly. +func TestDeepSeekProvider_EndToEndRequest(t *testing.T) { + t.Parallel() + + const apiKey = "sk-test-deepseek-key" + + var ( + mu sync.Mutex + receivedMethod string + receivedAuth string + receivedPath string + receivedModel string + receivedMessages string + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + receivedMethod = r.Method + receivedAuth = r.Header.Get("Authorization") + receivedPath = r.URL.Path + mu.Unlock() + + var payload map[string]any + if err := json.NewDecoder(r.Body).Decode(&payload); err == nil { + mu.Lock() + if m, ok := payload["model"].(string); ok { + receivedModel = m + } + if msgs, err := json.Marshal(payload["messages"]); err == nil { + receivedMessages = string(msgs) + } + mu.Unlock() + } + + w.Header().Set("Content-Type", "text/event-stream") + flusher, _ := w.(http.Flusher) + + for _, delta := range []string{"Hello", " from", " DeepSeek"} { + writeSSEChunk(w, map[string]any{ + "id": "chatcmpl-test", "object": "chat.completion.chunk", "model": "deepseek-chat", + "choices": []map[string]any{{"index": 0, "delta": map[string]any{"content": delta}, "finish_reason": nil}}, + }) + flusher.Flush() + } + writeSSEChunk(w, map[string]any{ + "id": "chatcmpl-test", "object": "chat.completion.chunk", "model": "deepseek-chat", + "choices": []map[string]any{{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}}, + }) + _, _ = w.Write([]byte("data: [DONE]\n\n")) + flusher.Flush() + })) + defer server.Close() + + // BaseURL points at the mock server; TokenKey and api_type are left unset so + // they are filled in from the built-in deepseek alias, exercising the real + // resolution path. + modelCfg := &latest.ModelConfig{ + Provider: "deepseek", + Model: "deepseek-chat", + BaseURL: server.URL, + } + env := environment.NewMapEnvProvider(map[string]string{"DEEPSEEK_API_KEY": apiKey}) + + provider, err := fullTestRegistry().New(t.Context(), modelCfg, env) + require.NoError(t, err) + + stream, err := provider.CreateChatCompletionStream( + t.Context(), + []chat.Message{{Role: chat.MessageRoleUser, Content: "Hi"}}, + []tools.Tool{}, + ) + require.NoError(t, err) + defer stream.Close() + + content := collectStreamContent(t, stream) + + mu.Lock() + defer mu.Unlock() + assert.Equal(t, http.MethodPost, receivedMethod, "chat completions must be sent as a POST") + assert.Equal(t, "Bearer "+apiKey, receivedAuth, "auth must use the DEEPSEEK_API_KEY from the alias TokenEnvVar") + assert.Equal(t, "/chat/completions", receivedPath, "deepseek alias must route to the chat-completions endpoint") + assert.Equal(t, "deepseek-chat", receivedModel, "the configured model must be sent verbatim") + assert.Contains(t, receivedMessages, `"role":"user"`, "the outgoing request must carry the user message role") + assert.Contains(t, receivedMessages, "Hi", "the outgoing request must carry the user message content") + assert.Equal(t, "Hello from DeepSeek", content, "streamed deltas must be reassembled in order") +} + +// TestDeepSeekLiveAPI performs a real request against the DeepSeek API. It is +// skipped unless DEEPSEEK_API_KEY is set in the environment, so the default +// test run stays hermetic while allowing an on-demand real check via: +// +// DEEPSEEK_API_KEY=sk-... go test -run TestDeepSeekLiveAPI ./pkg/model/provider/ +func TestDeepSeekLiveAPI(t *testing.T) { + apiKey := os.Getenv("DEEPSEEK_API_KEY") + if apiKey == "" { + t.Skip("DEEPSEEK_API_KEY not set; skipping live DeepSeek API test") + } + + // No BaseURL/TokenKey: both come from the built-in deepseek alias, so this + // hits https://api.deepseek.com/v1 for real. + modelCfg := &latest.ModelConfig{ + Provider: "deepseek", + Model: "deepseek-chat", + } + + provider, err := fullTestRegistry().New(t.Context(), modelCfg, environment.NewOsEnvProvider()) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second) + defer cancel() + + stream, err := provider.CreateChatCompletionStream( + ctx, + []chat.Message{{Role: chat.MessageRoleUser, Content: "Reply with the single word: pong"}}, + []tools.Tool{}, + ) + require.NoError(t, err) + defer stream.Close() + + content := collectStreamContent(t, stream) + require.NotEmpty(t, content, "live DeepSeek API must return a non-empty completion") + t.Logf("DeepSeek live response: %q", content) +} + +// collectStreamContent drains a message stream and returns the concatenated +// text of all content deltas. +func collectStreamContent(t *testing.T, stream chat.MessageStream) string { + t.Helper() + + var b strings.Builder + for { + resp, err := stream.Recv() + for _, choice := range resp.Choices { + b.WriteString(choice.Delta.Content) + } + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + } + return b.String() +} diff --git a/pkg/model/provider/openai/repro_issue3344_test.go b/pkg/model/provider/openai/repro_issue3344_test.go index 992870aec..63db783d5 100644 --- a/pkg/model/provider/openai/repro_issue3344_test.go +++ b/pkg/model/provider/openai/repro_issue3344_test.go @@ -197,6 +197,7 @@ func TestShouldMergeConsecutiveMessages_Gating(t *testing.T) { // First-party APIs with fixed model lineups: unchanged (no merge). {"first-party mistral", &latest.ModelConfig{Provider: "mistral", Model: "mistral-small"}, false}, {"first-party xai", &latest.ModelConfig{Provider: "xai", Model: "grok-4"}, false}, + {"first-party deepseek", &latest.ModelConfig{Provider: "deepseek", Model: "deepseek-chat"}, false}, } for _, tt := range tests {