detached e169d60

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 UnverifiedB5690EEEBB952194 The 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) {
3939 }
4040 }
4141
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+
4272 func TestBedrockDeveloperContextFollowsStableCacheBoundary(t *testing.T) {
4373 req, err := (&bedrockClient{region: "us-east-1"}).buildRequest(Request{
4474 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) {
328328 switch msg.Role {
329329 case RoleDeveloper:
330330 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})
332336 case RoleUser:
333337 content := buildOAIUserContent(msg.Content, textOnly)
334338 out.Messages = append(out.Messages, oaiMessage{Role: "user", Content: content})
@@ -429,6 +433,11 @@ func isKimiDeferredModel(model string) bool {
429433 return model == "kimi-k3" || strings.HasSuffix(model, "/kimi-k3")
430434 }
431435
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+
432441 func makeOAITool(t Tool) oaiTool {
433442 var tool oaiTool
434443 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 oaiTool442 var tool oaiTool
434 tool.Type = "function"443 tool.Type = "function"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close