mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 09:56:49 +08:00
feat(kimi): implement native thinking object extraction and update reasoning handling
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user