fix(provider): honor explicit reasoning mappings
- commit
- 11ffb00360ec93fef89853f50a594e3a6aadaa99
- author
- brice · · 2026-08-10 13:56 UTC
- committer
- brice · · 2026-08-11 07:21 UTC
- parent
- d451e98
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +73 -17
M README.md +1 -1
| @@ -533,7 +533,7 @@ Place a `models.json` in `$ZUT_HOME` (`$XDG_STATE_HOME/zut/` when set, otherwise | |||
| 533 | 533 | ||
| 534 | 534 | Supported fields per model: `id` (required), `name`, `reasoning`, `reasoningLevelMap`, `contextWindow`, `maxTokens`, `baseUrl`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`. | |
| 535 | 535 | ||
| 536 | - | `reasoningLevelMap` is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using `minimum`, `low`, `medium`, `high`, `xhigh`, or `max` as keys. Map a key to another level when both inputs are equivalent, or to an empty string or `off` to remove it. The same effective mapping drives `/reasoning` and provider requests. | |
| 536 | + | `reasoningLevelMap` is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using `minimum`, `low`, `medium`, `high`, `xhigh`, or `max` as keys. Map a key to another level when both inputs are equivalent, use an identity mapping such as `"max": "max"` to enable a level beyond the protocol default, or map it to an empty string or `off` to remove it. The same effective mapping drives `/reasoning` and provider requests. | |
| 537 | 537 | ||
| 538 | 538 | Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`, `moonshot`, `moonshot-ai`, and `kimi-code` map to `kimi`, and `deepseek-chat` and `deepseek-ai` map to `deepseek`. Built-in provider ids such as `groq`, `openrouter`, `github-copilot`, `amazon-bedrock`, `google-vertex`, `azure-openai-responses`, `fireworks`, `vercel-ai-gateway`, `mistral`, and `xai` can also be used directly. | |
| 539 | 539 | ||
| @@ -533,7 +533,7 @@ Place a `models.json` in `$ZUT_HOME` (`$XDG_STATE_HOME/zut/` when set, otherwise | |||||
| 533 | 533 | ||||
| 534 | Supported fields per model: `id` (required), `name`, `reasoning`, `reasoningLevelMap`, `contextWindow`, `maxTokens`, `baseUrl`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`. | 534 | Supported fields per model: `id` (required), `name`, `reasoning`, `reasoningLevelMap`, `contextWindow`, `maxTokens`, `baseUrl`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`. | ||
| 535 | 535 | ||||
| 536 | - | `reasoningLevelMap` is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using `minimum`, `low`, `medium`, `high`, `xhigh`, or `max` as keys. Map a key to another level when both inputs are equivalent, or to an empty string or `off` to remove it. The same effective mapping drives `/reasoning` and provider requests. | 536 | + | `reasoningLevelMap` is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using `minimum`, `low`, `medium`, `high`, `xhigh`, or `max` as keys. Map a key to another level when both inputs are equivalent, use an identity mapping such as `"max": "max"` to enable a level beyond the protocol default, or map it to an empty string or `off` to remove it. The same effective mapping drives `/reasoning` and provider requests. |
| 537 | 537 | ||||
| 538 | Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`, `moonshot`, `moonshot-ai`, and `kimi-code` map to `kimi`, and `deepseek-chat` and `deepseek-ai` map to `deepseek`. Built-in provider ids such as `groq`, `openrouter`, `github-copilot`, `amazon-bedrock`, `google-vertex`, `azure-openai-responses`, `fireworks`, `vercel-ai-gateway`, `mistral`, and `xai` can also be used directly. | 538 | Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`, `moonshot`, `moonshot-ai`, and `kimi-code` map to `kimi`, and `deepseek-chat` and `deepseek-ai` map to `deepseek`. Built-in provider ids such as `groq`, `openrouter`, `github-copilot`, `amazon-bedrock`, `google-vertex`, `azure-openai-responses`, `fireworks`, `vercel-ai-gateway`, `mistral`, and `xai` can also be used directly. | ||
| 539 | 539 | ||||
M packages/provider/openai.go +5 -0
| @@ -264,6 +264,11 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 264 | 264 | // don't clamp zut's "maximum" to "high" for those models. | |
| 265 | 265 | effort = OpenAICompatAnthropicEffort(reasoning) | |
| 266 | 266 | } | |
| 267 | + | if hasReasoningLevelOverride(m, req.Reasoning) { | |
| 268 | + | // Explicit model mappings describe the endpoint's actual effort | |
| 269 | + | // values and take precedence over conservative protocol defaults. | |
| 270 | + | effort = reasoning | |
| 271 | + | } | |
| 267 | 272 | if effort != "" { | |
| 268 | 273 | out.ReasoningEffort = effort | |
| 269 | 274 | } | |
| @@ -264,6 +264,11 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 264 | // don't clamp zut's "maximum" to "high" for those models. | 264 | // don't clamp zut's "maximum" to "high" for those models. | ||
| 265 | effort = OpenAICompatAnthropicEffort(reasoning) | 265 | effort = OpenAICompatAnthropicEffort(reasoning) | ||
| 266 | } | 266 | } | ||
| 267 | + | if hasReasoningLevelOverride(m, req.Reasoning) { | |||
| 268 | + | // Explicit model mappings describe the endpoint's actual effort | |||
| 269 | + | // values and take precedence over conservative protocol defaults. | |||
| 270 | + | effort = reasoning | |||
| 271 | + | } | |||
| 267 | if effort != "" { | 272 | if effort != "" { | ||
| 268 | out.ReasoningEffort = effort | 273 | out.ReasoningEffort = effort | ||
| 269 | } | 274 | } | ||
M packages/provider/openai_codex.go +5 -1
| @@ -206,7 +206,11 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||
| 206 | 206 | body.ServiceTier = fastModeServiceTier | |
| 207 | 207 | } | |
| 208 | 208 | if m.Reasoning { | |
| 209 | - | if effort := OpenAICodexReasoningEffort(reasoning, req.Model); effort != "" { | |
| 209 | + | effort := OpenAICodexReasoningEffort(reasoning, req.Model) | |
| 210 | + | if hasReasoningLevelOverride(m, req.Reasoning) { | |
| 211 | + | effort = reasoning | |
| 212 | + | } | |
| 213 | + | if effort != "" { | |
| 210 | 214 | body.Reasoning = &codexReasoningConfig{Effort: effort} | |
| 211 | 215 | } | |
| 212 | 216 | } | |
| @@ -206,7 +206,11 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||||
| 206 | body.ServiceTier = fastModeServiceTier | 206 | body.ServiceTier = fastModeServiceTier | ||
| 207 | } | 207 | } | ||
| 208 | if m.Reasoning { | 208 | if m.Reasoning { | ||
| 209 | - | if effort := OpenAICodexReasoningEffort(reasoning, req.Model); effort != "" { | 209 | + | effort := OpenAICodexReasoningEffort(reasoning, req.Model) |
| 210 | + | if hasReasoningLevelOverride(m, req.Reasoning) { | |||
| 211 | + | effort = reasoning | |||
| 212 | + | } | |||
| 213 | + | if effort != "" { | |||
| 210 | body.Reasoning = &codexReasoningConfig{Effort: effort} | 214 | body.Reasoning = &codexReasoningConfig{Effort: effort} | ||
| 211 | } | 215 | } | ||
| 212 | } | 216 | } | ||
M packages/provider/openai_codex_test.go +21 -0
| @@ -126,6 +126,27 @@ func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |||
| 126 | 126 | } | |
| 127 | 127 | } | |
| 128 | 128 | ||
| 129 | + | func TestResponsesRequestUsesExplicitReasoningLevelMap(t *testing.T) { | |
| 130 | + | SetLiveModels([]Model{{ | |
| 131 | + | Provider: "custom-responses", | |
| 132 | + | ID: "reasoning-model", | |
| 133 | + | API: APIResponses, | |
| 134 | + | Reasoning: true, | |
| 135 | + | ReasoningLevelMap: map[string]string{"max": "max"}, | |
| 136 | + | }}) | |
| 137 | + | t.Cleanup(func() { SetLiveModels(nil) }) | |
| 138 | + | ||
| 139 | + | named := NewOpenAIResponsesNamed("token", "https://example.test/v1", "custom-responses").(*renamedClient) | |
| 140 | + | c := named.inner.(*codexClient) | |
| 141 | + | wire, err := c.buildRequest(Request{Model: "reasoning-model", Reasoning: "max"}) | |
| 142 | + | if err != nil { | |
| 143 | + | t.Fatal(err) | |
| 144 | + | } | |
| 145 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |
| 146 | + | t.Fatalf("reasoning = %+v", wire.Reasoning) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 129 | 150 | func TestXAIResponsesUsesNamedProviderCatalogAndEndpoint(t *testing.T) { | |
| 130 | 151 | named := NewOpenAIResponsesNamed("token", "https://api.x.ai/v1", "xai").(*renamedClient) | |
| 131 | 152 | c := named.inner.(*codexClient) | |
| @@ -126,6 +126,27 @@ func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |||||
| 126 | } | 126 | } | ||
| 127 | } | 127 | } | ||
| 128 | 128 | ||||
| 129 | + | func TestResponsesRequestUsesExplicitReasoningLevelMap(t *testing.T) { | |||
| 130 | + | SetLiveModels([]Model{{ | |||
| 131 | + | Provider: "custom-responses", | |||
| 132 | + | ID: "reasoning-model", | |||
| 133 | + | API: APIResponses, | |||
| 134 | + | Reasoning: true, | |||
| 135 | + | ReasoningLevelMap: map[string]string{"max": "max"}, | |||
| 136 | + | }}) | |||
| 137 | + | t.Cleanup(func() { SetLiveModels(nil) }) | |||
| 138 | + | ||||
| 139 | + | named := NewOpenAIResponsesNamed("token", "https://example.test/v1", "custom-responses").(*renamedClient) | |||
| 140 | + | c := named.inner.(*codexClient) | |||
| 141 | + | wire, err := c.buildRequest(Request{Model: "reasoning-model", Reasoning: "max"}) | |||
| 142 | + | if err != nil { | |||
| 143 | + | t.Fatal(err) | |||
| 144 | + | } | |||
| 145 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |||
| 146 | + | t.Fatalf("reasoning = %+v", wire.Reasoning) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 129 | func TestXAIResponsesUsesNamedProviderCatalogAndEndpoint(t *testing.T) { | 150 | func TestXAIResponsesUsesNamedProviderCatalogAndEndpoint(t *testing.T) { | ||
| 130 | named := NewOpenAIResponsesNamed("token", "https://api.x.ai/v1", "xai").(*renamedClient) | 151 | named := NewOpenAIResponsesNamed("token", "https://api.x.ai/v1", "xai").(*renamedClient) | ||
| 131 | c := named.inner.(*codexClient) | 152 | c := named.inner.(*codexClient) | ||
M packages/provider/reasoning.go +7 -5
| @@ -5,8 +5,8 @@ import "strings" | |||
| 5 | 5 | var reasoningLevelOrder = []string{"", "minimum", "low", "medium", "high", "xhigh", "max"} | |
| 6 | 6 | ||
| 7 | 7 | // AvailableReasoningLevels returns the distinct reasoning levels supported by | |
| 8 | - | // a model. Protocol defaults are adjusted by optional per-model overrides. The | |
| 9 | - | // empty string represents off. | |
| 8 | + | // a model. Optional per-model overrides can remove, remap, or extend protocol | |
| 9 | + | // defaults. The empty string represents off. | |
| 10 | 10 | func AvailableReasoningLevels(model Model) []string { | |
| 11 | 11 | defaults := defaultReasoningLevels(model) | |
| 12 | 12 | if !model.Reasoning || len(model.ReasoningLevelMap) == 0 { | |
| @@ -24,9 +24,6 @@ func AvailableReasoningLevels(model Model) []string { | |||
| 24 | 24 | if mapped, overridden := model.ReasoningLevelMap[level]; overridden { | |
| 25 | 25 | effective = NormalizeReasoning(mapped) | |
| 26 | 26 | } | |
| 27 | - | if effective != "" && !containsReasoningLevel(defaults, effective) { | |
| 28 | - | effective = nearestReasoningLevel(defaults, effective) | |
| 29 | - | } | |
| 30 | 27 | if reasoningLevelRank(effective) > 0 { | |
| 31 | 28 | available[effective] = true | |
| 32 | 29 | } | |
| @@ -90,6 +87,11 @@ func containsReasoningLevel(levels []string, target string) bool { | |||
| 90 | 87 | return false | |
| 91 | 88 | } | |
| 92 | 89 | ||
| 90 | + | func hasReasoningLevelOverride(model Model, level string) bool { | |
| 91 | + | _, ok := model.ReasoningLevelMap[NormalizeReasoning(level)] | |
| 92 | + | return ok | |
| 93 | + | } | |
| 94 | + | ||
| 93 | 95 | // ClampReasoningForModel maps a configured level to the nearest level exposed | |
| 94 | 96 | // for the active model. Ties prefer the higher level. | |
| 95 | 97 | func ClampReasoningForModel(model Model, level string) string { | |
| @@ -5,8 +5,8 @@ import "strings" | |||||
| 5 | var reasoningLevelOrder = []string{"", "minimum", "low", "medium", "high", "xhigh", "max"} | 5 | var reasoningLevelOrder = []string{"", "minimum", "low", "medium", "high", "xhigh", "max"} | ||
| 6 | 6 | ||||
| 7 | // AvailableReasoningLevels returns the distinct reasoning levels supported by | 7 | // AvailableReasoningLevels returns the distinct reasoning levels supported by | ||
| 8 | - | // a model. Protocol defaults are adjusted by optional per-model overrides. The | 8 | + | // a model. Optional per-model overrides can remove, remap, or extend protocol |
| 9 | - | // empty string represents off. | 9 | + | // defaults. The empty string represents off. |
| 10 | func AvailableReasoningLevels(model Model) []string { | 10 | func AvailableReasoningLevels(model Model) []string { | ||
| 11 | defaults := defaultReasoningLevels(model) | 11 | defaults := defaultReasoningLevels(model) | ||
| 12 | if !model.Reasoning || len(model.ReasoningLevelMap) == 0 { | 12 | if !model.Reasoning || len(model.ReasoningLevelMap) == 0 { | ||
| @@ -24,9 +24,6 @@ func AvailableReasoningLevels(model Model) []string { | |||||
| 24 | if mapped, overridden := model.ReasoningLevelMap[level]; overridden { | 24 | if mapped, overridden := model.ReasoningLevelMap[level]; overridden { | ||
| 25 | effective = NormalizeReasoning(mapped) | 25 | effective = NormalizeReasoning(mapped) | ||
| 26 | } | 26 | } | ||
| 27 | - | if effective != "" && !containsReasoningLevel(defaults, effective) { | |||
| 28 | - | effective = nearestReasoningLevel(defaults, effective) | |||
| 29 | - | } | |||
| 30 | if reasoningLevelRank(effective) > 0 { | 27 | if reasoningLevelRank(effective) > 0 { | ||
| 31 | available[effective] = true | 28 | available[effective] = true | ||
| 32 | } | 29 | } | ||
| @@ -90,6 +87,11 @@ func containsReasoningLevel(levels []string, target string) bool { | |||||
| 90 | return false | 87 | return false | ||
| 91 | } | 88 | } | ||
| 92 | 89 | ||||
| 90 | + | func hasReasoningLevelOverride(model Model, level string) bool { | |||
| 91 | + | _, ok := model.ReasoningLevelMap[NormalizeReasoning(level)] | |||
| 92 | + | return ok | |||
| 93 | + | } | |||
| 94 | + | ||||
| 93 | // ClampReasoningForModel maps a configured level to the nearest level exposed | 95 | // ClampReasoningForModel maps a configured level to the nearest level exposed | ||
| 94 | // for the active model. Ties prefer the higher level. | 96 | // for the active model. Ties prefer the higher level. | ||
| 95 | func ClampReasoningForModel(model Model, level string) string { | 97 | func ClampReasoningForModel(model Model, level string) string { | ||
M packages/provider/reasoning_test.go +34 -10
| @@ -29,7 +29,17 @@ func TestAvailableReasoningLevels(t *testing.T) { | |||
| 29 | 29 | Provider: "anthropic", Reasoning: true, | |
| 30 | 30 | ReasoningLevelMap: map[string]string{"minimum": "low", "high": "", "max": "max"}, | |
| 31 | 31 | }, | |
| 32 | - | want: []string{"", "low", "medium", "xhigh"}, | |
| 32 | + | want: []string{"", "low", "medium", "xhigh", "max"}, | |
| 33 | + | }, | |
| 34 | + | { | |
| 35 | + | name: "explicit levels extend generic defaults", | |
| 36 | + | model: Model{ | |
| 37 | + | Provider: "custom", Reasoning: true, | |
| 38 | + | ReasoningLevelMap: map[string]string{ | |
| 39 | + | "minimum": "", "low": "", "medium": "", "high": "high", "xhigh": "xhigh", "max": "max", | |
| 40 | + | }, | |
| 41 | + | }, | |
| 42 | + | want: []string{"", "high", "xhigh", "max"}, | |
| 33 | 43 | }, | |
| 34 | 44 | } | |
| 35 | 45 | for _, tt := range tests { | |
| @@ -59,6 +69,12 @@ func TestClampReasoningForModel(t *testing.T) { | |||
| 59 | 69 | level: "minimum", | |
| 60 | 70 | want: "high", | |
| 61 | 71 | }, | |
| 72 | + | { | |
| 73 | + | name: "explicit max extends generic defaults", | |
| 74 | + | model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"max": "max"}}, | |
| 75 | + | level: "max", | |
| 76 | + | want: "max", | |
| 77 | + | }, | |
| 62 | 78 | } | |
| 63 | 79 | for _, tt := range tests { | |
| 64 | 80 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -71,20 +87,28 @@ func TestClampReasoningForModel(t *testing.T) { | |||
| 71 | 87 | ||
| 72 | 88 | func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) { | |
| 73 | 89 | SetLiveModels([]Model{{ | |
| 74 | - | Provider: "openai", | |
| 90 | + | Provider: "custom", | |
| 75 | 91 | ID: "mapped-reasoning-model", | |
| 76 | 92 | Reasoning: true, | |
| 77 | - | ReasoningLevelMap: map[string]string{"high": "low"}, | |
| 93 | + | ReasoningLevelMap: map[string]string{"high": "low", "max": "max"}, | |
| 78 | 94 | }}) | |
| 79 | 95 | t.Cleanup(func() { SetLiveModels(nil) }) | |
| 80 | 96 | ||
| 81 | - | client := NewOpenAI("test", "").(*openaiClient) | |
| 82 | - | request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: "high"}) | |
| 83 | - | if err != nil { | |
| 84 | - | t.Fatal(err) | |
| 85 | - | } | |
| 86 | - | if request.ReasoningEffort != "low" { | |
| 87 | - | t.Fatalf("reasoning effort = %q, want low", request.ReasoningEffort) | |
| 97 | + | client := NewOpenAICompat("custom", "test", "", "").(*openaiClient) | |
| 98 | + | for _, tt := range []struct { | |
| 99 | + | requested string | |
| 100 | + | want string | |
| 101 | + | }{ | |
| 102 | + | {requested: "high", want: "low"}, | |
| 103 | + | {requested: "max", want: "max"}, | |
| 104 | + | } { | |
| 105 | + | request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: tt.requested}) | |
| 106 | + | if err != nil { | |
| 107 | + | t.Fatal(err) | |
| 108 | + | } | |
| 109 | + | if request.ReasoningEffort != tt.want { | |
| 110 | + | t.Errorf("reasoning effort for %q = %q, want %q", tt.requested, request.ReasoningEffort, tt.want) | |
| 111 | + | } | |
| 88 | 112 | } | |
| 89 | 113 | } | |
| 90 | 114 | ||
| @@ -29,7 +29,17 @@ func TestAvailableReasoningLevels(t *testing.T) { | |||||
| 29 | Provider: "anthropic", Reasoning: true, | 29 | Provider: "anthropic", Reasoning: true, | ||
| 30 | ReasoningLevelMap: map[string]string{"minimum": "low", "high": "", "max": "max"}, | 30 | ReasoningLevelMap: map[string]string{"minimum": "low", "high": "", "max": "max"}, | ||
| 31 | }, | 31 | }, | ||
| 32 | - | want: []string{"", "low", "medium", "xhigh"}, | 32 | + | want: []string{"", "low", "medium", "xhigh", "max"}, |
| 33 | + | }, | |||
| 34 | + | { | |||
| 35 | + | name: "explicit levels extend generic defaults", | |||
| 36 | + | model: Model{ | |||
| 37 | + | Provider: "custom", Reasoning: true, | |||
| 38 | + | ReasoningLevelMap: map[string]string{ | |||
| 39 | + | "minimum": "", "low": "", "medium": "", "high": "high", "xhigh": "xhigh", "max": "max", | |||
| 40 | + | }, | |||
| 41 | + | }, | |||
| 42 | + | want: []string{"", "high", "xhigh", "max"}, | |||
| 33 | }, | 43 | }, | ||
| 34 | } | 44 | } | ||
| 35 | for _, tt := range tests { | 45 | for _, tt := range tests { | ||
| @@ -59,6 +69,12 @@ func TestClampReasoningForModel(t *testing.T) { | |||||
| 59 | level: "minimum", | 69 | level: "minimum", | ||
| 60 | want: "high", | 70 | want: "high", | ||
| 61 | }, | 71 | }, | ||
| 72 | + | { | |||
| 73 | + | name: "explicit max extends generic defaults", | |||
| 74 | + | model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"max": "max"}}, | |||
| 75 | + | level: "max", | |||
| 76 | + | want: "max", | |||
| 77 | + | }, | |||
| 62 | } | 78 | } | ||
| 63 | for _, tt := range tests { | 79 | for _, tt := range tests { | ||
| 64 | t.Run(tt.name, func(t *testing.T) { | 80 | t.Run(tt.name, func(t *testing.T) { | ||
| @@ -71,20 +87,28 @@ func TestClampReasoningForModel(t *testing.T) { | |||||
| 71 | 87 | ||||
| 72 | func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) { | 88 | func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) { | ||
| 73 | SetLiveModels([]Model{{ | 89 | SetLiveModels([]Model{{ | ||
| 74 | - | Provider: "openai", | 90 | + | Provider: "custom", |
| 75 | ID: "mapped-reasoning-model", | 91 | ID: "mapped-reasoning-model", | ||
| 76 | Reasoning: true, | 92 | Reasoning: true, | ||
| 77 | - | ReasoningLevelMap: map[string]string{"high": "low"}, | 93 | + | ReasoningLevelMap: map[string]string{"high": "low", "max": "max"}, |
| 78 | }}) | 94 | }}) | ||
| 79 | t.Cleanup(func() { SetLiveModels(nil) }) | 95 | t.Cleanup(func() { SetLiveModels(nil) }) | ||
| 80 | 96 | ||||
| 81 | - | client := NewOpenAI("test", "").(*openaiClient) | 97 | + | client := NewOpenAICompat("custom", "test", "", "").(*openaiClient) |
| 82 | - | request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: "high"}) | 98 | + | for _, tt := range []struct { |
| 83 | - | if err != nil { | 99 | + | requested string |
| 84 | - | t.Fatal(err) | 100 | + | want string |
| 85 | - | } | 101 | + | }{ |
| 86 | - | if request.ReasoningEffort != "low" { | 102 | + | {requested: "high", want: "low"}, |
| 87 | - | t.Fatalf("reasoning effort = %q, want low", request.ReasoningEffort) | 103 | + | {requested: "max", want: "max"}, |
| 104 | + | } { | |||
| 105 | + | request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: tt.requested}) | |||
| 106 | + | if err != nil { | |||
| 107 | + | t.Fatal(err) | |||
| 108 | + | } | |||
| 109 | + | if request.ReasoningEffort != tt.want { | |||
| 110 | + | t.Errorf("reasoning effort for %q = %q, want %q", tt.requested, request.ReasoningEffort, tt.want) | |||
| 111 | + | } | |||
| 88 | } | 112 | } | ||
| 89 | } | 113 | } | ||
| 90 | 114 | ||||