Merge branch 'pr-529'

# Conflicts:
#	README.md
#	README_CN.md
#	README_JA.md
#	cmd/server/main.go
#	config.example.yaml
#	internal/api/handlers/management/auth_files.go
#	internal/api/server.go
#	internal/auth/iflow/iflow_auth.go
#	internal/cmd/auth_manager.go
#	internal/config/config.go
#	sdk/cliproxy/auth/oauth_model_alias.go
#	sdk/cliproxy/auth/types.go
#	test/thinking_conversion_test.go
This commit is contained in:
Luis Pater
2026-04-17 01:42:35 +08:00
33 changed files with 272 additions and 755 deletions

View File

@@ -380,9 +380,6 @@ nonstream-keepalive-interval: 0
# codex:
# - name: "gpt-5"
# alias: "g5"
# iflow:
# - name: "glm-4.7"
# alias: "glm-god"
# kimi:
# - name: "kimi-k2.5"
# alias: "k2.5"
@@ -411,8 +408,6 @@ nonstream-keepalive-interval: 0
# - "claude-3-5-haiku-20241022"
# codex:
# - "gpt-5-codex-mini"
# iflow:
# - "tstars2.0"
# kimi:
# - "kimi-k2-thinking"
# kiro:

View File

@@ -109,10 +109,19 @@ wait_for_service() {
sleep 2
}
if [[ "${1:-}" == "--with-usage" ]]; then
case "${1:-}" in
"")
;;
"--with-usage")
WITH_USAGE=true
export_stats_api_secret
fi
;;
*)
echo "Error: unknown option '${1}'. Did you mean '--with-usage'?"
echo "Usage: ./docker-build.sh [--with-usage]"
exit 1
;;
esac
# --- Step 1: Choose Environment ---
echo "Please select an option:"

View File

@@ -232,8 +232,6 @@ func NormalizeOAuthProvider(provider string) (string, error) {
return "gitlab", nil
case "gemini", "google":
return "gemini", nil
case "iflow", "i-flow":
return "iflow", nil
case "antigravity", "anti-gravity":
return "antigravity", nil
case "kiro":

View File

@@ -437,20 +437,6 @@ func (s *Server) setupRoutes() {
c.String(http.StatusOK, oauthCallbackSuccessHTML)
})
s.engine.GET("/iflow/callback", func(c *gin.Context) {
code := c.Query("code")
state := c.Query("state")
errStr := c.Query("error")
if errStr == "" {
errStr = c.Query("error_description")
}
if state != "" {
_, _ = managementHandlers.WriteOAuthCallbackFileForPendingSession(s.cfg.AuthDir, "iflow", state, code, errStr)
}
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, oauthCallbackSuccessHTML)
})
s.engine.GET("/antigravity/callback", func(c *gin.Context) {
code := c.Query("code")
state := c.Query("state")

View File

@@ -59,10 +59,30 @@ type ClaudeAuth struct {
// Returns:
// - *ClaudeAuth: A new Claude authentication service instance
func NewClaudeAuth(cfg *config.Config) *ClaudeAuth {
return NewClaudeAuthWithProxyURL(cfg, "")
}
// NewClaudeAuthWithProxyURL creates a new Anthropic authentication service with a proxy override.
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
func NewClaudeAuthWithProxyURL(cfg *config.Config, proxyURL string) *ClaudeAuth {
effectiveProxyURL := strings.TrimSpace(proxyURL)
var sdkCfg *config.SDKConfig
if cfg != nil {
sdkCfgCopy := cfg.SDKConfig
if effectiveProxyURL == "" {
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
}
sdkCfgCopy.ProxyURL = effectiveProxyURL
sdkCfg = &sdkCfgCopy
} else if effectiveProxyURL != "" {
sdkCfgCopy := config.SDKConfig{ProxyURL: effectiveProxyURL}
sdkCfg = &sdkCfgCopy
}
// Use custom HTTP client with Firefox TLS fingerprint to bypass
// Cloudflare's bot detection on Anthropic domains
return &ClaudeAuth{
httpClient: NewAnthropicHttpClient(&cfg.SDKConfig),
httpClient: NewAnthropicHttpClient(sdkCfg),
}
}

View File

@@ -0,0 +1,33 @@
package claude
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"golang.org/x/net/proxy"
)
func TestNewClaudeAuthWithProxyURL_OverrideDirectTakesPrecedence(t *testing.T) {
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "socks5://proxy.example.com:1080"}}
auth := NewClaudeAuthWithProxyURL(cfg, "direct")
transport, ok := auth.httpClient.Transport.(*utlsRoundTripper)
if !ok || transport == nil {
t.Fatalf("expected utlsRoundTripper, got %T", auth.httpClient.Transport)
}
if transport.dialer != proxy.Direct {
t.Fatalf("expected proxy.Direct, got %T", transport.dialer)
}
}
func TestNewClaudeAuthWithProxyURL_OverrideProxyAppliedWithoutConfig(t *testing.T) {
auth := NewClaudeAuthWithProxyURL(nil, "socks5://proxy.example.com:1080")
transport, ok := auth.httpClient.Transport.(*utlsRoundTripper)
if !ok || transport == nil {
t.Fatalf("expected utlsRoundTripper, got %T", auth.httpClient.Transport)
}
if transport.dialer == proxy.Direct {
t.Fatalf("expected proxy dialer, got %T", transport.dialer)
}
}

