fix(provider): map DeepSeek developer context to system (#199)
- commit
- e169d60d5390015061e16ece792a69eb4484877f
- author
- bnema · · 2026-09-10 11:42 UTC
- committer
- GitHub · · 2026-09-10 11:42 UTC
- parent
- a11d3a0
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
2 files · +40 -1
M packages/provider/developer_context_test.go +30 -0
| @@ -39,6 +39,36 @@ func TestAnthropicDeveloperContextFollowsStableCacheBoundary(t *testing.T) { | |||
| 39 | 39 | } | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | + | func TestOpenAICompatUsesSystemRoleForDeepSeekDeveloperContext(t *testing.T) { | |
| 43 | + | tests := []struct { | |
| 44 | + | name string | |
| 45 | + | client *openaiClient | |
| 46 | + | model string | |
| 47 | + | want string | |
| 48 | + | }{ | |
| 49 | + | {name: "direct DeepSeek", client: NewDeepSeek("key", "").(*openaiClient), model: "deepseek-v4-flash", want: "system"}, | |
| 50 | + | {name: "OpenCode Go DeepSeek", client: NewOpenAICompat(ProviderOpenCodeGo, "key", "https://example.test/v1", "").(*openaiClient), model: "deepseek/deepseek-v4.1-flash", want: "system"}, | |
| 51 | + | {name: "OpenCode Go other model", client: NewOpenAICompat(ProviderOpenCodeGo, "key", "https://example.test/v1", "").(*openaiClient), model: "other/model", want: "developer"}, | |
| 52 | + | } | |
| 53 | + | for _, test := range tests { | |
| 54 | + | t.Run(test.name, func(t *testing.T) { | |
| 55 | + | req, err := test.client.buildRequest(Request{ | |
| 56 | + | Model: test.model, | |
| 57 | + | Messages: []Message{ | |
| 58 | + | {Role: RoleDeveloper, Content: []Content{TextBlock{Text: "dynamic host context"}}}, | |
| 59 | + | {Role: RoleUser, Content: []Content{TextBlock{Text: "user task"}}}, | |
| 60 | + | }, | |
| 61 | + | }) | |
| 62 | + | if err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | if len(req.Messages) != 2 || req.Messages[0].Role != test.want || req.Messages[0].Content != "dynamic host context" { | |
| 66 | + | t.Fatalf("messages = %#v, want developer context as %q", req.Messages, test.want) | |
| 67 | + | } | |
| 68 | + | }) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 42 | 72 | func TestBedrockDeveloperContextFollowsStableCacheBoundary(t *testing.T) { | |
| 43 | 73 | req, err := (&bedrockClient{region: "us-east-1"}).buildRequest(Request{ | |
| 44 | 74 | Model: "anthropic.claude-sonnet-4-5-20250929-v1:0", | |
| @@ -39,6 +39,36 @@ func TestAnthropicDeveloperContextFollowsStableCacheBoundary(t *testing.T) { | |||||
| 39 | } | 39 | } | ||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | + | func TestOpenAICompatUsesSystemRoleForDeepSeekDeveloperContext(t *testing.T) { | |||
| 43 | + | tests := []struct { | |||
| 44 | + | name string | |||
| 45 | + | client *openaiClient | |||
| 46 | + | model string | |||
| 47 | + | want string | |||
| 48 | + | }{ | |||
| 49 | + | {name: "direct DeepSeek", client: NewDeepSeek("key", "").(*openaiClient), model: "deepseek-v4-flash", want: "system"}, | |||
| 50 | + | {name: "OpenCode Go DeepSeek", client: NewOpenAICompat(ProviderOpenCodeGo, "key", "https://example.test/v1", "").(*openaiClient), model: "deepseek/deepseek-v4.1-flash", want: "system"}, | |||
| 51 | + | {name: "OpenCode Go other model", client: NewOpenAICompat(ProviderOpenCodeGo, "key", "https://example.test/v1", "").(*openaiClient), model: "other/model", want: "developer"}, | |||
| 52 | + | } | |||
| 53 | + | for _, test := range tests { | |||
| 54 | + | t.Run(test.name, func(t *testing.T) { | |||
| 55 | + | req, err := test.client.buildRequest(Request{ | |||
| 56 | + | Model: test.model, | |||
| 57 | + | Messages: []Message{ | |||
| 58 | + | {Role: RoleDeveloper, Content: []Content{TextBlock{Text: "dynamic host context"}}}, | |||
| 59 | + | {Role: RoleUser, Content: []Content{TextBlock{Text: "user task"}}}, | |||
| 60 | + | }, | |||
| 61 | + | }) | |||
| 62 | + | if err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | if len(req.Messages) != 2 || req.Messages[0].Role != test.want || req.Messages[0].Content != "dynamic host context" { | |||
| 66 | + | t.Fatalf("messages = %#v, want developer context as %q", req.Messages, test.want) | |||
| 67 | + | } | |||
| 68 | + | }) | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 42 | func TestBedrockDeveloperContextFollowsStableCacheBoundary(t *testing.T) { | 72 | func TestBedrockDeveloperContextFollowsStableCacheBoundary(t *testing.T) { | ||
| 43 | req, err := (&bedrockClient{region: "us-east-1"}).buildRequest(Request{ | 73 | req, err := (&bedrockClient{region: "us-east-1"}).buildRequest(Request{ | ||
| 44 | Model: "anthropic.claude-sonnet-4-5-20250929-v1:0", | 74 | Model: "anthropic.claude-sonnet-4-5-20250929-v1:0", | ||
M packages/provider/openai.go +10 -1
| @@ -328,7 +328,11 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 328 | 328 | switch msg.Role { | |
| 329 | 329 | case RoleDeveloper: | |
| 330 | 330 | content := buildOAIUserContent(msg.Content, textOnly) | |
| 331 | - | out.Messages = append(out.Messages, oaiMessage{Role: "developer", Content: content}) | |
| 331 | + | role := "developer" | |
| 332 | + | if c.name == ProviderDeepSeek || (c.name == ProviderOpenCodeGo && isDeepSeekModel(req.Model)) { | |
| 333 | + | role = "system" | |
| 334 | + | } | |
| 335 | + | out.Messages = append(out.Messages, oaiMessage{Role: role, Content: content}) | |
| 332 | 336 | case RoleUser: | |
| 333 | 337 | content := buildOAIUserContent(msg.Content, textOnly) | |
| 334 | 338 | out.Messages = append(out.Messages, oaiMessage{Role: "user", Content: content}) | |
| @@ -429,6 +433,11 @@ func isKimiDeferredModel(model string) bool { | |||
| 429 | 433 | return model == "kimi-k3" || strings.HasSuffix(model, "/kimi-k3") | |
| 430 | 434 | } | |
| 431 | 435 | ||
| 436 | + | func isDeepSeekModel(model string) bool { | |
| 437 | + | model = strings.ToLower(strings.TrimSpace(model)) | |
| 438 | + | return strings.HasPrefix(model, "deepseek-") || strings.Contains(model, "/deepseek-") | |
| 439 | + | } | |
| 440 | + | ||
| 432 | 441 | func makeOAITool(t Tool) oaiTool { | |
| 433 | 442 | var tool oaiTool | |
| 434 | 443 | tool.Type = "function" | |
| @@ -328,7 +328,11 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 328 | switch msg.Role { | 328 | switch msg.Role { | ||
| 329 | case RoleDeveloper: | 329 | case RoleDeveloper: | ||
| 330 | content := buildOAIUserContent(msg.Content, textOnly) | 330 | content := buildOAIUserContent(msg.Content, textOnly) | ||
| 331 | - | out.Messages = append(out.Messages, oaiMessage{Role: "developer", Content: content}) | 331 | + | role := "developer" |
| 332 | + | if c.name == ProviderDeepSeek || (c.name == ProviderOpenCodeGo && isDeepSeekModel(req.Model)) { | |||
| 333 | + | role = "system" | |||
| 334 | + | } | |||
| 335 | + | out.Messages = append(out.Messages, oaiMessage{Role: role, Content: content}) | |||
| 332 | case RoleUser: | 336 | case RoleUser: | ||
| 333 | content := buildOAIUserContent(msg.Content, textOnly) | 337 | content := buildOAIUserContent(msg.Content, textOnly) | ||
| 334 | out.Messages = append(out.Messages, oaiMessage{Role: "user", Content: content}) | 338 | out.Messages = append(out.Messages, oaiMessage{Role: "user", Content: content}) | ||
| @@ -429,6 +433,11 @@ func isKimiDeferredModel(model string) bool { | |||||
| 429 | return model == "kimi-k3" || strings.HasSuffix(model, "/kimi-k3") | 433 | return model == "kimi-k3" || strings.HasSuffix(model, "/kimi-k3") | ||
| 430 | } | 434 | } | ||
| 431 | 435 | ||||
| 436 | + | func isDeepSeekModel(model string) bool { | |||
| 437 | + | model = strings.ToLower(strings.TrimSpace(model)) | |||
| 438 | + | return strings.HasPrefix(model, "deepseek-") || strings.Contains(model, "/deepseek-") | |||
| 439 | + | } | |||
| 440 | + | ||||
| 432 | func makeOAITool(t Tool) oaiTool { | 441 | func makeOAITool(t Tool) oaiTool { | ||
| 433 | var tool oaiTool | 442 | var tool oaiTool | ||
| 434 | tool.Type = "function" | 443 | tool.Type = "function" | ||