fix(thinking): honor provider visibility semantics

This commit is contained in:
sususu
2026-07-31 00:05:59 +08:00
parent 5d307c195d
commit 87ceaf83bb
8 changed files with 187 additions and 79 deletions

View File

@@ -225,7 +225,7 @@ func applyThinking(body, sourceBody []byte, model string, fromFormat string, toF
// Unknown models are treated as user-defined so thinking config can still be applied.
// The upstream service is responsible for validating the configuration.
if IsUserDefinedModel(modelInfo) {
return applyUserDefinedModel(body, modelInfo, fromFormat, providerFormat, suffixResult, summaryConfig)
return applyUserDefinedModel(body, modelInfo, fromFormat, providerFormat, providerKey, suffixResult, summaryConfig)
}
if modelInfo.Thinking == nil {
config := extractThinkingConfig(body, providerFormat)
@@ -277,7 +277,7 @@ func applyThinking(body, sourceBody []byte, model string, fromFormat string, toF
"provider": providerFormat,
"model": modelInfo.ID,
}).Debug("thinking: no config found, passthrough |")
return applySummaryConfigForModel(body, providerFormat, baseModel, modelInfo, summaryConfig), nil
return applySummaryConfigForProvider(body, providerFormat, baseModel, providerKey, modelInfo, summaryConfig), nil
}
if modelInfoResolved && config.Mode == ModeLevel && modelInfo != nil && modelInfo.Thinking != nil && shouldMapConfiguredHighIntent(fromFormat, providerFormat, modelInfo) {
config.Level = mapConfiguredHighIntent(config.Level, modelInfo)
@@ -320,7 +320,17 @@ func applyThinking(body, sourceBody []byte, model string, fromFormat string, toF
if err != nil {
return applied, err
}
return applySummaryConfigForModel(applied, providerFormat, baseModel, modelInfo, summaryConfig), nil
// A fully disabled amount takes precedence over visibility. Re-applying a
// summary-only field can recreate an otherwise removed provider config and
// make a default-on model think again.
if thinkingIsFullyDisabled(*validated) {
return applied, nil
}
return applySummaryConfigForProvider(applied, providerFormat, baseModel, providerKey, modelInfo, summaryConfig), nil
}
func thinkingIsFullyDisabled(config ThinkingConfig) bool {
return config.Mode == ModeNone && config.Budget == 0 && config.Level == ""
}
func shouldMapConfiguredHighIntent(fromFormat, toFormat string, modelInfo *registry.ModelInfo) bool {
@@ -409,7 +419,7 @@ func parseSuffixToConfig(rawSuffix, provider, model string) ThinkingConfig {
// applyUserDefinedModel applies thinking configuration for user-defined models
// without ThinkingSupport validation.
func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromFormat, toFormat string, suffixResult SuffixResult, summaryConfig SummaryConfig) ([]byte, error) {
func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromFormat, toFormat, providerKey string, suffixResult SuffixResult, summaryConfig SummaryConfig) ([]byte, error) {
// Get model ID for logging
modelID := ""
if modelInfo != nil {
@@ -450,7 +460,7 @@ func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromForma
"model": modelID,
"provider": toFormat,
}).Debug("thinking: user-defined model, passthrough (no config) |")
return applySummaryConfigForModel(body, toFormat, modelID, modelInfo, summaryConfig), nil
return applySummaryConfigForProvider(body, toFormat, modelID, providerKey, modelInfo, summaryConfig), nil
}
applier := GetProviderApplier(toFormat)
@@ -474,7 +484,10 @@ func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromForma
if err != nil {
return applied, err
}
return applySummaryConfigForModel(applied, toFormat, modelID, modelInfo, summaryConfig), nil
if thinkingIsFullyDisabled(config) {
return applied, nil
}
return applySummaryConfigForProvider(applied, toFormat, modelID, providerKey, modelInfo, summaryConfig), nil
}
func normalizeUserDefinedConfig(config ThinkingConfig, fromFormat, toFormat string) ThinkingConfig {

View File

@@ -133,6 +133,61 @@ func TestApplyThinkingWithModelInfoDoesNotActivateClaudeForDisabledSummary(t *te
}
}
func TestApplyThinkingWithModelInfoSummaryOnlyDoesNotInventOpenAIEffort(t *testing.T) {
modelInfo := &registry.ModelInfo{
ID: "private-openai",
Type: "openai",
Thinking: &registry.ThinkingSupport{Levels: []string{"high", "max"}},
}
out, err := thinking.ApplyThinkingWithModelInfo(
[]byte(`{"model":"private-openai","messages":[{"role":"user","content":"hi"}]}`),
[]byte(`{"model":"private-openai","reasoning":{"summary":"auto"},"input":"hi"}`),
"private-openai", "openai-response", "openai", "openai", modelInfo,
)
if err != nil {
t.Fatalf("ApplyThinkingWithModelInfo() error = %v; body=%s", err, out)
}
if gjson.GetBytes(out, "reasoning_effort").Exists() {
t.Fatalf("summary-only request invented reasoning_effort: %s", out)
}
}
func TestApplyThinkingWithSummaryKeepsOpenAIChatSuffixNone(t *testing.T) {
out, err := thinking.ApplyThinkingWithSummary(
[]byte(`{"model":"private-openai","messages":[{"role":"user","content":"hi"}]}`),
"private-openai(none)", "openai-response", "openai", "openai",
thinking.SummaryConfig{Mode: thinking.SummaryEnabled, Detail: "auto"},
)
if err != nil {
t.Fatalf("ApplyThinkingWithSummary() error = %v; body=%s", err, out)
}
if got := gjson.GetBytes(out, "reasoning_effort").String(); got != "none" {
t.Fatalf("reasoning_effort = %q, want none; body=%s", got, out)
}
}
func TestApplyThinkingWithModelInfoUsesOpenRouterVisibility(t *testing.T) {
modelInfo := &registry.ModelInfo{
ID: "openrouter-model",
Type: "openai-compatibility",
Thinking: &registry.ThinkingSupport{Levels: []string{"high", "max"}},
}
out, err := thinking.ApplyThinkingWithModelInfo(
[]byte(`{"model":"openrouter-model","messages":[{"role":"user","content":"hi"}]}`),
[]byte(`{"model":"openrouter-model","reasoning":{"summary":"auto"},"input":"hi"}`),
"openrouter-model", "openai-response", "openai", "openrouter", modelInfo,
)
if err != nil {
t.Fatalf("ApplyThinkingWithModelInfo() error = %v; body=%s", err, out)
}
if exclude := gjson.GetBytes(out, "reasoning.exclude"); !exclude.Exists() || exclude.Bool() {
t.Fatalf("OpenRouter summary visibility not enabled: %s", out)
}
if gjson.GetBytes(out, "reasoning_effort").Exists() {
t.Fatalf("OpenRouter summary visibility invented reasoning_effort: %s", out)
}
}
func TestApplyThinkingWithModelInfoUsesOriginalResponsesEffort(t *testing.T) {
modelInfo := &registry.ModelInfo{
ID: "claude-upstream",

View File

@@ -104,8 +104,11 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
if config.Mode == thinking.ModeNone {
if config.Budget == 0 && config.Level == "" {
// With the amount fully disabled, visibility is irrelevant. Restoring
// includeThoughts alone would recreate thinkingConfig and let a
// default-on model think again.
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig")
return applyAntigravityIncludeThoughts(result, body), nil
return result, nil
}
if config.Level != "" {
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", string(config.Level))

View File

@@ -128,8 +128,11 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
if config.Mode == thinking.ModeNone {
if config.Budget == 0 && config.Level == "" {
// With the amount fully disabled, visibility is irrelevant. Restoring
// includeThoughts alone would recreate thinkingConfig and let a
// default-on model think again.
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig")
return applyGeminiIncludeThoughts(result, body), nil
return result, nil
}
if config.Level != "" {
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingLevel", string(config.Level))

View File

@@ -95,6 +95,12 @@ func ExtractSummaryConfig(body []byte, format string) SummaryConfig {
return config
}
}
// Existing Interactions translators accept the OpenAI-style top-level
// compatibility object. Keep the official generation_config selector
// authoritative when both are present.
if config, ok := interactionsSummaryConfig(body, "reasoning.summary"); ok {
return config
}
if config, ok := firstSummaryBoolConfig(body, []string{
"generation_config.thinking_config.include_thoughts",
"generation_config.thinking_config.includeThoughts",
@@ -123,6 +129,12 @@ func ApplySummaryConfigForModel(body []byte, format, model string, config Summar
// applySummaryConfigForModel uses the resolved model definition when execution
// selected a configured API-key model whose capability is not globally visible.
func applySummaryConfigForModel(body []byte, format, model string, modelInfo *registry.ModelInfo, config SummaryConfig) []byte {
return applySummaryConfigForProvider(body, format, model, "", modelInfo, config)
}
// applySummaryConfigForProvider uses the execution provider identity for Chat
// dialects whose visibility controls are not part of the OpenAI wire format.
func applySummaryConfigForProvider(body []byte, format, model, provider string, modelInfo *registry.ModelInfo, config SummaryConfig) []byte {
normalized := strings.ToLower(strings.TrimSpace(format))
if config.Mode == SummaryUnspecified || !summaryFormatSupported(normalized) || len(body) == 0 || !gjson.ValidBytes(body) {
return body
@@ -131,7 +143,7 @@ func applySummaryConfigForModel(body []byte, format, model string, modelInfo *re
enabled := config.Mode == SummaryEnabled
switch normalized {
case "openai":
body = applyOpenAIChatSummaryConfig(body, model, enabled)
body = applyOpenAIChatSummaryConfig(body, provider, enabled)
case "claude":
// Anthropic documents display as invalid with thinking.type=disabled and
// requires it alongside adaptive or enabled thinking. Model defaults differ:
@@ -234,65 +246,45 @@ func claudeThinkingAcceptsDisplay(body []byte) bool {
}
}
// applyOpenAIChatSummaryConfig writes summary visibility intent for the Chat
// Completions protocol.
// applyOpenAIChatSummaryConfig writes only documented Chat visibility controls.
//
// Four dialects share this protocol and only OpenAI's is authoritative. OpenAI
// documents no reasoning-visibility field at all (Chat Completions never returns
// reasoning text) and rejects unknown body parameters, so reasoning_effort is the
// only field that is always safe to write here. OpenRouter's documented
// "reason but hide" bits (reasoning.exclude and its legacy include_reasoning
// alias) are updated only when the body already carries them, which is exactly
// when the upstream is known to understand them.
func applyOpenAIChatSummaryConfig(body []byte, model string, enabled bool) []byte {
if gjson.GetBytes(body, "reasoning").IsObject() {
// OpenAI Chat Completions exposes reasoning_effort but no reasoning summary or
// visibility parameter. DeepSeek and Kimi Chat return reasoning_content while
// thinking is active, but likewise document no independent hide/show switch.
// Summary intent must therefore never invent or overwrite thinking effort for
// those dialects. OpenRouter is the exception: reasoning.exclude is its
// documented "reason but hide" control, and include_reasoning is its deprecated
// inverse alias. Unknown OpenAI-compatible providers are handled conservatively
// by updating those fields only when the payload already carries them.
//
// Docs:
// https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create
// https://openrouter.ai/docs/guides/best-practices/reasoning-tokens
// https://api-docs.deepseek.com/guides/thinking_mode
// https://platform.kimi.ai/docs/api/chat
func applyOpenAIChatSummaryConfig(body []byte, provider string, enabled bool) []byte {
if isOpenRouterProvider(provider) || gjson.GetBytes(body, "reasoning.exclude").IsBool() {
body, _ = sjson.SetBytes(body, "reasoning.exclude", !enabled)
}
if gjson.GetBytes(body, "include_reasoning").IsBool() {
body, _ = sjson.SetBytes(body, "include_reasoning", enabled)
}
if !enabled {
// Chat has no portable way to keep reasoning while hiding its summary.
// reasoning_effort:"none" would disable reasoning instead of hiding it,
// and Google documents that it is not even honored on Gemini 2.5 Pro or
// 3 models, so leave the effort the client asked for untouched.
return body
}
effort := gjson.GetBytes(body, "reasoning_effort")
if effort.Type != gjson.String || strings.TrimSpace(effort.String()) == "" || strings.EqualFold(strings.TrimSpace(effort.String()), "none") {
body, _ = sjson.SetBytes(body, "reasoning_effort", openAIChatSummaryEffort(body, model))
}
return body
}
// openAIChatSummaryEffort picks an active reasoning effort that the target model
// documents. Chat exposes reasoning only while an effort is active, so a summary
// request has to select one when the client left it unset.
func openAIChatSummaryEffort(body []byte, model string) string {
baseModel := ParseSuffix(model).ModelName
if baseModel == "" {
baseModel = ParseSuffix(gjson.GetBytes(body, "model").String()).ModelName
func isOpenRouterProvider(provider string) bool {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "openrouter" {
return true
}
modelInfo := registry.LookupModelInfo(baseModel, "openai")
if modelInfo == nil || modelInfo.Thinking == nil || len(modelInfo.Thinking.Levels) == 0 {
return "medium"
}
levels := make([]string, 0, len(modelInfo.Thinking.Levels))
for _, level := range modelInfo.Thinking.Levels {
normalized := strings.ToLower(strings.TrimSpace(level))
if normalized == "" || normalized == "none" {
continue
for _, part := range strings.FieldsFunc(provider, func(r rune) bool {
return r == '-' || r == '_' || r == '/' || r == '.' || r == ':'
}) {
if part == "openrouter" {
return true
}
if normalized == "medium" {
return "medium"
}
levels = append(levels, normalized)
}
if len(levels) == 0 {
return "medium"
}
return levels[len(levels)/2]
return false
}
func extractOpenAIExplicitSummaryConfig(body []byte) (SummaryConfig, bool) {

View File

@@ -55,6 +55,9 @@ func TestExtractSummaryConfig(t *testing.T) {
{name: "interactions nested camel include true", format: "interactions", body: `{"generation_config":{"thinking_config":{"includeThoughts":true}}}`, wantMode: SummaryEnabled, wantDetail: "auto"},
{name: "interactions camel config snake include true", format: "interactions", body: `{"generation_config":{"thinkingConfig":{"include_thoughts":true}}}`, wantMode: SummaryEnabled, wantDetail: "auto"},
{name: "interactions camel config camel include false", format: "interactions", body: `{"generation_config":{"thinkingConfig":{"includeThoughts":false}}}`, wantMode: SummaryDisabled},
{name: "interactions enum wins over compatibility reasoning", format: "interactions", body: `{"generation_config":{"thinking_summaries":"none"},"reasoning":{"summary":"auto"}}`, wantMode: SummaryDisabled},
{name: "interactions compatibility reasoning auto", format: "interactions", body: `{"reasoning":{"summary":"auto"}}`, wantMode: SummaryEnabled, wantDetail: "auto"},
{name: "interactions compatibility reasoning none", format: "interactions", body: `{"reasoning":{"summary":"none"}}`, wantMode: SummaryDisabled},
{name: "interactions enum wins over include alias", format: "interactions", body: `{"generation_config":{"thinking_summaries":"none","thinking_config":{"include_thoughts":true}}}`, wantMode: SummaryDisabled},
{name: "interactions string include alias is invalid", format: "interactions", body: `{"generation_config":{"thinking_config":{"include_thoughts":"false"}}}`, wantMode: SummaryUnspecified},
{name: "interactions detailed is invalid", format: "interactions", body: `{"generation_config":{"thinking_summaries":"detailed"}}`, wantMode: SummaryUnspecified},
@@ -81,8 +84,9 @@ func TestApplySummaryConfig(t *testing.T) {
path string
want string
}{
{name: "chat enabled creates compatibility effort", format: "openai", config: SummaryConfig{Mode: SummaryEnabled}, path: "reasoning_effort", want: "medium"},
{name: "chat enabled invents no effort", format: "openai", config: SummaryConfig{Mode: SummaryEnabled}, path: "reasoning_effort", want: ""},
{name: "chat enabled preserves active effort", format: "openai", body: `{"reasoning_effort":"high"}`, config: SummaryConfig{Mode: SummaryEnabled}, path: "reasoning_effort", want: "high"},
{name: "chat enabled preserves disabled effort", format: "openai", body: `{"reasoning_effort":"none"}`, config: SummaryConfig{Mode: SummaryEnabled}, path: "reasoning_effort", want: "none"},
// Chat cannot express "reason but hide", so disabling must not fall back to
// reasoning_effort:"none", which would disable reasoning altogether.
{name: "chat disabled preserves requested effort", format: "openai", body: `{"reasoning_effort":"high"}`, config: SummaryConfig{Mode: SummaryDisabled}, path: "reasoning_effort", want: "high"},
@@ -114,6 +118,46 @@ func TestApplySummaryConfig(t *testing.T) {
}
}
func TestApplySummaryConfig_OpenAIChatProviderDialects(t *testing.T) {
tests := []struct {
name string
provider string
body string
mode SummaryMode
wantExclude string
wantExisting bool
wantEffort string
}{
{name: "OpenAI does not invent visibility", provider: "openai", body: `{}`, mode: SummaryEnabled},
{name: "OpenRouter enables visibility", provider: "openrouter", body: `{}`, mode: SummaryEnabled, wantExclude: "false", wantExisting: true},
{name: "OpenRouter disables visibility", provider: "prod-openrouter", body: `{}`, mode: SummaryDisabled, wantExclude: "true", wantExisting: true},
{name: "DeepSeek preserves documented effort", provider: "deepseek", body: `{"reasoning_effort":"high"}`, mode: SummaryDisabled, wantEffort: "high"},
{name: "Kimi preserves documented K3 effort", provider: "kimi", body: `{"reasoning_effort":"max"}`, mode: SummaryEnabled, wantEffort: "max"},
{name: "Moonshot does not invent visibility", provider: "moonshot", body: `{"thinking":{"type":"enabled"}}`, mode: SummaryEnabled},
{name: "generic provider updates existing OpenRouter field", provider: "openai-compatibility", body: `{"reasoning":{"exclude":false}}`, mode: SummaryDisabled, wantExclude: "true", wantExisting: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
out := applySummaryConfigForProvider([]byte(test.body), "openai", "model", test.provider, nil, SummaryConfig{Mode: test.mode})
exclude := gjson.GetBytes(out, "reasoning.exclude")
if exclude.Exists() != test.wantExisting {
t.Fatalf("reasoning.exclude exists = %v, want %v; body=%s", exclude.Exists(), test.wantExisting, out)
}
if test.wantExisting && exclude.String() != test.wantExclude {
t.Fatalf("reasoning.exclude = %q, want %q; body=%s", exclude.String(), test.wantExclude, out)
}
effort := gjson.GetBytes(out, "reasoning_effort")
if test.wantEffort == "" {
if effort.Exists() {
t.Fatalf("summary visibility invented reasoning_effort: %s", out)
}
} else if effort.String() != test.wantEffort {
t.Fatalf("reasoning_effort = %q, want %q; body=%s", effort.String(), test.wantEffort, out)
}
})
}
}
func TestApplySummaryConfigNormalizesTargetAliases(t *testing.T) {
tests := []struct {
format string

View File

@@ -40,7 +40,7 @@ func TestSummaryIntentTranslation(t *testing.T) {
{name: "Claude summarized enables Codex summary", from: sdktranslator.FormatClaude, to: sdktranslator.FormatCodex, body: `{"model":"gpt-5.4","max_tokens":1024,"thinking":{"type":"adaptive","display":"summarized"},"output_config":{"effort":"high"},"messages":[{"role":"user","content":"hi"}]}`, path: "reasoning.summary", want: "auto", wantExists: true},
{name: "Interactions none omits Codex summary", from: sdktranslator.FormatInteractions, to: sdktranslator.FormatCodex, body: `{"model":"gpt-5.4","generation_config":{"thinking_level":"high","thinking_summaries":"none"},"input":"hi"}`, path: "reasoning.summary"},
{name: "Chat effort enables Codex summary", from: sdktranslator.FormatOpenAI, to: sdktranslator.FormatCodex, body: `{"model":"gpt-5.4","reasoning_effort":"high","messages":[{"role":"user","content":"hi"}]}`, path: "reasoning.summary", want: "auto", wantExists: true},
{name: "Responses summary only enables Chat compatibility effort", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatOpenAI, body: `{"model":"gpt-5.4","reasoning":{"summary":"auto"},"input":"hi"}`, path: "reasoning_effort", want: "medium", wantExists: true},
{name: "Responses summary only invents no Chat effort", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatOpenAI, body: `{"model":"gpt-5.4","reasoning":{"summary":"auto"},"input":"hi"}`, path: "reasoning_effort"},
// Chat has no field for "reason but hide": OpenAI documents none and rejects
// unknown parameters, so a disabled summary must leave the requested effort
// alone instead of turning reasoning off upstream.
@@ -127,10 +127,12 @@ func TestSummaryIntentFinalPipeline(t *testing.T) {
{name: "Responses default keeps Claude display default", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","input":"hi"}`, path: "thinking.display"},
{name: "Chat summary alias only activates valid Claude thinking", from: sdktranslator.FormatOpenAI, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"exclude":false},"messages":[{"role":"user","content":"hi"}]}`, path: "thinking.display", want: "summarized", wantExists: true},
{name: "Interactions summary only activates valid Claude thinking", from: sdktranslator.FormatInteractions, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","generation_config":{"thinking_summaries":"auto"},"input":"hi"}`, path: "thinking.display", want: "summarized", wantExists: true},
{name: "Interactions compatibility summary activates valid Claude thinking", from: sdktranslator.FormatInteractions, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"summary":"auto"},"input":"hi"}`, path: "thinking.display", want: "summarized", wantExists: true},
{name: "Claude suffix none removes otherwise enabled display", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model(none)", body: `{"model":"claude-sonnet-4-6-model(none)","reasoning":{"summary":"auto"},"input":"hi"}`, path: "thinking.display"},
{name: "Claude suffix preserves explicit disabled summary", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model(high)", body: `{"model":"claude-sonnet-4-6-model(high)","reasoning":{"summary":null},"input":"hi"}`, path: "thinking.display", want: "omitted", wantExists: true},
{name: "Responses effort alone stays omitted on Antigravity", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatAntigravity, model: "antigravity-budget-model", body: `{"model":"antigravity-budget-model","reasoning":{"effort":"medium"},"input":"hi"}`, path: "request.generationConfig.thinkingConfig.includeThoughts"},
{name: "Responses summary reaches Antigravity", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatAntigravity, model: "antigravity-budget-model", body: `{"model":"antigravity-budget-model","reasoning":{"effort":"medium","summary":"auto"},"input":"hi"}`, path: "request.generationConfig.thinkingConfig.includeThoughts", want: "true", wantExists: true},
{name: "Responses null summary alone hides default Gemini thoughts", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatGemini, model: "gemini-mixed-model", body: `{"model":"gemini-mixed-model","reasoning":{"summary":null},"input":"hi"}`, path: "generationConfig.thinkingConfig.includeThoughts", want: "false", wantExists: true},
{name: "Google Chat extension false survives Gemini applier", from: sdktranslator.FormatOpenAI, to: sdktranslator.FormatGemini, model: "gemini-mixed-model", body: `{"model":"gemini-mixed-model","reasoning_effort":"high","extra_body":{"google":{"thinking_config":{"include_thoughts":false}}},"messages":[{"role":"user","content":"hi"}]}`, path: "generationConfig.thinkingConfig.includeThoughts", want: "false", wantExists: true},
// Captured from isolated Claude Code 2.1.220 with
// alwaysThinkingEnabled:true. Sonnet uses adaptive thinking, while Haiku

View File

@@ -1456,30 +1456,26 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
includeThoughts: "false",
expectErr: false,
},
// Case 31A: reasoning_effort=none with zero allowed removes the amount but
// preserves Chat's explicit disabled summary intent.
// Case 31A: reasoning_effort=none with zero allowed removes the entire
// thinking config. includeThoughts alone would restore the model default.
{
name: "31A",
from: "openai",
to: "gemini",
model: "gemini-toggle-mixed-model",
inputJSON: `{"model":"gemini-toggle-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "generationConfig.thinkingConfig.includeThoughts",
expectValue: "false",
includeThoughts: "false",
expectErr: false,
name: "31A",
from: "openai",
to: "gemini",
model: "gemini-toggle-mixed-model",
inputJSON: `{"model":"gemini-toggle-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "",
expectErr: false,
},
// Case 31B: the same explicit disabled intent survives Antigravity.
// Case 31B: Antigravity keeps the same fully disabled representation.
{
name: "31B",
from: "openai",
to: "antigravity",
model: "gemini-toggle-mixed-model",
inputJSON: `{"model":"gemini-toggle-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "request.generationConfig.thinkingConfig.includeThoughts",
expectValue: "false",
includeThoughts: "false",
expectErr: false,
name: "31B",
from: "openai",
to: "antigravity",
model: "gemini-toggle-mixed-model",
inputJSON: `{"model":"gemini-toggle-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "",
expectErr: false,
},
// Case 31C: reasoning.effort=none with zero allowed → delete thinkingConfig
{