detached c2267b3

Fix DeepSeek max reasoning effort

commit
c2267b3f6d6674c51b183ca9b59d700bd1d1b5a5
author
patriceckhart · · 2026-09-25 06:04 UTC
parent
c5883a4
signature
unsigned
refs
tag: v0.3.97
4 files · +42 -1
M README.md +1 -1
@@ -701,7 +701,7 @@ By default this uses:
701701 - model: `deepseek-v4-pro`
702702 - base URL: `https://api.deepseek.com/v1`
703703
704-Catalog ships with `deepseek-v4-pro` (reasoning) and `deepseek-v4-flash`. These are exactly the IDs returned by `GET https://api.deepseek.com/models` today. You can add additional model IDs to `models.json` under the `deepseek` provider.
704+Catalog ships with `deepseek-v4-pro` (reasoning) and `deepseek-v4-flash`. These are exactly the IDs returned by `GET https://api.deepseek.com/models` today. You can add additional model IDs to `models.json` under the `deepseek` provider. For direct DeepSeek reasoning models, `/reasoning` offers off, low, high, and max. DeepSeek maps medium and xhigh to high, while max sends the distinct `reasoning_effort: "max"` value. Other OpenAI-compatible providers retain their own effort limits.
705705
706706 Credential lookup order for DeepSeek:
707707
@@ -701,7 +701,7 @@ By default this uses:
701 - model: `deepseek-v4-pro`701 - model: `deepseek-v4-pro`
702 - base URL: `https://api.deepseek.com/v1`702 - base URL: `https://api.deepseek.com/v1`
703 703
704-Catalog ships with `deepseek-v4-pro` (reasoning) and `deepseek-v4-flash`. These are exactly the IDs returned by `GET https://api.deepseek.com/models` today. You can add additional model IDs to `models.json` under the `deepseek` provider.704+Catalog ships with `deepseek-v4-pro` (reasoning) and `deepseek-v4-flash`. These are exactly the IDs returned by `GET https://api.deepseek.com/models` today. You can add additional model IDs to `models.json` under the `deepseek` provider. For direct DeepSeek reasoning models, `/reasoning` offers off, low, high, and max. DeepSeek maps medium and xhigh to high, while max sends the distinct `reasoning_effort: "max"` value. Other OpenAI-compatible providers retain their own effort limits.
705 705
706 Credential lookup order for DeepSeek:706 Credential lookup order for DeepSeek:
707 707
M packages/provider/openai.go +3 -0
@@ -263,6 +263,9 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
263263 out.MaxCompletionTok = &maxTok
264264 }
265265 effort := OpenAIReasoningEffort(reasoning)
266+ if c.Name() == "deepseek" && reasoning == "max" {
267+ effort = "max"
268+ }
266269 if usesAdaptiveThinking(m) {
267270 // Some gateways expose adaptive-thinking Anthropic models through
268271 // the OpenAI-compatible chat-completions wire. They accept the
@@ -263,6 +263,9 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
263 out.MaxCompletionTok = &maxTok263 out.MaxCompletionTok = &maxTok
264 }264 }
265 effort := OpenAIReasoningEffort(reasoning)265 effort := OpenAIReasoningEffort(reasoning)
266+ if c.Name() == "deepseek" && reasoning == "max" {
267+ effort = "max"
268+ }
266 if usesAdaptiveThinking(m) {269 if usesAdaptiveThinking(m) {
267 // Some gateways expose adaptive-thinking Anthropic models through270 // Some gateways expose adaptive-thinking Anthropic models through
268 // the OpenAI-compatible chat-completions wire. They accept the271 // the OpenAI-compatible chat-completions wire. They accept the
M packages/provider/reasoning.go +7 -0
@@ -54,6 +54,10 @@ func defaultReasoningLevels(model Model) []string {
5454 }
5555 return []string{"", "minimum", "low", "medium", "high"}
5656 }
57+ if model.Provider == "deepseek" {
58+ // DeepSeek maps medium and xhigh to high; max is a distinct effort.
59+ return []string{"", "low", "high", "max"}
60+ }
5761 if model.AdaptiveThinkingCompat {
5862 return []string{"", "high"}
5963 }
@@ -112,6 +116,9 @@ func ClampReasoningForModel(model Model, level string) string {
112116 return candidate
113117 }
114118 }
119+ if model.Provider == "deepseek" && (normalized == "medium" || normalized == "xhigh") && containsReasoningLevel(available, "high") {
120+ return "high"
121+ }
115122 return nearestReasoningLevel(available, normalized)
116123 }
117124
@@ -54,6 +54,10 @@ func defaultReasoningLevels(model Model) []string {
54 }54 }
55 return []string{"", "minimum", "low", "medium", "high"}55 return []string{"", "minimum", "low", "medium", "high"}
56 }56 }
57+ if model.Provider == "deepseek" {
58+ // DeepSeek maps medium and xhigh to high; max is a distinct effort.
59+ return []string{"", "low", "high", "max"}
60+ }
57 if model.AdaptiveThinkingCompat {61 if model.AdaptiveThinkingCompat {
58 return []string{"", "high"}62 return []string{"", "high"}
59 }63 }
@@ -112,6 +116,9 @@ func ClampReasoningForModel(model Model, level string) string {
112 return candidate116 return candidate
113 }117 }
114 }118 }
119+ if model.Provider == "deepseek" && (normalized == "medium" || normalized == "xhigh") && containsReasoningLevel(available, "high") {
120+ return "high"
121+ }
115 return nearestReasoningLevel(available, normalized)122 return nearestReasoningLevel(available, normalized)
116 }123 }
117 124
M packages/provider/reasoning_test.go +31 -0
@@ -13,6 +13,7 @@ func TestAvailableReasoningLevels(t *testing.T) {
1313 }{
1414 {name: "unsupported", model: Model{}, want: []string{""}},
1515 {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}},
16+ {name: "direct deepseek", model: Model{Provider: "deepseek", Reasoning: true}, want: []string{"", "low", "high", "max"}},
1617 {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}},
1718 {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
1819 {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
@@ -63,6 +64,8 @@ func TestClampReasoningForModel(t *testing.T) {
6364 }{
6465 {name: "unsupported", model: Model{}, level: "high", want: ""},
6566 {name: "generic max", model: Model{Reasoning: true}, level: "max", want: "high"},
67+ {name: "deepseek xhigh maps to high", model: Model{Provider: "deepseek", Reasoning: true}, level: "xhigh", want: "high"},
68+ {name: "deepseek max stays max", model: Model{Provider: "deepseek", Reasoning: true}, level: "max", want: "max"},
6669 {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"},
6770 {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"},
6871 {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"},
@@ -89,6 +92,34 @@ func TestClampReasoningForModel(t *testing.T) {
8992 }
9093 }
9194
95+func TestDeepSeekReasoningEffort(t *testing.T) {
96+ client := NewDeepSeek("test", "").(*openaiClient)
97+ for _, model := range []string{"deepseek-v4-pro", "deepseek-v4-flash"} {
98+ m, err := FindModel("deepseek", model)
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ if got := AvailableReasoningLevels(m); !slices.Equal(got, []string{"", "low", "high", "max"}) {
103+ t.Fatalf("%s: levels = %q, want off/low/high/max", model, got)
104+ }
105+ for _, tc := range []struct{ requested, want string }{
106+ {"low", "low"},
107+ {"medium", "high"},
108+ {"high", "high"},
109+ {"xhigh", "high"},
110+ {"max", "max"},
111+ } {
112+ wire, err := client.buildRequest(Request{Model: model, Reasoning: tc.requested})
113+ if err != nil {
114+ t.Fatal(err)
115+ }
116+ if wire.ReasoningEffort != tc.want {
117+ t.Errorf("%s %s: reasoning_effort = %q, want %q", model, tc.requested, wire.ReasoningEffort, tc.want)
118+ }
119+ }
120+ }
121+}
122+
92123 func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) {
93124 SetLiveModels([]Model{{
94125 Provider: "custom",
@@ -13,6 +13,7 @@ func TestAvailableReasoningLevels(t *testing.T) {
13 }{13 }{
14 {name: "unsupported", model: Model{}, want: []string{""}},14 {name: "unsupported", model: Model{}, want: []string{""}},
15 {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}},15 {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}},
16+ {name: "direct deepseek", model: Model{Provider: "deepseek", Reasoning: true}, want: []string{"", "low", "high", "max"}},
16 {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}},17 {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}},
17 {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},18 {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
18 {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},19 {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
@@ -63,6 +64,8 @@ func TestClampReasoningForModel(t *testing.T) {
63 }{64 }{
64 {name: "unsupported", model: Model{}, level: "high", want: ""},65 {name: "unsupported", model: Model{}, level: "high", want: ""},
65 {name: "generic max", model: Model{Reasoning: true}, level: "max", want: "high"},66 {name: "generic max", model: Model{Reasoning: true}, level: "max", want: "high"},
67+ {name: "deepseek xhigh maps to high", model: Model{Provider: "deepseek", Reasoning: true}, level: "xhigh", want: "high"},
68+ {name: "deepseek max stays max", model: Model{Provider: "deepseek", Reasoning: true}, level: "max", want: "max"},
66 {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"},69 {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"},
67 {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"},70 {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"},
68 {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"},71 {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"},
@@ -89,6 +92,34 @@ func TestClampReasoningForModel(t *testing.T) {
89 }92 }
90 }93 }
91 94
95+func TestDeepSeekReasoningEffort(t *testing.T) {
96+ client := NewDeepSeek("test", "").(*openaiClient)
97+ for _, model := range []string{"deepseek-v4-pro", "deepseek-v4-flash"} {
98+ m, err := FindModel("deepseek", model)
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ if got := AvailableReasoningLevels(m); !slices.Equal(got, []string{"", "low", "high", "max"}) {
103+ t.Fatalf("%s: levels = %q, want off/low/high/max", model, got)
104+ }
105+ for _, tc := range []struct{ requested, want string }{
106+ {"low", "low"},
107+ {"medium", "high"},
108+ {"high", "high"},
109+ {"xhigh", "high"},
110+ {"max", "max"},
111+ } {
112+ wire, err := client.buildRequest(Request{Model: model, Reasoning: tc.requested})
113+ if err != nil {
114+ t.Fatal(err)
115+ }
116+ if wire.ReasoningEffort != tc.want {
117+ t.Errorf("%s %s: reasoning_effort = %q, want %q", model, tc.requested, wire.ReasoningEffort, tc.want)
118+ }
119+ }
120+ }
121+}
122+
92 func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) {123 func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) {
93 SetLiveModels([]Model{{124 SetLiveModels([]Model{{
94 Provider: "custom",125 Provider: "custom",

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