mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
fix(thinking): honor provider visibility semantics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -133,6 +133,61 @@ func TestApplyThinkingWithModelInfoDoesNotActivateClaudeForDisabledSummary(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyThinkingWithModelInfoSummaryOnlyDoesNotInventOpenAIEffort(t *testing.T) {
|
||||
modelInfo := ®istry.ModelInfo{
|
||||
ID: "private-openai",
|
||||
Type: "openai",
|
||||
Thinking: ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "openrouter-model",
|
||||
Type: "openai-compatibility",
|
||||
Thinking: ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "claude-upstream",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user