mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 01:46:48 +08:00
Merge pull request #4387 from router-for-me/think
Enhance Kimi thinking configuration and tests
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])
|
||||
}
|
||||
}
|
||||
|
||||
+268
-192
@@ -35,6 +35,9 @@ type thinkingTestCase struct {
|
||||
expectValue string
|
||||
expectField2 string
|
||||
expectValue2 string
|
||||
expectField3 string
|
||||
expectValue3 string
|
||||
expectAbsent []string
|
||||
includeThoughts string
|
||||
expectErr bool
|
||||
}
|
||||
@@ -241,6 +244,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
||||
includeThoughts: "true",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 17A: auto → medium → clamped to low when low/high are equally close
|
||||
{
|
||||
name: "17A",
|
||||
from: "openai",
|
||||
to: "codex",
|
||||
model: "level-subset-model(auto)",
|
||||
inputJSON: `{"model":"level-subset-model(auto)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "reasoning.effort",
|
||||
expectValue: "low",
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// gemini-budget-model (Min=128, Max=20000, ZeroAllowed=false, DynamicAllowed=true)
|
||||
|
||||
@@ -1041,37 +1055,12 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
||||
expectValue: "128000",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 88: Antigravity to Antigravity, budget 8192 → passthrough thinkingBudget
|
||||
{
|
||||
name: "88",
|
||||
from: "antigravity",
|
||||
to: "antigravity",
|
||||
model: "antigravity-budget-model(8192)",
|
||||
inputJSON: `{"model":"antigravity-budget-model(8192)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||
expectValue: "8192",
|
||||
includeThoughts: "true",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 89: Antigravity to Antigravity, budget 64000 → clamped to Max
|
||||
{
|
||||
name: "89",
|
||||
from: "antigravity",
|
||||
to: "antigravity",
|
||||
model: "antigravity-budget-model(64000)",
|
||||
inputJSON: `{"model":"antigravity-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||
expectValue: "20000",
|
||||
includeThoughts: "true",
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Gemini Family Cross-Channel Consistency (Cases 90-95)
|
||||
// Gemini Family Cross-Channel Consistency (Cases 88-89)
|
||||
// Tests that gemini/antigravity as same API family should have consistent validation behavior
|
||||
|
||||
// Case 90: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max
|
||||
// Case 88: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max
|
||||
{
|
||||
name: "90",
|
||||
name: "88",
|
||||
from: "gemini",
|
||||
to: "antigravity",
|
||||
model: "gemini-budget-model(64000)",
|
||||
@@ -1081,9 +1070,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
||||
includeThoughts: "true",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 94: Gemini to Antigravity, budget 8192 → passthrough (normal value)
|
||||
// Case 89: Gemini to Antigravity, budget 8192 → passthrough (normal value)
|
||||
{
|
||||
name: "94",
|
||||
name: "89",
|
||||
from: "gemini",
|
||||
to: "antigravity",
|
||||
model: "gemini-budget-model(8192)",
|
||||
@@ -1472,38 +1461,38 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
||||
name: "31A",
|
||||
from: "openai",
|
||||
to: "gemini",
|
||||
model: "gemini-zero-mixed-model",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
|
||||
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 to Antigravity → delete thinkingConfig
|
||||
// Case 31B: reasoning_effort=none with zero allowed to Antigravity → delete thinkingConfig
|
||||
{
|
||||
name: "31C",
|
||||
name: "31B",
|
||||
from: "openai",
|
||||
to: "antigravity",
|
||||
model: "gemini-zero-mixed-model",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
|
||||
model: "gemini-toggle-mixed-model",
|
||||
inputJSON: `{"model":"gemini-toggle-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
|
||||
expectField: "",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 31D: reasoning.effort=none with zero allowed → delete thinkingConfig
|
||||
// Case 31C: reasoning.effort=none with zero allowed → delete thinkingConfig
|
||||
{
|
||||
name: "31C",
|
||||
from: "openai-response",
|
||||
to: "gemini",
|
||||
model: "gemini-toggle-mixed-model",
|
||||
inputJSON: `{"model":"gemini-toggle-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
expectField: "",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 31D: reasoning.effort=none with zero allowed to Antigravity → delete thinkingConfig
|
||||
{
|
||||
name: "31D",
|
||||
from: "openai-response",
|
||||
to: "gemini",
|
||||
model: "gemini-zero-mixed-model",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
expectField: "",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 31F: reasoning.effort=none with zero allowed to Antigravity → delete thinkingConfig
|
||||
{
|
||||
name: "31F",
|
||||
from: "openai-response",
|
||||
to: "antigravity",
|
||||
model: "gemini-zero-mixed-model",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
model: "gemini-toggle-mixed-model",
|
||||
inputJSON: `{"model":"gemini-toggle-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
expectField: "",
|
||||
expectErr: false,
|
||||
},
|
||||
@@ -2136,35 +2125,12 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
||||
expectField: "",
|
||||
expectErr: true,
|
||||
},
|
||||
// Case 88: Antigravity to Antigravity, thinkingBudget=8192 → passthrough
|
||||
{
|
||||
name: "88",
|
||||
from: "antigravity",
|
||||
to: "antigravity",
|
||||
model: "antigravity-budget-model",
|
||||
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}}`,
|
||||
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||
expectValue: "8192",
|
||||
includeThoughts: "true",
|
||||
expectErr: false,
|
||||
},
|
||||
// Case 89: Antigravity to Antigravity, thinkingBudget=64000 → exceeds Max error
|
||||
{
|
||||
name: "89",
|
||||
from: "antigravity",
|
||||
to: "antigravity",
|
||||
model: "antigravity-budget-model",
|
||||
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":64000}}}}`,
|
||||
expectField: "",
|
||||
expectErr: true,
|
||||
},
|
||||
|
||||
// Gemini Family Cross-Channel Consistency (Cases 90-95)
|
||||
// Gemini Family Cross-Channel Consistency (Cases 88-89)
|
||||
// Tests that gemini/antigravity as same API family should have consistent validation behavior
|
||||
|
||||
// Case 90: Gemini to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation)
|
||||
// Case 88: Gemini to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation)
|
||||
{
|
||||
name: "90",
|
||||
name: "88",
|
||||
from: "gemini",
|
||||
to: "antigravity",
|
||||
model: "gemini-budget-model",
|
||||
@@ -2172,9 +2138,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
||||
expectField: "",
|
||||
expectErr: true,
|
||||
},
|
||||
// Case 94: Gemini to Antigravity, thinkingBudget=8192 → passthrough (normal value)
|
||||
// Case 89: Gemini to Antigravity, thinkingBudget=8192 → passthrough (normal value)
|
||||
{
|
||||
name: "94",
|
||||
name: "89",
|
||||
from: "gemini",
|
||||
to: "antigravity",
|
||||
model: "gemini-budget-model",
|
||||
@@ -2189,88 +2155,205 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
||||
runThinkingTests(t, cases)
|
||||
}
|
||||
|
||||
// TestThinkingE2ENewProviderTargets covers provider-specific targets that do not
|
||||
// have their own public translator format but do have ApplyThinking providers.
|
||||
func TestThinkingE2ENewProviderTargets(t *testing.T) {
|
||||
// TestThinkingE2EProviderTargets covers provider-specific targets that are not part of the main matrix.
|
||||
func TestThinkingE2EProviderTargets(t *testing.T) {
|
||||
reg := registry.GetGlobalRegistry()
|
||||
uid := fmt.Sprintf("thinking-e2e-new-providers-%d", time.Now().UnixNano())
|
||||
uid := fmt.Sprintf("thinking-e2e-provider-targets-%d", time.Now().UnixNano())
|
||||
|
||||
reg.RegisterClient(uid, "test", getTestModels())
|
||||
defer reg.UnregisterClient(uid)
|
||||
|
||||
cases := []thinkingTestCase{
|
||||
// Kimi target: enabled thinking uses reasoning_effort, explicit disable uses thinking.type=disabled.
|
||||
// Kimi target: emit the native thinking object and accept reasoning_effort only as legacy input.
|
||||
{
|
||||
name: "K1",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model(high)",
|
||||
inputJSON: `{"model":"kimi-level-model(high)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "reasoning_effort",
|
||||
expectValue: "high",
|
||||
name: "K1",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model(high)",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model(high)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K2",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model(none)",
|
||||
inputJSON: `{"model":"kimi-level-model(none)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
name: "K2",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model(none)",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model(none)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
expectAbsent: []string{"thinking.effort", "reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K3",
|
||||
from: "gemini",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model(32768)",
|
||||
inputJSON: `{"model":"kimi-level-model(32768)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
||||
expectField: "reasoning_effort",
|
||||
expectValue: "high",
|
||||
name: "K3",
|
||||
from: "gemini",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model(32768)",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model(32768)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K4",
|
||||
from: "claude",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model(0)",
|
||||
inputJSON: `{"model":"kimi-level-model(0)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
name: "K4",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model(auto)",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model(auto)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "medium",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K5",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high"}`,
|
||||
expectField: "reasoning_effort",
|
||||
expectValue: "high",
|
||||
name: "K5",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-tiered-thinking-model(none)",
|
||||
inputJSON: `{"model":"kimi-tiered-thinking-model(none)","messages":[{"role":"user","content":"hi"}]}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "low",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K6",
|
||||
from: "openai-response",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
name: "K6",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high"}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K7",
|
||||
from: "gemini",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":32768}}}`,
|
||||
expectField: "reasoning_effort",
|
||||
expectValue: "high",
|
||||
name: "K7",
|
||||
from: "openai-response",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
expectAbsent: []string{"thinking.effort", "reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K8",
|
||||
from: "claude",
|
||||
to: "kimi",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":0}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
name: "K8",
|
||||
from: "gemini",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":32768}}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K9",
|
||||
from: "gemini",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "medium",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K10",
|
||||
from: "claude",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":0}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "disabled",
|
||||
expectAbsent: []string{"thinking.effort", "reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K11",
|
||||
from: "claude",
|
||||
to: "kimi",
|
||||
model: "kimi-tiered-thinking-model",
|
||||
inputJSON: `{"model":"kimi-tiered-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":0}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "low",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K12",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high","thinking":{"keep":"all"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectField3: "thinking.keep",
|
||||
expectValue3: "all",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K13",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","effort":"high","keep":"all"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectField3: "thinking.keep",
|
||||
expectValue3: "all",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K14",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","keep":"all"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.keep",
|
||||
expectValue2: "all",
|
||||
expectAbsent: []string{"thinking.effort", "reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K15",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"effort":"high"},"reasoning_effort":"low"}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "high",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
{
|
||||
name: "K16",
|
||||
from: "openai",
|
||||
to: "kimi",
|
||||
model: "kimi-toggle-thinking-model",
|
||||
inputJSON: `{"model":"kimi-toggle-thinking-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"auto"}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "enabled",
|
||||
expectField2: "thinking.effort",
|
||||
expectValue2: "medium",
|
||||
expectAbsent: []string{"reasoning_effort"},
|
||||
},
|
||||
|
||||
// xAI target: Grok uses Responses-compatible reasoning.effort with Grok-specific levels.
|
||||
@@ -2370,8 +2453,8 @@ func TestThinkingE2ENewProviderTargets(t *testing.T) {
|
||||
name: "I1",
|
||||
from: "interactions",
|
||||
to: "interactions",
|
||||
model: "gemini-zero-mixed-model",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model","generation_config":{"thinking_level":"high","thinking_summaries":"auto"},"input":"hi"}`,
|
||||
model: "level-model",
|
||||
inputJSON: `{"model":"level-model","generation_config":{"thinking_level":"high","thinking_summaries":"auto"},"input":"hi"}`,
|
||||
expectField: "generation_config.thinking_level",
|
||||
expectValue: "high",
|
||||
expectField2: "generation_config.thinking_summaries",
|
||||
@@ -2381,8 +2464,8 @@ func TestThinkingE2ENewProviderTargets(t *testing.T) {
|
||||
name: "I2",
|
||||
from: "interactions",
|
||||
to: "interactions",
|
||||
model: "gemini-zero-mixed-model(8192)",
|
||||
inputJSON: `{"model":"gemini-zero-mixed-model(8192)","input":"hi"}`,
|
||||
model: "level-model(8192)",
|
||||
inputJSON: `{"model":"level-model(8192)","input":"hi"}`,
|
||||
expectField: "generation_config.thinking_level",
|
||||
expectValue: "medium",
|
||||
expectField2: "generation_config.thinking_summaries",
|
||||
@@ -2816,7 +2899,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "C21",
|
||||
name: "C19",
|
||||
from: "claude",
|
||||
to: "antigravity",
|
||||
model: "antigravity-budget-model",
|
||||
@@ -2828,7 +2911,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
},
|
||||
|
||||
{
|
||||
name: "C22",
|
||||
name: "C20",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-sonnet-4-6-model",
|
||||
@@ -2840,7 +2923,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "C23",
|
||||
name: "C21",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-opus-4-6-model",
|
||||
@@ -2852,7 +2935,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "C24",
|
||||
name: "C22",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-opus-4-6-model",
|
||||
@@ -2860,7 +2943,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "C25",
|
||||
name: "C23",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-sonnet-4-6-model",
|
||||
@@ -2872,7 +2955,7 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "C26",
|
||||
name: "C24",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-sonnet-4-6-model",
|
||||
@@ -2880,40 +2963,13 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "C27",
|
||||
name: "C25",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "claude-sonnet-4-6-model",
|
||||
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
|
||||
expectErr: true,
|
||||
},
|
||||
// Kimi models exposed via Claude-compatible /v1/messages keep wire format
|
||||
// claude→claude, but the model type is kimi. Claude Code often sends
|
||||
// effort=max; clamp to the highest Kimi-supported level (high).
|
||||
{
|
||||
name: "C28",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "adaptive",
|
||||
expectField2: "output_config.effort",
|
||||
expectValue2: "high",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "C29",
|
||||
from: "claude",
|
||||
to: "claude",
|
||||
model: "kimi-level-model",
|
||||
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
|
||||
expectField: "thinking.type",
|
||||
expectValue: "adaptive",
|
||||
expectField2: "output_config.effort",
|
||||
expectValue2: "high",
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
runThinkingTests(t, cases)
|
||||
@@ -2959,13 +3015,13 @@ func getTestModels() []*registry.ModelInfo {
|
||||
Thinking: ®istry.ThinkingSupport{Min: 128, Max: 32768, Levels: []string{"low", "high"}, ZeroAllowed: false, DynamicAllowed: true},
|
||||
},
|
||||
{
|
||||
ID: "gemini-zero-mixed-model",
|
||||
ID: "gemini-toggle-mixed-model",
|
||||
Object: "model",
|
||||
Created: 1700000000,
|
||||
OwnedBy: "test",
|
||||
Type: "gemini",
|
||||
DisplayName: "Gemini Zero Mixed Model",
|
||||
Thinking: ®istry.ThinkingSupport{Min: 1, Max: 65535, Levels: []string{"minimal", "low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: true},
|
||||
DisplayName: "Gemini Toggle Mixed Model",
|
||||
Thinking: ®istry.ThinkingSupport{Min: 128, Max: 32768, Levels: []string{"low", "high"}, ZeroAllowed: true, DynamicAllowed: true},
|
||||
},
|
||||
{
|
||||
ID: "claude-budget-model",
|
||||
@@ -2976,17 +3032,6 @@ func getTestModels() []*registry.ModelInfo {
|
||||
DisplayName: "Claude Budget Model",
|
||||
Thinking: ®istry.ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false},
|
||||
},
|
||||
{
|
||||
ID: "claude-sonnet-4-6-model",
|
||||
Object: "model",
|
||||
Created: 1771372800, // 2026-02-17
|
||||
OwnedBy: "anthropic",
|
||||
Type: "claude",
|
||||
DisplayName: "Claude 4.6 Sonnet",
|
||||
ContextLength: 200000,
|
||||
MaxCompletionTokens: 64000,
|
||||
Thinking: ®istry.ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false, Levels: []string{"low", "medium", "high"}},
|
||||
},
|
||||
{
|
||||
ID: "claude-opus-4-6-model",
|
||||
Object: "model",
|
||||
@@ -2999,6 +3044,17 @@ func getTestModels() []*registry.ModelInfo {
|
||||
MaxCompletionTokens: 128000,
|
||||
Thinking: ®istry.ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false, Levels: []string{"low", "medium", "high", "max"}},
|
||||
},
|
||||
{
|
||||
ID: "claude-sonnet-4-6-model",
|
||||
Object: "model",
|
||||
Created: 1771372800, // 2026-02-17
|
||||
OwnedBy: "anthropic",
|
||||
Type: "claude",
|
||||
DisplayName: "Claude 4.6 Sonnet",
|
||||
ContextLength: 200000,
|
||||
MaxCompletionTokens: 64000,
|
||||
Thinking: ®istry.ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false, Levels: []string{"low", "medium", "high"}},
|
||||
},
|
||||
{
|
||||
ID: "antigravity-budget-model",
|
||||
Object: "model",
|
||||
@@ -3009,14 +3065,23 @@ func getTestModels() []*registry.ModelInfo {
|
||||
Thinking: ®istry.ThinkingSupport{Min: 128, Max: 20000, ZeroAllowed: true, DynamicAllowed: true},
|
||||
},
|
||||
{
|
||||
ID: "kimi-level-model",
|
||||
ID: "kimi-toggle-thinking-model",
|
||||
Object: "model",
|
||||
Created: 1700000000,
|
||||
OwnedBy: "moonshot",
|
||||
Type: "kimi",
|
||||
DisplayName: "Kimi Level Model",
|
||||
DisplayName: "Kimi Toggle Thinking Model",
|
||||
Thinking: ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: false},
|
||||
},
|
||||
{
|
||||
ID: "kimi-tiered-thinking-model",
|
||||
Object: "model",
|
||||
Created: 1700000000,
|
||||
OwnedBy: "moonshot",
|
||||
Type: "kimi",
|
||||
DisplayName: "Kimi Tiered Thinking Model",
|
||||
Thinking: ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high"}, ZeroAllowed: false, DynamicAllowed: false},
|
||||
},
|
||||
{
|
||||
ID: "xai-level-model",
|
||||
Object: "model",
|
||||
@@ -3089,6 +3154,12 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
|
||||
t.Fatalf("unexpected error: %v, body=%s", err, string(body))
|
||||
}
|
||||
|
||||
for _, fieldPath := range tc.expectAbsent {
|
||||
if gjson.GetBytes(body, fieldPath).Exists() {
|
||||
t.Fatalf("expected field %s to be absent, body=%s", fieldPath, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectField == "" {
|
||||
var hasThinking bool
|
||||
switch tc.to {
|
||||
@@ -3102,6 +3173,8 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
|
||||
hasThinking = gjson.GetBytes(body, "reasoning_effort").Exists()
|
||||
case "codex":
|
||||
hasThinking = gjson.GetBytes(body, "reasoning.effort").Exists() || gjson.GetBytes(body, "reasoning").Exists()
|
||||
case "kimi":
|
||||
hasThinking = gjson.GetBytes(body, "thinking").Exists() || gjson.GetBytes(body, "reasoning_effort").Exists()
|
||||
}
|
||||
if hasThinking {
|
||||
t.Fatalf("expected no thinking field but found one, body=%s", string(body))
|
||||
@@ -3127,6 +3200,9 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
|
||||
if tc.expectField2 != "" {
|
||||
assertField(tc.expectField2, tc.expectValue2)
|
||||
}
|
||||
if tc.expectField3 != "" {
|
||||
assertField(tc.expectField3, tc.expectValue3)
|
||||
}
|
||||
|
||||
if tc.includeThoughts != "" && (tc.to == "gemini" || tc.to == "antigravity") {
|
||||
path := "generationConfig.thinkingConfig.includeThoughts"
|
||||
|
||||
Reference in New Issue
Block a user