diff --git a/config.example.yaml b/config.example.yaml index a2e9d03b..8daf90d2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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: diff --git a/docker-build.sh b/docker-build.sh index 944f3e78..4538b807 100644 --- a/docker-build.sh +++ b/docker-build.sh @@ -109,10 +109,19 @@ wait_for_service() { sleep 2 } -if [[ "${1:-}" == "--with-usage" ]]; then - WITH_USAGE=true - export_stats_api_secret -fi +case "${1:-}" in + "") + ;; + "--with-usage") + WITH_USAGE=true + export_stats_api_secret + ;; + *) + 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:" diff --git a/internal/api/handlers/management/oauth_sessions.go b/internal/api/handlers/management/oauth_sessions.go index 156eeb79..8c985e09 100644 --- a/internal/api/handlers/management/oauth_sessions.go +++ b/internal/api/handlers/management/oauth_sessions.go @@ -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": diff --git a/internal/api/server.go b/internal/api/server.go index 6dd6916d..ee24aead 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -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") @@ -683,18 +669,18 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/gitlab-auth-url", s.mgmt.RequestGitLabToken) mgmt.POST("/gitlab-auth-url", s.mgmt.RequestGitLabPATToken) mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken) - mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken) - mgmt.GET("/kilo-auth-url", s.mgmt.RequestKiloToken) - mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken) - mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken) - mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken) - mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken) - mgmt.GET("/cursor-auth-url", s.mgmt.RequestCursorToken) - mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken) - mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback) - mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) + mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken) + mgmt.GET("/kilo-auth-url", s.mgmt.RequestKiloToken) + mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken) + mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken) + mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken) + mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken) + mgmt.GET("/cursor-auth-url", s.mgmt.RequestCursorToken) + mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken) + mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback) + mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) + } } -} func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/auth/claude/anthropic_auth.go b/internal/auth/claude/anthropic_auth.go index 12bb53ac..6c770abf 100644 --- a/internal/auth/claude/anthropic_auth.go +++ b/internal/auth/claude/anthropic_auth.go @@ -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), } } diff --git a/internal/auth/claude/anthropic_auth_proxy_test.go b/internal/auth/claude/anthropic_auth_proxy_test.go new file mode 100644 index 00000000..50c48757 --- /dev/null +++ b/internal/auth/claude/anthropic_auth_proxy_test.go @@ -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) + } +} diff --git a/internal/auth/codex/openai_auth.go b/internal/auth/codex/openai_auth.go index 64bc00a6..67b54b17 100644 --- a/internal/auth/codex/openai_auth.go +++ b/internal/auth/codex/openai_auth.go @@ -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{}), } } diff --git a/internal/auth/codex/openai_auth_test.go b/internal/auth/codex/openai_auth_test.go index 3327eb4a..a7fe8307 100644 --- a/internal/auth/codex/openai_auth_test.go +++ b/internal/auth/codex/openai_auth_test.go @@ -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) + } +} diff --git a/internal/auth/kimi/kimi.go b/internal/auth/kimi/kimi.go index 8427a057..ccb1a6c2 100644 --- a/internal/auth/kimi/kimi.go +++ b/internal/auth/kimi/kimi.go @@ -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 { + 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 { - client = util.SetProxy(&cfg.SDKConfig, client) + 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() diff --git a/internal/auth/kimi/kimi_proxy_test.go b/internal/auth/kimi/kimi_proxy_test.go new file mode 100644 index 00000000..130f34f5 --- /dev/null +++ b/internal/auth/kimi/kimi_proxy_test.go @@ -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) + } +} diff --git a/internal/cmd/auth_manager.go b/internal/cmd/auth_manager.go index 03938648..fe6bfe33 100644 --- a/internal/cmd/auth_manager.go +++ b/internal/cmd/auth_manager.go @@ -15,7 +15,6 @@ func newAuthManager() *sdkAuth.Manager { sdkAuth.NewGeminiAuthenticator(), sdkAuth.NewCodexAuthenticator(), sdkAuth.NewClaudeAuthenticator(), - sdkAuth.NewIFlowAuthenticator(), sdkAuth.NewAntigravityAuthenticator(), sdkAuth.NewKimiAuthenticator(), sdkAuth.NewKiroAuthenticator(), diff --git a/internal/config/config.go b/internal/config/config.go index dc870267..775d15f2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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. diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 7bca859d..28706795 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -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(), diff --git a/internal/registry/model_updater.go b/internal/registry/model_updater.go index 9ed09c2f..2512a296 100644 --- a/internal/registry/model_updater.go +++ b/internal/registry/model_updater.go @@ -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}, } diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json index d4788ec1..65d83251 100644 --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -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", @@ -2022,4 +1864,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 5c94c76c..51a12348 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -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 diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index acca590a..41b1c325 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -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 diff --git a/internal/runtime/executor/helps/thinking_providers.go b/internal/runtime/executor/helps/thinking_providers.go index 36b63c90..bbd01962 100644 --- a/internal/runtime/executor/helps/thinking_providers.go +++ b/internal/runtime/executor/helps/thinking_providers.go @@ -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" ) diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index 0c911085..931e3a56 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -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 diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index c79ecd8e..1edeac87 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -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{} -} diff --git a/internal/thinking/convert.go b/internal/thinking/convert.go index 89db7745..b22a0879 100644 --- a/internal/thinking/convert.go +++ b/internal/thinking/convert.go @@ -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). diff --git a/internal/thinking/strip.go b/internal/thinking/strip.go index 85498c01..1e1712d1 100644 --- a/internal/thinking/strip.go +++ b/internal/thinking/strip.go @@ -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 } diff --git a/internal/thinking/types.go b/internal/thinking/types.go index 5e45fc6b..a31d7981 100644 --- a/internal/thinking/types.go +++ b/internal/thinking/types.go @@ -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" diff --git a/internal/tui/oauth_tab.go b/internal/tui/oauth_tab.go index 1df045ac..bed17e4f 100644 --- a/internal/tui/oauth_tab.go +++ b/internal/tui/oauth_tab.go @@ -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 } diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 168a5263..5c416219 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -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 } diff --git a/sdk/api/handlers/handlers_metadata_test.go b/sdk/api/handlers/handlers_metadata_test.go new file mode 100644 index 00000000..99af872d --- /dev/null +++ b/sdk/api/handlers/handlers_metadata_test.go @@ -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]) + } +} diff --git a/sdk/api/management.go b/sdk/api/management.go index b1a7f8e3..a5a1cfc4 100644 --- a/sdk/api/management.go +++ b/sdk/api/management.go @@ -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) } diff --git a/sdk/auth/refresh_registry.go b/sdk/auth/refresh_registry.go index f3419de5..c72dd6ea 100644 --- a/sdk/auth/refresh_registry.go +++ b/sdk/auth/refresh_registry.go @@ -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() }) diff --git a/sdk/cliproxy/auth/conductor_overrides_test.go b/sdk/cliproxy/auth/conductor_overrides_test.go index 0adc83a6..f74621be 100644 --- a/sdk/cliproxy/auth/conductor_overrides_test.go +++ b/sdk/cliproxy/auth/conductor_overrides_test.go @@ -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) } diff --git a/sdk/cliproxy/auth/oauth_model_alias_test.go b/sdk/cliproxy/auth/oauth_model_alias_test.go index 2d92b010..47bb285e 100644 --- a/sdk/cliproxy/auth/oauth_model_alias_test.go +++ b/sdk/cliproxy/auth/oauth_model_alias_test.go @@ -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": diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 3784e0af..16814eb8 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -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 != "" { diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index c6a4f15e..d46de64f 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -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) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index a6219053..984d3564 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -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", @@ -3447,10 +3069,10 @@ func getTestModels() []*registry.ModelInfo { UserDefined: true, Thinking: nil, }, - { - ID: "glm-test", - Object: "model", - Created: 1700000000, + { + ID: "glm-test", + Object: "model", + Created: 1700000000, OwnedBy: "test", Type: "iflow", DisplayName: "GLM Test Model", @@ -3507,11 +3129,11 @@ func getTestModels() []*registry.ModelInfo { Created: 1700000000, OwnedBy: "github-copilot", Type: "github-copilot", - DisplayName: "GPT-5.2 Codex", - Thinking: ®istry.ThinkingSupport{Levels: []string{"none", "low", "medium", "high", "xhigh"}, ZeroAllowed: true, DynamicAllowed: false}, - }, + DisplayName: "GPT-5.2 Codex", + Thinking: ®istry.ThinkingSupport{Levels: []string{"none", "low", "medium", "high", "xhigh"}, ZeroAllowed: true, DynamicAllowed: false}, + }, + } } -} // runThinkingTests runs thinking test cases using the real data flow path. func runThinkingTests(t *testing.T, cases []thinkingTestCase) { @@ -3522,23 +3144,23 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) { suffixResult := thinking.ParseSuffix(tc.model) baseModel := suffixResult.ModelName - translateTo := tc.to - applyTo := tc.to - if tc.to == "iflow" { - translateTo = "openai" - applyTo = "iflow" - } + translateTo := tc.to + applyTo := tc.to + if tc.to == "iflow" { + translateTo = "openai" + applyTo = "iflow" + } if tc.to == "github-copilot" { if tc.from == "openai-response" { translateTo = "codex" applyTo = "codex" } else { translateTo = "openai" - applyTo = "openai" + applyTo = "openai" + } } - } - body := sdktranslator.TranslateRequest( + body := sdktranslator.TranslateRequest( sdktranslator.FromString(tc.from), sdktranslator.FromString(translateTo), baseModel, @@ -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)) - } - } }) } }