feat(kimi): implement native thinking object extraction and update reasoning handling

This commit is contained in:
hkfires
2026-07-17 13:33:44 +08:00
parent 74a98761f6
commit 255df2db3c
4 changed files with 96 additions and 37 deletions
+17 -20
View File
@@ -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": [
+45 -2
View File
@@ -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):
+21 -12
View File
@@ -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=<level>
// - 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
}
+13 -3
View File
@@ -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])
}
}