mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
Merge pull request #3873 from router-for-me/thinking
This commit is contained in:
@@ -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 := ®istry.ModelInfo{
|
||||
ID: "kimi-k2.5",
|
||||
Thinking: ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "kimi-k2.5",
|
||||
Thinking: ®istry.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 := ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "grok-4.3",
|
||||
Thinking: ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "grok-3-mini",
|
||||
Thinking: ®istry.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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: ®istry.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: ®istry.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: ®istry.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),
|
||||
|
||||
Reference in New Issue
Block a user