Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
19 kB · 512 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513package thinking
import ( "strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry" "github.com/tidwall/gjson" "github.com/tidwall/sjson")
// SummaryMode represents whether the client explicitly requested reasoning summaries.type SummaryMode int
const ( SummaryUnspecified SummaryMode = iota SummaryDisabled SummaryEnabled)
// SummaryConfig is the provider-neutral reasoning-summary visibility intent.// Detail preserves protocols that distinguish auto, concise, and detailed summaries.type SummaryConfig struct { Mode SummaryMode Detail string}
// ExtractSummaryConfig reads protocol-specific summary visibility intent.//// OpenAI Chat is the one protocol where effort implies summaries: chat// completions has no summary field of its own, and clients that send// reasoning_effort have always received reasoning summaries here, so treating a// non-none effort as an explicit request preserves that contract. Every other// protocol carries a dedicated summary field, so effort alone means nothing.func ExtractSummaryConfig(body []byte, format string) SummaryConfig { normalized := strings.ToLower(strings.TrimSpace(format)) // Check the format first so unsupported targets skip whole-body validation. if !summaryFormatSupported(normalized) || len(body) == 0 || !gjson.ValidBytes(body) { return SummaryConfig{} }
switch normalized { case "openai": if config, ok := extractOpenAIExplicitSummaryConfig(body); ok { return config } if effort := gjson.GetBytes(body, "reasoning_effort"); effort.Type == gjson.String { value := strings.ToLower(strings.TrimSpace(effort.String())) if value == "" { return SummaryConfig{} } if value == "none" { return SummaryConfig{Mode: SummaryDisabled} } return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"} } case "openai-response", "codex": if config, ok := responsesSummaryConfig(body, "reasoning.summary"); ok { return config } if config, ok := responsesSummaryConfig(body, "reasoning.generate_summary"); ok { return config } case "claude": // Anthropic only accepts display alongside active adaptive/manual thinking. if !claudeThinkingAcceptsDisplay(body) { return SummaryConfig{} } if config, ok := claudeSummaryConfig(body, "thinking.display"); ok { return config } case "gemini": if config, ok := firstSummaryBoolConfig(body, []string{ "generationConfig.thinkingConfig.includeThoughts", "generationConfig.thinkingConfig.include_thoughts", "generation_config.thinking_config.include_thoughts", "generation_config.thinking_config.includeThoughts", }); ok { return config } case "antigravity": if config, ok := firstSummaryBoolConfig(body, []string{ "request.generationConfig.thinkingConfig.includeThoughts", "request.generationConfig.thinkingConfig.include_thoughts", "request.generationConfig.thinking_config.includeThoughts", "request.generationConfig.thinking_config.include_thoughts", }); ok { return config } case "interactions": for _, path := range []string{ "generation_config.thinking_summaries", "generation_config.thinkingSummaries", } { if config, ok := interactionsSummaryConfig(body, path); ok { 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", "generation_config.thinkingConfig.include_thoughts", "generation_config.thinkingConfig.includeThoughts", }); ok { return config } }
return SummaryConfig{}}
// ExtractExplicitSummaryConfig reads only explicit visibility controls from a// provider payload. Unlike ExtractSummaryConfig, OpenAI Chat reasoning_effort// is not treated as a summary proxy. This lets executor post-processing tell// whether a request normalizer retained or removed the translated target field.func ExtractExplicitSummaryConfig(body []byte, format string) SummaryConfig { normalized := strings.ToLower(strings.TrimSpace(format)) if normalized != "openai" { return ExtractSummaryConfig(body, normalized) } if len(body) == 0 || !gjson.ValidBytes(body) { return SummaryConfig{} } config, _ := extractOpenAIExplicitSummaryConfig(body) return config}
// ApplySummaryConfig writes canonical summary intent in the target protocol.func ApplySummaryConfig(body []byte, format string, config SummaryConfig) []byte { return ApplySummaryConfigForModel(body, format, "", config)}
// ApplySummaryConfigForModel writes canonical summary intent in the target// protocol and uses target model capabilities when a valid target request must// activate thinking before it can request summaries.func ApplySummaryConfigForModel(body []byte, format, model string, config SummaryConfig) []byte { return applySummaryConfigForModel(body, format, model, nil, config)}
// 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 }
enabled := config.Mode == SummaryEnabled switch normalized { case "openai": 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: // Opus 5 and Sonnet 5 default to adaptive thinking; Fable/Mythos 5 are always // on. Opus 4.8/4.7/4.6, Sonnet 4.6, and the 4.5 models default to thinking // off. The newest models also default display to omitted. Keeping a missing // thinking block absent therefore preserves both kinds of model default; // absence does not mean every Claude model runs without thinking. Only an // enabled summary may activate a valid target thinking mode so that summarized // text can be returned. A disabled summary only adds omitted to an // already-active target mode. // // Anthropic docs: // https://platform.claude.com/docs/en/build-with-claude/thinking // https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models if enabled && !gjson.GetBytes(body, "thinking.type").Exists() { body = enableClaudeThinkingForSummary(body, model, modelInfo) } if !claudeThinkingAcceptsDisplay(body) { return body } value := "omitted" if enabled { value = "summarized" } body, _ = sjson.SetBytes(body, "thinking.display", value) case "gemini": body, _ = sjson.SetBytes(body, "generationConfig.thinkingConfig.includeThoughts", enabled) for _, path := range []string{ "generationConfig.thinkingConfig.include_thoughts", "generation_config.thinking_config.include_thoughts", "generation_config.thinking_config.includeThoughts", } { body, _ = sjson.DeleteBytes(body, path) } case "antigravity": body, _ = sjson.SetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts", enabled) for _, path := range []string{ "request.generationConfig.thinkingConfig.include_thoughts", "request.generationConfig.thinking_config.include_thoughts", "request.generationConfig.thinking_config.includeThoughts", } { body, _ = sjson.DeleteBytes(body, path) } case "interactions": // Google Interactions only accepts auto or none. OpenAI's concise and // detailed selectors therefore collapse to the supported enabled value. value := "none" if enabled { value = "auto" } body, _ = sjson.SetBytes(body, "generation_config.thinking_summaries", value) body, _ = sjson.DeleteBytes(body, "generation_config.thinkingSummaries") case "openai-response", "codex": if enabled { body, _ = sjson.SetBytes(body, "reasoning.summary", normalizedSummaryDetail(config.Detail)) body, _ = sjson.DeleteBytes(body, "reasoning.generate_summary") break } // Omitting the field is the documented way to disable summaries; an // explicit null is not accepted by every Responses-compatible backend. body, _ = sjson.DeleteBytes(body, "reasoning.summary") body, _ = sjson.DeleteBytes(body, "reasoning.generate_summary") if reasoning := gjson.GetBytes(body, "reasoning"); reasoning.IsObject() && len(reasoning.Map()) == 0 { body, _ = sjson.DeleteBytes(body, "reasoning") } } return body}
// summaryFormatSupported reports whether a protocol carries summary visibility// intent that this package can read or write.func summaryFormatSupported(format string) bool { switch format { case "openai", "openai-response", "codex", "claude", "gemini", "antigravity", "interactions": return true default: return false }}
// claudeThinkingAcceptsDisplay reports whether the body carries an active// thinking block that can hold a display field.func claudeThinkingAcceptsDisplay(body []byte) bool { switch strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) { case "adaptive": return true case "enabled": // This runs before ApplyThinking normalizes the request, so a missing // budget_tokens is an unfinished body rather than inactive thinking. CPA // also accepts -1 as its compatibility representation for auto thinking. budget := gjson.GetBytes(body, "thinking.budget_tokens") if budget.Type != gjson.Number { return true } value := budget.Int() return value == -1 || value > 0 default: return false }}
// applyOpenAIChatSummaryConfig writes only documented Chat visibility controls.//// 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/chatfunc 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) } return body}
func isOpenRouterProvider(provider string) bool { provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "openrouter" { return true } for _, part := range strings.FieldsFunc(provider, func(r rune) bool { return r == '-' || r == '_' || r == '/' || r == '.' || r == ':' }) { if part == "openrouter" { return true } } return false}
func extractOpenAIExplicitSummaryConfig(body []byte) (SummaryConfig, bool) { // Google's documented Chat Completions extension is the authoritative // explicit visibility control when present, ahead of CPA compatibility // aliases and Chat's reasoning_effort fallback. for _, path := range []string{ "extra_body.google.thinking_config.include_thoughts", "extra_body.google.thinking_config.includeThoughts", "extra_body.google.thinkingConfig.include_thoughts", "extra_body.google.thinkingConfig.includeThoughts", "extra_body.extra_body.google.thinking_config.include_thoughts", "extra_body.extra_body.google.thinking_config.includeThoughts", "google.thinking_config.include_thoughts", "google.thinking_config.includeThoughts", "thinking.includeThoughts", "thinking.include_thoughts", "reasoning.includeThoughts", "reasoning.include_thoughts", "generationConfig.thinkingConfig.includeThoughts", "generationConfig.thinkingConfig.include_thoughts", "generation_config.thinking_config.include_thoughts", "generation_config.thinking_config.includeThoughts", } { if config, ok := summaryBoolConfig(body, path); ok { return config, true } }
for _, path := range []string{ "reasoning.summary", "reasoning.generate_summary", } { if config, ok := responsesSummaryConfig(body, path); ok { return config, true } }
// reasoning.exclude is OpenRouter's documented "reason but hide" bit, not an // OpenAI wire field; include_reasoning is its documented legacy alias // (include_reasoning: false is equivalent to reasoning: {exclude: true}). // Only accept actual JSON booleans. if exclude := gjson.GetBytes(body, "reasoning.exclude"); exclude.IsBool() { if exclude.Bool() { return SummaryConfig{Mode: SummaryDisabled}, true } return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true } if include := gjson.GetBytes(body, "include_reasoning"); include.IsBool() { if include.Bool() { return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true } return SummaryConfig{Mode: SummaryDisabled}, true } // OpenRouter's reasoning.enabled turns reasoning on "with no exclusions", so // it also decides visibility when no dedicated bit was sent. if enabled := gjson.GetBytes(body, "reasoning.enabled"); enabled.IsBool() { if enabled.Bool() { return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true } return SummaryConfig{Mode: SummaryDisabled}, true } return SummaryConfig{}, false}
func firstSummaryBoolConfig(body []byte, paths []string) (SummaryConfig, bool) { for _, path := range paths { if config, ok := summaryBoolConfig(body, path); ok { return config, true } } return SummaryConfig{}, false}
func summaryBoolConfig(body []byte, path string) (SummaryConfig, bool) { switch value := gjson.GetBytes(body, path); value.Type { case gjson.True: return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true case gjson.False: return SummaryConfig{Mode: SummaryDisabled}, true default: return SummaryConfig{}, false }}
func responsesSummaryConfig(body []byte, path string) (SummaryConfig, bool) { value := gjson.GetBytes(body, path) if value.Raw == "" { return SummaryConfig{}, false } if value.Type == gjson.Null { return SummaryConfig{Mode: SummaryDisabled}, true } if value.Type != gjson.String { return SummaryConfig{}, false }
raw := strings.ToLower(strings.TrimSpace(value.String())) switch raw { case "auto", "concise", "detailed": return SummaryConfig{Mode: SummaryEnabled, Detail: raw}, true case "none": // Compatibility with clients that expose a none enum; the OpenAI wire // representation disables summaries by omitting the field. return SummaryConfig{Mode: SummaryDisabled}, true default: return SummaryConfig{}, false }}
func claudeSummaryConfig(body []byte, path string) (SummaryConfig, bool) { value := gjson.GetBytes(body, path) if value.Type != gjson.String { return SummaryConfig{}, false } switch strings.ToLower(strings.TrimSpace(value.String())) { case "summarized": return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true case "omitted": return SummaryConfig{Mode: SummaryDisabled}, true default: return SummaryConfig{}, false }}
func interactionsSummaryConfig(body []byte, path string) (SummaryConfig, bool) { value := gjson.GetBytes(body, path) if value.Type != gjson.String { return SummaryConfig{}, false } switch strings.ToLower(strings.TrimSpace(value.String())) { case "auto": return SummaryConfig{Mode: SummaryEnabled, Detail: "auto"}, true case "none": return SummaryConfig{Mode: SummaryDisabled}, true default: return SummaryConfig{}, false }}
// stripInferredClaudeSummaryActivation removes a globally inferred adaptive// mode when the selected API-key model supports only manual extended thinking.// The exact model-aware summary pass can then activate enabled thinking with a// valid budget, or leave thinking absent when max_tokens cannot accommodate it.func stripInferredClaudeSummaryActivation(body []byte, modelInfo *registry.ModelInfo) []byte { if modelInfo == nil || modelInfo.Thinking == nil || len(modelInfo.Thinking.Levels) > 0 || modelInfo.Thinking.Min <= 0 { return body } if !strings.EqualFold(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String()), "adaptive") { return body }
for _, path := range []string{ "thinking.type", "thinking.budget_tokens", "thinking.display", "output_config.effort", } { body, _ = sjson.DeleteBytes(body, path) } for _, path := range []string{"thinking", "output_config"} { if object := gjson.GetBytes(body, path); object.Exists() && object.IsObject() && len(object.Map()) == 0 { body, _ = sjson.DeleteBytes(body, path) } } return body}
func enableClaudeThinkingForSummary(body []byte, model string, resolvedModelInfo *registry.ModelInfo) []byte { modelInfo := resolvedModelInfo if modelInfo == nil { baseModel := ParseSuffix(model).ModelName if baseModel == "" { baseModel = ParseSuffix(gjson.GetBytes(body, "model").String()).ModelName } modelInfo = registry.LookupModelInfo(baseModel, "claude") } if modelInfo == nil || modelInfo.Thinking == nil { return body }
if len(modelInfo.Thinking.Levels) > 0 { body, _ = sjson.SetBytes(body, "thinking.type", "adaptive") body, _ = sjson.DeleteBytes(body, "thinking.budget_tokens") return body }
budget := modelInfo.Thinking.Min if budget <= 0 { return body } if maxTokens := gjson.GetBytes(body, "max_tokens"); maxTokens.Exists() && maxTokens.Int() <= int64(budget) { return body } body, _ = sjson.SetBytes(body, "thinking.type", "enabled") body, _ = sjson.SetBytes(body, "thinking.budget_tokens", budget) return body}
func normalizedSummaryDetail(detail string) string { switch strings.ToLower(strings.TrimSpace(detail)) { case "concise": return "concise" case "detailed": return "detailed" default: return "auto" }}