mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-05-13 21:42:44 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
internal/auth/claude/anthropic_auth_proxy_test.go
Normal file
33
internal/auth/claude/anthropic_auth_proxy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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{}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
42
internal/auth/kimi/kimi_proxy_test.go
Normal file
42
internal/auth/kimi/kimi_proxy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ func newAuthManager() *sdkAuth.Manager {
|
||||
sdkAuth.NewGeminiAuthenticator(),
|
||||
sdkAuth.NewCodexAuthenticator(),
|
||||
sdkAuth.NewClaudeAuthenticator(),
|
||||
sdkAuth.NewIFlowAuthenticator(),
|
||||
sdkAuth.NewAntigravityAuthenticator(),
|
||||
sdkAuth.NewKimiAuthenticator(),
|
||||
sdkAuth.NewKiroAuthenticator(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
20
sdk/api/handlers/handlers_metadata_test.go
Normal file
20
sdk/api/handlers/handlers_metadata_test.go
Normal 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])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user