Merge pull request #3873 from router-for-me/thinking

This commit is contained in:
Luis Pater
2026-06-17 09:16:04 +08:00
committed by GitHub
6 changed files with 206 additions and 210 deletions
@@ -1,55 +0,0 @@
package thinking_test
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/claude"
"github.com/tidwall/gjson"
)
func TestApplyThinking_UserDefinedClaudePreservesAdaptiveLevel(t *testing.T) {
reg := registry.GetGlobalRegistry()
clientID := "test-user-defined-claude-" + t.Name()
modelID := "custom-claude-4-6"
reg.RegisterClient(clientID, "claude", []*registry.ModelInfo{{ID: modelID, UserDefined: true}})
t.Cleanup(func() {
reg.UnregisterClient(clientID)
})
tests := []struct {
name string
model string
body []byte
}{
{
name: "claude adaptive effort body",
model: modelID,
body: []byte(`{"thinking":{"type":"adaptive"},"output_config":{"effort":"high"}}`),
},
{
name: "suffix level",
model: modelID + "(high)",
body: []byte(`{}`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out, err := thinking.ApplyThinking(tt.body, tt.model, "openai", "claude", "claude")
if err != nil {
t.Fatalf("ApplyThinking() error = %v", err)
}
if got := gjson.GetBytes(out, "thinking.type").String(); got != "adaptive" {
t.Fatalf("thinking.type = %q, want %q, body=%s", got, "adaptive", string(out))
}
if got := gjson.GetBytes(out, "output_config.effort").String(); got != "high" {
t.Fatalf("output_config.effort = %q, want %q, body=%s", got, "high", string(out))
}
if gjson.GetBytes(out, "thinking.budget_tokens").Exists() {
t.Fatalf("thinking.budget_tokens should be removed, body=%s", string(out))
}
})
}
}
@@ -1,72 +0,0 @@
package kimi
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/tidwall/gjson"
)
func TestApply_ModeNone_UsesDisabledThinking(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "kimi-k2.5",
Thinking: &registry.ThinkingSupport{Min: 1024, Max: 32000, ZeroAllowed: true, DynamicAllowed: true},
}
body := []byte(`{"model":"kimi-k2.5","reasoning_effort":"none","thinking":{"type":"enabled","budget_tokens":2048}}`)
out, errApply := applier.Apply(body, thinking.ThinkingConfig{Mode: thinking.ModeNone}, modelInfo)
if errApply != nil {
t.Fatalf("Apply() error = %v", errApply)
}
if got := gjson.GetBytes(out, "thinking.type").String(); got != "disabled" {
t.Fatalf("thinking.type = %q, want %q, body=%s", got, "disabled", string(out))
}
if gjson.GetBytes(out, "thinking.budget_tokens").Exists() {
t.Fatalf("thinking.budget_tokens should be removed, body=%s", string(out))
}
if gjson.GetBytes(out, "reasoning_effort").Exists() {
t.Fatalf("reasoning_effort should be removed in ModeNone, body=%s", string(out))
}
}
func TestApply_ModeLevel_UsesReasoningEffort(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "kimi-k2.5",
Thinking: &registry.ThinkingSupport{Min: 1024, Max: 32000, ZeroAllowed: true, DynamicAllowed: true},
}
body := []byte(`{"model":"kimi-k2.5","thinking":{"type":"disabled"}}`)
out, errApply := applier.Apply(body, thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, modelInfo)
if errApply != nil {
t.Fatalf("Apply() error = %v", errApply)
}
if got := gjson.GetBytes(out, "reasoning_effort").String(); got != "high" {
t.Fatalf("reasoning_effort = %q, want %q, body=%s", got, "high", string(out))
}
if gjson.GetBytes(out, "thinking").Exists() {
t.Fatalf("thinking should be removed when reasoning_effort is used, body=%s", string(out))
}
}
func TestApply_UserDefinedModeNone_UsesDisabledThinking(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "custom-kimi-model",
UserDefined: true,
}
body := []byte(`{"model":"custom-kimi-model","reasoning_effort":"none"}`)
out, errApply := applier.Apply(body, thinking.ThinkingConfig{Mode: thinking.ModeNone}, modelInfo)
if errApply != nil {
t.Fatalf("Apply() error = %v", errApply)
}
if got := gjson.GetBytes(out, "thinking.type").String(); got != "disabled" {
t.Fatalf("thinking.type = %q, want %q, body=%s", got, "disabled", string(out))
}
if gjson.GetBytes(out, "reasoning_effort").Exists() {
t.Fatalf("reasoning_effort should be removed in ModeNone, body=%s", string(out))
}
}
@@ -1,51 +0,0 @@
package xai
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/tidwall/gjson"
)
func TestApplySetsReasoningEffort(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "grok-4.3",
Thinking: &registry.ThinkingSupport{
ZeroAllowed: true,
Levels: []string{"none", "low", "medium", "high"},
},
}
out, err := applier.Apply([]byte(`{"input":"hello"}`), thinking.ThinkingConfig{
Mode: thinking.ModeLevel,
Level: thinking.LevelHigh,
}, modelInfo)
if err != nil {
t.Fatalf("Apply() error = %v", err)
}
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" {
t.Fatalf("reasoning.effort = %q, want high; body=%s", got, string(out))
}
}
func TestApplyNoneFallsBackToLowestLevelWhenDisableUnsupported(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "grok-3-mini",
Thinking: &registry.ThinkingSupport{
Levels: []string{"low", "medium", "high"},
},
}
out, err := applier.Apply([]byte(`{"input":"hello"}`), thinking.ThinkingConfig{
Mode: thinking.ModeNone,
}, modelInfo)
if err != nil {
t.Fatalf("Apply() error = %v", err)
}
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "low" {
t.Fatalf("reasoning.effort = %q, want low; body=%s", got, string(out))
}
}
@@ -1,31 +0,0 @@
package thinking
import "testing"
func TestExtractReasoningEffortUsesSuffixOverBody(t *testing.T) {
got := ExtractReasoningEffort([]byte(`{"reasoning_effort":"low"}`), "openai", "gpt-5.4(high)")
if got != "high" {
t.Fatalf("ExtractReasoningEffort() = %q, want %q", got, "high")
}
}
func TestExtractReasoningEffortConvertsBudgetToLevel(t *testing.T) {
got := ExtractReasoningEffort([]byte(`{"thinking":{"type":"enabled","budget_tokens":8192}}`), "claude", "claude-sonnet-4-5")
if got != "medium" {
t.Fatalf("ExtractReasoningEffort() = %q, want %q", got, "medium")
}
}
func TestExtractReasoningEffortSupportsOpenAIResponses(t *testing.T) {
got := ExtractReasoningEffort([]byte(`{"reasoning":{"effort":"medium"}}`), "openai-response", "gpt-5.4")
if got != "medium" {
t.Fatalf("ExtractReasoningEffort() = %q, want %q", got, "medium")
}
}
func TestExtractReasoningEffortMissingConfigIsEmpty(t *testing.T) {
got := ExtractReasoningEffort([]byte(`{"messages":[{"role":"user","content":"hi"}]}`), "openai", "gpt-5.4")
if got != "" {
t.Fatalf("ExtractReasoningEffort() = %q, want empty", got)
}
}
+1 -1
View File
@@ -357,7 +357,7 @@ func isGeminiFamily(provider string) bool {
func isOpenAIFamily(provider string) bool {
switch provider {
case "openai", "openai-response", "codex", "xai":
case "openai", "openai-response", "codex":
return true
default:
return false
+205
View File
@@ -15,6 +15,7 @@ import (
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/geminicli"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/openai"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/xai"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
@@ -2238,6 +2239,186 @@ 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) {
reg := registry.GetGlobalRegistry()
uid := fmt.Sprintf("thinking-e2e-new-providers-%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.
{
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: "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: "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: "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: "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: "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: "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: "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",
},
// xAI target: Grok uses Responses-compatible reasoning.effort with Grok-specific levels.
{
name: "X1",
from: "openai",
to: "xai",
model: "xai-level-model(high)",
inputJSON: `{"model":"xai-level-model(high)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning.effort",
expectValue: "high",
},
{
name: "X2",
from: "openai",
to: "xai",
model: "xai-level-model(xhigh)",
inputJSON: `{"model":"xai-level-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning.effort",
expectValue: "high",
},
{
name: "X3",
from: "openai-response",
to: "xai",
model: "xai-level-model(max)",
inputJSON: `{"model":"xai-level-model(max)","input":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning.effort",
expectValue: "high",
},
{
name: "X4",
from: "gemini",
to: "xai",
model: "xai-level-model(512)",
inputJSON: `{"model":"xai-level-model(512)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "reasoning.effort",
expectValue: "low",
},
{
name: "X5",
from: "claude",
to: "xai",
model: "xai-level-model(0)",
inputJSON: `{"model":"xai-level-model(0)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning.effort",
expectValue: "none",
},
{
name: "X6",
from: "openai",
to: "xai",
model: "xai-level-model",
inputJSON: `{"model":"xai-level-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"xhigh"}`,
expectField: "reasoning.effort",
expectValue: "high",
},
{
name: "X7",
from: "openai-response",
to: "xai",
model: "xai-level-model",
inputJSON: `{"model":"xai-level-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"minimal"}}`,
expectField: "reasoning.effort",
expectValue: "low",
},
{
name: "X8",
from: "gemini",
to: "xai",
model: "xai-level-model",
inputJSON: `{"model":"xai-level-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":32768}}}`,
expectField: "reasoning.effort",
expectValue: "high",
},
{
name: "X9",
from: "claude",
to: "xai",
model: "xai-level-model",
inputJSON: `{"model":"xai-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":0}}`,
expectField: "reasoning.effort",
expectValue: "none",
},
{
name: "X10",
from: "claude",
to: "xai",
model: "xai-level-model",
inputJSON: `{"model":"xai-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`,
expectField: "reasoning.effort",
expectValue: "high",
},
}
runThinkingTests(t, cases)
}
// TestThinkingE2EClaudeAdaptive_Body covers Group 3 cases in docs/thinking-e2e-test-cases.md.
// It focuses on Claude 4.6 adaptive thinking and effort/level cross-protocol semantics (body-only).
func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
@@ -2817,6 +2998,24 @@ func getTestModels() []*registry.ModelInfo {
DisplayName: "Antigravity Budget Model",
Thinking: &registry.ThinkingSupport{Min: 128, Max: 20000, ZeroAllowed: true, DynamicAllowed: true},
},
{
ID: "kimi-level-model",
Object: "model",
Created: 1700000000,
OwnedBy: "moonshot",
Type: "kimi",
DisplayName: "Kimi Level Model",
Thinking: &registry.ThinkingSupport{Levels: []string{"low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: false},
},
{
ID: "xai-level-model",
Object: "model",
Created: 1700000000,
OwnedBy: "xai",
Type: "xai",
DisplayName: "xAI Level Model",
Thinking: &registry.ThinkingSupport{Levels: []string{"none", "low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: false},
},
{
ID: "no-thinking-model",
Object: "model",
@@ -2850,6 +3049,12 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
translateTo := tc.to
applyTo := tc.to
switch applyTo {
case "kimi":
translateTo = "openai"
case "xai":
translateTo = "codex"
}
body := sdktranslator.TranslateRequest(
sdktranslator.FromString(tc.from),