View File

@@ -37,8 +37,23 @@ type CodexAuth struct {
// NewCodexAuth creates a new CodexAuth service instance.
// It initializes an HTTP client with proxy settings from the provided configuration.
func NewCodexAuth(cfg *config.Config) *CodexAuth {
return NewCodexAuthWithProxyURL(cfg, "")
}
// NewCodexAuthWithProxyURL creates a new CodexAuth service instance.
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
func NewCodexAuthWithProxyURL(cfg *config.Config, proxyURL string) *CodexAuth {
effectiveProxyURL := strings.TrimSpace(proxyURL)
var sdkCfg config.SDKConfig
if cfg != nil {
sdkCfg = cfg.SDKConfig
if effectiveProxyURL == "" {
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
}
}
sdkCfg.ProxyURL = effectiveProxyURL
return &CodexAuth{
httpClient: util.SetProxy(&cfg.SDKConfig, &http.Client{}),
httpClient: util.SetProxy(&sdkCfg, &http.Client{}),
}
}

View File

@@ -7,6 +7,8 @@ import (
"strings"
"sync/atomic"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
@@ -42,3 +44,37 @@ func TestRefreshTokensWithRetry_NonRetryableOnlyAttemptsOnce(t *testing.T) {
t.Fatalf("expected 1 refresh attempt, got %d", got)
}
}
func TestNewCodexAuthWithProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://proxy.example.com:8080"}}
auth := NewCodexAuthWithProxyURL(cfg, "direct")
transport, ok := auth.httpClient.Transport.(*http.Transport)
if !ok || transport == nil {
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
}
if transport.Proxy != nil {
t.Fatal("expected direct transport to disable proxy function")
}
}
func TestNewCodexAuthWithProxyURL_OverrideProxyTakesPrecedence(t *testing.T) {
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://global.example.com:8080"}}
auth := NewCodexAuthWithProxyURL(cfg, "http://override.example.com:8081")
transport, ok := auth.httpClient.Transport.(*http.Transport)
if !ok || transport == nil {
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
}
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
if errReq != nil {
t.Fatalf("new request: %v", errReq)
}
proxyURL, errProxy := transport.Proxy(req)
if errProxy != nil {
t.Fatalf("proxy func: %v", errProxy)
}
if proxyURL == nil || proxyURL.String() != "http://override.example.com:8081" {
t.Fatalf("proxy URL = %v, want http://override.example.com:8081", proxyURL)
}
}

View File

@@ -102,10 +102,24 @@ func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient {
// NewDeviceFlowClientWithDeviceID creates a new device flow client with the specified device ID.
func NewDeviceFlowClientWithDeviceID(cfg *config.Config, deviceID string) *DeviceFlowClient {
client := &http.Client{Timeout: 30 * time.Second}
if cfg != nil {
client = util.SetProxy(&cfg.SDKConfig, client)
return NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, deviceID, "")
}
// NewDeviceFlowClientWithDeviceIDAndProxyURL creates a new device flow client with a proxy override.
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
func NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg *config.Config, deviceID string, proxyURL string) *DeviceFlowClient {
client := &http.Client{Timeout: 30 * time.Second}
effectiveProxyURL := strings.TrimSpace(proxyURL)
var sdkCfg config.SDKConfig
if cfg != nil {
sdkCfg = cfg.SDKConfig
if effectiveProxyURL == "" {
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
}
}
sdkCfg.ProxyURL = effectiveProxyURL
client = util.SetProxy(&sdkCfg, client)
resolvedDeviceID := strings.TrimSpace(deviceID)
if resolvedDeviceID == "" {
resolvedDeviceID = getOrCreateDeviceID()

View File

@@ -0,0 +1,42 @@
package kimi
import (
"net/http"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://proxy.example.com:8080"}}
client := NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, "device-1", "direct")
transport, ok := client.httpClient.Transport.(*http.Transport)
if !ok || transport == nil {
t.Fatalf("expected http.Transport, got %T", client.httpClient.Transport)
}
if transport.Proxy != nil {
t.Fatal("expected direct transport to disable proxy function")
}
}
func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideProxyTakesPrecedence(t *testing.T) {
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://global.example.com:8080"}}
client := NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, "device-1", "http://override.example.com:8081")
transport, ok := client.httpClient.Transport.(*http.Transport)
if !ok || transport == nil {
t.Fatalf("expected http.Transport, got %T", client.httpClient.Transport)
}
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
if errReq != nil {
t.Fatalf("new request: %v", errReq)
}
proxyURL, errProxy := transport.Proxy(req)
if errProxy != nil {
t.Fatalf("proxy func: %v", errProxy)
}
if proxyURL == nil || proxyURL.String() != "http://override.example.com:8081" {
t.Fatalf("proxy URL = %v, want http://override.example.com:8081", proxyURL)
}
}

