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: | |||
| 701 | 701 | - model: `deepseek-v4-pro` | |
| 702 | 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 | 706 | Credential lookup order for DeepSeek: | |
| 707 | 707 | ||
| @@ -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) { | |||
| 263 | 263 | out.MaxCompletionTok = &maxTok | |
| 264 | 264 | } | |
| 265 | 265 | effort := OpenAIReasoningEffort(reasoning) | |
| 266 | + | if c.Name() == "deepseek" && reasoning == "max" { | |
| 267 | + | effort = "max" | |
| 268 | + | } | |
| 266 | 269 | if usesAdaptiveThinking(m) { | |
| 267 | 270 | // Some gateways expose adaptive-thinking Anthropic models through | |
| 268 | 271 | // the OpenAI-compatible chat-completions wire. They accept the | |
| @@ -263,6 +263,9 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 263 | out.MaxCompletionTok = &maxTok | 263 | 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 through | 270 | // Some gateways expose adaptive-thinking Anthropic models through | ||
| 268 | // the OpenAI-compatible chat-completions wire. They accept the | 271 | // 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 { | |||
| 54 | 54 | } | |
| 55 | 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 | 61 | if model.AdaptiveThinkingCompat { | |
| 58 | 62 | return []string{"", "high"} | |
| 59 | 63 | } | |
| @@ -112,6 +116,9 @@ func ClampReasoningForModel(model Model, level string) string { | |||
| 112 | 116 | return candidate | |
| 113 | 117 | } | |
| 114 | 118 | } | |
| 119 | + | if model.Provider == "deepseek" && (normalized == "medium" || normalized == "xhigh") && containsReasoningLevel(available, "high") { | |
| 120 | + | return "high" | |
| 121 | + | } | |
| 115 | 122 | return nearestReasoningLevel(available, normalized) | |
| 116 | 123 | } | |
| 117 | 124 | ||
| @@ -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 candidate | 116 | 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) { | |||
| 13 | 13 | }{ | |
| 14 | 14 | {name: "unsupported", model: Model{}, want: []string{""}}, | |
| 15 | 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 | 17 | {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}}, | |
| 17 | 18 | {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | |
| 18 | 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 | 65 | {name: "unsupported", model: Model{}, level: "high", want: ""}, | |
| 65 | 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 | 69 | {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"}, | |
| 67 | 70 | {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"}, | |
| 68 | 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 | 123 | func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) { | |
| 93 | 124 | SetLiveModels([]Model{{ | |
| 94 | 125 | 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", | ||