Skip to content

Commit 3c38802

Browse files
committed
fix(thinking): clamp unsupported levels by model info
1 parent 2884a67 commit 3c38802

2 files changed

Lines changed: 57 additions & 48 deletions

File tree

internal/thinking/validate.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,6 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
5353
return &config, nil
5454
}
5555

56-
// allowClampUnsupported determines whether to clamp unsupported levels instead of returning an error.
57-
// This applies when crossing provider families (e.g., openai→gemini, claude→gemini) and the target
58-
// model supports discrete levels. Same-family conversions require strict validation.
59-
toCapability := detectModelCapability(modelInfo)
60-
toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid
61-
allowClampUnsupported := toHasLevelSupport && !isSameProviderFamily(fromFormat, toFormat)
62-
6356
// strictBudget determines whether to enforce strict budget range validation.
6457
// This applies when: (1) config comes from request body (not suffix), (2) source format is known,
6558
// and (3) source and target are in the same provider family. Cross-family or suffix-based configs
@@ -115,12 +108,10 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
115108

116109
if len(support.Levels) > 0 && config.Mode == ModeLevel {
117110
if !isLevelSupported(string(config.Level), support.Levels) {
118-
if allowClampUnsupported {
119-
config.Level = clampLevel(config.Level, modelInfo, toFormat)
120-
}
111+
// Clamp against the target model's advertised levels, regardless of translator family.
112+
config.Level = clampLevel(config.Level, modelInfo, toFormat)
121113
if !isLevelSupported(string(config.Level), support.Levels) {
122-
// User explicitly specified an unsupported level - return error
123-
// (budget-derived levels may be clamped based on source format)
114+
// User explicitly specified an unsupported level - return error.
124115
validLevels := normalizeLevels(support.Levels)
125116
message := fmt.Sprintf("level %q not supported, valid levels: %s", strings.ToLower(string(config.Level)), strings.Join(validLevels, ", "))
126117
return nil, NewThinkingError(ErrLevelNotSupported, message)

test/thinking_conversion_test.go

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,16 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
7373
expectValue: "medium",
7474
expectErr: false,
7575
},
76-
// Case 3: Specified xhigh → out of range error
76+
// Case 3: Specified xhigh → clamped to high
7777
{
7878
name: "3",
7979
from: "openai",
8080
to: "codex",
8181
model: "level-model(xhigh)",
8282
inputJSON: `{"model":"level-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`,
83-
expectField: "",
84-
expectErr: true,
83+
expectField: "reasoning.effort",
84+
expectValue: "high",
85+
expectErr: false,
8586
},
8687
// Case 4: Level none → clamped to minimal (ZeroAllowed=false)
8788
{
@@ -963,15 +964,16 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
963964
expectValue: "high",
964965
expectErr: false,
965966
},
966-
// Case 81: OpenAI to OpenAI, level xhigh → out of range error
967+
// Case 81: OpenAI to OpenAI, level xhigh → clamped to high
967968
{
968969
name: "81",
969970
from: "openai",
970971
to: "openai",
971972
model: "level-model(xhigh)",
972973
inputJSON: `{"model":"level-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`,
973-
expectField: "",
974-
expectErr: true,
974+
expectField: "reasoning_effort",
975+
expectValue: "high",
976+
expectErr: false,
975977
},
976978
// Case 82: OpenAI-Response to Codex, level high → passthrough reasoning.effort
977979
{
@@ -984,15 +986,16 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
984986
expectValue: "high",
985987
expectErr: false,
986988
},
987-
// Case 83: OpenAI-Response to Codex, level xhigh → out of range error
989+
// Case 83: OpenAI-Response to Codex, level xhigh → clamped to high
988990
{
989991
name: "83",
990992
from: "openai-response",
991993
to: "codex",
992994
model: "level-model(xhigh)",
993995
inputJSON: `{"model":"level-model(xhigh)","input":[{"role":"user","content":"hi"}]}`,
994-
expectField: "",
995-
expectErr: true,
996+
expectField: "reasoning.effort",
997+
expectValue: "high",
998+
expectErr: false,
996999
},
9971000
// Case 84: Gemini to Gemini, budget 8192 → passthrough thinkingBudget
9981001
{
@@ -1179,15 +1182,16 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
11791182
expectValue: "medium",
11801183
expectErr: false,
11811184
},
1182-
// Case 3: reasoning_effort=xhigh → out of range error
1185+
// Case 3: reasoning_effort=xhigh → clamped to high
11831186
{
11841187
name: "3",
11851188
from: "openai",
11861189
to: "codex",
11871190
model: "level-model",
11881191
inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"xhigh"}`,
1189-
expectField: "",
1190-
expectErr: true,
1192+
expectField: "reasoning.effort",
1193+
expectValue: "high",
1194+
expectErr: false,
11911195
},
11921196
// Case 4: reasoning_effort=none → clamped to minimal
11931197
{
@@ -2069,15 +2073,16 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
20692073
expectValue: "high",
20702074
expectErr: false,
20712075
},
2072-
// Case 81: OpenAI to OpenAI, reasoning_effort=xhigh → out of range error
2076+
// Case 81: OpenAI to OpenAI, reasoning_effort=xhigh → clamped to high
20732077
{
20742078
name: "81",
20752079
from: "openai",
20762080
to: "openai",
20772081
model: "level-model",
20782082
inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"xhigh"}`,
2079-
expectField: "",
2080-
expectErr: true,
2083+
expectField: "reasoning_effort",
2084+
expectValue: "high",
2085+
expectErr: false,
20812086
},
20822087
// Case 82: OpenAI-Response to Codex, reasoning.effort=high → passthrough
20832088
{
@@ -2090,15 +2095,16 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
20902095
expectValue: "high",
20912096
expectErr: false,
20922097
},
2093-
// Case 83: OpenAI-Response to Codex, reasoning.effort=xhigh → out of range error
2098+
// Case 83: OpenAI-Response to Codex, reasoning.effort=xhigh → clamped to high
20942099
{
20952100
name: "83",
20962101
from: "openai-response",
20972102
to: "codex",
20982103
model: "level-model",
20992104
inputJSON: `{"model":"level-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"xhigh"}}`,
2100-
expectField: "",
2101-
expectErr: true,
2105+
expectField: "reasoning.effort",
2106+
expectValue: "high",
2107+
expectErr: false,
21022108
},
21032109
// Case 84: Gemini to Gemini, thinkingBudget=8192 → passthrough
21042110
{
@@ -2697,12 +2703,16 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
26972703
expectErr: false,
26982704
},
26992705
{
2700-
name: "C24",
2701-
from: "claude",
2702-
to: "claude",
2703-
model: "claude-opus-4-6-model",
2704-
inputJSON: `{"model":"claude-opus-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
2705-
expectErr: true,
2706+
name: "C24",
2707+
from: "claude",
2708+
to: "claude",
2709+
model: "claude-opus-4-6-model",
2710+
inputJSON: `{"model":"claude-opus-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
2711+
expectField: "thinking.type",
2712+
expectValue: "adaptive",
2713+
expectField2: "output_config.effort",
2714+
expectValue2: "max",
2715+
expectErr: false,
27062716
},
27072717
{
27082718
name: "C25",
@@ -2717,20 +2727,28 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
27172727
expectErr: false,
27182728
},
27192729
{
2720-
name: "C26",
2721-
from: "claude",
2722-
to: "claude",
2723-
model: "claude-sonnet-4-6-model",
2724-
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`,
2725-
expectErr: true,
2730+
name: "C26",
2731+
from: "claude",
2732+
to: "claude",
2733+
model: "claude-sonnet-4-6-model",
2734+
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`,
2735+
expectField: "thinking.type",
2736+
expectValue: "adaptive",
2737+
expectField2: "output_config.effort",
2738+
expectValue2: "high",
2739+
expectErr: false,
27262740
},
27272741
{
2728-
name: "C27",
2729-
from: "claude",
2730-
to: "claude",
2731-
model: "claude-sonnet-4-6-model",
2732-
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
2733-
expectErr: true,
2742+
name: "C27",
2743+
from: "claude",
2744+
to: "claude",
2745+
model: "claude-sonnet-4-6-model",
2746+
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
2747+
expectField: "thinking.type",
2748+
expectValue: "adaptive",
2749+
expectField2: "output_config.effort",
2750+
expectValue2: "high",
2751+
expectErr: false,
27342752
},
27352753
}
27362754

0 commit comments

Comments
 (0)