View File

@@ -15,7 +15,6 @@ func newAuthManager() *sdkAuth.Manager {
sdkAuth.NewGeminiAuthenticator(),
sdkAuth.NewCodexAuthenticator(),
sdkAuth.NewClaudeAuthenticator(),
sdkAuth.NewIFlowAuthenticator(),
sdkAuth.NewAntigravityAuthenticator(),
sdkAuth.NewKimiAuthenticator(),
sdkAuth.NewKiroAuthenticator(),

View File

@@ -135,12 +135,12 @@ type Config struct {
AmpCode AmpCode `yaml:"ampcode" json:"ampcode"`
// OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries.
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot.
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi.
OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"`
// OAuthModelAlias defines global model name aliases for OAuth/file-backed auth channels.
// These aliases affect both model listing and model routing for supported channels:
// gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot.
// gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi.
//
// NOTE: This does not apply to existing per-credential model alias features under:
// gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.

View File

@@ -17,7 +17,6 @@ type staticModelsJSON struct {
CodexTeam []*ModelInfo `json:"codex-team"`
CodexPlus []*ModelInfo `json:"codex-plus"`
CodexPro []*ModelInfo `json:"codex-pro"`
IFlow []*ModelInfo `json:"iflow"`
Kimi []*ModelInfo `json:"kimi"`
Antigravity []*ModelInfo `json:"antigravity"`
}
@@ -67,11 +66,6 @@ func GetCodexProModels() []*ModelInfo {
return cloneModelInfos(getModels().CodexPro)
}
// GetIFlowModels returns the standard iFlow model definitions.
func GetIFlowModels() []*ModelInfo {
return cloneModelInfos(getModels().IFlow)
}
// GetKimiModels returns the standard Kimi (Moonshot AI) model definitions.
func GetKimiModels() []*ModelInfo {
return cloneModelInfos(getModels().Kimi)
@@ -233,7 +227,6 @@ func cloneModelInfos(models []*ModelInfo) []*ModelInfo {
// - gemini-cli
// - aistudio
// - codex
// - iflow
// - kimi
// - kilo
// - github-copilot
@@ -254,8 +247,6 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
return GetAIStudioModels()
case "codex":
return GetCodexProModels()
case "iflow":
return GetIFlowModels()
case "kimi":
return GetKimiModels()
case "github-copilot":
@@ -304,7 +295,6 @@ func LookupStaticModelInfo(modelID string) *ModelInfo {
data.GeminiCLI,
data.AIStudio,
data.CodexPro,
data.IFlow,
data.Kimi,
data.Antigravity,
GetGitHubCopilotModels(),

View File

@@ -213,7 +213,6 @@ func detectChangedProviders(oldData, newData *staticModelsJSON) []string {
{"codex", oldData.CodexTeam, newData.CodexTeam},
{"codex", oldData.CodexPlus, newData.CodexPlus},
{"codex", oldData.CodexPro, newData.CodexPro},
{"iflow", oldData.IFlow, newData.IFlow},
{"kimi", oldData.Kimi, newData.Kimi},
{"antigravity", oldData.Antigravity, newData.Antigravity},
}
@@ -334,7 +333,6 @@ func validateModelsCatalog(data *staticModelsJSON) error {
{name: "codex-team", models: data.CodexTeam},
{name: "codex-plus", models: data.CodexPlus},
{name: "codex-pro", models: data.CodexPro},
{name: "iflow", models: data.IFlow},
{name: "kimi", models: data.Kimi},
{name: "antigravity", models: data.Antigravity},
}

View File

@@ -72,6 +72,29 @@
]
}
},
{
"id": "claude-opus-4-7",
"object": "model",
"created": 1776297600,
"owned_by": "anthropic",
"type": "claude",
"display_name": "Claude Opus 4.7",
"description": "Premium model combining maximum intelligence with practical performance",
"context_length": 1000000,
"max_completion_tokens": 128000,
"thinking": {
"min": 1024,
"max": 128000,
"zero_allowed": true,
"levels": [
"low",
"medium",
"high",
"xhigh",
"max"
]
}
},
{
"id": "claude-opus-4-5-20251101",
"object": "model",
@@ -1602,187 +1625,6 @@
}
}
],
"iflow": [
{
"id": "qwen3-coder-plus",
"object": "model",
"created": 1753228800,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-Coder-Plus",
"description": "Qwen3 Coder Plus code generation"
},
{
"id": "qwen3-max",
"object": "model",
"created": 1758672000,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-Max",
"description": "Qwen3 flagship model"
},
{
"id": "qwen3-vl-plus",
"object": "model",
"created": 1758672000,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-VL-Plus",
"description": "Qwen3 multimodal vision-language"
},
{
"id": "qwen3-max-preview",
"object": "model",
"created": 1757030400,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-Max-Preview",
"description": "Qwen3 Max preview build",
"thinking": {
"levels": [
"none",
"auto",
"minimal",
"low",
"medium",
"high",
"xhigh"
]
}
},
{
"id": "glm-4.6",
"object": "model",
"created": 1759190400,
"owned_by": "iflow",
"type": "iflow",
"display_name": "GLM-4.6",
"description": "Zhipu GLM 4.6 general model",
"thinking": {
"levels": [
"none",
"auto",
"minimal",
"low",
"medium",
"high",
"xhigh"
]
}
},
{
"id": "kimi-k2",
"object": "model",
"created": 1752192000,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Kimi-K2",
"description": "Moonshot Kimi K2 general model"
},
{
"id": "deepseek-v3.2",
"object": "model",
"created": 1759104000,
"owned_by": "iflow",
"type": "iflow",
"display_name": "DeepSeek-V3.2-Exp",
"description": "DeepSeek V3.2 experimental",
"thinking": {
"levels": [
"none",
"auto",
"minimal",
"low",
"medium",
"high",
"xhigh"
]
}
},
{
"id": "deepseek-v3.1",
"object": "model",
"created": 1756339200,
"owned_by": "iflow",
"type": "iflow",
"display_name": "DeepSeek-V3.1-Terminus",
"description": "DeepSeek V3.1 Terminus",
"thinking": {
"levels": [
"none",
"auto",
"minimal",
"low",
"medium",
"high",
"xhigh"
]
}
},
{
"id": "deepseek-r1",
"object": "model",
"created": 1737331200,
"owned_by": "iflow",
"type": "iflow",
"display_name": "DeepSeek-R1",
"description": "DeepSeek reasoning model R1"
},
{
"id": "deepseek-v3",
"object": "model",
"created": 1734307200,
"owned_by": "iflow",
"type": "iflow",
"display_name": "DeepSeek-V3-671B",
"description": "DeepSeek V3 671B"
},
{
"id": "qwen3-32b",
"object": "model",
"created": 1747094400,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-32B",
"description": "Qwen3 32B"
},
{
"id": "qwen3-235b-a22b-thinking-2507",
"object": "model",
"created": 1753401600,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-235B-A22B-Thinking",
"description": "Qwen3 235B A22B Thinking (2507)"
},
{
"id": "qwen3-235b-a22b-instruct",
"object": "model",
"created": 1753401600,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-235B-A22B-Instruct",
"description": "Qwen3 235B A22B Instruct"
},
{
"id": "qwen3-235b",
"object": "model",
"created": 1753401600,
"owned_by": "iflow",
"type": "iflow",
"display_name": "Qwen3-235B-A22B",
"description": "Qwen3 235B A22B"
},
{
"id": "iflow-rome-30ba3b",
"object": "model",
"created": 1736899200,
"owned_by": "iflow",
"type": "iflow",
"display_name": "iFlow-ROME",
"description": "iFlow Rome 30BA3B model"
}
],
"kimi": [
{
"id": "kimi-k2",

View File

@@ -659,7 +659,7 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (
if refreshToken == "" {
return auth, nil
}
svc := claudeauth.NewClaudeAuth(e.cfg)
svc := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
td, err := svc.RefreshTokens(ctx, refreshToken)
if err != nil {
return nil, err

View File

@@ -612,7 +612,7 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
if refreshToken == "" {
return auth, nil
}
svc := codexauth.NewCodexAuth(e.cfg)
svc := codexauth.NewCodexAuthWithProxyURL(e.cfg, auth.ProxyURL)
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
if err != nil {
return nil, err

View File

@@ -6,7 +6,6 @@ import (
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/codex"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/gemini"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/geminicli"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/iflow"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/kimi"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/openai"
)

View File

@@ -472,7 +472,7 @@ func (e *KimiExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c
return auth, nil
}
client := kimiauth.NewDeviceFlowClientWithDeviceID(e.cfg, resolveKimiDeviceID(auth))
client := kimiauth.NewDeviceFlowClientWithDeviceIDAndProxyURL(e.cfg, resolveKimiDeviceID(auth), auth.ProxyURL)
td, err := client.RefreshToken(ctx, refreshToken)
if err != nil {
return nil, err

View File

@@ -16,7 +16,6 @@ var providerAppliers = map[string]ProviderApplier{
"claude": nil,
"openai": nil,
"codex": nil,
"iflow": nil,
"antigravity": nil,
"kimi": nil,
}
@@ -63,7 +62,7 @@ func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool {
// - body: Original request body JSON
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
// - fromFormat: Source request format (e.g., openai, codex, gemini)
// - toFormat: Target provider format for the request body (gemini, gemini-cli, antigravity, claude, openai, codex, iflow)
// - toFormat: Target provider format for the request body (gemini, gemini-cli, antigravity, claude, openai, codex, kimi)
// - providerKey: Provider identifier used for registry model lookups (may differ from toFormat, e.g., openrouter -> openai)
//
// Returns:
@@ -327,12 +326,6 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
return extractOpenAIConfig(body)
case "codex":
return extractCodexConfig(body)
case "iflow":
config := extractIFlowConfig(body)
if hasThinkingConfig(config) {
return config
}
return extractOpenAIConfig(body)
case "kimi":
// Kimi uses OpenAI-compatible reasoning_effort format
return extractOpenAIConfig(body)
@@ -494,34 +487,3 @@ func extractCodexConfig(body []byte) ThinkingConfig {
return ThinkingConfig{}
}
// extractIFlowConfig extracts thinking configuration from iFlow format request body.
//
// iFlow API format (supports multiple model families):
// - GLM format: chat_template_kwargs.enable_thinking (boolean)
// - MiniMax format: reasoning_split (boolean)
//
// Returns ModeBudget with Budget=1 as a sentinel value indicating "enabled".
// The actual budget/configuration is determined by the iFlow applier based on model capabilities.
// Budget=1 is used because iFlow models don't use numeric budgets; they only support on/off.
func extractIFlowConfig(body []byte) ThinkingConfig {
// GLM format: chat_template_kwargs.enable_thinking
if enabled := gjson.GetBytes(body, "chat_template_kwargs.enable_thinking"); enabled.Exists() {
if enabled.Bool() {
// Budget=1 is a sentinel meaning "enabled" (iFlow doesn't use numeric budgets)
return ThinkingConfig{Mode: ModeBudget, Budget: 1}
}
return ThinkingConfig{Mode: ModeNone, Budget: 0}
}
// MiniMax format: reasoning_split
if split := gjson.GetBytes(body, "reasoning_split"); split.Exists() {
if split.Bool() {
// Budget=1 is a sentinel meaning "enabled" (iFlow doesn't use numeric budgets)
return ThinkingConfig{Mode: ModeBudget, Budget: 1}
}
return ThinkingConfig{Mode: ModeNone, Budget: 0}
}
return ThinkingConfig{}
}

View File

@@ -155,7 +155,7 @@ const (
// It analyzes the model's ThinkingSupport configuration to classify the model:
// - CapabilityNone: modelInfo.Thinking is nil (model doesn't support thinking)
// - CapabilityBudgetOnly: Has Min/Max but no Levels (Claude, Gemini 2.5)
// - CapabilityLevelOnly: Has Levels but no Min/Max (OpenAI, iFlow)
// - CapabilityLevelOnly: Has Levels but no Min/Max (OpenAI, Codex, Kimi)
// - CapabilityHybrid: Has both Min/Max and Levels (Gemini 3)
//
// Note: Returns a special sentinel value when modelInfo itself is nil (unknown model).

View File

@@ -44,13 +44,6 @@ func StripThinkingConfig(body []byte, provider string) []byte {
}
case "codex":
paths = []string{"reasoning.effort"}
case "iflow":
paths = []string{
"chat_template_kwargs.enable_thinking",
"chat_template_kwargs.clear_thinking",
"reasoning_split",
"reasoning_effort",
}
default:
return body
}

View File

@@ -1,7 +1,7 @@
// Package thinking provides unified thinking configuration processing.
//
// This package offers a unified interface for parsing, validating, and applying
// thinking configurations across various AI providers (Claude, Gemini, OpenAI, iFlow).
// thinking configurations across various AI providers (Claude, Gemini, OpenAI, Codex, Antigravity, Kimi).
package thinking
import "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"

View File

@@ -24,7 +24,6 @@ var oauthProviders = []oauthProvider{
{"Codex (OpenAI)", "codex-auth-url", "🟩"},
{"Antigravity", "antigravity-auth-url", "🟪"},
{"Kimi", "kimi-auth-url", "🟫"},
{"IFlow", "iflow-auth-url", "⬜"},
}
// oauthTabModel handles OAuth login flows.
@@ -281,8 +280,6 @@ func (m oauthTabModel) submitCallback(callbackURL string) tea.Cmd {
providerKey = "antigravity"
case "kimi-auth-url":
providerKey = "kimi"
case "iflow-auth-url":
providerKey = "iflow"
}
break
}

View File

@@ -194,11 +194,11 @@ func requestExecutionMetadata(ctx context.Context) map[string]any {
key = strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key"))
}
}
if key == "" {
return make(map[string]any)
}
meta := map[string]any{idempotencyKeyMetadataKey: key}
meta := make(map[string]any)
if key != "" {
meta[idempotencyKeyMetadataKey] = key
}
if pinnedAuthID := pinnedAuthIDFromContext(ctx); pinnedAuthID != "" {
meta[coreexecutor.PinnedAuthMetadataKey] = pinnedAuthID
}

View File

@@ -0,0 +1,20 @@
package handlers
import (
"testing"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
"golang.org/x/net/context"
)
func TestRequestExecutionMetadataIncludesExecutionSessionWithoutIdempotencyKey(t *testing.T) {
ctx := WithExecutionSessionID(context.Background(), "session-1")
meta := requestExecutionMetadata(ctx)
if got := meta[coreexecutor.ExecutionSessionMetadataKey]; got != "session-1" {
t.Fatalf("ExecutionSessionMetadataKey = %v, want %q", got, "session-1")
}
if _, ok := meta[idempotencyKeyMetadataKey]; ok {
t.Fatalf("unexpected idempotency key in metadata: %v", meta[idempotencyKeyMetadataKey])
}
}

View File

@@ -18,8 +18,6 @@ type ManagementTokenRequester interface {
RequestCodexToken(*gin.Context)
RequestAntigravityToken(*gin.Context)
RequestKimiToken(*gin.Context)
RequestIFlowToken(*gin.Context)
RequestIFlowCookieToken(*gin.Context)
GetAuthStatus(c *gin.Context)
PostOAuthCallback(c *gin.Context)
}
@@ -55,14 +53,6 @@ func (m *managementTokenRequester) RequestKimiToken(c *gin.Context) {
m.handler.RequestKimiToken(c)
}
func (m *managementTokenRequester) RequestIFlowToken(c *gin.Context) {
m.handler.RequestIFlowToken(c)
}
func (m *managementTokenRequester) RequestIFlowCookieToken(c *gin.Context) {
m.handler.RequestIFlowCookieToken(c)
}
func (m *managementTokenRequester) GetAuthStatus(c *gin.Context) {
m.handler.GetAuthStatus(c)
}

View File

@@ -9,7 +9,6 @@ import (
func init() {
registerRefreshLead("codex", func() Authenticator { return NewCodexAuthenticator() })
registerRefreshLead("claude", func() Authenticator { return NewClaudeAuthenticator() })
registerRefreshLead("iflow", func() Authenticator { return NewIFlowAuthenticator() })
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
registerRefreshLead("antigravity", func() Authenticator { return NewAntigravityAuthenticator() })

View File

@@ -69,7 +69,7 @@ func TestManager_ShouldRetryAfterError_UsesOAuthModelAliasForCooldown(t *testing
m := NewManager(nil, nil, nil)
m.SetRetryConfig(3, 30*time.Second, 0)
m.SetOAuthModelAlias(map[string][]internalconfig.OAuthModelAlias{
"iflow": {
"kimi": {
{Name: "deepseek-v3.1", Alias: "pool-model"},
},
})
@@ -80,7 +80,7 @@ func TestManager_ShouldRetryAfterError_UsesOAuthModelAliasForCooldown(t *testing
auth := &Auth{
ID: "auth-1",
Provider: "iflow",
Provider: "kimi",
ModelStates: map[string]*ModelState{
upstreamModel: {
Unavailable: true,
@@ -99,7 +99,7 @@ func TestManager_ShouldRetryAfterError_UsesOAuthModelAliasForCooldown(t *testing
}
_, _, maxWait := m.retrySettings()
wait, shouldRetry := m.shouldRetryAfterError(&Error{HTTPStatus: 429, Message: "quota"}, 0, []string{"iflow"}, routeModel, maxWait)
wait, shouldRetry := m.shouldRetryAfterError(&Error{HTTPStatus: 429, Message: "quota"}, 0, []string{"kimi"}, routeModel, maxWait)
if !shouldRetry {
t.Fatalf("expected shouldRetry=true, got false (wait=%v)", wait)
}

View File

@@ -184,8 +184,6 @@ func createAuthForChannel(channel string) *Auth {
return &Auth{Provider: "aistudio"}
case "antigravity":
return &Auth{Provider: "antigravity"}
case "iflow":
return &Auth{Provider: "iflow"}
case "kimi":
return &Auth{Provider: "kimi"}
case "kiro":

View File

@@ -406,30 +406,6 @@ func (a *Auth) AccountInfo() (string, string) {
}
}
// For iFlow provider, prioritize OAuth type if email is present
if strings.ToLower(a.Provider) == "iflow" {
if a.Metadata != nil {
if email, ok := a.Metadata["email"].(string); ok {
email = strings.TrimSpace(email)
if email != "" {
return "oauth", email
}
}
}
}
// For GitHub provider (including github-copilot), return username
if strings.HasPrefix(strings.ToLower(a.Provider), "github") {
if a.Metadata != nil {
if username, ok := a.Metadata["username"].(string); ok {
username = strings.TrimSpace(username)
if username != "" {
return "oauth", username
}
}
}
}
// Check metadata for email first (OAuth-style auth)
if a.Metadata != nil {
if method, ok := a.Metadata["auth_method"].(string); ok {
@@ -453,6 +429,14 @@ func (a *Auth) AccountInfo() (string, string) {
return "personal_access_token", ""
}
}
// For GitHub provider (including github-copilot), return username when email isn't available.
if strings.HasPrefix(strings.ToLower(a.Provider), "github") {
if username, ok := a.Metadata["username"].(string); ok {
if trimmed := strings.TrimSpace(username); trimmed != "" {
return "oauth", trimmed
}
}
}
if v, ok := a.Metadata["email"].(string); ok {
email := strings.TrimSpace(v)
if email != "" {

View File

@@ -404,7 +404,7 @@ func (s *Service) ensureExecutorsForAuthWithMode(a *coreauth.Auth, forceReplace
}
// Skip disabled auth entries when (re)binding executors.
// Disabled auths can linger during config reloads (e.g., removed OpenAI-compat entries)
// and must not override active provider executors (such as iFlow OAuth accounts).
// and must not override active provider executors.
if a.Disabled {
return
}
@@ -434,8 +434,6 @@ func (s *Service) ensureExecutorsForAuthWithMode(a *coreauth.Auth, forceReplace
s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(s.cfg))
case "claude":
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
case "iflow":
s.coreManager.RegisterExecutor(executor.NewIFlowExecutor(s.cfg))
case "kimi":
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg))
case "kiro":
@@ -962,9 +960,6 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
}
}
models = applyExcludedModels(models, excluded)
case "iflow":
models = registry.GetIFlowModels()
models = applyExcludedModels(models, excluded)
case "kimi":
models = registry.GetKimiModels()
models = applyExcludedModels(models, excluded)

View File

@@ -14,7 +14,6 @@ import (
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/codex"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/gemini"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/geminicli"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/iflow"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/kimi"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/openai"
@@ -1067,184 +1066,6 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
expectErr: false,
},
// iflow tests: glm-test and minimax-test (Cases 90-105)
// glm-test (from: openai, claude)
// Case 90: OpenAI to iflow, no suffix → passthrough
{
name: "90",
from: "openai",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 91: OpenAI to iflow, (medium) → enable_thinking=true
{
name: "91",
from: "openai",
to: "iflow",
model: "glm-test(medium)",
inputJSON: `{"model":"glm-test(medium)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 92: OpenAI to iflow, (auto) → enable_thinking=true
{
name: "92",
from: "openai",
to: "iflow",
model: "glm-test(auto)",
inputJSON: `{"model":"glm-test(auto)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 93: OpenAI to iflow, (none) → enable_thinking=false
{
name: "93",
from: "openai",
to: "iflow",
model: "glm-test(none)",
inputJSON: `{"model":"glm-test(none)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "false",
expectErr: false,
},
// Case 94: Claude to iflow, no suffix → passthrough
{
name: "94",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 95: Claude to iflow, (8192) → enable_thinking=true
{
name: "95",
from: "claude",
to: "iflow",
model: "glm-test(8192)",
inputJSON: `{"model":"glm-test(8192)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 96: Claude to iflow, (-1) → enable_thinking=true
{
name: "96",
from: "claude",
to: "iflow",
model: "glm-test(-1)",
inputJSON: `{"model":"glm-test(-1)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 97: Claude to iflow, (0) → enable_thinking=false
{
name: "97",
from: "claude",
to: "iflow",
model: "glm-test(0)",
inputJSON: `{"model":"glm-test(0)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "false",
expectErr: false,
},
// minimax-test (from: openai, gemini)
// Case 98: OpenAI to iflow, no suffix → passthrough
{
name: "98",
from: "openai",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 99: OpenAI to iflow, (medium) → reasoning_split=true
{
name: "99",
from: "openai",
to: "iflow",
model: "minimax-test(medium)",
inputJSON: `{"model":"minimax-test(medium)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 100: OpenAI to iflow, (auto) → reasoning_split=true
{
name: "100",
from: "openai",
to: "iflow",
model: "minimax-test(auto)",
inputJSON: `{"model":"minimax-test(auto)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 101: OpenAI to iflow, (none) → reasoning_split=false
{
name: "101",
from: "openai",
to: "iflow",
model: "minimax-test(none)",
inputJSON: `{"model":"minimax-test(none)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "reasoning_split",
expectValue: "false",
expectErr: false,
},
// Case 102: Gemini to iflow, no suffix → passthrough
{
name: "102",
from: "gemini",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "",
expectErr: false,
},
// Case 103: Gemini to iflow, (8192) → reasoning_split=true
{
name: "103",
from: "gemini",
to: "iflow",
model: "minimax-test(8192)",
inputJSON: `{"model":"minimax-test(8192)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 104: Gemini to iflow, (-1) → reasoning_split=true
{
name: "104",
from: "gemini",
to: "iflow",
model: "minimax-test(-1)",
inputJSON: `{"model":"minimax-test(-1)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 105: Gemini to iflow, (0) → reasoning_split=false
{
name: "105",
from: "gemini",
to: "iflow",
model: "minimax-test(0)",
inputJSON: `{"model":"minimax-test(0)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "reasoning_split",
expectValue: "false",
expectErr: false,
},
// Gemini Family Cross-Channel Consistency (Cases 106-114)
// Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior
@@ -2462,184 +2283,6 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
expectErr: true,
},
// iflow tests: glm-test and minimax-test (Cases 90-105)
// glm-test (from: openai, claude)
// Case 90: OpenAI to iflow, no param → passthrough
{
name: "90",
from: "openai",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 91: OpenAI to iflow, reasoning_effort=medium → enable_thinking=true
{
name: "91",
from: "openai",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"medium"}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 92: OpenAI to iflow, reasoning_effort=auto → enable_thinking=true
{
name: "92",
from: "openai",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"auto"}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 93: OpenAI to iflow, reasoning_effort=none → enable_thinking=false
{
name: "93",
from: "openai",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "false",
expectErr: false,
},
// Case 94: Claude to iflow, no param → passthrough
{
name: "94",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 95: Claude to iflow, thinking.budget_tokens=8192 → enable_thinking=true
{
name: "95",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":8192}}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 96: Claude to iflow, thinking.budget_tokens=-1 → enable_thinking=true
{
name: "96",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":-1}}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
// Case 97: Claude to iflow, thinking.budget_tokens=0 → enable_thinking=false
{
name: "97",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":0}}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "false",
expectErr: false,
},
// minimax-test (from: openai, gemini)
// Case 98: OpenAI to iflow, no param → passthrough
{
name: "98",
from: "openai",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}]}`,
expectField: "",
expectErr: false,
},
// Case 99: OpenAI to iflow, reasoning_effort=medium → reasoning_split=true
{
name: "99",
from: "openai",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"medium"}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 100: OpenAI to iflow, reasoning_effort=auto → reasoning_split=true
{
name: "100",
from: "openai",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"auto"}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 101: OpenAI to iflow, reasoning_effort=none → reasoning_split=false
{
name: "101",
from: "openai",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`,
expectField: "reasoning_split",
expectValue: "false",
expectErr: false,
},
// Case 102: Gemini to iflow, no param → passthrough
{
name: "102",
from: "gemini",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
expectField: "",
expectErr: false,
},
// Case 103: Gemini to iflow, thinkingBudget=8192 → reasoning_split=true
{
name: "103",
from: "gemini",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 104: Gemini to iflow, thinkingBudget=-1 → reasoning_split=true
{
name: "104",
from: "gemini",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":-1}}}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
// Case 105: Gemini to iflow, thinkingBudget=0 → reasoning_split=false
{
name: "105",
from: "gemini",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}`,
expectField: "reasoning_split",
expectValue: "false",
expectErr: false,
},
// Gemini Family Cross-Channel Consistency (Cases 106-114)
// Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior
@@ -3250,27 +2893,6 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
expectValue: "high",
expectErr: false,
},
{
name: "C19",
from: "claude",
to: "iflow",
model: "glm-test",
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"minimal"}}`,
expectField: "chat_template_kwargs.enable_thinking",
expectValue: "true",
expectErr: false,
},
{
name: "C20",
from: "claude",
to: "iflow",
model: "minimax-test",
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"high"}}`,
expectField: "reasoning_split",
expectValue: "true",
expectErr: false,
},
{
name: "C21",
from: "claude",
@@ -3576,8 +3198,6 @@ 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 "iflow":
hasThinking = gjson.GetBytes(body, "chat_template_kwargs.enable_thinking").Exists() || gjson.GetBytes(body, "reasoning_split").Exists()
}
if hasThinking {
t.Fatalf("expected no thinking field but found one, body=%s", string(body))
@@ -3618,23 +3238,6 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
t.Fatalf("includeThoughts: expected %s, got %s, body=%s", tc.includeThoughts, actual, string(body))
}
}
// Verify clear_thinking for iFlow GLM models when enable_thinking=true
if tc.to == "iflow" && tc.expectField == "chat_template_kwargs.enable_thinking" && tc.expectValue == "true" {
baseModel := thinking.ParseSuffix(tc.model).ModelName
isGLM := strings.HasPrefix(strings.ToLower(baseModel), "glm")
ctVal := gjson.GetBytes(body, "chat_template_kwargs.clear_thinking")
if isGLM {
if !ctVal.Exists() {
t.Fatalf("expected clear_thinking field not found for GLM model, body=%s", string(body))
}
if ctVal.Bool() != false {
t.Fatalf("clear_thinking: expected false, got %v, body=%s", ctVal.Bool(), string(body))
}
} else if ctVal.Exists() {
t.Fatalf("expected no clear_thinking field for non-GLM enable_thinking model, body=%s", string(body))
}
}
})
}
}