diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json index 1934f8d0..10784301 100644 --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -2092,13 +2092,9 @@ "context_length": 131072, "max_completion_tokens": 32768, "thinking": { - "min": 1024, - "max": 32000, "zero_allowed": true, - "dynamic_allowed": true, "levels": [ "low", - "medium", "high" ] } @@ -2114,13 +2110,9 @@ "context_length": 262144, "max_completion_tokens": 32768, "thinking": { - "min": 1024, - "max": 32000, "zero_allowed": true, - "dynamic_allowed": true, "levels": [ "low", - "medium", "high" ] } @@ -2136,13 +2128,9 @@ "context_length": 262144, "max_completion_tokens": 65536, "thinking": { - "min": 1024, - "max": 32000, "zero_allowed": true, - "dynamic_allowed": true, "levels": [ "low", - "medium", "high" ] } @@ -2158,13 +2146,9 @@ "context_length": 262144, "max_completion_tokens": 65536, "thinking": { - "min": 1024, - "max": 32000, "zero_allowed": false, - "dynamic_allowed": true, "levels": [ "low", - "medium", "high" ] } @@ -2180,16 +2164,29 @@ "context_length": 262144, "max_completion_tokens": 65536, "thinking": { - "min": 1024, - "max": 32000, "zero_allowed": false, - "dynamic_allowed": true, "levels": [ "low", - "medium", "high" ] } + }, + { + "id": "kimi-k3", + "object": "model", + "created": 1784073600, + "owned_by": "moonshot", + "type": "kimi", + "display_name": "Kimi K3", + "description": "Kimi K3 - Moonshot AI's next-generation flagship model (~2.5T MoE) with multimodal input and 1M context", + "context_length": 1048576, + "max_completion_tokens": 65536, + "thinking": { + "zero_allowed": false, + "levels": [ + "max" + ] + } } ], "antigravity": [ diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index 7194988c..8a6f873e 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -421,8 +421,7 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig { case "codex", "xai": return extractCodexConfig(body) case "kimi": - // Kimi uses OpenAI-compatible reasoning_effort format - return extractOpenAIConfig(body) + return extractKimiConfig(body) default: return ThinkingConfig{} } @@ -680,6 +679,50 @@ func extractOpenAIConfig(body []byte) ThinkingConfig { return ThinkingConfig{} } +// extractKimiConfig extracts Kimi's native thinking object while retaining +// reasoning_effort as a legacy input fallback. +// +// Native fields take precedence over reasoning_effort. In particular, +// thinking.type="enabled" without an explicit effort means "use the upstream +// default" and therefore returns an empty config so ApplyThinking preserves the +// request unchanged instead of interpreting it as CPA's ModeAuto. +func extractKimiConfig(body []byte) ThinkingConfig { + thinkingType := gjson.GetBytes(body, "thinking.type") + if thinkingType.Exists() { + switch strings.ToLower(strings.TrimSpace(thinkingType.String())) { + case "disabled": + return ThinkingConfig{Mode: ModeNone, Budget: 0} + case "enabled": + if !gjson.GetBytes(body, "thinking.effort").Exists() { + return ThinkingConfig{} + } + } + } + + if effort := gjson.GetBytes(body, "thinking.effort"); effort.Exists() { + value := strings.ToLower(strings.TrimSpace(effort.String())) + switch value { + case "": + return ThinkingConfig{} + case "none": + return ThinkingConfig{Mode: ModeNone, Budget: 0} + case "auto": + return ThinkingConfig{Mode: ModeAuto, Budget: -1} + default: + return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} + } + } + + // An explicit native thinking object without an effort should be left for + // the Kimi upstream to interpret and must not be overridden by the legacy + // field. + if thinkingType.Exists() { + return ThinkingConfig{} + } + + return extractOpenAIConfig(body) +} + // extractCodexConfig extracts thinking configuration from Codex format request body. // // Codex API format (OpenAI Responses API): diff --git a/internal/thinking/provider/kimi/apply.go b/internal/thinking/provider/kimi/apply.go index ea3ed572..6ed85049 100644 --- a/internal/thinking/provider/kimi/apply.go +++ b/internal/thinking/provider/kimi/apply.go @@ -1,7 +1,8 @@ // Package kimi implements thinking configuration for Kimi (Moonshot AI) models. // -// Kimi models use the OpenAI-compatible reasoning_effort format for enabled thinking -// levels, but use thinking.type=disabled when thinking is explicitly turned off. +// Kimi models use a native thinking object for both enabled and disabled thinking. +// The top-level reasoning_effort field is accepted only as a legacy input by the +// unified extraction layer and is removed from the final Kimi payload. package kimi import ( @@ -16,9 +17,10 @@ import ( // Applier implements thinking.ProviderApplier for Kimi models. // // Kimi-specific behavior: -// - Enabled thinking: reasoning_effort (string levels) +// - Enabled thinking: thinking.type="enabled" + thinking.effort= // - Disabled thinking: thinking.type="disabled" // - Supports budget-to-level conversion +// - Preserves existing thinking.keep when enabling or changing effort type Applier struct{} var _ thinking.ProviderApplier = (*Applier)(nil) @@ -37,7 +39,10 @@ func init() { // Expected output format (enabled): // // { -// "reasoning_effort": "high" +// "thinking": { +// "type": "enabled", +// "effort": "high" +// } // } // // Expected output format (disabled): @@ -91,7 +96,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo * if effort == "" { return body, nil } - return applyReasoningEffort(body, effort) + return applyEnabledThinking(body, effort) } // applyCompatibleKimi applies thinking config for user-defined Kimi models. @@ -127,17 +132,21 @@ func applyCompatibleKimi(body []byte, config thinking.ThinkingConfig) ([]byte, e return body, nil } - return applyReasoningEffort(body, effort) + return applyEnabledThinking(body, effort) } -func applyReasoningEffort(body []byte, effort string) ([]byte, error) { - result, errDeleteThinking := sjson.DeleteBytes(body, "thinking") - if errDeleteThinking != nil { - return body, fmt.Errorf("kimi thinking: failed to clear thinking object: %w", errDeleteThinking) +func applyEnabledThinking(body []byte, effort string) ([]byte, error) { + result, errDeleteLegacyEffort := sjson.DeleteBytes(body, "reasoning_effort") + if errDeleteLegacyEffort != nil { + return body, fmt.Errorf("kimi thinking: failed to clear reasoning_effort: %w", errDeleteLegacyEffort) } - result, errSetEffort := sjson.SetBytes(result, "reasoning_effort", effort) + result, errSetType := sjson.SetBytes(result, "thinking.type", "enabled") + if errSetType != nil { + return body, fmt.Errorf("kimi thinking: failed to set thinking.type: %w", errSetType) + } + result, errSetEffort := sjson.SetBytes(result, "thinking.effort", effort) if errSetEffort != nil { - return body, fmt.Errorf("kimi thinking: failed to set reasoning_effort: %w", errSetEffort) + return body, fmt.Errorf("kimi thinking: failed to set thinking.effort: %w", errSetEffort) } return result, nil } diff --git a/internal/thinking/validate.go b/internal/thinking/validate.go index 2352862f..7e92a771 100644 --- a/internal/thinking/validate.go +++ b/internal/thinking/validate.go @@ -157,6 +157,13 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo // Convert ModeAuto to mid-range if dynamic not allowed if config.Mode == ModeAuto && !support.DynamicAllowed { config = convertAutoToMidRange(config, support, toFormat, model) + // The canonical mid-range level may not be present in a model's discrete + // level subset (for example, Levels=[low, high]). Clamp the generated + // fallback just like a budget-derived level so providers never receive an + // unsupported value. + if config.Mode == ModeLevel && len(support.Levels) > 0 && !isLevelSupported(string(config.Level), support.Levels) { + config.Level = clampLevel(config.Level, modelInfo, toFormat) + } } if config.Mode == ModeNone && toFormat == "claude" { @@ -170,9 +177,12 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo config.Budget = clampBudget(config.Budget, modelInfo, toFormat) } - // ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models - // This ensures Apply layer doesn't need to access support.Levels - if config.Mode == ModeNone && config.Budget > 0 && len(support.Levels) > 0 { + // ModeNone for a model that cannot be disabled falls back to the lowest + // supported level. Budget-capable models reach this path with Budget > 0; + // level-only models need the capability flags checked explicitly because + // their Min/Max range is zero. + cannotDisableLevelModel := !support.ZeroAllowed && !isLevelSupported(string(LevelNone), support.Levels) + if config.Mode == ModeNone && len(support.Levels) > 0 && (config.Budget > 0 || cannotDisableLevelModel) { config.Level = ThinkingLevel(support.Levels[0]) } }