diff --git a/cmd/fetch_antigravity_models/main.go b/cmd/fetch_antigravity_models/main.go index d4328eb32f..250bcbdfa3 100644 --- a/cmd/fetch_antigravity_models/main.go +++ b/cmd/fetch_antigravity_models/main.go @@ -25,11 +25,11 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - sdkauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + sdkauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/cmd/server/main.go b/cmd/server/main.go index b10bc9c8dd..1ef8300661 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,28 +10,31 @@ import ( "fmt" "io" "io/fs" + "net" "net/url" "os" "path/filepath" + "strconv" "strings" "time" "github.com/joho/godotenv" - configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" - "github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cmd" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/store" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" - "github.com/router-for-me/CLIProxyAPI/v6/internal/tui" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access" + "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cmd" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/store" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/tui" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) @@ -70,6 +73,8 @@ func main() { var vertexImportPrefix string var configPath string var password string + var homeAddr string + var homePassword string var tuiMode bool var standalone bool var localModel bool @@ -88,6 +93,8 @@ func main() { flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file") flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)") flag.StringVar(&password, "password", "", "") + flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port format (loads config from home and skips local config file)") + flag.StringVar(&homePassword, "home-password", "", "Home control plane password (Redis AUTH)") flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI") flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server") flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching") @@ -126,6 +133,7 @@ func main() { var err error var cfg *config.Config var isCloudDeploy bool + var configLoadedFromHome bool var ( usePostgresStore bool pgStoreDSN string @@ -236,7 +244,68 @@ func main() { // Determine and load the configuration file. // Prefer the Postgres store when configured, otherwise fallback to git or local files. var configFilePath string - if usePostgresStore { + if strings.TrimSpace(homeAddr) != "" { + configLoadedFromHome = true + trimmedHomePassword := strings.TrimSpace(homePassword) + host, portStr, errSplit := net.SplitHostPort(strings.TrimSpace(homeAddr)) + if errSplit != nil { + log.Errorf("invalid -home address %q (expected host:port): %v", homeAddr, errSplit) + return + } + host = strings.TrimSpace(host) + if host == "" { + log.Errorf("invalid -home address %q: host is empty", homeAddr) + return + } + port, errPort := strconv.Atoi(strings.TrimSpace(portStr)) + if errPort != nil || port <= 0 { + log.Errorf("invalid -home address %q: invalid port %q", homeAddr, portStr) + return + } + + homeCfg := config.HomeConfig{ + Enabled: true, + Host: host, + Port: port, + Password: trimmedHomePassword, + } + homeClient := home.New(homeCfg) + defer homeClient.Close() + + ctxHome, cancelHome := context.WithTimeout(context.Background(), 30*time.Second) + raw, errGetConfig := homeClient.GetConfig(ctxHome) + cancelHome() + if errGetConfig != nil { + log.Errorf("failed to fetch config from home: %v", errGetConfig) + return + } + + parsed, errParseConfig := config.ParseConfigBytes(raw) + if errParseConfig != nil { + log.Errorf("failed to parse config payload from home: %v", errParseConfig) + return + } + if parsed == nil { + parsed = &config.Config{} + } + parsed.Home = homeCfg + parsed.Port = 8317 // Default to 8317 for home mode, can be overridden by home config + parsed.UsageStatisticsEnabled = true + cfg = parsed + + // Keep a non-empty config path for downstream components (log paths, management assets, etc), + // but do not require the file to exist when loading config from home. + if strings.TrimSpace(configPath) != "" { + configFilePath = configPath + } else { + configFilePath = filepath.Join(wd, "config.yaml") + } + + // Local stores are intentionally disabled when config is loaded from home. + usePostgresStore = false + useObjectStore = false + useGitStore = false + } else if usePostgresStore { if pgStoreLocalPath == "" { pgStoreLocalPath = wd } @@ -400,21 +469,25 @@ func main() { // In cloud deploy mode, check if we have a valid configuration var configFileExists bool if isCloudDeploy { - if info, errStat := os.Stat(configFilePath); errStat != nil { - // Don't mislead: API server will not start until configuration is provided. - log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration") - configFileExists = false - } else if info.IsDir() { - log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration") - configFileExists = false - } else if cfg.Port == 0 { - // LoadConfigOptional returns empty config when file is empty or invalid. - // Config file exists but is empty or invalid; treat as missing config - log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration") - configFileExists = false + if configLoadedFromHome && cfg != nil { + configFileExists = cfg.Port != 0 } else { - log.Info("Cloud deploy mode: Configuration file detected; starting service") - configFileExists = true + if info, errStat := os.Stat(configFilePath); errStat != nil { + // Don't mislead: API server will not start until configuration is provided. + log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration") + configFileExists = false + } else if info.IsDir() { + log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration") + configFileExists = false + } else if cfg.Port == 0 { + // LoadConfigOptional returns empty config when file is empty or invalid. + // Config file exists but is empty or invalid; treat as missing config + log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration") + configFileExists = false + } else { + log.Info("Cloud deploy mode: Configuration file detected; starting service") + configFileExists = true + } } } redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled) @@ -496,8 +569,10 @@ func main() { // Standalone mode: start an embedded local server and connect TUI client to it. managementasset.StartAutoUpdater(context.Background(), configFilePath) misc.StartAntigravityVersionUpdater(context.Background()) - if !localModel { + if !localModel && !cfg.Home.Enabled { registry.StartModelsUpdater(context.Background()) + } else if cfg.Home.Enabled { + log.Info("Home mode: remote model updates disabled") } hook := tui.NewLogHook(2000) hook.SetFormatter(&logging.LogFormatter{}) @@ -572,8 +647,10 @@ func main() { // Start the main proxy service managementasset.StartAutoUpdater(context.Background(), configFilePath) misc.StartAntigravityVersionUpdater(context.Background()) - if !localModel { + if !localModel && !cfg.Home.Enabled { registry.StartModelsUpdater(context.Background()) + } else if cfg.Home.Enabled { + log.Info("Home mode: remote model updates disabled") } cmd.StartService(cfg, configFilePath, password) } diff --git a/config.example.yaml b/config.example.yaml index d7d5a9f56b..886d775a5d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -11,6 +11,13 @@ tls: cert: "" key: "" +# Optional "home" control plane integration over Redis protocol. +home: + enabled: false + host: "127.0.0.1" + port: 6379 + password: "" + # Management API settings remote-management: # Whether to allow remote (non-localhost) management access. @@ -67,6 +74,7 @@ error-logs-max-files: 10 usage-statistics-enabled: false # How long (in seconds) Redis usage queue items are retained in memory for the RESP interface (LPOP/RPOP). +# Note: the in-process Redis RESP usage output is disabled when home.enabled is true. # Default: 60. Max: 3600. redis-usage-queue-retention-seconds: 60 @@ -149,6 +157,7 @@ nonstream-keepalive-interval: 0 # gemini-api-key: # - api-key: "AIzaSy...01" # prefix: "test" # optional: require calls like "test/gemini-3-pro-preview" to target this credential +# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling # base-url: "https://generativelanguage.googleapis.com" # headers: # X-Custom-Header: "custom-value" @@ -168,6 +177,7 @@ nonstream-keepalive-interval: 0 # codex-api-key: # - api-key: "sk-atSM..." # prefix: "test" # optional: require calls like "test/gpt-5-codex" to target this credential +# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling # base-url: "https://www.example.com" # use the custom codex API endpoint # headers: # X-Custom-Header: "custom-value" @@ -187,6 +197,7 @@ nonstream-keepalive-interval: 0 # - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url # - api-key: "sk-atSM..." # prefix: "test" # optional: require calls like "test/claude-sonnet-latest" to target this credential +# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling # base-url: "https://www.example.com" # use the custom claude API endpoint # headers: # X-Custom-Header: "custom-value" @@ -242,6 +253,7 @@ nonstream-keepalive-interval: 0 # disabled: false # optional: set to true to disable this provider without removing it # prefix: "test" # optional: require calls like "test/kimi-k2" to target this provider's credentials # base-url: "https://openrouter.ai/api/v1" # The base URL of the provider. +# disable-cooling: false # optional: per-provider override for auth/model cooldown scheduling # headers: # X-Custom-Header: "custom-value" # api-key-entries: diff --git a/examples/custom-provider/main.go b/examples/custom-provider/main.go index fdbae275e8..6f37c341de 100644 --- a/examples/custom-provider/main.go +++ b/examples/custom-provider/main.go @@ -24,14 +24,14 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/logging" - sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/logging" + sdktr "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) const ( diff --git a/examples/http-request/main.go b/examples/http-request/main.go index a667a9ca0c..1e0215ecea 100644 --- a/examples/http-request/main.go +++ b/examples/http-request/main.go @@ -16,8 +16,8 @@ import ( "strings" "time" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" ) diff --git a/examples/translator/main.go b/examples/translator/main.go index 88f142a3d2..524a303eb8 100644 --- a/examples/translator/main.go +++ b/examples/translator/main.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" - _ "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator/builtin" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" + _ "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator/builtin" ) func main() { diff --git a/go.mod b/go.mod index 7ad363a716..9ad89ae44c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/router-for-me/CLIProxyAPI/v6 +module github.com/router-for-me/CLIProxyAPI/v7 go 1.26.0 @@ -31,6 +31,12 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/redis/go-redis/v9 v9.19.0 // indirect + go.uber.org/atomic v1.11.0 // indirect +) + require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect diff --git a/go.sum b/go.sum index e811b0123b..5f0a03fbef 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -158,6 +160,8 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -203,6 +207,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/internal/access/config_access/provider.go b/internal/access/config_access/provider.go index 84e8abcb0e..915160b76f 100644 --- a/internal/access/config_access/provider.go +++ b/internal/access/config_access/provider.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) // Register ensures the config-access provider is available to the access manager. diff --git a/internal/access/reconcile.go b/internal/access/reconcile.go index 36601f9998..d71e2b8d28 100644 --- a/internal/access/reconcile.go +++ b/internal/access/reconcile.go @@ -6,9 +6,9 @@ import ( "sort" "strings" - configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" + configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" log "github.com/sirupsen/logrus" ) diff --git a/internal/api/handlers/management/api_key_usage.go b/internal/api/handlers/management/api_key_usage.go index 3361da5d28..dbe6fbd998 100644 --- a/internal/api/handlers/management/api_key_usage.go +++ b/internal/api/handlers/management/api_key_usage.go @@ -6,7 +6,7 @@ import ( "time" "github.com/gin-gonic/gin" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) type apiKeyUsageEntry struct { diff --git a/internal/api/handlers/management/api_key_usage_test.go b/internal/api/handlers/management/api_key_usage_test.go index 2880567f8c..f2be17d7db 100644 --- a/internal/api/handlers/management/api_key_usage_test.go +++ b/internal/api/handlers/management/api_key_usage_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) { diff --git a/internal/api/handlers/management/api_tools.go b/internal/api/handlers/management/api_tools.go index 51b08cea4f..f10850701a 100644 --- a/internal/api/handlers/management/api_tools.go +++ b/internal/api/handlers/management/api_tools.go @@ -11,10 +11,10 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/geminicli" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/oauth2" "golang.org/x/oauth2/google" diff --git a/internal/api/handlers/management/api_tools_test.go b/internal/api/handlers/management/api_tools_test.go index b27fe6395a..b089eb4a6e 100644 --- a/internal/api/handlers/management/api_tools_test.go +++ b/internal/api/handlers/management/api_tools_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestAPICallTransportDirectBypassesGlobalProxy(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 285b3ae291..d7e798977e 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -22,17 +22,17 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/antigravity" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/antigravity" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + geminiAuth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "golang.org/x/oauth2" diff --git a/internal/api/handlers/management/auth_files_batch_test.go b/internal/api/handlers/management/auth_files_batch_test.go index 44cdbd5b5f..ec001ae586 100644 --- a/internal/api/handlers/management/auth_files_batch_test.go +++ b/internal/api/handlers/management/auth_files_batch_test.go @@ -12,8 +12,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestUploadAuthFile_BatchMultipart(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files_delete_test.go b/internal/api/handlers/management/auth_files_delete_test.go index 7b7b888c4b..a57c9993ad 100644 --- a/internal/api/handlers/management/auth_files_delete_test.go +++ b/internal/api/handlers/management/auth_files_delete_test.go @@ -11,8 +11,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestDeleteAuthFile_UsesAuthPathFromManager(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files_download_test.go b/internal/api/handlers/management/auth_files_download_test.go index a2a20d305a..88024fbba5 100644 --- a/internal/api/handlers/management/auth_files_download_test.go +++ b/internal/api/handlers/management/auth_files_download_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestDownloadAuthFile_ReturnsFile(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files_download_windows_test.go b/internal/api/handlers/management/auth_files_download_windows_test.go index 8c174ccf51..88fc7f1146 100644 --- a/internal/api/handlers/management/auth_files_download_windows_test.go +++ b/internal/api/handlers/management/auth_files_download_windows_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestDownloadAuthFile_PreventsWindowsSlashTraversal(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files_patch_fields_test.go b/internal/api/handlers/management/auth_files_patch_fields_test.go index 3ca70012c0..568700a0d6 100644 --- a/internal/api/handlers/management/auth_files_patch_fields_test.go +++ b/internal/api/handlers/management/auth_files_patch_fields_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestPatchAuthFileFields_MergeHeadersAndDeleteEmptyValues(t *testing.T) { diff --git a/internal/api/handlers/management/auth_files_recent_requests_test.go b/internal/api/handlers/management/auth_files_recent_requests_test.go index 979040f58b..404bf4848f 100644 --- a/internal/api/handlers/management/auth_files_recent_requests_test.go +++ b/internal/api/handlers/management/auth_files_recent_requests_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) { diff --git a/internal/api/handlers/management/config_auth_index.go b/internal/api/handlers/management/config_auth_index.go index 7b01512559..f2bbc2ff38 100644 --- a/internal/api/handlers/management/config_auth_index.go +++ b/internal/api/handlers/management/config_auth_index.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer" ) type geminiKeyWithAuthIndex struct { diff --git a/internal/api/handlers/management/config_basic.go b/internal/api/handlers/management/config_basic.go index f77e91e9ba..a0818aa8ae 100644 --- a/internal/api/handlers/management/config_basic.go +++ b/internal/api/handlers/management/config_basic.go @@ -11,9 +11,9 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index e487627a00..f8ef3203c7 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // Generic helpers for list[string] diff --git a/internal/api/handlers/management/config_lists_delete_keys_test.go b/internal/api/handlers/management/config_lists_delete_keys_test.go index aaa43910e7..a548805eda 100644 --- a/internal/api/handlers/management/config_lists_delete_keys_test.go +++ b/internal/api/handlers/management/config_lists_delete_keys_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func writeTestConfigFile(t *testing.T) string { diff --git a/internal/api/handlers/management/handler.go b/internal/api/handlers/management/handler.go index 9abc8a5c8a..0f884ef05a 100644 --- a/internal/api/handlers/management/handler.go +++ b/internal/api/handlers/management/handler.go @@ -13,10 +13,10 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "golang.org/x/crypto/bcrypt" ) diff --git a/internal/api/handlers/management/handler_test.go b/internal/api/handlers/management/handler_test.go index f3a6086e95..a77dc36f35 100644 --- a/internal/api/handlers/management/handler_test.go +++ b/internal/api/handlers/management/handler_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestAuthenticateManagementKey_LocalhostIPBan_BlocksCorrectKeyDuringBan(t *testing.T) { diff --git a/internal/api/handlers/management/logs.go b/internal/api/handlers/management/logs.go index b64cd61938..ca6d7eda81 100644 --- a/internal/api/handlers/management/logs.go +++ b/internal/api/handlers/management/logs.go @@ -13,7 +13,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" ) const ( diff --git a/internal/api/handlers/management/model_definitions.go b/internal/api/handlers/management/model_definitions.go index 85ff314bf4..0d1b8af437 100644 --- a/internal/api/handlers/management/model_definitions.go +++ b/internal/api/handlers/management/model_definitions.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" ) // GetStaticModelDefinitions returns static model metadata for a given channel. diff --git a/internal/api/handlers/management/test_store_test.go b/internal/api/handlers/management/test_store_test.go index cf7dbaf7d0..2eaacd904f 100644 --- a/internal/api/handlers/management/test_store_test.go +++ b/internal/api/handlers/management/test_store_test.go @@ -4,7 +4,7 @@ import ( "context" "sync" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) type memoryAuthStore struct { diff --git a/internal/api/handlers/management/usage.go b/internal/api/handlers/management/usage.go index dfddf50346..c1602c0423 100644 --- a/internal/api/handlers/management/usage.go +++ b/internal/api/handlers/management/usage.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" ) type usageQueueRecord []byte diff --git a/internal/api/handlers/management/usage_test.go b/internal/api/handlers/management/usage_test.go index ca46d976f5..bdb8aa2e29 100644 --- a/internal/api/handlers/management/usage_test.go +++ b/internal/api/handlers/management/usage_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" ) func TestGetUsageQueuePopsRequestedRecords(t *testing.T) { diff --git a/internal/api/handlers/management/vertex_import.go b/internal/api/handlers/management/vertex_import.go index bad066a270..bb064b9fb9 100644 --- a/internal/api/handlers/management/vertex_import.go +++ b/internal/api/handlers/management/vertex_import.go @@ -9,8 +9,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // ImportVertexCredential handles uploading a Vertex service account JSON and saving it as an auth record. diff --git a/internal/api/middleware/request_logging.go b/internal/api/middleware/request_logging.go index b57dd8aa42..7a10fad8a1 100644 --- a/internal/api/middleware/request_logging.go +++ b/internal/api/middleware/request_logging.go @@ -11,8 +11,8 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" ) const maxErrorOnlyCapturedRequestBodyBytes int64 = 1 << 20 // 1 MiB diff --git a/internal/api/middleware/response_writer.go b/internal/api/middleware/response_writer.go index 7f4892674a..5a89ed0fdf 100644 --- a/internal/api/middleware/response_writer.go +++ b/internal/api/middleware/response_writer.go @@ -10,8 +10,8 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" ) const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE" diff --git a/internal/api/middleware/response_writer_test.go b/internal/api/middleware/response_writer_test.go index f5c21deb8a..fa0bd54854 100644 --- a/internal/api/middleware/response_writer_test.go +++ b/internal/api/middleware/response_writer_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" ) func TestExtractRequestBodyPrefersOverride(t *testing.T) { diff --git a/internal/api/modules/amp/amp.go b/internal/api/modules/amp/amp.go index a12733e2a1..18c8ac1ef0 100644 --- a/internal/api/modules/amp/amp.go +++ b/internal/api/modules/amp/amp.go @@ -9,9 +9,9 @@ import ( "sync" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" log "github.com/sirupsen/logrus" ) diff --git a/internal/api/modules/amp/amp_test.go b/internal/api/modules/amp/amp_test.go index 430c4b62a7..5ca01754a2 100644 --- a/internal/api/modules/amp/amp_test.go +++ b/internal/api/modules/amp/amp_test.go @@ -9,10 +9,10 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" ) func TestAmpModule_Name(t *testing.T) { diff --git a/internal/api/modules/amp/fallback_handlers.go b/internal/api/modules/amp/fallback_handlers.go index e4e0f8a650..06e0a035d0 100644 --- a/internal/api/modules/amp/fallback_handlers.go +++ b/internal/api/modules/amp/fallback_handlers.go @@ -8,8 +8,8 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/api/modules/amp/fallback_handlers_test.go b/internal/api/modules/amp/fallback_handlers_test.go index a687fd116b..1aacaae21f 100644 --- a/internal/api/modules/amp/fallback_handlers_test.go +++ b/internal/api/modules/amp/fallback_handlers_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" ) func TestFallbackHandler_ModelMapping_PreservesThinkingSuffixAndRewritesResponse(t *testing.T) { diff --git a/internal/api/modules/amp/model_mapping.go b/internal/api/modules/amp/model_mapping.go index 4159a2b576..2b68866edf 100644 --- a/internal/api/modules/amp/model_mapping.go +++ b/internal/api/modules/amp/model_mapping.go @@ -7,9 +7,9 @@ import ( "strings" "sync" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/internal/api/modules/amp/model_mapping_test.go b/internal/api/modules/amp/model_mapping_test.go index 53165d22c3..dcfb07ee5e 100644 --- a/internal/api/modules/amp/model_mapping_test.go +++ b/internal/api/modules/amp/model_mapping_test.go @@ -3,8 +3,8 @@ package amp import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" ) func TestNewModelMapper(t *testing.T) { diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index c8010854f3..54f4b734ba 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -14,7 +14,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" log "github.com/sirupsen/logrus" ) diff --git a/internal/api/modules/amp/proxy_test.go b/internal/api/modules/amp/proxy_test.go index 49dba956c0..2852efde3a 100644 --- a/internal/api/modules/amp/proxy_test.go +++ b/internal/api/modules/amp/proxy_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // Helper: compress data with gzip diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index b7253c3458..84023d156d 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -9,11 +9,11 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai" log "github.com/sirupsen/logrus" ) @@ -21,12 +21,12 @@ import ( // from gin.Context to the request context for SecretSource lookup. type clientAPIKeyContextKey struct{} -// clientAPIKeyMiddleware injects the authenticated client API key from gin.Context["apiKey"] +// clientAPIKeyMiddleware injects the authenticated client API key from gin.Context["userApiKey"] // into the request context so that SecretSource can look it up for per-client upstream routing. func clientAPIKeyMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Extract the client API key from gin context (set by AuthMiddleware) - if apiKey, exists := c.Get("apiKey"); exists { + if apiKey, exists := c.Get("userApiKey"); exists { if keyStr, ok := apiKey.(string); ok && keyStr != "" { // Inject into request context for SecretSource.Get(ctx) to read ctx := context.WithValue(c.Request.Context(), clientAPIKeyContextKey{}, keyStr) diff --git a/internal/api/modules/amp/routes_test.go b/internal/api/modules/amp/routes_test.go index 2308a153bb..a500f8150c 100644 --- a/internal/api/modules/amp/routes_test.go +++ b/internal/api/modules/amp/routes_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" ) func TestRegisterManagementRoutes(t *testing.T) { diff --git a/internal/api/modules/amp/secret.go b/internal/api/modules/amp/secret.go index f91c72ba9c..512d263d0c 100644 --- a/internal/api/modules/amp/secret.go +++ b/internal/api/modules/amp/secret.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" log "github.com/sirupsen/logrus" ) diff --git a/internal/api/modules/amp/secret_test.go b/internal/api/modules/amp/secret_test.go index 6a6f6ba265..17a75b15de 100644 --- a/internal/api/modules/amp/secret_test.go +++ b/internal/api/modules/amp/secret_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" log "github.com/sirupsen/logrus" "github.com/sirupsen/logrus/hooks/test" ) diff --git a/internal/api/modules/modules.go b/internal/api/modules/modules.go index 8c5447d96d..5ddfa609c8 100644 --- a/internal/api/modules/modules.go +++ b/internal/api/modules/modules.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" ) // Context encapsulates the dependencies exposed to routing modules during diff --git a/internal/api/protocol_multiplexer.go b/internal/api/protocol_multiplexer.go index 14068dc556..607d55a7ce 100644 --- a/internal/api/protocol_multiplexer.go +++ b/internal/api/protocol_multiplexer.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "strings" + "time" log "github.com/sirupsen/logrus" ) @@ -48,62 +49,89 @@ func (s *Server) acceptMuxConnections(listener net.Listener, httpListener *muxLi continue } - tlsConn, ok := conn.(*tls.Conn) - if ok { - if errHandshake := tlsConn.Handshake(); errHandshake != nil { + // Dispatch each connection to a goroutine so that slow/idle clients + // cannot block the accept loop. Previously, TLS handshake and + // reader.Peek(1) were performed inline; an idle TCP connection that + // never sent bytes would block Peek indefinitely, preventing all + // subsequent connections from being accepted (issue #3267). + go s.routeMuxConnection(conn, httpListener) + } +} + +// routeMuxConnection performs per-connection protocol detection and routing. +func (s *Server) routeMuxConnection(conn net.Conn, httpListener *muxListener) { + // Set a read deadline so that idle connections that never send bytes do not + // leak goroutines and file descriptors. The deadline is cleared once the + // connection is successfully routed to its handler. + const muxSniffDeadline = 10 * time.Second + _ = conn.SetReadDeadline(time.Now().Add(muxSniffDeadline)) + + tlsConn, ok := conn.(*tls.Conn) + if ok { + if errHandshake := tlsConn.Handshake(); errHandshake != nil { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close connection after TLS handshake error: %v", errClose) + } + return + } + proto := strings.TrimSpace(tlsConn.ConnectionState().NegotiatedProtocol) + if proto == "h2" || proto == "http/1.1" { + if httpListener == nil { if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection after TLS handshake error: %v", errClose) + log.Errorf("failed to close connection: %v", errClose) } - continue + return } - proto := strings.TrimSpace(tlsConn.ConnectionState().NegotiatedProtocol) - if proto == "h2" || proto == "http/1.1" { - if httpListener == nil { - if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection: %v", errClose) - } - continue - } - if errPut := httpListener.Put(tlsConn); errPut != nil { - if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection after HTTP routing failure: %v", errClose) - } + if errPut := httpListener.Put(tlsConn); errPut != nil { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close connection after HTTP routing failure: %v", errClose) } - continue + } else { + _ = conn.SetReadDeadline(time.Time{}) } + return } + } - reader := bufio.NewReader(conn) - prefix, errPeek := reader.Peek(1) - if errPeek != nil { - if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection after protocol peek failure: %v", errClose) - } - continue + reader := bufio.NewReader(conn) + prefix, errPeek := reader.Peek(1) + if errPeek != nil { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close connection after protocol peek failure: %v", errClose) } + return + } - if isRedisRESPPrefix(prefix[0]) { - if !s.managementRoutesEnabled.Load() { - if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close redis connection while management is disabled: %v", errClose) - } - continue + if isRedisRESPPrefix(prefix[0]) { + if s.cfg != nil && s.cfg.Home.Enabled { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close redis connection while home mode is enabled: %v", errClose) } - go s.handleRedisConnection(conn, reader) - continue + return } - - if httpListener == nil { + if !s.managementRoutesEnabled.Load() { if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection without HTTP listener: %v", errClose) + log.Errorf("failed to close redis connection while management is disabled: %v", errClose) } - continue + return } + _ = conn.SetReadDeadline(time.Time{}) + s.handleRedisConnection(conn, reader) + return + } - if errPut := httpListener.Put(&bufferedConn{Conn: conn, reader: reader}); errPut != nil { - if errClose := conn.Close(); errClose != nil { - log.Errorf("failed to close connection after HTTP routing failure: %v", errClose) - } + if httpListener == nil { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close connection without HTTP listener: %v", errClose) + } + return + } + + if errPut := httpListener.Put(&bufferedConn{Conn: conn, reader: reader}); errPut != nil { + if errClose := conn.Close(); errClose != nil { + log.Errorf("failed to close connection after HTTP routing failure: %v", errClose) } + } else { + _ = conn.SetReadDeadline(time.Time{}) } } diff --git a/internal/api/protocol_multiplexer_test.go b/internal/api/protocol_multiplexer_test.go new file mode 100644 index 0000000000..6769c76afb --- /dev/null +++ b/internal/api/protocol_multiplexer_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "net" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" +) + +func TestAcceptMuxNotBlockedByIdleConnection(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer listener.Close() + + var routed atomic.Int32 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + routed.Add(1) + w.WriteHeader(http.StatusOK) + }) + srv := httptest.NewUnstartedServer(handler) + defer srv.Close() + + muxLn := newMuxListener(listener.Addr(), 1024) + server := &Server{managementRoutesEnabled: atomic.Bool{}} + server.managementRoutesEnabled.Store(false) + + errCh := make(chan error, 1) + go func() { + errCh <- server.acceptMuxConnections(listener, muxLn) + }() + + srv.Listener = muxLn + srv.Start() + + // Open an idle TCP connection that never sends any bytes. + idleConn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second) + if err != nil { + t.Fatalf("failed to dial idle connection: %v", err) + } + defer idleConn.Close() + + // Give the accept loop time to pick up the idle connection. + time.Sleep(50 * time.Millisecond) + + // Send a real HTTP request. Before the fix, the accept loop would be + // blocked on Peek(1) for the idle connection, causing this request to + // time out. + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Get("http://" + listener.Addr().String() + "/") + if err != nil { + listener.Close() + t.Fatalf("HTTP request failed (accept loop may be blocked by idle connection): %v", err) + } + resp.Body.Close() + + listener.Close() + + if routed.Load() == 0 { + t.Error("expected at least one request to be routed") + } +} diff --git a/internal/api/redis_queue_protocol.go b/internal/api/redis_queue_protocol.go index caaba2316d..6f3622d7bf 100644 --- a/internal/api/redis_queue_protocol.go +++ b/internal/api/redis_queue_protocol.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" log "github.com/sirupsen/logrus" ) @@ -45,6 +45,12 @@ func (s *Server) handleRedisConnection(conn net.Conn, reader *bufio.Reader) { return true } + if s.cfg != nil && s.cfg.Home.Enabled { + _ = writeRedisError(writer, "ERR redis usage output disabled in home mode") + _ = writer.Flush() + return + } + for { if !s.managementRoutesEnabled.Load() { return diff --git a/internal/api/redis_queue_protocol_integration_test.go b/internal/api/redis_queue_protocol_integration_test.go index 93bfeb8663..1586d37c85 100644 --- a/internal/api/redis_queue_protocol_integration_test.go +++ b/internal/api/redis_queue_protocol_integration_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" ) type remoteAddrConn struct { @@ -204,6 +204,43 @@ func TestRedisProtocol_ManagementDisabled_RejectsConnection(t *testing.T) { } } +func TestRedisProtocol_HomeEnabled_DisablesConnection(t *testing.T) { + t.Setenv("MANAGEMENT_PASSWORD", "test-management-password") + redisqueue.SetEnabled(false) + t.Cleanup(func() { redisqueue.SetEnabled(false) }) + + server := newTestServer(t) + if !server.managementRoutesEnabled.Load() { + t.Fatalf("expected managementRoutesEnabled to be true") + } + if server.cfg == nil { + t.Fatalf("expected server cfg to be non-nil") + } + server.cfg.Home.Enabled = true + redisqueue.SetEnabled(true) + + addr, stop := startRedisMuxListener(t, server) + t.Cleanup(stop) + + conn, errDial := net.DialTimeout("tcp", addr, time.Second) + if errDial != nil { + t.Fatalf("failed to dial redis listener: %v", errDial) + } + t.Cleanup(func() { _ = conn.Close() }) + + _ = conn.SetDeadline(time.Now().Add(2 * time.Second)) + _ = writeTestRESPCommand(conn, "PING") + + buf := make([]byte, 1) + _, errRead := conn.Read(buf) + if errRead == nil { + t.Fatalf("expected connection to be closed when home mode is enabled") + } + if ne, ok := errRead.(net.Error); ok && ne.Timeout() { + t.Fatalf("expected connection to be closed when home mode is enabled, got timeout: %v", errRead) + } +} + func TestRedisProtocol_AUTH_And_PopContracts(t *testing.T) { const managementPassword = "test-management-password" diff --git a/internal/api/server.go b/internal/api/server.go index 487ea571e6..492061a477 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -8,6 +8,7 @@ import ( "context" "crypto/subtle" "crypto/tls" + "encoding/json" "errors" "fmt" "net" @@ -15,30 +16,32 @@ import ( "os" "path/filepath" "reflect" + "sort" "strings" "sync" "sync/atomic" "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/access" - managementHandlers "github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api/middleware" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" - ampmodule "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules/amp" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/access" + managementHandlers "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api/middleware" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules" + ampmodule "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules/amp" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" "golang.org/x/net/http2" "gopkg.in/yaml.v3" @@ -64,7 +67,9 @@ type ServerOption func(*serverOptionConfig) func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger { configDir := filepath.Dir(configPath) logsDir := logging.ResolveLogDirectory(cfg) - return logging.NewFileRequestLogger(cfg.RequestLog, logsDir, configDir, cfg.ErrorLogsMaxFiles) + logger := logging.NewFileRequestLogger(cfg.RequestLog, logsDir, configDir, cfg.ErrorLogsMaxFiles) + logger.SetHomeEnabled(cfg != nil && cfg.Home.Enabled) + return logger } // WithMiddleware appends additional Gin middleware during server construction. @@ -284,6 +289,10 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk } s.localPassword = optionState.localPassword + // Home heartbeat gate: when home is enabled, block all endpoints with 503 until the + // subscribe-config heartbeat connection is healthy. + engine.Use(s.homeHeartbeatMiddleware()) + // Setup routes s.setupRoutes() @@ -308,7 +317,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk // or when a local management password is provided (e.g. TUI mode). hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != "" s.managementRoutesEnabled.Store(hasManagementSecret) - redisqueue.SetEnabled(hasManagementSecret) + redisqueue.SetEnabled(hasManagementSecret || (cfg != nil && cfg.Home.Enabled)) if hasManagementSecret { s.registerManagementRoutes() } @@ -326,6 +335,28 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk return s } +func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if s == nil || s.cfg == nil || !s.cfg.Home.Enabled { + c.Next() + return + } + if c != nil && c.Request != nil { + path := c.Request.URL.Path + if strings.HasPrefix(path, "/v0/management/") || path == "/v0/management" || path == "/management.html" { + c.Next() + return + } + } + client := home.Current() + if client == nil || !client.HeartbeatOK() { + c.AbortWithStatus(http.StatusServiceUnavailable) + return + } + c.Next() + } +} + // setupRoutes configures the API routes for the server. // It defines the endpoints and associates them with their respective handlers. func (s *Server) setupRoutes() { @@ -376,9 +407,9 @@ func (s *Server) setupRoutes() { v1beta := s.engine.Group("/v1beta") v1beta.Use(AuthMiddleware(s.accessManager)) { - v1beta.GET("/models", geminiHandlers.GeminiModels) + v1beta.GET("/models", s.geminiModelsHandler(geminiHandlers)) v1beta.POST("/models/*action", geminiHandlers.GeminiHandler) - v1beta.GET("/models/*action", geminiHandlers.GeminiGetHandler) + v1beta.GET("/models/*action", s.geminiGetHandler(geminiHandlers)) } // Root endpoint @@ -661,6 +692,14 @@ func (s *Server) registerManagementRoutes() { func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + if s == nil || s.cfg == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + if s.cfg.Home.Enabled { + c.AbortWithStatus(http.StatusNotFound) + return + } if !s.managementRoutesEnabled.Load() { c.AbortWithStatus(http.StatusNotFound) return @@ -671,7 +710,7 @@ func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc { func (s *Server) serveManagementControlPanel(c *gin.Context) { cfg := s.cfg - if cfg == nil || cfg.RemoteManagement.DisableControlPanel { + if cfg == nil || cfg.Home.Enabled || cfg.RemoteManagement.DisableControlPanel { c.AbortWithStatus(http.StatusNotFound) return } @@ -783,6 +822,11 @@ func (s *Server) watchKeepAlive() { // otherwise it routes to OpenAI handler. func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, claudeHandler *claude.ClaudeCodeAPIHandler) gin.HandlerFunc { return func(c *gin.Context) { + if s != nil && s.cfg != nil && s.cfg.Home.Enabled { + s.handleHomeModels(c) + return + } + userAgent := c.GetHeader("User-Agent") // Route to Claude handler if User-Agent starts with "claude-cli" @@ -796,6 +840,279 @@ func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, cl } } +func (s *Server) geminiModelsHandler(geminiHandler *gemini.GeminiAPIHandler) gin.HandlerFunc { + return func(c *gin.Context) { + if s != nil && s.cfg != nil && s.cfg.Home.Enabled { + s.handleHomeGeminiModels(c) + return + } + + geminiHandler.GeminiModels(c) + } +} + +func (s *Server) geminiGetHandler(geminiHandler *gemini.GeminiAPIHandler) gin.HandlerFunc { + return func(c *gin.Context) { + if s != nil && s.cfg != nil && s.cfg.Home.Enabled { + s.handleHomeGeminiModel(c) + return + } + + geminiHandler.GeminiGetHandler(c) + } +} + +type homeModelEntry struct { + id string + created int64 + ownedBy string + displayName string +} + +func (s *Server) handleHomeModels(c *gin.Context) { + entries, ok := s.loadHomeModelEntries(c) + if !ok { + return + } + + userAgent := c.GetHeader("User-Agent") + isClaude := strings.HasPrefix(userAgent, "claude-cli") + + if isClaude { + out := make([]map[string]any, 0, len(entries)) + for _, entry := range entries { + model := map[string]any{ + "id": entry.id, + "object": "model", + "owned_by": entry.ownedBy, + } + if entry.created > 0 { + model["created_at"] = entry.created + } + if entry.displayName != "" { + model["display_name"] = entry.displayName + } + out = append(out, model) + } + firstID := "" + lastID := "" + if len(out) > 0 { + if id, okID := out[0]["id"].(string); okID { + firstID = id + } + if id, okID := out[len(out)-1]["id"].(string); okID { + lastID = id + } + } + c.JSON(http.StatusOK, gin.H{ + "data": out, + "has_more": false, + "first_id": firstID, + "last_id": lastID, + }) + return + } + + filtered := make([]map[string]any, 0, len(entries)) + for _, entry := range entries { + model := map[string]any{ + "id": entry.id, + "object": "model", + } + if entry.created > 0 { + model["created"] = entry.created + } + if entry.ownedBy != "" { + model["owned_by"] = entry.ownedBy + } + filtered = append(filtered, model) + } + c.JSON(http.StatusOK, gin.H{ + "object": "list", + "data": filtered, + }) +} + +func (s *Server) handleHomeGeminiModels(c *gin.Context) { + entries, ok := s.loadHomeModelEntries(c) + if !ok { + return + } + + c.JSON(http.StatusOK, gin.H{ + "models": formatHomeGeminiModels(entries), + }) +} + +func (s *Server) handleHomeGeminiModel(c *gin.Context) { + entries, ok := s.loadHomeModelEntries(c) + if !ok { + return + } + + action := strings.TrimPrefix(c.Param("action"), "/") + action = strings.TrimSpace(action) + for _, entry := range entries { + if homeGeminiModelMatches(entry, action) { + c.JSON(http.StatusOK, formatHomeGeminiModel(entry)) + return + } + } + + c.JSON(http.StatusNotFound, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Not Found", + Type: "not_found", + }, + }) +} + +func (s *Server) loadHomeModelEntries(c *gin.Context) ([]homeModelEntry, bool) { + if s == nil || c == nil || c.Request == nil { + return nil, false + } + client := home.Current() + if client == nil { + c.JSON(http.StatusServiceUnavailable, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "home control center unavailable", + Type: "server_error", + }, + }) + return nil, false + } + + raw, errGet := client.GetModels(c.Request.Context()) + if errGet != nil { + c.JSON(http.StatusBadGateway, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: errGet.Error(), + Type: "server_error", + }, + }) + return nil, false + } + + entries, errDecode := decodeHomeModels(raw) + if errDecode != nil { + c.JSON(http.StatusBadGateway, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: errDecode.Error(), + Type: "server_error", + }, + }) + return nil, false + } + + return entries, true +} + +func formatHomeGeminiModels(entries []homeModelEntry) []map[string]any { + out := make([]map[string]any, 0, len(entries)) + for _, entry := range entries { + out = append(out, formatHomeGeminiModel(entry)) + } + return out +} + +func formatHomeGeminiModel(entry homeModelEntry) map[string]any { + name := entry.id + if !strings.HasPrefix(name, "models/") { + name = "models/" + name + } + displayName := entry.displayName + if displayName == "" { + displayName = entry.id + } + return map[string]any{ + "name": name, + "displayName": displayName, + "description": displayName, + "supportedGenerationMethods": []string{"generateContent"}, + } +} + +func homeGeminiModelMatches(entry homeModelEntry, action string) bool { + id := strings.TrimSpace(entry.id) + if id == "" || action == "" { + return false + } + normalizedAction := strings.TrimPrefix(action, "models/") + normalizedID := strings.TrimPrefix(id, "models/") + return action == id || action == "models/"+id || normalizedAction == normalizedID +} + +func decodeHomeModels(raw []byte) ([]homeModelEntry, error) { + if len(raw) == 0 { + return nil, fmt.Errorf("home models payload is empty") + } + + var bySection map[string][]map[string]any + if err := json.Unmarshal(raw, &bySection); err != nil { + return nil, fmt.Errorf("parse home models payload: %w", err) + } + if len(bySection) == 0 { + return nil, fmt.Errorf("home models payload has no sections") + } + + seen := make(map[string]struct{}) + out := make([]homeModelEntry, 0, 256) + for _, models := range bySection { + for _, model := range models { + id, _ := model["id"].(string) + id = strings.TrimSpace(id) + if id == "" { + name, _ := model["name"].(string) + name = strings.TrimSpace(name) + id = strings.TrimPrefix(name, "models/") + } + if id == "" { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + + created := int64(0) + switch v := model["created"].(type) { + case float64: + created = int64(v) + case int64: + created = v + case int: + created = int64(v) + case json.Number: + if n, err := v.Int64(); err == nil { + created = n + } + } + + ownedBy, _ := model["owned_by"].(string) + ownedBy = strings.TrimSpace(ownedBy) + displayName, _ := model["display_name"].(string) + displayName = strings.TrimSpace(displayName) + if displayName == "" { + displayName, _ = model["displayName"].(string) + displayName = strings.TrimSpace(displayName) + } + + out = append(out, homeModelEntry{ + id: id, + created: created, + ownedBy: ownedBy, + displayName: displayName, + }) + } + } + + sort.Slice(out, func(i, j int) bool { return out[i].id < out[j].id }) + if len(out) == 0 { + return nil, fmt.Errorf("home models payload contains no models") + } + return out, nil +} + // Start begins listening for and serving HTTP or HTTPS requests. // It's a blocking call and will only return on an unrecoverable error. // @@ -991,6 +1308,12 @@ func (s *Server) UpdateClients(cfg *config.Config) { } } + if oldCfg == nil || oldCfg.Home.Enabled != cfg.Home.Enabled { + if setter, ok := s.requestLogger.(interface{ SetHomeEnabled(bool) }); ok { + setter.SetHomeEnabled(cfg.Home.Enabled) + } + } + if oldCfg == nil || oldCfg.LoggingToFile != cfg.LoggingToFile || oldCfg.LogsMaxTotalSizeMB != cfg.LogsMaxTotalSizeMB { if err := logging.ConfigureLogOutput(cfg); err != nil { log.Errorf("failed to reconfigure log output: %v", err) @@ -1061,7 +1384,7 @@ func (s *Server) UpdateClients(cfg *config.Config) { s.managementRoutesEnabled.Store(!newSecretEmpty) } } - redisqueue.SetEnabled(s.managementRoutesEnabled.Load()) + redisqueue.SetEnabled(s.managementRoutesEnabled.Load() || (cfg != nil && cfg.Home.Enabled)) s.applyAccessConfig(oldCfg, cfg) s.cfg = cfg @@ -1094,11 +1417,14 @@ func (s *Server) UpdateClients(cfg *config.Config) { } // Count client sources from configuration and auth store. - tokenStore := sdkAuth.GetTokenStore() - if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok { - dirSetter.SetBaseDir(cfg.AuthDir) + authEntries := 0 + if cfg != nil && !cfg.Home.Enabled { + tokenStore := sdkAuth.GetTokenStore() + if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok { + dirSetter.SetBaseDir(cfg.AuthDir) + } + authEntries = util.CountAuthFiles(context.Background(), tokenStore) } - authEntries := util.CountAuthFiles(context.Background(), tokenStore) geminiAPIKeyCount := len(cfg.GeminiKey) claudeAPIKeyCount := len(cfg.ClaudeKey) codexAPIKeyCount := len(cfg.CodexKey) @@ -1146,7 +1472,7 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc { result, err := manager.Authenticate(c.Request.Context(), c.Request) if err == nil { if result != nil { - c.Set("apiKey", result.Principal) + c.Set("userApiKey", result.Principal) c.Set("accessProvider", result.Provider) if len(result.Metadata) > 0 { c.Set("accessMetadata", result.Metadata) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index fe37cb72ef..e107702a88 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -11,12 +11,12 @@ import ( "time" gin "github.com/gin-gonic/gin" - proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + proxyconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func newTestServer(t *testing.T) *Server { @@ -147,6 +147,32 @@ func TestManagementUsageRequiresManagementAuthAndPopsArray(t *testing.T) { } } +func TestHomeEnabledHidesManagementEndpointsAndControlPanel(t *testing.T) { + t.Setenv("MANAGEMENT_PASSWORD", "test-management-key") + + server := newTestServer(t) + server.cfg.Home.Enabled = true + + t.Run("management endpoints return 404", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v0/management/config", nil) + req.Header.Set("Authorization", "Bearer test-management-key") + rr := httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusNotFound, rr.Body.String()) + } + }) + + t.Run("management control panel returns 404", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/management.html", nil) + rr := httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusNotFound, rr.Body.String()) + } + }) +} + func TestAmpProviderModelRoutes(t *testing.T) { testCases := []struct { name string diff --git a/internal/auth/antigravity/auth.go b/internal/auth/antigravity/auth.go index 8d3b216fbc..7bee09bb66 100644 --- a/internal/auth/antigravity/auth.go +++ b/internal/auth/antigravity/auth.go @@ -11,9 +11,9 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/internal/auth/claude/anthropic_auth.go b/internal/auth/claude/anthropic_auth.go index 60c71b3512..d7ca154296 100644 --- a/internal/auth/claude/anthropic_auth.go +++ b/internal/auth/claude/anthropic_auth.go @@ -15,7 +15,7 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" log "github.com/sirupsen/logrus" "golang.org/x/sync/singleflight" ) diff --git a/internal/auth/claude/anthropic_auth_proxy_test.go b/internal/auth/claude/anthropic_auth_proxy_test.go index 50c4875791..7cab9cd2f1 100644 --- a/internal/auth/claude/anthropic_auth_proxy_test.go +++ b/internal/auth/claude/anthropic_auth_proxy_test.go @@ -3,7 +3,7 @@ package claude import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "golang.org/x/net/proxy" ) diff --git a/internal/auth/claude/token.go b/internal/auth/claude/token.go index 6ebb0f2f8c..10aa3b4344 100644 --- a/internal/auth/claude/token.go +++ b/internal/auth/claude/token.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" ) // ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication. diff --git a/internal/auth/claude/utls_transport.go b/internal/auth/claude/utls_transport.go index 88b69c9bd9..f41087819f 100644 --- a/internal/auth/claude/utls_transport.go +++ b/internal/auth/claude/utls_transport.go @@ -8,8 +8,8 @@ import ( "sync" tls "github.com/refraction-networking/utls" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/net/http2" "golang.org/x/net/proxy" diff --git a/internal/auth/codex/openai_auth.go b/internal/auth/codex/openai_auth.go index 67b54b172d..681747caf5 100644 --- a/internal/auth/codex/openai_auth.go +++ b/internal/auth/codex/openai_auth.go @@ -14,8 +14,8 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/internal/auth/codex/openai_auth_test.go b/internal/auth/codex/openai_auth_test.go index a7fe83072d..e7d939b0a3 100644 --- a/internal/auth/codex/openai_auth_test.go +++ b/internal/auth/codex/openai_auth_test.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) type roundTripFunc func(*http.Request) (*http.Response, error) diff --git a/internal/auth/codex/token.go b/internal/auth/codex/token.go index 7f03207195..b2a7bcf21a 100644 --- a/internal/auth/codex/token.go +++ b/internal/auth/codex/token.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" ) // CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication. diff --git a/internal/auth/gemini/gemini_auth.go b/internal/auth/gemini/gemini_auth.go index 2995a1cb5e..5b9ee82d26 100644 --- a/internal/auth/gemini/gemini_auth.go +++ b/internal/auth/gemini/gemini_auth.go @@ -13,12 +13,12 @@ import ( "net/http" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" diff --git a/internal/auth/gemini/gemini_token.go b/internal/auth/gemini/gemini_token.go index 6848b708e2..a6ea8c5151 100644 --- a/internal/auth/gemini/gemini_token.go +++ b/internal/auth/gemini/gemini_token.go @@ -10,7 +10,7 @@ import ( "path/filepath" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" log "github.com/sirupsen/logrus" ) diff --git a/internal/auth/kimi/kimi.go b/internal/auth/kimi/kimi.go index ccb1a6c2ff..27c5f73b42 100644 --- a/internal/auth/kimi/kimi.go +++ b/internal/auth/kimi/kimi.go @@ -15,8 +15,8 @@ import ( "time" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/internal/auth/kimi/kimi_proxy_test.go b/internal/auth/kimi/kimi_proxy_test.go index 130f34f52b..a95ba01dba 100644 --- a/internal/auth/kimi/kimi_proxy_test.go +++ b/internal/auth/kimi/kimi_proxy_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideDirectDisablesProxy(t *testing.T) { diff --git a/internal/auth/kimi/token.go b/internal/auth/kimi/token.go index 7320d760ef..347b546cbd 100644 --- a/internal/auth/kimi/token.go +++ b/internal/auth/kimi/token.go @@ -10,7 +10,7 @@ import ( "path/filepath" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" ) // KimiTokenStorage stores OAuth2 token information for Kimi API authentication. diff --git a/internal/auth/vertex/vertex_credentials.go b/internal/auth/vertex/vertex_credentials.go index 9f830994ed..db214bd6e2 100644 --- a/internal/auth/vertex/vertex_credentials.go +++ b/internal/auth/vertex/vertex_credentials.go @@ -8,7 +8,7 @@ import ( "os" "path/filepath" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/anthropic_login.go b/internal/cmd/anthropic_login.go index f7381461a6..cc1bfc8e7c 100644 --- a/internal/cmd/anthropic_login.go +++ b/internal/cmd/anthropic_login.go @@ -6,9 +6,9 @@ import ( "fmt" "os" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/antigravity_login.go b/internal/cmd/antigravity_login.go index 2efbaeee01..f2bd5505a2 100644 --- a/internal/cmd/antigravity_login.go +++ b/internal/cmd/antigravity_login.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/auth_manager.go b/internal/cmd/auth_manager.go index 2654717901..7896a7023a 100644 --- a/internal/cmd/auth_manager.go +++ b/internal/cmd/auth_manager.go @@ -1,7 +1,7 @@ package cmd import ( - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" ) // newAuthManager creates a new authentication manager instance with all supported diff --git a/internal/cmd/kimi_login.go b/internal/cmd/kimi_login.go index eb5f11fb37..ffc470fda0 100644 --- a/internal/cmd/kimi_login.go +++ b/internal/cmd/kimi_login.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 22404dac9c..a71bb28263 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -17,12 +17,12 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/internal/cmd/openai_device_login.go b/internal/cmd/openai_device_login.go index 1b7351e63a..3fa9307b9c 100644 --- a/internal/cmd/openai_device_login.go +++ b/internal/cmd/openai_device_login.go @@ -6,9 +6,9 @@ import ( "fmt" "os" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/openai_login.go b/internal/cmd/openai_login.go index 783a948400..ee8a025067 100644 --- a/internal/cmd/openai_login.go +++ b/internal/cmd/openai_login.go @@ -6,9 +6,9 @@ import ( "fmt" "os" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index d8c4f01938..38f189b4a9 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -10,9 +10,9 @@ import ( "syscall" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy" log "github.com/sirupsen/logrus" ) diff --git a/internal/cmd/vertex_import.go b/internal/cmd/vertex_import.go index 4aa0d74b59..ffb6200b1a 100644 --- a/internal/cmd/vertex_import.go +++ b/internal/cmd/vertex_import.go @@ -9,11 +9,11 @@ import ( "os" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/config/config.go b/internal/config/config.go index 46ce4f5099..e032b43d41 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,7 +13,7 @@ import ( "strings" "syscall" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" @@ -22,6 +22,7 @@ import ( const ( DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center" DefaultPprofAddr = "127.0.0.1:8316" + DefaultAuthDir = "~/.cli-proxy-api" ) // Config represents the application's configuration, loaded from a YAML file. @@ -36,6 +37,9 @@ type Config struct { // TLS config controls HTTPS server settings. TLS TLSConfig `yaml:"tls" json:"tls"` + // Home config enables the Redis-based control plane integration. + Home HomeConfig `yaml:"home" json:"-"` + // RemoteManagement nests management-related options under 'remote-management'. RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"` @@ -223,12 +227,6 @@ type RoutingConfig struct { // Supported values: "round-robin" (default), "fill-first". Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` - // ClaudeCodeSessionAffinity enables session-sticky routing for Claude Code clients. - // When enabled, requests with the same session ID (extracted from metadata.user_id) - // are routed to the same auth credential when available. - // Deprecated: Use SessionAffinity instead for universal session support. - ClaudeCodeSessionAffinity bool `yaml:"claude-code-session-affinity,omitempty" json:"claude-code-session-affinity,omitempty"` - // SessionAffinity enables universal session-sticky routing for all clients. // Session IDs are extracted from multiple sources: // metadata.user_id (Claude Code session format), X-Session-ID, Session_id (Codex), @@ -400,6 +398,9 @@ type ClaudeKey struct { // ExcludedModels lists model IDs that should be excluded for this provider. ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` + // DisableCooling disables auth/model cooldown scheduling for this credential when true. + DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"` + // Cloak configures request cloaking for non-Claude-Code clients. Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"` @@ -455,6 +456,9 @@ type CodexKey struct { // ExcludedModels lists model IDs that should be excluded for this provider. ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` + + // DisableCooling disables auth/model cooldown scheduling for this credential when true. + DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"` } func (k CodexKey) GetAPIKey() string { return k.APIKey } @@ -499,6 +503,9 @@ type GeminiKey struct { // ExcludedModels lists model IDs that should be excluded for this provider. ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` + + // DisableCooling disables auth/model cooldown scheduling for this credential when true. + DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"` } func (k GeminiKey) GetAPIKey() string { return k.APIKey } @@ -543,6 +550,9 @@ type OpenAICompatibility struct { // Headers optionally adds extra HTTP headers for requests sent to this provider. Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + + // DisableCooling disables auth/model cooldown scheduling for this provider when true. + DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"` } // OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting. diff --git a/internal/config/home.go b/internal/config/home.go new file mode 100644 index 0000000000..03c9173239 --- /dev/null +++ b/internal/config/home.go @@ -0,0 +1,9 @@ +package config + +// HomeConfig configures the optional "home" control plane integration over Redis protocol. +type HomeConfig struct { + Enabled bool `yaml:"enabled" json:"enabled"` + Host string `yaml:"host" json:"-"` + Port int `yaml:"port" json:"-"` + Password string `yaml:"password" json:"-"` +} diff --git a/internal/config/parse.go b/internal/config/parse.go new file mode 100644 index 0000000000..283740e5f0 --- /dev/null +++ b/internal/config/parse.go @@ -0,0 +1,89 @@ +package config + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" + "gopkg.in/yaml.v3" +) + +// ParseConfigBytes parses a YAML configuration payload into Config and applies the same +// in-memory normalizations as LoadConfigOptional, without persisting any changes to disk. +func ParseConfigBytes(data []byte) (*Config, error) { + if len(data) == 0 { + return nil, fmt.Errorf("config payload is empty") + } + + var cfg Config + // Keep defaults aligned with LoadConfigOptional. + cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6) + cfg.LoggingToFile = false + cfg.LogsMaxTotalSizeMB = 0 + cfg.ErrorLogsMaxFiles = 10 + cfg.UsageStatisticsEnabled = false + cfg.RedisUsageQueueRetentionSeconds = 60 + cfg.DisableCooling = false + cfg.DisableImageGeneration = DisableImageGenerationOff + cfg.Pprof.Enable = false + cfg.Pprof.Addr = DefaultPprofAddr + cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient + cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository + + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config payload: %w", err) + } + + // Hash remote management key if plaintext is detected (nested), but do NOT persist. + if cfg.RemoteManagement.SecretKey != "" && !looksLikeBcrypt(cfg.RemoteManagement.SecretKey) { + hashed, errHash := bcrypt.GenerateFromPassword([]byte(cfg.RemoteManagement.SecretKey), bcrypt.DefaultCost) + if errHash != nil { + return nil, fmt.Errorf("hash remote management key: %w", errHash) + } + cfg.RemoteManagement.SecretKey = string(hashed) + } + + cfg.RemoteManagement.PanelGitHubRepository = strings.TrimSpace(cfg.RemoteManagement.PanelGitHubRepository) + if cfg.RemoteManagement.PanelGitHubRepository == "" { + cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository + } + + cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr) + if cfg.Pprof.Addr == "" { + cfg.Pprof.Addr = DefaultPprofAddr + } + + if cfg.LogsMaxTotalSizeMB < 0 { + cfg.LogsMaxTotalSizeMB = 0 + } + + if cfg.ErrorLogsMaxFiles < 0 { + cfg.ErrorLogsMaxFiles = 10 + } + + if cfg.RedisUsageQueueRetentionSeconds <= 0 { + cfg.RedisUsageQueueRetentionSeconds = 60 + } else if cfg.RedisUsageQueueRetentionSeconds > 3600 { + log.WithField("value", cfg.RedisUsageQueueRetentionSeconds).Warn("redis-usage-queue-retention-seconds too large; clamping to 3600") + cfg.RedisUsageQueueRetentionSeconds = 3600 + } + + if cfg.MaxRetryCredentials < 0 { + cfg.MaxRetryCredentials = 0 + } + + // Apply the same sanitization pipeline. + cfg.SanitizeGeminiKeys() + cfg.SanitizeVertexCompatKeys() + cfg.SanitizeCodexKeys() + cfg.SanitizeCodexHeaderDefaults() + cfg.SanitizeClaudeHeaderDefaults() + cfg.SanitizeClaudeKeys() + cfg.SanitizeOpenAICompatibility() + cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels) + cfg.SanitizeOAuthModelAlias() + cfg.SanitizePayloadRules() + + return &cfg, nil +} diff --git a/internal/home/client.go b/internal/home/client.go new file mode 100644 index 0000000000..40a191fe21 --- /dev/null +++ b/internal/home/client.go @@ -0,0 +1,601 @@ +package home + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/redis/go-redis/v9" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + log "github.com/sirupsen/logrus" +) + +const ( + redisKeyConfig = "config" + redisChannelConfig = "config" + redisKeyModels = "models" + redisKeyUsage = "usage" + redisKeyRequestLog = "request-log" + + homeReconnectInterval = time.Second + homeReconnectFailoverThreshold = 3 +) + +var ( + ErrDisabled = errors.New("home client disabled") + ErrNotConnected = errors.New("home not connected") + ErrEmptyResponse = errors.New("home returned empty response") + ErrAuthNotFound = errors.New("home auth not found") + ErrConfigNotFound = errors.New("home config not found") + ErrModelsNotFound = errors.New("home models not found") +) + +type clusterNode struct { + IP string `json:"ip"` + Port int `json:"port"` + ClientCount int `json:"client_count"` + IsMaster bool `json:"is_master"` + LastSeenAt time.Time `json:"last_seen_at"` +} + +type clusterNodesEnvelope struct { + OK bool `json:"ok"` + Nodes []clusterNode `json:"nodes"` +} + +type Client struct { + mu sync.Mutex + + homeCfg config.HomeConfig + seedHost string + seedPort int + + cmd *redis.Client + sub *redis.Client + + heartbeatOK atomic.Bool + clusterNodes []clusterNode + reconnectFailures int +} + +func New(homeCfg config.HomeConfig) *Client { + return &Client{ + homeCfg: homeCfg, + seedHost: strings.TrimSpace(homeCfg.Host), + seedPort: homeCfg.Port, + } +} + +func (c *Client) Enabled() bool { + if c == nil { + return false + } + c.mu.Lock() + defer c.mu.Unlock() + return c.homeCfg.Enabled +} + +func (c *Client) HeartbeatOK() bool { + if c == nil { + return false + } + if !c.Enabled() { + return false + } + return c.heartbeatOK.Load() +} + +func (c *Client) Close() { + if c == nil { + return + } + c.heartbeatOK.Store(false) + c.mu.Lock() + defer c.mu.Unlock() + c.closeClientsLocked() +} + +func (c *Client) closeClientsLocked() { + if c.cmd != nil { + _ = c.cmd.Close() + } + if c.sub != nil { + _ = c.sub.Close() + } + c.cmd = nil + c.sub = nil +} + +func (c *Client) addr() (string, bool) { + if c == nil { + return "", false + } + c.mu.Lock() + defer c.mu.Unlock() + return c.addrLocked() +} + +func (c *Client) addrLocked() (string, bool) { + host := strings.TrimSpace(c.homeCfg.Host) + if host == "" { + return "", false + } + if c.homeCfg.Port <= 0 { + return "", false + } + return fmt.Sprintf("%s:%d", host, c.homeCfg.Port), true +} + +func (c *Client) ensureClients() error { + if c == nil { + return ErrDisabled + } + if !c.Enabled() { + return ErrDisabled + } + c.mu.Lock() + defer c.mu.Unlock() + + addr, ok := c.addrLocked() + if !ok { + return fmt.Errorf("home: invalid address (host=%q port=%d)", c.homeCfg.Host, c.homeCfg.Port) + } + + if c.cmd == nil { + c.cmd = redis.NewClient(&redis.Options{ + Addr: addr, + Password: c.homeCfg.Password, + }) + } + if c.sub == nil { + c.sub = redis.NewClient(&redis.Options{ + Addr: addr, + Password: c.homeCfg.Password, + }) + } + return nil +} + +func (c *Client) commandClient() (*redis.Client, error) { + if errEnsure := c.ensureClients(); errEnsure != nil { + return nil, errEnsure + } + c.mu.Lock() + cmd := c.cmd + c.mu.Unlock() + if cmd == nil { + return nil, ErrNotConnected + } + return cmd, nil +} + +func (c *Client) subscriptionClient() (*redis.Client, error) { + if errEnsure := c.ensureClients(); errEnsure != nil { + return nil, errEnsure + } + c.mu.Lock() + sub := c.sub + c.mu.Unlock() + if sub == nil { + return nil, ErrNotConnected + } + return sub, nil +} + +func (c *Client) Ping(ctx context.Context) error { + cmd, errClient := c.commandClient() + if errClient != nil { + return errClient + } + return cmd.Ping(ctx).Err() +} + +func (c *Client) refreshBestClusterNode(ctx context.Context) { + switched, errRefresh := c.refreshClusterNodes(ctx) + if errRefresh != nil { + log.Debugf("home cluster nodes unavailable: %v", errRefresh) + return + } + if switched { + if addr, ok := c.addr(); ok { + log.Infof("home cluster target switched to %s", addr) + } + } +} + +func (c *Client) refreshClusterNodes(ctx context.Context) (bool, error) { + if ctx == nil { + ctx = context.Background() + } + cmd, errClient := c.commandClient() + if errClient != nil { + return false, errClient + } + raw, errDo := cmd.Do(ctx, "CLUSTER", "NODES").Text() + if errDo != nil { + return false, errDo + } + + var envelope clusterNodesEnvelope + if errUnmarshal := json.Unmarshal([]byte(raw), &envelope); errUnmarshal != nil { + return false, errUnmarshal + } + nodes := normalizeClusterNodes(envelope.Nodes) + if len(nodes) == 0 { + return false, nil + } + + c.mu.Lock() + defer c.mu.Unlock() + c.clusterNodes = nodes + c.reconnectFailures = 0 + return c.switchToNodeLocked(nodes[0]), nil +} + +func normalizeClusterNodes(nodes []clusterNode) []clusterNode { + out := make([]clusterNode, 0, len(nodes)) + for _, node := range nodes { + node.IP = strings.TrimSpace(node.IP) + if node.IP == "" || node.Port <= 0 { + continue + } + if node.ClientCount < 0 { + node.ClientCount = 0 + } + out = append(out, node) + } + sort.SliceStable(out, func(i, j int) bool { + return out[i].ClientCount < out[j].ClientCount + }) + return out +} + +func (c *Client) switchToNodeLocked(node clusterNode) bool { + host := strings.TrimSpace(node.IP) + if host == "" || node.Port <= 0 { + return false + } + if strings.TrimSpace(c.homeCfg.Host) == host && c.homeCfg.Port == node.Port { + return false + } + c.homeCfg.Host = host + c.homeCfg.Port = node.Port + c.closeClientsLocked() + return true +} + +func (c *Client) markReconnectFailure(reason string) { + switched, addr := c.failoverAfterReconnectFailure() + if switched { + log.Warnf("home control center unavailable after repeated %s failures; switching to %s", reason, addr) + } +} + +func (c *Client) failoverAfterReconnectFailure() (bool, string) { + if c == nil { + return false, "" + } + c.mu.Lock() + defer c.mu.Unlock() + + c.reconnectFailures++ + if c.reconnectFailures < homeReconnectFailoverThreshold { + return false, "" + } + c.reconnectFailures = 0 + + currentHost := strings.TrimSpace(c.homeCfg.Host) + currentPort := c.homeCfg.Port + candidates := append([]clusterNode(nil), c.clusterNodes...) + if strings.TrimSpace(c.seedHost) != "" && c.seedPort > 0 { + candidates = append(candidates, clusterNode{IP: c.seedHost, Port: c.seedPort}) + } + for _, node := range candidates { + host := strings.TrimSpace(node.IP) + if host == "" || node.Port <= 0 { + continue + } + if host == currentHost && node.Port == currentPort { + continue + } + if c.switchToNodeLocked(clusterNode{IP: host, Port: node.Port}) { + addr, _ := c.addrLocked() + return true, addr + } + } + return false, "" +} + +func (c *Client) resetReconnectFailures() { + if c == nil { + return + } + c.mu.Lock() + c.reconnectFailures = 0 + c.mu.Unlock() +} + +func (c *Client) GetConfig(ctx context.Context) ([]byte, error) { + c.refreshBestClusterNode(ctx) + cmd, errClient := c.commandClient() + if errClient != nil { + return nil, errClient + } + raw, err := cmd.Get(ctx, redisKeyConfig).Bytes() + if errors.Is(err, redis.Nil) { + return nil, ErrConfigNotFound + } + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, ErrEmptyResponse + } + return raw, nil +} + +func (c *Client) GetModels(ctx context.Context) ([]byte, error) { + cmd, errClient := c.commandClient() + if errClient != nil { + return nil, errClient + } + raw, err := cmd.Get(ctx, redisKeyModels).Bytes() + if errors.Is(err, redis.Nil) { + return nil, ErrModelsNotFound + } + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, ErrEmptyResponse + } + return raw, nil +} + +func headersToLowerMap(headers http.Header) map[string]string { + if len(headers) == 0 { + return nil + } + out := make(map[string]string, len(headers)) + for key, values := range headers { + k := strings.ToLower(strings.TrimSpace(key)) + if k == "" { + continue + } + if len(values) == 0 { + out[k] = "" + continue + } + trimmed := make([]string, 0, len(values)) + for _, v := range values { + trimmed = append(trimmed, strings.TrimSpace(v)) + } + out[k] = strings.Join(trimmed, ", ") + } + if len(out) == 0 { + return nil + } + return out +} + +func newAuthDispatchRequest(requestedModel string, sessionID string, headers http.Header, count int) authDispatchRequest { + if count <= 0 { + count = 1 + } + return authDispatchRequest{ + Type: "auth", + Model: requestedModel, + Count: count, + SessionID: strings.TrimSpace(sessionID), + Headers: headersToLowerMap(headers), + } +} + +func (c *Client) RPopAuth(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int) ([]byte, error) { + cmd, errClient := c.commandClient() + if errClient != nil { + return nil, errClient + } + requestedModel = strings.TrimSpace(requestedModel) + if requestedModel == "" { + return nil, fmt.Errorf("home: requested model is empty") + } + req := newAuthDispatchRequest(requestedModel, sessionID, headers, count) + keyBytes, err := json.Marshal(&req) + if err != nil { + return nil, err + } + + raw, err := cmd.RPop(ctx, string(keyBytes)).Bytes() + if errors.Is(err, redis.Nil) { + return nil, ErrAuthNotFound + } + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, ErrEmptyResponse + } + return raw, nil +} + +func (c *Client) GetRefreshAuth(ctx context.Context, authIndex string) ([]byte, error) { + cmd, errClient := c.commandClient() + if errClient != nil { + return nil, errClient + } + authIndex = strings.TrimSpace(authIndex) + if authIndex == "" { + return nil, fmt.Errorf("home: auth_index is empty") + } + req := refreshRequest{ + Type: "refresh", + AuthIndex: authIndex, + } + keyBytes, err := json.Marshal(&req) + if err != nil { + return nil, err + } + + raw, err := cmd.Get(ctx, string(keyBytes)).Bytes() + if errors.Is(err, redis.Nil) { + return nil, ErrAuthNotFound + } + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, ErrEmptyResponse + } + return raw, nil +} + +func (c *Client) LPushUsage(ctx context.Context, payload []byte) error { + cmd, errClient := c.commandClient() + if errClient != nil { + return errClient + } + if len(payload) == 0 { + return nil + } + return cmd.LPush(ctx, redisKeyUsage, payload).Err() +} + +func (c *Client) RPushRequestLog(ctx context.Context, payload []byte) error { + cmd, errClient := c.commandClient() + if errClient != nil { + return errClient + } + if len(payload) == 0 { + return nil + } + return cmd.RPush(ctx, redisKeyRequestLog, payload).Err() +} + +// StartConfigSubscriber connects to home, fetches config once via GET config, then subscribes to +// the "config" channel to receive runtime config updates. +// +// The subscription connection is treated as the home heartbeat. HeartbeatOK is set to true only +// after the initial GET config succeeds and the SUBSCRIBE connection is established. When the +// subscription ends unexpectedly, HeartbeatOK becomes false and the loop reconnects. +func (c *Client) StartConfigSubscriber(ctx context.Context, onConfig func([]byte) error) { + if c == nil { + return + } + if !c.Enabled() { + return + } + if onConfig == nil { + return + } + + for { + if ctx != nil { + select { + case <-ctx.Done(): + c.heartbeatOK.Store(false) + return + default: + } + } + + c.heartbeatOK.Store(false) + c.Close() + + if errEnsure := c.ensureClients(); errEnsure != nil { + log.Warn("unable to connect to home control center, retrying in 1 second") + c.markReconnectFailure("connect") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + if errPing := c.Ping(ctx); errPing != nil { + log.Warn("unable to connect to home control center, retrying in 1 second") + c.markReconnectFailure("ping") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + raw, errGet := c.GetConfig(ctx) + if errGet != nil { + log.Warn("unable to fetch config from home control center, retrying in 1 second") + c.markReconnectFailure("config fetch") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + if errApply := onConfig(raw); errApply != nil { + log.Warn("unable to apply config from home control center, retrying in 1 second") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + sub, errSubClient := c.subscriptionClient() + if errSubClient != nil { + c.markReconnectFailure("subscribe client") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + pubsub := sub.Subscribe(ctx, redisChannelConfig) + if pubsub == nil { + c.markReconnectFailure("subscribe") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + // Ensure the subscription is established before marking heartbeat OK. + if _, errReceive := pubsub.Receive(ctx); errReceive != nil { + _ = pubsub.Close() + c.markReconnectFailure("subscribe") + sleepWithContext(ctx, homeReconnectInterval) + continue + } + + c.resetReconnectFailures() + c.heartbeatOK.Store(true) + + for { + msg, errMsg := pubsub.ReceiveMessage(ctx) + if errMsg != nil { + _ = pubsub.Close() + c.heartbeatOK.Store(false) + c.markReconnectFailure("subscription") + sleepWithContext(ctx, homeReconnectInterval) + break + } + if msg == nil { + continue + } + if payload := strings.TrimSpace(msg.Payload); payload != "" { + if errApply := onConfig([]byte(payload)); errApply != nil { + log.Warn("failed to apply config update from home control center, ignoring") + } + } + } + } +} + +func sleepWithContext(ctx context.Context, d time.Duration) { + if d <= 0 { + return + } + timer := time.NewTimer(d) + defer timer.Stop() + if ctx == nil { + <-timer.C + return + } + select { + case <-ctx.Done(): + return + case <-timer.C: + return + } +} diff --git a/internal/home/client_test.go b/internal/home/client_test.go new file mode 100644 index 0000000000..625e77bcac --- /dev/null +++ b/internal/home/client_test.go @@ -0,0 +1,32 @@ +package home + +import ( + "encoding/json" + "net/http" + "testing" +) + +func TestAuthDispatchRequestIncludesCount(t *testing.T) { + req := newAuthDispatchRequest("gpt-5.4", "session-1", http.Header{"Authorization": {"Bearer test"}}, 2) + + raw, err := json.Marshal(&req) + if err != nil { + t.Fatalf("marshal auth dispatch request: %v", err) + } + + var payload map[string]any + if err := json.Unmarshal(raw, &payload); err != nil { + t.Fatalf("unmarshal auth dispatch request: %v", err) + } + if got := int(payload["count"].(float64)); got != 2 { + t.Fatalf("count = %d, want 2", got) + } +} + +func TestAuthDispatchRequestDefaultsCountToOne(t *testing.T) { + req := newAuthDispatchRequest("gpt-5.4", "", nil, 0) + + if req.Count != 1 { + t.Fatalf("count = %d, want 1", req.Count) + } +} diff --git a/internal/home/global.go b/internal/home/global.go new file mode 100644 index 0000000000..a79121a487 --- /dev/null +++ b/internal/home/global.go @@ -0,0 +1,25 @@ +package home + +import "sync/atomic" + +var currentClient atomic.Value // *Client + +// SetCurrent sets the active home client used by runtime integrations. +func SetCurrent(client *Client) { + currentClient.Store(client) +} + +// Current returns the active home client instance, if any. +func Current() *Client { + if v := currentClient.Load(); v != nil { + if client, ok := v.(*Client); ok { + return client + } + } + return nil +} + +// ClearCurrent removes the active home client. +func ClearCurrent() { + currentClient.Store((*Client)(nil)) +} diff --git a/internal/home/requests.go b/internal/home/requests.go new file mode 100644 index 0000000000..0757766468 --- /dev/null +++ b/internal/home/requests.go @@ -0,0 +1,14 @@ +package home + +type authDispatchRequest struct { + Type string `json:"type"` + Model string `json:"model"` + Count int `json:"count"` + SessionID string `json:"session_id,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +type refreshRequest struct { + Type string `json:"type"` + AuthIndex string `json:"auth_index"` +} diff --git a/internal/interfaces/types.go b/internal/interfaces/types.go index 9fb1e7f3b8..dfdfc02a84 100644 --- a/internal/interfaces/types.go +++ b/internal/interfaces/types.go @@ -3,7 +3,7 @@ // transformation operations, maintaining compatibility with the SDK translator package. package interfaces -import sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" +import sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" // Backwards compatible aliases for translator function types. type TranslateRequestFunc = sdktranslator.RequestTransform diff --git a/internal/logging/gin_logger.go b/internal/logging/gin_logger.go index 4d6d088c03..6e3559b8c3 100644 --- a/internal/logging/gin_logger.go +++ b/internal/logging/gin_logger.go @@ -12,7 +12,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/internal/logging/global_logger.go b/internal/logging/global_logger.go index 372222a545..4b4ef62c85 100644 --- a/internal/logging/global_logger.go +++ b/internal/logging/global_logger.go @@ -10,8 +10,8 @@ import ( "sync" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" ) diff --git a/internal/logging/request_logger.go b/internal/logging/request_logger.go index 2db2a504d3..44b2c95264 100644 --- a/internal/logging/request_logger.go +++ b/internal/logging/request_logger.go @@ -8,6 +8,8 @@ import ( "bytes" "compress/flate" "compress/gzip" + "context" + "encoding/json" "fmt" "io" "os" @@ -22,13 +24,23 @@ import ( "github.com/klauspost/compress/zstd" log "github.com/sirupsen/logrus" - "github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" ) var requestLogID atomic.Uint64 +type homeRequestLogClient interface { + HeartbeatOK() bool + RPushRequestLog(ctx context.Context, payload []byte) error +} + +var currentHomeRequestLogClient = func() homeRequestLogClient { + return home.Current() +} + // RequestLogger defines the interface for logging HTTP requests and responses. // It provides methods for logging both regular and streaming HTTP request/response cycles. type RequestLogger interface { @@ -148,6 +160,58 @@ type FileRequestLogger struct { // errorLogsMaxFiles limits the number of error log files retained. errorLogsMaxFiles int + + homeEnabled bool +} + +type homeRequestLogPayload struct { + Headers map[string][]string `json:"headers,omitempty"` + RequestLog string `json:"request_log,omitempty"` +} + +func cloneHeaders(headers map[string][]string) map[string][]string { + if len(headers) == 0 { + return nil + } + out := make(map[string][]string, len(headers)) + for key, values := range headers { + if strings.TrimSpace(key) == "" { + continue + } + if values == nil { + out[key] = nil + continue + } + copied := make([]string, len(values)) + copy(copied, values) + out[key] = copied + } + if len(out) == 0 { + return nil + } + return out +} + +func (l *FileRequestLogger) forwardRequestLogToHome(ctx context.Context, headers map[string][]string, logText string) error { + if l == nil || !l.homeEnabled { + return nil + } + client := currentHomeRequestLogClient() + if client == nil || !client.HeartbeatOK() { + return nil + } + payload := homeRequestLogPayload{ + Headers: cloneHeaders(headers), + RequestLog: logText, + } + raw, errMarshal := json.Marshal(&payload) + if errMarshal != nil { + return errMarshal + } + if ctx == nil { + ctx = context.Background() + } + return client.RPushRequestLog(ctx, raw) } // NewFileRequestLogger creates a new file-based request logger. @@ -173,7 +237,17 @@ func NewFileRequestLogger(enabled bool, logsDir string, configDir string, errorL enabled: enabled, logsDir: logsDir, errorLogsMaxFiles: errorLogsMaxFiles, + homeEnabled: false, + } +} + +// SetHomeEnabled toggles home request-log forwarding. +// When enabled, request logs are not written to disk and are instead forwarded to home via Redis RESP. +func (l *FileRequestLogger) SetHomeEnabled(enabled bool) { + if l == nil { + return } + l.homeEnabled = enabled } // IsEnabled returns whether request logging is currently enabled. @@ -231,6 +305,38 @@ func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[st return nil } + if l.homeEnabled && l.enabled { + responseToWrite, decompressErr := l.decompressResponse(responseHeaders, response) + if decompressErr != nil { + responseToWrite = response + } + + var buf bytes.Buffer + writeErr := l.writeNonStreamingLog( + &buf, + url, + method, + requestHeaders, + body, + "", + websocketTimeline, + apiRequest, + apiResponse, + apiWebsocketTimeline, + apiResponseErrors, + statusCode, + responseHeaders, + responseToWrite, + decompressErr, + requestTimestamp, + apiResponseTimestamp, + ) + if writeErr != nil { + return fmt.Errorf("failed to build request log content: %w", writeErr) + } + return l.forwardRequestLogToHome(context.Background(), requestHeaders, buf.String()) + } + // Ensure logs directory exists if errEnsure := l.ensureLogsDir(); errEnsure != nil { return fmt.Errorf("failed to create logs directory: %w", errEnsure) @@ -321,6 +427,14 @@ func (l *FileRequestLogger) LogStreamingRequest(url, method string, headers map[ return &NoOpStreamingLogWriter{}, nil } + if l.homeEnabled { + client := home.Current() + if client == nil || !client.HeartbeatOK() { + return &NoOpStreamingLogWriter{}, nil + } + return newHomeStreamingLogWriter(url, method, headers, body, requestID), nil + } + // Ensure logs directory exists if err := l.ensureLogsDir(); err != nil { return nil, fmt.Errorf("failed to create logs directory: %w", err) @@ -1498,3 +1612,165 @@ func (w *NoOpStreamingLogWriter) SetFirstChunkTimestamp(_ time.Time) {} // Returns: // - error: Always returns nil func (w *NoOpStreamingLogWriter) Close() error { return nil } + +type homeStreamingLogWriter struct { + url string + method string + timestamp time.Time + + requestHeaders map[string][]string + requestBody []byte + + chunkChan chan []byte + doneChan chan struct{} + + responseStatus int + statusWritten bool + responseHeaders map[string][]string + responseBody bytes.Buffer + apiRequest []byte + apiResponse []byte + apiWebsocketTime []byte + apiResponseTS time.Time + firstChunkTS time.Time +} + +func newHomeStreamingLogWriter(url, method string, headers map[string][]string, body []byte, _ string) *homeStreamingLogWriter { + requestHeaders := make(map[string][]string, len(headers)) + for key, values := range headers { + headerValues := make([]string, len(values)) + copy(headerValues, values) + requestHeaders[key] = headerValues + } + + writer := &homeStreamingLogWriter{ + url: url, + method: method, + timestamp: time.Now(), + requestHeaders: requestHeaders, + requestBody: append([]byte(nil), body...), + chunkChan: make(chan []byte, 100), + doneChan: make(chan struct{}), + } + + go writer.asyncWriter() + return writer +} + +func (w *homeStreamingLogWriter) asyncWriter() { + defer close(w.doneChan) + for chunk := range w.chunkChan { + if len(chunk) == 0 { + continue + } + _, _ = w.responseBody.Write(chunk) + } +} + +func (w *homeStreamingLogWriter) WriteChunkAsync(chunk []byte) { + if w == nil || w.chunkChan == nil || len(chunk) == 0 { + return + } + select { + case w.chunkChan <- append([]byte(nil), chunk...): + default: + } +} + +func (w *homeStreamingLogWriter) WriteStatus(status int, headers map[string][]string) error { + if w == nil || status == 0 { + return nil + } + w.responseStatus = status + w.statusWritten = true + if headers != nil { + w.responseHeaders = make(map[string][]string, len(headers)) + for key, values := range headers { + copied := make([]string, len(values)) + copy(copied, values) + w.responseHeaders[key] = copied + } + } + return nil +} + +func (w *homeStreamingLogWriter) WriteAPIRequest(apiRequest []byte) error { + if w == nil || len(apiRequest) == 0 { + return nil + } + w.apiRequest = bytes.Clone(apiRequest) + return nil +} + +func (w *homeStreamingLogWriter) WriteAPIResponse(apiResponse []byte) error { + if w == nil || len(apiResponse) == 0 { + return nil + } + w.apiResponse = bytes.Clone(apiResponse) + return nil +} + +func (w *homeStreamingLogWriter) WriteAPIWebsocketTimeline(apiWebsocketTimeline []byte) error { + if w == nil || len(apiWebsocketTimeline) == 0 { + return nil + } + w.apiWebsocketTime = bytes.Clone(apiWebsocketTimeline) + return nil +} + +func (w *homeStreamingLogWriter) SetFirstChunkTimestamp(timestamp time.Time) { + if w == nil { + return + } + if !timestamp.IsZero() { + w.firstChunkTS = timestamp + w.apiResponseTS = timestamp + } +} + +func (w *homeStreamingLogWriter) Close() error { + if w == nil { + return nil + } + + client := currentHomeRequestLogClient() + if client == nil || !client.HeartbeatOK() { + return nil + } + + if w.chunkChan != nil { + close(w.chunkChan) + <-w.doneChan + w.chunkChan = nil + } + + responsePayload := w.responseBody.Bytes() + + var buf bytes.Buffer + upstreamTransport := inferUpstreamTransport(w.apiRequest, w.apiResponse, w.apiWebsocketTime, nil) + if errWrite := writeRequestInfoWithBody(&buf, w.url, w.method, w.requestHeaders, w.requestBody, "", w.timestamp, "http", upstreamTransport, true); errWrite != nil { + return errWrite + } + if errWrite := writeAPISection(&buf, "=== API WEBSOCKET TIMELINE ===\n", "=== API WEBSOCKET TIMELINE", w.apiWebsocketTime, time.Time{}); errWrite != nil { + return errWrite + } + if errWrite := writeAPISection(&buf, "=== API REQUEST ===\n", "=== API REQUEST", w.apiRequest, time.Time{}); errWrite != nil { + return errWrite + } + if errWrite := writeAPISection(&buf, "=== API RESPONSE ===\n", "=== API RESPONSE", w.apiResponse, w.apiResponseTS); errWrite != nil { + return errWrite + } + if errWrite := writeResponseSection(&buf, w.responseStatus, w.statusWritten, w.responseHeaders, bytes.NewReader(responsePayload), nil, false); errWrite != nil { + return errWrite + } + + payload := homeRequestLogPayload{ + Headers: cloneHeaders(w.requestHeaders), + RequestLog: buf.String(), + } + raw, errMarshal := json.Marshal(&payload) + if errMarshal != nil { + return errMarshal + } + return client.RPushRequestLog(context.Background(), raw) +} diff --git a/internal/logging/request_logger_home_test.go b/internal/logging/request_logger_home_test.go new file mode 100644 index 0000000000..f8cdf1e453 --- /dev/null +++ b/internal/logging/request_logger_home_test.go @@ -0,0 +1,154 @@ +package logging + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "os" + "testing" + "time" +) + +type stubHomeRequestLogClient struct { + heartbeatOK bool + pushed [][]byte +} + +func (c *stubHomeRequestLogClient) HeartbeatOK() bool { return c.heartbeatOK } + +func (c *stubHomeRequestLogClient) RPushRequestLog(_ context.Context, payload []byte) error { + c.pushed = append(c.pushed, bytes.Clone(payload)) + return nil +} + +func TestFileRequestLogger_HomeEnabled_ForwardsWhenRequestLogEnabled(t *testing.T) { + original := currentHomeRequestLogClient + defer func() { + currentHomeRequestLogClient = original + }() + + stub := &stubHomeRequestLogClient{heartbeatOK: true} + currentHomeRequestLogClient = func() homeRequestLogClient { + return stub + } + + logsDir := t.TempDir() + logger := NewFileRequestLogger(true, logsDir, "", 0) + logger.SetHomeEnabled(true) + + requestHeaders := map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {"Bearer secret"}, + } + + errLog := logger.LogRequest( + "/v1/chat/completions", + http.MethodPost, + requestHeaders, + []byte(`{"input":"hello"}`), + http.StatusOK, + map[string][]string{"Content-Type": {"application/json"}}, + []byte(`{"ok":true}`), + nil, + nil, + nil, + nil, + nil, + "req-1", + time.Now(), + time.Now(), + ) + if errLog != nil { + t.Fatalf("LogRequest error: %v", errLog) + } + + entries, errRead := os.ReadDir(logsDir) + if errRead != nil { + t.Fatalf("failed to read logs dir: %v", errRead) + } + if len(entries) != 0 { + t.Fatalf("expected no local request log files, got entries: %+v", entries) + } + + if len(stub.pushed) != 1 { + t.Fatalf("home pushed records = %d, want 1", len(stub.pushed)) + } + + var got struct { + Headers map[string][]string `json:"headers"` + RequestLog string `json:"request_log"` + } + if errUnmarshal := json.Unmarshal(stub.pushed[0], &got); errUnmarshal != nil { + t.Fatalf("unmarshal payload: %v payload=%s", errUnmarshal, string(stub.pushed[0])) + } + if got.Headers == nil || got.Headers["Content-Type"][0] != "application/json" { + t.Fatalf("headers.content-type = %+v, want application/json", got.Headers["Content-Type"]) + } + if got.Headers == nil || got.Headers["Authorization"][0] != "Bearer secret" { + t.Fatalf("headers.authorization = %+v, want Bearer secret", got.Headers["Authorization"]) + } + if got.RequestLog == "" { + t.Fatalf("request_log empty, want non-empty") + } +} + +func TestFileRequestLogger_HomeEnabled_DoesNotForwardForcedErrorLogsWhenRequestLogDisabled(t *testing.T) { + original := currentHomeRequestLogClient + defer func() { + currentHomeRequestLogClient = original + }() + + stub := &stubHomeRequestLogClient{heartbeatOK: true} + currentHomeRequestLogClient = func() homeRequestLogClient { + return stub + } + + logsDir := t.TempDir() + logger := NewFileRequestLogger(false, logsDir, "", 0) + logger.SetHomeEnabled(true) + + errLog := logger.LogRequestWithOptions( + "/v1/chat/completions", + http.MethodPost, + map[string][]string{"Content-Type": {"application/json"}}, + []byte(`{"input":"hello"}`), + http.StatusBadGateway, + map[string][]string{"Content-Type": {"application/json"}}, + []byte(`{"error":"upstream failure"}`), + nil, + nil, + nil, + nil, + nil, + true, + "req-2", + time.Now(), + time.Now(), + ) + if errLog != nil { + t.Fatalf("LogRequestWithOptions error: %v", errLog) + } + + if len(stub.pushed) != 0 { + t.Fatalf("home pushed records = %d, want 0", len(stub.pushed)) + } + + entries, errRead := os.ReadDir(logsDir) + if errRead != nil { + t.Fatalf("failed to read logs dir: %v", errRead) + } + found := false + for _, entry := range entries { + if entry.IsDir() { + continue + } + if entry.Name() != "" { + found = true + break + } + } + if !found { + t.Fatalf("expected local forced error log file when request-log disabled") + } +} diff --git a/internal/managementasset/updater.go b/internal/managementasset/updater.go index ae2bc81956..ea7ca3f502 100644 --- a/internal/managementasset/updater.go +++ b/internal/managementasset/updater.go @@ -17,9 +17,9 @@ import ( "sync/atomic" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" log "github.com/sirupsen/logrus" "golang.org/x/sync/singleflight" ) diff --git a/internal/redisqueue/plugin.go b/internal/redisqueue/plugin.go index b33bc8fd95..057052d143 100644 --- a/internal/redisqueue/plugin.go +++ b/internal/redisqueue/plugin.go @@ -6,8 +6,8 @@ import ( "strings" "time" - internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" ) func init() { @@ -49,11 +49,13 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec requestID := strings.TrimSpace(internallogging.GetRequestID(ctx)) tokens := tokenStats{ - InputTokens: record.Detail.InputTokens, - OutputTokens: record.Detail.OutputTokens, - ReasoningTokens: record.Detail.ReasoningTokens, - CachedTokens: record.Detail.CachedTokens, - TotalTokens: record.Detail.TotalTokens, + InputTokens: record.Detail.InputTokens, + OutputTokens: record.Detail.OutputTokens, + ReasoningTokens: record.Detail.ReasoningTokens, + CachedTokens: record.Detail.CachedTokens, + CacheReadTokens: record.Detail.CacheReadTokens, + CacheCreationTokens: record.Detail.CacheCreationTokens, + TotalTokens: record.Detail.TotalTokens, } if tokens.TotalTokens == 0 { tokens.TotalTokens = tokens.InputTokens + tokens.OutputTokens + tokens.ReasoningTokens @@ -66,6 +68,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec if !failed { failed = !resolveSuccess(ctx) } + fail := resolveFail(ctx, record, failed) detail := requestDetail{ Timestamp: timestamp, @@ -74,6 +77,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec AuthIndex: record.AuthIndex, Tokens: tokens, Failed: failed, + Fail: fail, } payload, err := json.Marshal(queuedUsageDetail{ @@ -110,14 +114,39 @@ type requestDetail struct { AuthIndex string `json:"auth_index"` Tokens tokenStats `json:"tokens"` Failed bool `json:"failed"` + Fail failDetail `json:"fail"` } type tokenStats struct { - InputTokens int64 `json:"input_tokens"` - OutputTokens int64 `json:"output_tokens"` - ReasoningTokens int64 `json:"reasoning_tokens"` - CachedTokens int64 `json:"cached_tokens"` - TotalTokens int64 `json:"total_tokens"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + ReasoningTokens int64 `json:"reasoning_tokens"` + CachedTokens int64 `json:"cached_tokens"` + CacheReadTokens int64 `json:"cache_read_tokens"` + CacheCreationTokens int64 `json:"cache_creation_tokens"` + TotalTokens int64 `json:"total_tokens"` +} + +type failDetail struct { + StatusCode int `json:"status_code"` + Body string `json:"body"` +} + +func resolveFail(ctx context.Context, record coreusage.Record, failed bool) failDetail { + fail := failDetail{ + StatusCode: record.Fail.StatusCode, + Body: strings.TrimSpace(record.Fail.Body), + } + if !failed { + return failDetail{StatusCode: 200} + } + if fail.StatusCode <= 0 { + fail.StatusCode = internallogging.GetResponseStatus(ctx) + } + if fail.StatusCode <= 0 { + fail.StatusCode = 500 + } + return fail } func resolveSuccess(ctx context.Context) bool { diff --git a/internal/redisqueue/plugin_test.go b/internal/redisqueue/plugin_test.go index 8dcade90ee..e2af6af709 100644 --- a/internal/redisqueue/plugin_test.go +++ b/internal/redisqueue/plugin_test.go @@ -9,8 +9,8 @@ import ( "time" "github.com/gin-gonic/gin" - internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" ) func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) { @@ -44,8 +44,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) { requireStringField(t, payload, "alias", "client-gpt") requireStringField(t, payload, "endpoint", "POST /v1/chat/completions") requireStringField(t, payload, "auth_type", "apikey") + requireMissingField(t, payload, "user_api_key") requireStringField(t, payload, "request_id", "ctx-request-id") requireBoolField(t, payload, "failed", false) + requireFailField(t, payload, http.StatusOK, "") }) } @@ -67,6 +69,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t Source: "user@example.com", RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC), Latency: 2500 * time.Millisecond, + Fail: coreusage.Failure{ + StatusCode: http.StatusInternalServerError, + Body: "upstream failed", + }, Detail: coreusage.Detail{ InputTokens: 10, OutputTokens: 20, @@ -80,8 +86,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t requireStringField(t, payload, "alias", "client-mini") requireStringField(t, payload, "endpoint", "GET /v1/responses") requireStringField(t, payload, "auth_type", "apikey") + requireMissingField(t, payload, "user_api_key") requireStringField(t, payload, "request_id", "gin-request-id") requireBoolField(t, payload, "failed", true) + requireFailField(t, payload, http.StatusInternalServerError, "upstream failed") }) } @@ -113,6 +121,10 @@ func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) { Source: "user@example.com", RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC), Latency: 1500 * time.Millisecond, + Fail: coreusage.Failure{ + StatusCode: http.StatusBadGateway, + Body: "bad gateway", + }, Detail: coreusage.Detail{ InputTokens: 10, OutputTokens: 20, @@ -123,8 +135,10 @@ func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) { payload := waitForSinglePayload(t, 2*time.Second) requireStringField(t, payload, "endpoint", "POST /v1/chat/completions") requireStringField(t, payload, "alias", "client-gpt") + requireMissingField(t, payload, "user_api_key") requireStringField(t, payload, "request_id", "ctx-request-id") requireBoolField(t, payload, "failed", true) + requireFailField(t, payload, http.StatusBadGateway, "bad gateway") }) } @@ -214,6 +228,14 @@ func requireStringField(t *testing.T, payload map[string]json.RawMessage, key, w } } +func requireMissingField(t *testing.T, payload map[string]json.RawMessage, key string) { + t.Helper() + + if _, ok := payload[key]; ok { + t.Fatalf("payload unexpectedly contains %q", key) + } +} + type pluginFunc func(context.Context, coreusage.Record) func (fn pluginFunc) HandleUsage(ctx context.Context, record coreusage.Record) { @@ -235,3 +257,22 @@ func requireBoolField(t *testing.T, payload map[string]json.RawMessage, key stri t.Fatalf("%s = %t, want %t", key, got, want) } } + +func requireFailField(t *testing.T, payload map[string]json.RawMessage, wantStatus int, wantBody string) { + t.Helper() + + raw, ok := payload["fail"] + if !ok { + t.Fatalf("payload missing %q", "fail") + } + var got struct { + StatusCode int `json:"status_code"` + Body string `json:"body"` + } + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("unmarshal fail: %v", err) + } + if got.StatusCode != wantStatus || got.Body != wantBody { + t.Fatalf("fail = {status_code:%d body:%q}, want {status_code:%d body:%q}", got.StatusCode, got.Body, wantStatus, wantBody) + } +} diff --git a/internal/registry/model_registry.go b/internal/registry/model_registry.go index 3f3f530d27..4c215bb7af 100644 --- a/internal/registry/model_registry.go +++ b/internal/registry/model_registry.go @@ -11,7 +11,7 @@ import ( "sync" "time" - misc "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + misc "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" log "github.com/sirupsen/logrus" ) diff --git a/internal/runtime/executor/aistudio_executor.go b/internal/runtime/executor/aistudio_executor.go index 37e85377b2..41365b5f7a 100644 --- a/internal/runtime/executor/aistudio_executor.go +++ b/internal/runtime/executor/aistudio_executor.go @@ -13,14 +13,14 @@ import ( "net/url" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/wsrelay" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -284,7 +284,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth processEvent := func(event wsrelay.StreamEvent) bool { if event.Err != nil { helps.RecordAPIResponseError(ctx, e.cfg, event.Err) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, event.Err) select { case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}: case <-ctx.Done(): @@ -336,7 +336,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth return false case wsrelay.MessageTypeError: helps.RecordAPIResponseError(ctx, e.cfg, event.Err) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, event.Err) select { case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}: case <-ctx.Done(): @@ -414,7 +414,10 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A } // Refresh refreshes the authentication credentials (no-op for AI Studio). -func (e *AIStudioExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { +func (e *AIStudioExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } return auth, nil } diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 418ed7b1c5..2f8dff927c 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -23,18 +23,18 @@ import ( "time" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - antigravityclaude "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + antigravityclaude "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -898,7 +898,7 @@ attemptLoop: } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) out <- cliproxyexecutor.StreamChunk{Err: errScan} } else { reporter.EnsurePublished(ctx) @@ -1374,7 +1374,7 @@ attemptLoop: } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -1402,6 +1402,9 @@ attemptLoop: // Refresh refreshes the authentication credentials using the refresh token. func (e *AntigravityExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } if auth == nil { return auth, nil } @@ -1589,6 +1592,18 @@ func (e *AntigravityExecutor) ensureAccessToken(ctx context.Context, auth *clipr refreshCtx = context.WithValue(refreshCtx, "cliproxy.roundtripper", rt) } } + if refreshed, handled, err := helps.RefreshAuthViaHome(refreshCtx, e.cfg, auth); handled { + if err != nil { + return "", nil, err + } + token := metaStringValue(refreshed.Metadata, "access_token") + if strings.TrimSpace(token) == "" { + return "", nil, statusErr{code: http.StatusUnauthorized, msg: "missing access token"} + } + e.maybeRefreshAntigravityCreditsHint(ctx, refreshed, token) + return token, refreshed, nil + } + updated, errRefresh := e.refreshToken(refreshCtx, auth.Clone()) if errRefresh != nil { return "", nil, errRefresh diff --git a/internal/runtime/executor/antigravity_executor_buildrequest_test.go b/internal/runtime/executor/antigravity_executor_buildrequest_test.go index ed2d79e632..f0711752e4 100644 --- a/internal/runtime/executor/antigravity_executor_buildrequest_test.go +++ b/internal/runtime/executor/antigravity_executor_buildrequest_test.go @@ -6,7 +6,7 @@ import ( "io" "testing" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestAntigravityBuildRequest_SanitizesGeminiToolSchema(t *testing.T) { diff --git a/internal/runtime/executor/antigravity_executor_credits_test.go b/internal/runtime/executor/antigravity_executor_credits_test.go index 4569f5dfd7..e16e64434f 100644 --- a/internal/runtime/executor/antigravity_executor_credits_test.go +++ b/internal/runtime/executor/antigravity_executor_credits_test.go @@ -10,10 +10,10 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) func resetAntigravityCreditsRetryState() { diff --git a/internal/runtime/executor/antigravity_executor_signature_test.go b/internal/runtime/executor/antigravity_executor_signature_test.go index 226daf5c67..7d84bfe890 100644 --- a/internal/runtime/executor/antigravity_executor_signature_test.go +++ b/internal/runtime/executor/antigravity_executor_signature_test.go @@ -10,10 +10,10 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) func testGeminiSignaturePayload() string { diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index b22f4e4486..eb17864d6e 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -17,16 +17,16 @@ import ( "github.com/andybalholm/brotli" "github.com/google/uuid" "github.com/klauspost/compress/zstd" - claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -472,7 +472,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -512,7 +512,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -691,6 +691,9 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("claude executor: refresh called") + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } if auth == nil { return nil, fmt.Errorf("claude executor: auth is nil") } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 2e91404405..f5bca55ab7 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -17,12 +17,12 @@ import ( "github.com/gin-gonic/gin" "github.com/klauspost/compress/zstd" xxHash64 "github.com/pierrec/xxHash/xxHash64" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/runtime/executor/claude_signing.go b/internal/runtime/executor/claude_signing.go index 697a688265..060e86e846 100644 --- a/internal/runtime/executor/claude_signing.go +++ b/internal/runtime/executor/claude_signing.go @@ -6,8 +6,8 @@ import ( "strings" xxHash64 "github.com/pierrec/xxHash/xxHash64" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 19cc8e7557..a1bbe6b84a 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -11,15 +11,15 @@ import ( "strings" "time" - codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + codexauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -524,7 +524,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -693,6 +693,9 @@ func countCodexInputTokens(enc tokenizer.Codec, body []byte) (int64, error) { func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("codex executor: refresh called") + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } if auth == nil { return nil, statusErr{code: 500, msg: "codex executor: auth is nil"} } diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index 7a24fd9643..cb96a90289 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -8,15 +8,15 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFromAPIKey(t *testing.T) { recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) - ginCtx.Set("apiKey", "test-api-key") + ginCtx.Set("userApiKey", "test-api-key") ctx := context.WithValue(context.Background(), "gin", ginCtx) executor := &CodexExecutor{} diff --git a/internal/runtime/executor/codex_executor_compact_test.go b/internal/runtime/executor/codex_executor_compact_test.go index 02c6db29fd..549cad9e77 100644 --- a/internal/runtime/executor/codex_executor_compact_test.go +++ b/internal/runtime/executor/codex_executor_compact_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/codex_executor_imagegen_test.go b/internal/runtime/executor/codex_executor_imagegen_test.go index 1657209a91..89d2a1c2a3 100644 --- a/internal/runtime/executor/codex_executor_imagegen_test.go +++ b/internal/runtime/executor/codex_executor_imagegen_test.go @@ -3,7 +3,7 @@ package executor import ( "testing" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/codex_executor_instructions_test.go b/internal/runtime/executor/codex_executor_instructions_test.go index c5dc5aa813..b3c8ac18ac 100644 --- a/internal/runtime/executor/codex_executor_instructions_test.go +++ b/internal/runtime/executor/codex_executor_instructions_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/codex_executor_stream_output_test.go b/internal/runtime/executor/codex_executor_stream_output_test.go index a2da45e199..b814c3e96d 100644 --- a/internal/runtime/executor/codex_executor_stream_output_test.go +++ b/internal/runtime/executor/codex_executor_stream_output_test.go @@ -7,11 +7,11 @@ import ( "net/http/httptest" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index 94b78b66d8..2b56f13b1c 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -18,15 +18,15 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/gorilla/websocket" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -580,7 +580,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr terminateReason = "read_error" terminateErr = errRead helps.RecordAPIWebsocketError(ctx, e.cfg, "read", errRead) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errRead) _ = send(cliproxyexecutor.StreamChunk{Err: errRead}) return } @@ -590,7 +590,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr terminateReason = "unexpected_binary" terminateErr = err helps.RecordAPIWebsocketError(ctx, e.cfg, "unexpected_binary", err) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, err) if sess != nil { e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err) } @@ -610,7 +610,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr terminateReason = "upstream_error" terminateErr = wsErr helps.RecordAPIWebsocketError(ctx, e.cfg, "upstream_error", wsErr) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, wsErr) if sess != nil { e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr) } diff --git a/internal/runtime/executor/codex_websockets_executor_store_test.go b/internal/runtime/executor/codex_websockets_executor_store_test.go index 1a23fa31b5..115ed066d2 100644 --- a/internal/runtime/executor/codex_websockets_executor_store_test.go +++ b/internal/runtime/executor/codex_websockets_executor_store_test.go @@ -3,7 +3,7 @@ package executor import ( "testing" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestCodexWebsocketsExecutor_SessionStoreSurvivesExecutorReplacement(t *testing.T) { diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index fbcf9c4527..4342ed8882 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -12,11 +12,11 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index b6210e6a1d..a298fe8a0e 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -16,15 +16,15 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/geminicli" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -430,7 +430,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -444,7 +444,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut data, errRead := io.ReadAll(resp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errRead) select { case out <- cliproxyexecutor.StreamChunk{Err: errRead}: case <-ctx.Done(): @@ -599,7 +599,10 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. } // Refresh refreshes the authentication credentials (no-op for Gemini CLI). -func (e *GeminiCLIExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { +func (e *GeminiCLIExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } return auth, nil } @@ -609,37 +612,43 @@ func prepareGeminiCLITokenSource(ctx context.Context, cfg *config.Config, auth * return nil, nil, fmt.Errorf("gemini-cli auth metadata missing") } - var base map[string]any - if tokenRaw, ok := metadata["token"].(map[string]any); ok && tokenRaw != nil { - base = cloneMap(tokenRaw) - } else { - base = make(map[string]any) - } + buildToken := func(meta map[string]any) (map[string]any, oauth2.Token) { + var base map[string]any + if tokenRaw, ok := meta["token"].(map[string]any); ok && tokenRaw != nil { + base = cloneMap(tokenRaw) + } else { + base = make(map[string]any) + } - var token oauth2.Token - if len(base) > 0 { - if raw, err := json.Marshal(base); err == nil { - _ = json.Unmarshal(raw, &token) + var token oauth2.Token + if len(base) > 0 { + if raw, err := json.Marshal(base); err == nil { + _ = json.Unmarshal(raw, &token) + } } - } - if token.AccessToken == "" { - token.AccessToken = stringValue(metadata, "access_token") - } - if token.RefreshToken == "" { - token.RefreshToken = stringValue(metadata, "refresh_token") - } - if token.TokenType == "" { - token.TokenType = stringValue(metadata, "token_type") - } - if token.Expiry.IsZero() { - if expiry := stringValue(metadata, "expiry"); expiry != "" { - if ts, err := time.Parse(time.RFC3339, expiry); err == nil { - token.Expiry = ts + if token.AccessToken == "" { + token.AccessToken = stringValue(meta, "access_token") + } + if token.RefreshToken == "" { + token.RefreshToken = stringValue(meta, "refresh_token") + } + if token.TokenType == "" { + token.TokenType = stringValue(meta, "token_type") + } + if token.Expiry.IsZero() { + if expiry := stringValue(meta, "expiry"); expiry != "" { + if ts, err := time.Parse(time.RFC3339, expiry); err == nil { + token.Expiry = ts + } } } + + return base, token } + base, token := buildToken(metadata) + conf := &oauth2.Config{ ClientID: geminiOAuthClientID, ClientSecret: geminiOAuthClientSecret, @@ -652,6 +661,29 @@ func prepareGeminiCLITokenSource(ctx context.Context, cfg *config.Config, auth * ctxToken = context.WithValue(ctxToken, oauth2.HTTPClient, httpClient) } + if cfg != nil && cfg.Home.Enabled { + now := time.Now() + if token.AccessToken == "" || (!token.Expiry.IsZero() && token.Expiry.Before(now.Add(30*time.Second))) { + refreshed, handled, errRefresh := helps.RefreshAuthViaHome(ctx, cfg, auth) + if handled { + if errRefresh != nil { + return nil, nil, errRefresh + } + auth = refreshed + metadata = geminiOAuthMetadata(auth) + if metadata == nil { + return nil, nil, fmt.Errorf("gemini-cli auth metadata missing") + } + base, token = buildToken(metadata) + } + } + if token.AccessToken == "" { + return nil, nil, fmt.Errorf("gemini-cli access token missing") + } + updateGeminiCLITokenMetadata(auth, base, &token) + return oauth2.StaticTokenSource(&token), base, nil + } + src := conf.TokenSource(ctxToken, &token) currentToken, err := src.Token() if err != nil { diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 2a6e9a6e79..e8fa2e405f 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -12,13 +12,13 @@ import ( "net/http" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -341,7 +341,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -437,7 +437,10 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut } // Refresh refreshes the authentication credentials (no-op for Gemini API key). -func (e *GeminiExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { +func (e *GeminiExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } return auth, nil } diff --git a/internal/runtime/executor/gemini_vertex_executor.go b/internal/runtime/executor/gemini_vertex_executor.go index 17a93d5150..b899524c6a 100644 --- a/internal/runtime/executor/gemini_vertex_executor.go +++ b/internal/runtime/executor/gemini_vertex_executor.go @@ -14,14 +14,14 @@ import ( "strings" "time" - vertexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + vertexauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -294,7 +294,10 @@ func (e *GeminiVertexExecutor) CountTokens(ctx context.Context, auth *cliproxyau } // Refresh refreshes the authentication credentials (no-op for Vertex). -func (e *GeminiVertexExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { +func (e *GeminiVertexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } return auth, nil } @@ -676,7 +679,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -818,7 +821,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): diff --git a/internal/runtime/executor/helps/claude_device_profile.go b/internal/runtime/executor/helps/claude_device_profile.go index 154901b53b..09f04929fe 100644 --- a/internal/runtime/executor/helps/claude_device_profile.go +++ b/internal/runtime/executor/helps/claude_device_profile.go @@ -11,8 +11,8 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) const ( diff --git a/internal/runtime/executor/helps/home_refresh.go b/internal/runtime/executor/helps/home_refresh.go new file mode 100644 index 0000000000..dc02704010 --- /dev/null +++ b/internal/runtime/executor/helps/home_refresh.go @@ -0,0 +1,102 @@ +package helps + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" +) + +type homeStatusErr struct { + code int + msg string +} + +func (e homeStatusErr) Error() string { + if e.msg != "" { + return e.msg + } + return fmt.Sprintf("status %d", e.code) +} + +func (e homeStatusErr) StatusCode() int { return e.code } + +type homeErrorEnvelope struct { + Error *homeErrorDetail `json:"error"` +} + +type homeErrorDetail struct { + Type string `json:"type"` + Message string `json:"message"` + Code string `json:"code,omitempty"` +} + +// RefreshAuthViaHome replaces local refresh logic when home control plane integration is enabled. +// It returns (updatedAuth, true, nil) when home refresh succeeds; (nil, true, err) when home is +// enabled but refresh fails; and (nil, false, nil) when home is disabled. +func RefreshAuthViaHome(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, bool, error) { + if cfg == nil || !cfg.Home.Enabled { + return nil, false, nil + } + if ctx == nil { + ctx = context.Background() + } + if auth == nil { + return nil, true, homeStatusErr{code: http.StatusInternalServerError, msg: "home refresh: auth is nil"} + } + + client := home.Current() + if client == nil || !client.HeartbeatOK() { + return nil, true, homeStatusErr{code: http.StatusServiceUnavailable, msg: "home control center unavailable"} + } + + authIndex := strings.TrimSpace(auth.Index) + if authIndex == "" { + authIndex = strings.TrimSpace(auth.EnsureIndex()) + } + if authIndex == "" { + return nil, true, homeStatusErr{code: http.StatusBadGateway, msg: "home refresh: auth_index is empty"} + } + + raw, err := client.GetRefreshAuth(ctx, authIndex) + if err != nil { + return nil, true, homeStatusErr{code: http.StatusBadGateway, msg: err.Error()} + } + + var env homeErrorEnvelope + if errUnmarshal := json.Unmarshal(raw, &env); errUnmarshal == nil && env.Error != nil { + code := strings.TrimSpace(env.Error.Type) + if code == "" { + code = strings.TrimSpace(env.Error.Code) + } + msg := strings.TrimSpace(env.Error.Message) + if msg == "" { + msg = "home returned error" + } + return nil, true, homeStatusErr{code: statusFromHomeErrorCode(code), msg: msg} + } + + var updated cliproxyauth.Auth + if errUnmarshal := json.Unmarshal(raw, &updated); errUnmarshal != nil { + return nil, true, homeStatusErr{code: http.StatusBadGateway, msg: "home returned invalid auth payload"} + } + updated.Index = authIndex + updated.EnsureIndex() + return &updated, true, nil +} + +func statusFromHomeErrorCode(code string) int { + switch strings.ToLower(strings.TrimSpace(code)) { + case "authentication_error", "unauthorized": + return http.StatusUnauthorized + case "model_not_found": + return http.StatusNotFound + default: + return http.StatusBadGateway + } +} diff --git a/internal/runtime/executor/helps/home_refresh_test.go b/internal/runtime/executor/helps/home_refresh_test.go new file mode 100644 index 0000000000..c4507fdcc1 --- /dev/null +++ b/internal/runtime/executor/helps/home_refresh_test.go @@ -0,0 +1,15 @@ +package helps + +import ( + "net/http" + "testing" +) + +func TestStatusFromHomeErrorCodeMapsAuthenticationErrorToUnauthorized(t *testing.T) { + if got := statusFromHomeErrorCode("authentication_error"); got != http.StatusUnauthorized { + t.Fatalf("statusFromHomeErrorCode(authentication_error) = %d, want %d", got, http.StatusUnauthorized) + } + if got := statusFromHomeErrorCode("unauthorized"); got != http.StatusUnauthorized { + t.Fatalf("statusFromHomeErrorCode(unauthorized) = %d, want %d", got, http.StatusUnauthorized) + } +} diff --git a/internal/runtime/executor/helps/logging_helpers.go b/internal/runtime/executor/helps/logging_helpers.go index a0b30f7099..fa7143347e 100644 --- a/internal/runtime/executor/helps/logging_helpers.go +++ b/internal/runtime/executor/helps/logging_helpers.go @@ -12,9 +12,9 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/helps/payload_helpers.go b/internal/runtime/executor/helps/payload_helpers.go index d6baba275b..af69a488c3 100644 --- a/internal/runtime/executor/helps/payload_helpers.go +++ b/internal/runtime/executor/helps/payload_helpers.go @@ -4,9 +4,9 @@ import ( "encoding/json" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/runtime/executor/helps/payload_helpers_disable_image_generation_test.go b/internal/runtime/executor/helps/payload_helpers_disable_image_generation_test.go index 6fd3a0e055..0faf012b35 100644 --- a/internal/runtime/executor/helps/payload_helpers_disable_image_generation_test.go +++ b/internal/runtime/executor/helps/payload_helpers_disable_image_generation_test.go @@ -3,7 +3,7 @@ package helps import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/tidwall/gjson" ) diff --git a/internal/runtime/executor/helps/proxy_helpers.go b/internal/runtime/executor/helps/proxy_helpers.go index 022bc65c17..91fdc9be49 100644 --- a/internal/runtime/executor/helps/proxy_helpers.go +++ b/internal/runtime/executor/helps/proxy_helpers.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" ) diff --git a/internal/runtime/executor/helps/proxy_helpers_test.go b/internal/runtime/executor/helps/proxy_helpers_test.go index 3311716765..fb57b6b745 100644 --- a/internal/runtime/executor/helps/proxy_helpers_test.go +++ b/internal/runtime/executor/helps/proxy_helpers_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) { diff --git a/internal/runtime/executor/helps/thinking_providers.go b/internal/runtime/executor/helps/thinking_providers.go index bbd019624d..a776136fde 100644 --- a/internal/runtime/executor/helps/thinking_providers.go +++ b/internal/runtime/executor/helps/thinking_providers.go @@ -1,11 +1,11 @@ package helps import ( - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/claude" - _ "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/kimi" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/openai" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/antigravity" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/codex" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/geminicli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/openai" ) diff --git a/internal/runtime/executor/helps/usage_helpers.go b/internal/runtime/executor/helps/usage_helpers.go index 312a1d35c3..a507a73e50 100644 --- a/internal/runtime/executor/helps/usage_helpers.go +++ b/internal/runtime/executor/helps/usage_helpers.go @@ -3,14 +3,15 @@ package helps import ( "bytes" "context" + "errors" "fmt" "strings" "sync" "time" "github.com/gin-gonic/gin" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -51,7 +52,7 @@ func NewUsageReporter(ctx context.Context, provider, model string, auth *cliprox } func (r *UsageReporter) Publish(ctx context.Context, detail usage.Detail) { - r.publishWithOutcome(ctx, detail, false) + r.publishWithOutcome(ctx, detail, false, usage.Failure{}) } func (r *UsageReporter) PublishAdditionalModel(ctx context.Context, model string, detail usage.Detail) { @@ -74,11 +75,11 @@ func (r *UsageReporter) buildAdditionalModelRecord(model string, detail usage.De if !hasNonZeroTokenUsage(detail) { return usage.Record{}, false } - return r.buildRecordForModel(model, detail, false), true + return r.buildRecordForModel(model, detail, false, usage.Failure{}), true } -func (r *UsageReporter) PublishFailure(ctx context.Context) { - r.publishWithOutcome(ctx, usage.Detail{}, true) +func (r *UsageReporter) PublishFailure(ctx context.Context, errs ...error) { + r.publishWithOutcome(ctx, usage.Detail{}, true, failFromErrors(errs...)) } func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) { @@ -86,17 +87,17 @@ func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) { return } if *errPtr != nil { - r.PublishFailure(ctx) + r.PublishFailure(ctx, *errPtr) } } -func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) { +func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool, fail usage.Failure) { if r == nil { return } detail = normalizeUsageDetailTotal(detail) r.once.Do(func() { - usage.PublishRecord(ctx, r.buildRecord(detail, failed)) + usage.PublishRecord(ctx, r.buildRecord(detail, failed, fail)) }) } @@ -115,6 +116,8 @@ func hasNonZeroTokenUsage(detail usage.Detail) bool { detail.OutputTokens != 0 || detail.ReasoningTokens != 0 || detail.CachedTokens != 0 || + detail.CacheReadTokens != 0 || + detail.CacheCreationTokens != 0 || detail.TotalTokens != 0 } @@ -127,20 +130,24 @@ func (r *UsageReporter) EnsurePublished(ctx context.Context) { return } r.once.Do(func() { - usage.PublishRecord(ctx, r.buildRecord(usage.Detail{}, false)) + usage.PublishRecord(ctx, r.buildRecord(usage.Detail{}, false, usage.Failure{})) }) } -func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool) usage.Record { +func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool, failures ...usage.Failure) usage.Record { + var fail usage.Failure + if len(failures) > 0 { + fail = failures[0] + } if r == nil { - return usage.Record{Detail: detail, Failed: failed} + return usage.Record{Detail: detail, Failed: failed, Fail: fail} } - return r.buildRecordForModel(r.model, detail, failed) + return r.buildRecordForModel(r.model, detail, failed, fail) } -func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, failed bool) usage.Record { +func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, failed bool, fail usage.Failure) usage.Record { if r == nil { - return usage.Record{Model: model, Detail: detail, Failed: failed} + return usage.Record{Model: model, Detail: detail, Failed: failed, Fail: fail} } return usage.Record{ Provider: r.provider, @@ -154,10 +161,28 @@ func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, f RequestedAt: r.requestedAt, Latency: r.latency(), Failed: failed, + Fail: fail, Detail: detail, } } +func failFromErrors(errs ...error) usage.Failure { + for _, err := range errs { + if err == nil { + continue + } + fail := usage.Failure{ + Body: strings.TrimSpace(err.Error()), + } + var se interface{ StatusCode() int } + if errors.As(err, &se) && se != nil { + fail.StatusCode = se.StatusCode() + } + return fail + } + return usage.Failure{} +} + func (r *UsageReporter) latency() time.Duration { if r == nil || r.requestedAt.IsZero() { return 0 @@ -177,7 +202,7 @@ func APIKeyFromContext(ctx context.Context) string { if !ok || ginCtx == nil { return "" } - if v, exists := ginCtx.Get("apiKey"); exists { + if v, exists := ginCtx.Get("userApiKey"); exists { switch value := v.(type) { case string: return value @@ -248,7 +273,7 @@ func resolveUsageAuthType(auth *cliproxyauth.Auth) string { func ParseCodexUsage(data []byte) (usage.Detail, bool) { usageNode := gjson.ParseBytes(data).Get("response.usage") - if !usageNode.Exists() { + if !hasOpenAIStyleUsageTokenFields(usageNode) { return usage.Detail{}, false } return parseOpenAIStyleUsageNode(usageNode), true @@ -256,7 +281,7 @@ func ParseCodexUsage(data []byte) (usage.Detail, bool) { func ParseCodexImageToolUsage(data []byte) (usage.Detail, bool) { usageNode := gjson.ParseBytes(data).Get("response.tool_usage.image_gen") - if !usageNode.Exists() || !usageNode.IsObject() { + if !hasOpenAIStyleUsageTokenFields(usageNode) { return usage.Detail{}, false } return parseOpenAIStyleUsageNode(usageNode), true @@ -264,12 +289,27 @@ func ParseCodexImageToolUsage(data []byte) (usage.Detail, bool) { func ParseOpenAIUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data).Get("usage") - if !usageNode.Exists() { + if !hasOpenAIStyleUsageTokenFields(usageNode) { return usage.Detail{} } return parseOpenAIStyleUsageNode(usageNode) } +func hasOpenAIStyleUsageTokenFields(usageNode gjson.Result) bool { + if !usageNode.Exists() || !usageNode.IsObject() { + return false + } + return usageNode.Get("prompt_tokens").Exists() || + usageNode.Get("input_tokens").Exists() || + usageNode.Get("completion_tokens").Exists() || + usageNode.Get("output_tokens").Exists() || + usageNode.Get("total_tokens").Exists() || + usageNode.Get("prompt_tokens_details.cached_tokens").Exists() || + usageNode.Get("input_tokens_details.cached_tokens").Exists() || + usageNode.Get("completion_tokens_details.reasoning_tokens").Exists() || + usageNode.Get("output_tokens_details.reasoning_tokens").Exists() +} + func parseOpenAIStyleUsageNode(usageNode gjson.Result) usage.Detail { inputNode := usageNode.Get("prompt_tokens") if !inputNode.Exists() { @@ -307,21 +347,10 @@ func ParseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { return usage.Detail{}, false } usageNode := gjson.GetBytes(payload, "usage") - if !usageNode.Exists() { + if !hasOpenAIStyleUsageTokenFields(usageNode) { return usage.Detail{}, false } - detail := usage.Detail{ - InputTokens: usageNode.Get("prompt_tokens").Int(), - OutputTokens: usageNode.Get("completion_tokens").Int(), - TotalTokens: usageNode.Get("total_tokens").Int(), - } - if cached := usageNode.Get("prompt_tokens_details.cached_tokens"); cached.Exists() { - detail.CachedTokens = cached.Int() - } - if reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens"); reasoning.Exists() { - detail.ReasoningTokens = reasoning.Int() - } - return detail, true + return parseOpenAIStyleUsageNode(usageNode), true } func ParseClaudeUsage(data []byte) usage.Detail { @@ -329,17 +358,7 @@ func ParseClaudeUsage(data []byte) usage.Detail { if !usageNode.Exists() { return usage.Detail{} } - detail := usage.Detail{ - InputTokens: usageNode.Get("input_tokens").Int(), - OutputTokens: usageNode.Get("output_tokens").Int(), - CachedTokens: usageNode.Get("cache_read_input_tokens").Int(), - } - if detail.CachedTokens == 0 { - // fall back to creation tokens when read tokens are absent - detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int() - } - detail.TotalTokens = detail.InputTokens + detail.OutputTokens - return detail + return parseClaudeUsageNode(usageNode) } func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) { @@ -351,16 +370,24 @@ func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) { if !usageNode.Exists() { return usage.Detail{}, false } + return parseClaudeUsageNode(usageNode), true +} + +func parseClaudeUsageNode(usageNode gjson.Result) usage.Detail { + cacheReadTokens := usageNode.Get("cache_read_input_tokens").Int() + cacheCreationTokens := usageNode.Get("cache_creation_input_tokens").Int() detail := usage.Detail{ - InputTokens: usageNode.Get("input_tokens").Int(), - OutputTokens: usageNode.Get("output_tokens").Int(), - CachedTokens: usageNode.Get("cache_read_input_tokens").Int(), + InputTokens: usageNode.Get("input_tokens").Int(), + OutputTokens: usageNode.Get("output_tokens").Int(), + CachedTokens: cacheReadTokens, + CacheReadTokens: cacheReadTokens, + CacheCreationTokens: cacheCreationTokens, } if detail.CachedTokens == 0 { - detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int() + detail.CachedTokens = detail.CacheCreationTokens } detail.TotalTokens = detail.InputTokens + detail.OutputTokens - return detail, true + return detail } func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail { diff --git a/internal/runtime/executor/helps/usage_helpers_test.go b/internal/runtime/executor/helps/usage_helpers_test.go index ef2c7de581..bd0a9c21ba 100644 --- a/internal/runtime/executor/helps/usage_helpers_test.go +++ b/internal/runtime/executor/helps/usage_helpers_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" ) func TestParseOpenAIUsageChatCompletions(t *testing.T) { @@ -48,6 +48,44 @@ func TestParseOpenAIUsageResponses(t *testing.T) { } } +func TestParseOpenAIUsageIgnoresNullUsage(t *testing.T) { + data := []byte(`{"usage":null}`) + detail := ParseOpenAIUsage(data) + if detail != (usage.Detail{}) { + t.Fatalf("detail = %+v, want zero detail", detail) + } +} + +func TestParseOpenAIStreamUsageIgnoresNullUsage(t *testing.T) { + line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":null}],"usage":null}`) + if detail, ok := ParseOpenAIStreamUsage(line); ok { + t.Fatalf("ParseOpenAIStreamUsage() = (%+v, true), want false for null usage", detail) + } +} + +func TestParseOpenAIStreamUsageResponsesFields(t *testing.T) { + line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","choices":[],"usage":{"input_tokens":8,"output_tokens":5,"total_tokens":13,"input_tokens_details":{"cached_tokens":3},"output_tokens_details":{"reasoning_tokens":2}}}`) + detail, ok := ParseOpenAIStreamUsage(line) + if !ok { + t.Fatal("ParseOpenAIStreamUsage() ok = false, want true") + } + if detail.InputTokens != 8 { + t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 8) + } + if detail.OutputTokens != 5 { + t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 5) + } + if detail.TotalTokens != 13 { + t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 13) + } + if detail.CachedTokens != 3 { + t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 3) + } + if detail.ReasoningTokens != 2 { + t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 2) + } +} + func TestParseGeminiCLIUsage_TopLevelUsageMetadata(t *testing.T) { data := []byte(`{"usageMetadata":{"promptTokenCount":11,"candidatesTokenCount":7,"thoughtsTokenCount":3,"totalTokenCount":21,"cachedContentTokenCount":5}}`) detail := ParseGeminiCLIUsage(data) diff --git a/internal/runtime/executor/helps/utls_client.go b/internal/runtime/executor/helps/utls_client.go index 39512a58de..29174e47b6 100644 --- a/internal/runtime/executor/helps/utls_client.go +++ b/internal/runtime/executor/helps/utls_client.go @@ -8,9 +8,9 @@ import ( "time" tls "github.com/refraction-networking/utls" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/net/http2" "golang.org/x/net/proxy" diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index 93125d9fcb..6cfaec2052 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -13,14 +13,14 @@ import ( "strings" "time" - kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + kimiauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -307,7 +307,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -569,6 +569,9 @@ func fallbackAssistantReasoning(msg gjson.Result, hasLatest bool, latest string) // Refresh refreshes the Kimi token using the refresh token. func (e *KimiExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("kimi executor: refresh called") + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } if auth == nil { return nil, fmt.Errorf("kimi executor: auth is nil") } diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 7e81637ca6..82fc9e97d8 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -10,13 +10,13 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/sjson" ) @@ -296,7 +296,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy if bytes.HasPrefix(trimmedLine, []byte("{")) || bytes.HasPrefix(trimmedLine, []byte("[")) { streamErr := statusErr{code: http.StatusBadGateway, msg: string(trimmedLine)} helps.RecordAPIResponseError(ctx, e.cfg, streamErr) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, streamErr) select { case out <- cliproxyexecutor.StreamChunk{Err: streamErr}: case <-ctx.Done(): @@ -318,7 +318,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) - reporter.PublishFailure(ctx) + reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): @@ -374,7 +374,9 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau // Refresh is a no-op for API-key based compatibility providers. func (e *OpenAICompatExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("openai compat executor: refresh called") - _ = ctx + if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { + return refreshed, err + } return auth, nil } diff --git a/internal/runtime/executor/openai_compat_executor_compact_test.go b/internal/runtime/executor/openai_compat_executor_compact_test.go index 49b2cccbbb..3aab5c9b01 100644 --- a/internal/runtime/executor/openai_compat_executor_compact_test.go +++ b/internal/runtime/executor/openai_compat_executor_compact_test.go @@ -8,10 +8,10 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/internal/store/gitstore.go b/internal/store/gitstore.go index bd84d99a23..ba9fe59e2b 100644 --- a/internal/store/gitstore.go +++ b/internal/store/gitstore.go @@ -18,7 +18,7 @@ import ( "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // gcInterval defines minimum time between garbage collection runs. @@ -287,10 +287,18 @@ func (s *GitTokenStore) Save(_ context.Context, auth *cliproxyauth.Auth) (string switch { case auth.Storage != nil: + if auth.Metadata == nil { + auth.Metadata = make(map[string]any) + } + auth.Metadata["disabled"] = auth.Disabled + if setter, ok := auth.Storage.(interface{ SetMetadata(map[string]any) }); ok { + setter.SetMetadata(auth.Metadata) + } if err = auth.Storage.SaveTokenToFile(path); err != nil { return "", err } case auth.Metadata != nil: + auth.Metadata["disabled"] = auth.Disabled raw, errMarshal := json.Marshal(auth.Metadata) if errMarshal != nil { return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal) diff --git a/internal/store/objectstore.go b/internal/store/objectstore.go index a33f6ef8f4..5626e6c65b 100644 --- a/internal/store/objectstore.go +++ b/internal/store/objectstore.go @@ -17,8 +17,8 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) @@ -184,10 +184,18 @@ func (s *ObjectTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (s switch { case auth.Storage != nil: + if auth.Metadata == nil { + auth.Metadata = make(map[string]any) + } + auth.Metadata["disabled"] = auth.Disabled + if setter, ok := auth.Storage.(interface{ SetMetadata(map[string]any) }); ok { + setter.SetMetadata(auth.Metadata) + } if err = auth.Storage.SaveTokenToFile(path); err != nil { return "", err } case auth.Metadata != nil: + auth.Metadata["disabled"] = auth.Disabled raw, errMarshal := json.Marshal(auth.Metadata) if errMarshal != nil { return "", fmt.Errorf("object store: marshal metadata: %w", errMarshal) diff --git a/internal/store/postgresstore.go b/internal/store/postgresstore.go index 527b25cc12..43b125003d 100644 --- a/internal/store/postgresstore.go +++ b/internal/store/postgresstore.go @@ -14,8 +14,8 @@ import ( "time" _ "github.com/jackc/pgx/v5/stdlib" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) @@ -214,10 +214,18 @@ func (s *PostgresStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (stri switch { case auth.Storage != nil: + if auth.Metadata == nil { + auth.Metadata = make(map[string]any) + } + auth.Metadata["disabled"] = auth.Disabled + if setter, ok := auth.Storage.(interface{ SetMetadata(map[string]any) }); ok { + setter.SetMetadata(auth.Metadata) + } if err = auth.Storage.SaveTokenToFile(path); err != nil { return "", err } case auth.Metadata != nil: + auth.Metadata["disabled"] = auth.Disabled raw, errMarshal := json.Marshal(auth.Metadata) if errMarshal != nil { return "", fmt.Errorf("postgres store: marshal metadata: %w", errMarshal) diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index 1edeac874c..d422a8d8b2 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -4,7 +4,7 @@ package thinking import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/internal/thinking/apply_user_defined_test.go b/internal/thinking/apply_user_defined_test.go index aa24ab8e9c..c485d2521a 100644 --- a/internal/thinking/apply_user_defined_test.go +++ b/internal/thinking/apply_user_defined_test.go @@ -3,9 +3,9 @@ package thinking_test import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/claude" "github.com/tidwall/gjson" ) diff --git a/internal/thinking/convert.go b/internal/thinking/convert.go index b22a0879ed..31945daa7c 100644 --- a/internal/thinking/convert.go +++ b/internal/thinking/convert.go @@ -3,7 +3,7 @@ package thinking import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" ) // levelToBudgetMap defines the standard Level → Budget mapping. diff --git a/internal/thinking/provider/antigravity/apply.go b/internal/thinking/provider/antigravity/apply.go index d202035fc6..0a8f1c4537 100644 --- a/internal/thinking/provider/antigravity/apply.go +++ b/internal/thinking/provider/antigravity/apply.go @@ -9,8 +9,8 @@ package antigravity import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/claude/apply.go b/internal/thinking/provider/claude/apply.go index 275be46924..140a8135f7 100644 --- a/internal/thinking/provider/claude/apply.go +++ b/internal/thinking/provider/claude/apply.go @@ -9,8 +9,8 @@ package claude import ( - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/codex/apply.go b/internal/thinking/provider/codex/apply.go index 0f33635950..83f5ae8457 100644 --- a/internal/thinking/provider/codex/apply.go +++ b/internal/thinking/provider/codex/apply.go @@ -7,8 +7,8 @@ package codex import ( - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/gemini/apply.go b/internal/thinking/provider/gemini/apply.go index 39bb4231d0..8e6e83f330 100644 --- a/internal/thinking/provider/gemini/apply.go +++ b/internal/thinking/provider/gemini/apply.go @@ -12,8 +12,8 @@ package gemini import ( - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/geminicli/apply.go b/internal/thinking/provider/geminicli/apply.go index 5908b6bce5..e9311e8c18 100644 --- a/internal/thinking/provider/geminicli/apply.go +++ b/internal/thinking/provider/geminicli/apply.go @@ -5,8 +5,8 @@ package geminicli import ( - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/kimi/apply.go b/internal/thinking/provider/kimi/apply.go index ff47c46d03..ea3ed572f0 100644 --- a/internal/thinking/provider/kimi/apply.go +++ b/internal/thinking/provider/kimi/apply.go @@ -7,8 +7,8 @@ package kimi import ( "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/provider/kimi/apply_test.go b/internal/thinking/provider/kimi/apply_test.go index 707f11c758..78069424ed 100644 --- a/internal/thinking/provider/kimi/apply_test.go +++ b/internal/thinking/provider/kimi/apply_test.go @@ -3,8 +3,8 @@ package kimi import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" ) diff --git a/internal/thinking/provider/openai/apply.go b/internal/thinking/provider/openai/apply.go index c77c1ab8e4..1e87b72b37 100644 --- a/internal/thinking/provider/openai/apply.go +++ b/internal/thinking/provider/openai/apply.go @@ -6,8 +6,8 @@ package openai import ( - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/thinking/types.go b/internal/thinking/types.go index a31d798197..39868a02f4 100644 --- a/internal/thinking/types.go +++ b/internal/thinking/types.go @@ -4,7 +4,7 @@ // thinking configurations across various AI providers (Claude, Gemini, OpenAI, Codex, Antigravity, Kimi). package thinking -import "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" +import "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" // ThinkingMode represents the type of thinking configuration mode. type ThinkingMode int diff --git a/internal/thinking/validate.go b/internal/thinking/validate.go index 4a3ca97ce8..2baa93f1da 100644 --- a/internal/thinking/validate.go +++ b/internal/thinking/validate.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus" ) diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 8ae69648db..7f36b11ccb 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -8,10 +8,10 @@ package claude import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/antigravity/claude/antigravity_claude_request_test.go b/internal/translator/antigravity/claude/antigravity_claude_request_test.go index 919e29062a..bb3cdf4f34 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" "github.com/tidwall/gjson" "google.golang.org/protobuf/encoding/protowire" ) diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index 17a31f217f..427551df6c 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -15,9 +15,9 @@ import ( "sync/atomic" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" diff --git a/internal/translator/antigravity/claude/antigravity_claude_response_test.go b/internal/translator/antigravity/claude/antigravity_claude_response_test.go index 05a3df899d..1490ab3cbd 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" ) // ============================================================================ diff --git a/internal/translator/antigravity/claude/init.go b/internal/translator/antigravity/claude/init.go index 21fe0b26ed..4d9bd721ff 100644 --- a/internal/translator/antigravity/claude/init.go +++ b/internal/translator/antigravity/claude/init.go @@ -1,9 +1,9 @@ package claude import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/antigravity/claude/signature_validation.go b/internal/translator/antigravity/claude/signature_validation.go index 63203abdce..f82fc2e364 100644 --- a/internal/translator/antigravity/claude/signature_validation.go +++ b/internal/translator/antigravity/claude/signature_validation.go @@ -53,7 +53,7 @@ import ( "strings" "unicode/utf8" - "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" + "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "google.golang.org/protobuf/encoding/protowire" diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_request.go b/internal/translator/antigravity/gemini/antigravity_gemini_request.go index 3612c0fb1a..b33b9c40e1 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_request.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_request.go @@ -9,8 +9,8 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_response.go b/internal/translator/antigravity/gemini/antigravity_gemini_response.go index 7b43c48db2..b0deb7320a 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_response.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_response.go @@ -9,7 +9,7 @@ import ( "bytes" "context" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/antigravity/gemini/init.go b/internal/translator/antigravity/gemini/init.go index 3955824863..dcb331618a 100644 --- a/internal/translator/antigravity/gemini/init.go +++ b/internal/translator/antigravity/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index b33be50bd0..0d9ee6fe0a 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go index 9188c75a2c..2be24102ff 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go @@ -13,10 +13,10 @@ import ( "sync/atomic" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/chat-completions" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/antigravity/openai/chat-completions/init.go b/internal/translator/antigravity/openai/chat-completions/init.go index 5c5c71e461..2217e7919c 100644 --- a/internal/translator/antigravity/openai/chat-completions/init.go +++ b/internal/translator/antigravity/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go index 90bfa14c05..94a6b852b0 100644 --- a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go +++ b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go @@ -1,8 +1,8 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/gemini" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/responses" ) func ConvertOpenAIResponsesRequestToAntigravity(modelName string, inputRawJSON []byte, stream bool) []byte { diff --git a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go index a087e0bd0f..3256950461 100644 --- a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go +++ b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go @@ -3,7 +3,7 @@ package responses import ( "context" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/responses" "github.com/tidwall/gjson" ) diff --git a/internal/translator/antigravity/openai/responses/init.go b/internal/translator/antigravity/openai/responses/init.go index 8d13703239..49041f2905 100644 --- a/internal/translator/antigravity/openai/responses/init.go +++ b/internal/translator/antigravity/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/claude/gemini-cli/claude_gemini-cli_request.go b/internal/translator/claude/gemini-cli/claude_gemini-cli_request.go index 831d784db3..fd68a957f5 100644 --- a/internal/translator/claude/gemini-cli/claude_gemini-cli_request.go +++ b/internal/translator/claude/gemini-cli/claude_gemini-cli_request.go @@ -6,7 +6,7 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/gemini" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go b/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go index 62e2650fd9..858886c272 100644 --- a/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go +++ b/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go @@ -7,8 +7,8 @@ package geminiCLI import ( "context" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/gemini" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" ) // ConvertClaudeResponseToGeminiCLI converts Claude Code streaming response format to Gemini CLI format. diff --git a/internal/translator/claude/gemini-cli/init.go b/internal/translator/claude/gemini-cli/init.go index ca364a6ee0..33a1332daf 100644 --- a/internal/translator/claude/gemini-cli/init.go +++ b/internal/translator/claude/gemini-cli/init.go @@ -1,9 +1,9 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index d2a215e7de..d716d28f35 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -14,9 +14,9 @@ import ( "strings" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/gemini/claude_gemini_response.go b/internal/translator/claude/gemini/claude_gemini_response.go index 846c26056f..3f127e3205 100644 --- a/internal/translator/claude/gemini/claude_gemini_response.go +++ b/internal/translator/claude/gemini/claude_gemini_response.go @@ -12,7 +12,7 @@ import ( "strings" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/gemini/init.go b/internal/translator/claude/gemini/init.go index 8924f62c87..0ed533cebf 100644 --- a/internal/translator/claude/gemini/init.go +++ b/internal/translator/claude/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index e9d8d35b09..bad56d1273 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -14,8 +14,8 @@ import ( "strings" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/openai/chat-completions/init.go b/internal/translator/claude/openai/chat-completions/init.go index a18840bace..7474fb2a38 100644 --- a/internal/translator/claude/openai/chat-completions/init.go +++ b/internal/translator/claude/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index c0479b87ea..1398749573 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -9,8 +9,8 @@ import ( "strings" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_response.go b/internal/translator/claude/openai/responses/claude_openai-responses_response.go index 10d12c9963..6c6b96b30d 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_response.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_response.go @@ -8,7 +8,7 @@ import ( "strings" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/claude/openai/responses/init.go b/internal/translator/claude/openai/responses/init.go index 595fecc6ef..575c9ec71a 100644 --- a/internal/translator/claude/openai/responses/init.go +++ b/internal/translator/claude/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 1e168f0993..029db14e7d 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index a401a1b7e5..7a40ca4c55 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -11,8 +11,8 @@ import ( "context" "strings" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/codex/claude/init.go b/internal/translator/codex/claude/init.go index 7126edc303..af44b9dd49 100644 --- a/internal/translator/codex/claude/init.go +++ b/internal/translator/codex/claude/init.go @@ -1,9 +1,9 @@ package claude import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/codex/gemini-cli/codex_gemini-cli_request.go b/internal/translator/codex/gemini-cli/codex_gemini-cli_request.go index 8b32453d26..b69bab11ee 100644 --- a/internal/translator/codex/gemini-cli/codex_gemini-cli_request.go +++ b/internal/translator/codex/gemini-cli/codex_gemini-cli_request.go @@ -6,7 +6,7 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/gemini" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go b/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go index 0f0068c842..01dbc0f831 100644 --- a/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go +++ b/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go @@ -7,8 +7,8 @@ package geminiCLI import ( "context" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/gemini" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" ) // ConvertCodexResponseToGeminiCLI converts Codex streaming response format to Gemini CLI format. diff --git a/internal/translator/codex/gemini-cli/init.go b/internal/translator/codex/gemini-cli/init.go index 8bcd3de5fd..2958e0a825 100644 --- a/internal/translator/codex/gemini-cli/init.go +++ b/internal/translator/codex/gemini-cli/init.go @@ -1,9 +1,9 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/codex/gemini/codex_gemini_request.go b/internal/translator/codex/gemini/codex_gemini_request.go index 373997007f..5789890f20 100644 --- a/internal/translator/codex/gemini/codex_gemini_request.go +++ b/internal/translator/codex/gemini/codex_gemini_request.go @@ -12,8 +12,8 @@ import ( "strconv" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/codex/gemini/codex_gemini_response.go b/internal/translator/codex/gemini/codex_gemini_response.go index a2e4e20ea2..ecf9cf4de8 100644 --- a/internal/translator/codex/gemini/codex_gemini_response.go +++ b/internal/translator/codex/gemini/codex_gemini_response.go @@ -11,7 +11,7 @@ import ( "strings" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/codex/gemini/init.go b/internal/translator/codex/gemini/init.go index 41d30559a6..b670d8d9b4 100644 --- a/internal/translator/codex/gemini/init.go +++ b/internal/translator/codex/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/codex/openai/chat-completions/init.go b/internal/translator/codex/openai/chat-completions/init.go index 8f782fdae1..94db2a7db8 100644 --- a/internal/translator/codex/openai/chat-completions/init.go +++ b/internal/translator/codex/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/codex/openai/responses/init.go b/internal/translator/codex/openai/responses/init.go index cab759f297..24e7e3561c 100644 --- a/internal/translator/codex/openai/responses/init.go +++ b/internal/translator/codex/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 57ebbc2cde..3e77b3f757 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -8,8 +8,8 @@ package claude import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go index 0bf4d6225c..607d6b9fc0 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go @@ -14,8 +14,8 @@ import ( "sync/atomic" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini-cli/claude/init.go b/internal/translator/gemini-cli/claude/init.go index 79ed03c68e..fa2fabdf77 100644 --- a/internal/translator/gemini-cli/claude/init.go +++ b/internal/translator/gemini-cli/claude/init.go @@ -1,9 +1,9 @@ package claude import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go index 9bdce33973..83dc626041 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go @@ -9,8 +9,8 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go index 8e23f1d3d6..0e100c1489 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go @@ -9,7 +9,7 @@ import ( "bytes" "context" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini-cli/gemini/init.go b/internal/translator/gemini-cli/gemini/init.go index fbad4ab50b..1c2f38f215 100644 --- a/internal/translator/gemini-cli/gemini/init.go +++ b/internal/translator/gemini-cli/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index 95bca2d7b6..1aa3132b49 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index 0947371a5a..926040588e 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -13,8 +13,8 @@ import ( "sync/atomic" "time" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/chat-completions" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini-cli/openai/chat-completions/init.go b/internal/translator/gemini-cli/openai/chat-completions/init.go index 3bd76c517d..fcd85f2450 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/init.go +++ b/internal/translator/gemini-cli/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go index 657e45fdb2..bea4b7a1fe 100644 --- a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go +++ b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go @@ -1,8 +1,8 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini-cli/gemini" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/responses" ) func ConvertOpenAIResponsesRequestToGeminiCLI(modelName string, inputRawJSON []byte, stream bool) []byte { diff --git a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go index 9bb3ced9ef..29db8c19ef 100644 --- a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go +++ b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go @@ -3,7 +3,7 @@ package responses import ( "context" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/responses" "github.com/tidwall/gjson" ) diff --git a/internal/translator/gemini-cli/openai/responses/init.go b/internal/translator/gemini-cli/openai/responses/init.go index b25d670851..e1d437715f 100644 --- a/internal/translator/gemini-cli/openai/responses/init.go +++ b/internal/translator/gemini-cli/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index e230f5fd0d..454668cbc2 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -9,9 +9,9 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/claude/gemini_claude_response.go b/internal/translator/gemini/claude/gemini_claude_response.go index 28722de1db..797636d857 100644 --- a/internal/translator/gemini/claude/gemini_claude_response.go +++ b/internal/translator/gemini/claude/gemini_claude_response.go @@ -13,8 +13,8 @@ import ( "strings" "sync/atomic" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/claude/init.go b/internal/translator/gemini/claude/init.go index 66fe51e739..d03140957c 100644 --- a/internal/translator/gemini/claude/init.go +++ b/internal/translator/gemini/claude/init.go @@ -1,9 +1,9 @@ package claude import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go index 1b2cdb4636..71e7b4a5fd 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go @@ -8,8 +8,8 @@ package geminiCLI import ( "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go index d15ea21acc..36fa0d39b5 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go @@ -8,7 +8,7 @@ import ( "bytes" "context" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/gemini-cli/init.go b/internal/translator/gemini/gemini-cli/init.go index 2c2224f7d0..ed18b5f0af 100644 --- a/internal/translator/gemini/gemini-cli/init.go +++ b/internal/translator/gemini/gemini-cli/init.go @@ -1,9 +1,9 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini/gemini/gemini_gemini_request.go b/internal/translator/gemini/gemini/gemini_gemini_request.go index abc176b2e2..35e22d7160 100644 --- a/internal/translator/gemini/gemini/gemini_gemini_request.go +++ b/internal/translator/gemini/gemini/gemini_gemini_request.go @@ -7,8 +7,8 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini/gemini/gemini_gemini_response.go b/internal/translator/gemini/gemini/gemini_gemini_response.go index 242dd98059..74669a7e72 100644 --- a/internal/translator/gemini/gemini/gemini_gemini_response.go +++ b/internal/translator/gemini/gemini/gemini_gemini_response.go @@ -4,7 +4,7 @@ import ( "bytes" "context" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" ) // PassthroughGeminiResponseStream forwards Gemini responses unchanged. diff --git a/internal/translator/gemini/gemini/init.go b/internal/translator/gemini/gemini/init.go index 28c9708338..ca9de2c672 100644 --- a/internal/translator/gemini/gemini/init.go +++ b/internal/translator/gemini/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) // Register a no-op response translator and a request normalizer for Gemini→Gemini. diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index c0c4d329f5..20eaec76f9 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index 3dc5b095c3..cc9117f905 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -13,7 +13,7 @@ import ( "sync/atomic" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/internal/translator/gemini/openai/chat-completions/init.go b/internal/translator/gemini/openai/chat-completions/init.go index 800e07db3d..2eb673310f 100644 --- a/internal/translator/gemini/openai/chat-completions/init.go +++ b/internal/translator/gemini/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 8f3a59fa45..e741757641 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -4,8 +4,8 @@ import ( "encoding/json" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go index 15729aae92..36d30df753 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go @@ -8,8 +8,8 @@ import ( "sync/atomic" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini/openai/responses/init.go b/internal/translator/gemini/openai/responses/init.go index b53cac3d81..404dd68ae5 100644 --- a/internal/translator/gemini/openai/responses/init.go +++ b/internal/translator/gemini/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/init.go b/internal/translator/init.go index 084ea7ac23..5f88a400ec 100644 --- a/internal/translator/init.go +++ b/internal/translator/init.go @@ -1,36 +1,36 @@ package translator import ( - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini-cli" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/gemini-cli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/openai/responses" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/claude" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini-cli" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/gemini-cli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/codex/openai/responses" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/claude" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini-cli/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini-cli/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini-cli/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini-cli/openai/responses" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/claude" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini-cli" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/gemini-cli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/openai/responses" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/claude" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini-cli" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/gemini-cli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/openai/responses" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/chat-completions" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/responses" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/openai/chat-completions" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/openai/responses" ) diff --git a/internal/translator/openai/claude/init.go b/internal/translator/openai/claude/init.go index 0e0f82eae9..baeeca84bc 100644 --- a/internal/translator/openai/claude/init.go +++ b/internal/translator/openai/claude/init.go @@ -1,9 +1,9 @@ package claude import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index f12dd0c694..99fc2763ff 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -8,7 +8,7 @@ package claude import ( "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/openai/claude/openai_claude_response.go b/internal/translator/openai/claude/openai_claude_response.go index af49d306d7..1925539c19 100644 --- a/internal/translator/openai/claude/openai_claude_response.go +++ b/internal/translator/openai/claude/openai_claude_response.go @@ -10,8 +10,8 @@ import ( "context" "strings" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/openai/gemini-cli/init.go b/internal/translator/openai/gemini-cli/init.go index 12aec5ec90..7b52d06dc0 100644 --- a/internal/translator/openai/gemini-cli/init.go +++ b/internal/translator/openai/gemini-cli/init.go @@ -1,9 +1,9 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/openai/gemini-cli/openai_gemini_request.go b/internal/translator/openai/gemini-cli/openai_gemini_request.go index 847c278f36..c651826669 100644 --- a/internal/translator/openai/gemini-cli/openai_gemini_request.go +++ b/internal/translator/openai/gemini-cli/openai_gemini_request.go @@ -6,7 +6,7 @@ package geminiCLI import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/gemini" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/openai/gemini-cli/openai_gemini_response.go b/internal/translator/openai/gemini-cli/openai_gemini_response.go index a7369dbfe9..e54e08fc27 100644 --- a/internal/translator/openai/gemini-cli/openai_gemini_response.go +++ b/internal/translator/openai/gemini-cli/openai_gemini_response.go @@ -8,8 +8,8 @@ package geminiCLI import ( "context" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/gemini" ) // ConvertOpenAIResponseToGeminiCLI converts OpenAI Chat Completions streaming response format to Gemini API format. diff --git a/internal/translator/openai/gemini/init.go b/internal/translator/openai/gemini/init.go index 4f056ace9f..24ae281eff 100644 --- a/internal/translator/openai/gemini/init.go +++ b/internal/translator/openai/gemini/init.go @@ -1,9 +1,9 @@ package gemini import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go index b4edbb1df6..7369de88df 100644 --- a/internal/translator/openai/gemini/openai_gemini_request.go +++ b/internal/translator/openai/gemini/openai_gemini_request.go @@ -11,7 +11,7 @@ import ( "math/big" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/openai/gemini/openai_gemini_response.go b/internal/translator/openai/gemini/openai_gemini_response.go index 092a778eac..439ae8fbd7 100644 --- a/internal/translator/openai/gemini/openai_gemini_response.go +++ b/internal/translator/openai/gemini/openai_gemini_response.go @@ -12,7 +12,7 @@ import ( "strconv" "strings" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/openai/openai/chat-completions/init.go b/internal/translator/openai/openai/chat-completions/init.go index 90fa3dcd90..bfe82cea72 100644 --- a/internal/translator/openai/openai/chat-completions/init.go +++ b/internal/translator/openai/openai/chat-completions/init.go @@ -1,9 +1,9 @@ package chat_completions import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/openai/openai/responses/init.go b/internal/translator/openai/openai/responses/init.go index e6f60e0e13..c47081bae3 100644 --- a/internal/translator/openai/openai/responses/init.go +++ b/internal/translator/openai/openai/responses/init.go @@ -1,9 +1,9 @@ package responses import ( - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator" ) func init() { diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request.go b/internal/translator/openai/openai/responses/openai_openai-responses_request.go index 2366c9c37b..15acf7cdb4 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request.go @@ -57,11 +57,72 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu // Convert input array to messages if input := root.Get("input"); input.Exists() && input.IsArray() { - input.ForEach(func(_, item gjson.Result) bool { + inputItems := input.Array() + outputCallIDs := make(map[string]struct{}) + for _, item := range inputItems { + if item.Get("type").String() != "function_call_output" { + continue + } + callID := strings.TrimSpace(item.Get("call_id").String()) + if callID == "" { + continue + } + outputCallIDs[callID] = struct{}{} + } + + pendingToolCalls := make([]interface{}, 0) + pendingToolCallIDs := make([]string, 0) + awaitingToolOutputs := make(map[string]struct{}) + deferredMessages := make([][]byte, 0) + + flushPendingToolCalls := func() { + if len(pendingToolCalls) == 0 { + return + } + assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`) + assistantMessage, _ = sjson.SetBytes(assistantMessage, "tool_calls", pendingToolCalls) + out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage) + for _, id := range pendingToolCallIDs { + if strings.TrimSpace(id) == "" { + continue + } + awaitingToolOutputs[id] = struct{}{} + } + pendingToolCalls = pendingToolCalls[:0] + pendingToolCallIDs = pendingToolCallIDs[:0] + } + flushDeferredMessages := func() { + for _, message := range deferredMessages { + out, _ = sjson.SetRawBytes(out, "messages.-1", message) + } + deferredMessages = deferredMessages[:0] + } + hasAwaitingToolOutput := func() bool { + for id := range awaitingToolOutputs { + if _, ok := outputCallIDs[id]; ok { + return true + } + } + return false + } + appendRegularMessage := func(message []byte) { + // Keep tool-call adjacency strict for providers that require + // assistant(tool_calls) -> tool(tool_call_id) with no message in between. + if hasAwaitingToolOutput() { + deferredMessages = append(deferredMessages, message) + return + } + out, _ = sjson.SetRawBytes(out, "messages.-1", message) + } + + for _, item := range inputItems { itemType := item.Get("type").String() if itemType == "" && item.Get("role").String() != "" { itemType = "message" } + if itemType != "function_call" { + flushPendingToolCalls() + } switch itemType { case "message", "": @@ -109,12 +170,10 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu message, _ = sjson.SetBytes(message, "content", content.String()) } - out, _ = sjson.SetRawBytes(out, "messages.-1", message) + appendRegularMessage(message) case "function_call": - // Handle function call conversion to assistant message with tool_calls - assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`) - + // Buffer consecutive function calls and emit them as one assistant message. toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) if callId := item.Get("call_id"); callId.Exists() { @@ -128,16 +187,19 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu if arguments := item.Get("arguments"); arguments.Exists() { toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", arguments.String()) } - - assistantMessage, _ = sjson.SetRawBytes(assistantMessage, "tool_calls.0", toolCall) - out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage) + pendingToolCalls = append(pendingToolCalls, gjson.ParseBytes(toolCall).Value()) + if callID := strings.TrimSpace(item.Get("call_id").String()); callID != "" { + pendingToolCallIDs = append(pendingToolCallIDs, callID) + } case "function_call_output": // Handle function call output conversion to tool message toolMessage := []byte(`{"role":"tool","tool_call_id":"","content":""}`) + callID := "" if callId := item.Get("call_id"); callId.Exists() { - toolMessage, _ = sjson.SetBytes(toolMessage, "tool_call_id", callId.String()) + callID = strings.TrimSpace(callId.String()) + toolMessage, _ = sjson.SetBytes(toolMessage, "tool_call_id", callID) } if output := item.Get("output"); output.Exists() { @@ -145,10 +207,17 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu } out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage) + if callID != "" { + delete(awaitingToolOutputs, callID) + } + if len(awaitingToolOutputs) == 0 && len(deferredMessages) > 0 { + flushDeferredMessages() + } } - return true - }) + } + flushPendingToolCalls() + flushDeferredMessages() } else if input.Type == gjson.String { msg := []byte(`{}`) msg, _ = sjson.SetBytes(msg, "role", "user") diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go new file mode 100644 index 0000000000..9dd0e288b2 --- /dev/null +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go @@ -0,0 +1,124 @@ +package responses + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/tidwall/gjson" +) + +func prettyJSONForTest(raw []byte) string { + if !gjson.ValidBytes(raw) { + return string(raw) + } + var out bytes.Buffer + if err := json.Indent(&out, raw, "", " "); err != nil { + return string(raw) + } + return out.String() +} + +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_MergeConsecutiveFunctionCalls(t *testing.T) { + raw := []byte(`{ + "input": [ + {"type":"function_call","call_id":"exec_command:0","name":"exec_command","arguments":"{\"cmd\":\"ls\"}"}, + {"type":"function_call","call_id":"exec_command:1","name":"exec_command","arguments":"{\"cmd\":\"pwd\"}"}, + {"type":"function_call_output","call_id":"exec_command:0","output":"ok0"}, + {"type":"function_call_output","call_id":"exec_command:1","output":"ok1"} + ] + }`) + t.Logf("input json:\n%s", prettyJSONForTest(raw)) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("kimi-k2.6", raw, true) + t.Logf("output json:\n%s", prettyJSONForTest(out)) + + msgs := gjson.GetBytes(out, "messages") + if !msgs.Exists() || !msgs.IsArray() { + t.Fatalf("messages should be an array") + } + if got := len(msgs.Array()); got != 3 { + t.Fatalf("messages count = %d, want %d", got, 3) + } + + if got := gjson.GetBytes(out, "messages.0.role").String(); got != "assistant" { + t.Fatalf("messages.0.role = %q, want %q", got, "assistant") + } + if got := len(gjson.GetBytes(out, "messages.0.tool_calls").Array()); got != 2 { + t.Fatalf("messages.0.tool_calls length = %d, want %d", got, 2) + } + if got := gjson.GetBytes(out, "messages.0.tool_calls.0.id").String(); got != "exec_command:0" { + t.Fatalf("messages.0.tool_calls.0.id = %q, want %q", got, "exec_command:0") + } + if got := gjson.GetBytes(out, "messages.0.tool_calls.1.id").String(); got != "exec_command:1" { + t.Fatalf("messages.0.tool_calls.1.id = %q, want %q", got, "exec_command:1") + } + + if got := gjson.GetBytes(out, "messages.1.tool_call_id").String(); got != "exec_command:0" { + t.Fatalf("messages.1.tool_call_id = %q, want %q", got, "exec_command:0") + } + if got := gjson.GetBytes(out, "messages.2.tool_call_id").String(); got != "exec_command:1" { + t.Fatalf("messages.2.tool_call_id = %q, want %q", got, "exec_command:1") + } +} + +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_SplitFunctionCallsWhenInterrupted(t *testing.T) { + raw := []byte(`{ + "input": [ + {"type":"function_call","call_id":"call_a","name":"tool_a","arguments":"{}"}, + {"type":"message","role":"user","content":"next"}, + {"type":"function_call","call_id":"call_b","name":"tool_b","arguments":"{}"} + ] + }`) + t.Logf("input json:\n%s", prettyJSONForTest(raw)) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("kimi-k2.6", raw, false) + t.Logf("output json:\n%s", prettyJSONForTest(out)) + + if got := len(gjson.GetBytes(out, "messages").Array()); got != 3 { + t.Fatalf("messages count = %d, want %d", got, 3) + } + if got := gjson.GetBytes(out, "messages.0.tool_calls.0.id").String(); got != "call_a" { + t.Fatalf("messages.0.tool_calls.0.id = %q, want %q", got, "call_a") + } + if got := gjson.GetBytes(out, "messages.2.tool_calls.0.id").String(); got != "call_b" { + t.Fatalf("messages.2.tool_calls.0.id = %q, want %q", got, "call_b") + } +} + +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_DefersMessageUntilToolOutput(t *testing.T) { + raw := []byte(`{ + "input": [ + {"type":"function_call","call_id":"call_x","name":"exec_command","arguments":"{\"cmd\":\"echo hi\"}"}, + {"type":"message","role":"user","content":"Approved command prefix saved"}, + {"type":"function_call_output","call_id":"call_x","output":"ok"}, + {"type":"message","role":"user","content":"next"} + ] + }`) + t.Logf("input json:\n%s", prettyJSONForTest(raw)) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("kimi-k2.6", raw, true) + t.Logf("output json:\n%s", prettyJSONForTest(out)) + + if got := len(gjson.GetBytes(out, "messages").Array()); got != 4 { + t.Fatalf("messages count = %d, want %d", got, 4) + } + if got := gjson.GetBytes(out, "messages.0.role").String(); got != "assistant" { + t.Fatalf("messages.0.role = %q, want %q", got, "assistant") + } + if got := gjson.GetBytes(out, "messages.1.role").String(); got != "tool" { + t.Fatalf("messages.1.role = %q, want %q", got, "tool") + } + if got := gjson.GetBytes(out, "messages.1.tool_call_id").String(); got != "call_x" { + t.Fatalf("messages.1.tool_call_id = %q, want %q", got, "call_x") + } + if got := gjson.GetBytes(out, "messages.2.role").String(); got != "user" { + t.Fatalf("messages.2.role = %q, want %q", got, "user") + } + if got := gjson.GetBytes(out, "messages.2.content").String(); got != "Approved command prefix saved" { + t.Fatalf("messages.2.content = %q, want %q", got, "Approved command prefix saved") + } + if got := gjson.GetBytes(out, "messages.3.content").String(); got != "next" { + t.Fatalf("messages.3.content = %q, want %q", got, "next") + } +} diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index 8a44aede44..8895b68445 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/translator/translator.go b/internal/translator/translator/translator.go index ab3f68a99d..88766a83bb 100644 --- a/internal/translator/translator/translator.go +++ b/internal/translator/translator/translator.go @@ -7,8 +7,8 @@ package translator import ( "context" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) // registry holds the default translator registry instance. diff --git a/internal/util/provider.go b/internal/util/provider.go index beee9add9d..6313f58e32 100644 --- a/internal/util/provider.go +++ b/internal/util/provider.go @@ -7,8 +7,8 @@ import ( "net/url" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus" ) diff --git a/internal/util/proxy.go b/internal/util/proxy.go index 9b57ca1733..781dd54dc0 100644 --- a/internal/util/proxy.go +++ b/internal/util/proxy.go @@ -6,8 +6,8 @@ package util import ( "net/http" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" ) diff --git a/internal/util/util.go b/internal/util/util.go index 9bf630f299..2c50cf67b5 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -11,7 +11,7 @@ import ( "regexp" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" log "github.com/sirupsen/logrus" ) @@ -73,9 +73,10 @@ func SetLogLevel(cfg *config.Config) { // ResolveAuthDir normalizes the auth directory path for consistent reuse throughout the app. // It expands a leading tilde (~) to the user's home directory and returns a cleaned path. +// If authDir is empty, it defaults to ~/.cli-proxy-api. func ResolveAuthDir(authDir string) (string, error) { if authDir == "" { - return "", nil + authDir = config.DefaultAuthDir } if strings.HasPrefix(authDir, "~") { home, err := os.UserHomeDir() diff --git a/internal/watcher/clients.go b/internal/watcher/clients.go index fb0d7865bc..0a46660e8b 100644 --- a/internal/watcher/clients.go +++ b/internal/watcher/clients.go @@ -13,11 +13,11 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/watcher/config_reload.go b/internal/watcher/config_reload.go index 1bbf4ef239..0471f8b3f2 100644 --- a/internal/watcher/config_reload.go +++ b/internal/watcher/config_reload.go @@ -9,9 +9,9 @@ import ( "reflect" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" "gopkg.in/yaml.v3" log "github.com/sirupsen/logrus" diff --git a/internal/watcher/diff/auth_diff.go b/internal/watcher/diff/auth_diff.go index 4b6e600852..39fe5e886d 100644 --- a/internal/watcher/diff/auth_diff.go +++ b/internal/watcher/diff/auth_diff.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // BuildAuthChangeDetails computes a redacted, human-readable list of auth field changes. diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index b414ed5adf..c206049e43 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -6,7 +6,7 @@ import ( "reflect" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // BuildConfigChangeDetails computes a redacted, human-readable list of config changes. diff --git a/internal/watcher/diff/config_diff_test.go b/internal/watcher/diff/config_diff_test.go index b9a9153b18..192791ea74 100644 --- a/internal/watcher/diff/config_diff_test.go +++ b/internal/watcher/diff/config_diff_test.go @@ -3,8 +3,8 @@ package diff import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestBuildConfigChangeDetails(t *testing.T) { diff --git a/internal/watcher/diff/model_hash.go b/internal/watcher/diff/model_hash.go index 5779faccd7..fed3386a7a 100644 --- a/internal/watcher/diff/model_hash.go +++ b/internal/watcher/diff/model_hash.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // ComputeOpenAICompatModelsHash returns a stable hash for OpenAI-compat models. diff --git a/internal/watcher/diff/model_hash_test.go b/internal/watcher/diff/model_hash_test.go index db06ebd12c..b687d4da2e 100644 --- a/internal/watcher/diff/model_hash_test.go +++ b/internal/watcher/diff/model_hash_test.go @@ -3,7 +3,7 @@ package diff import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestComputeOpenAICompatModelsHash_Deterministic(t *testing.T) { diff --git a/internal/watcher/diff/models_summary.go b/internal/watcher/diff/models_summary.go index 9c2aa91ac4..4c9b035a16 100644 --- a/internal/watcher/diff/models_summary.go +++ b/internal/watcher/diff/models_summary.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) type GeminiModelsSummary struct { diff --git a/internal/watcher/diff/oauth_excluded.go b/internal/watcher/diff/oauth_excluded.go index 2039cf4898..d632062840 100644 --- a/internal/watcher/diff/oauth_excluded.go +++ b/internal/watcher/diff/oauth_excluded.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) type ExcludedModelsSummary struct { diff --git a/internal/watcher/diff/oauth_excluded_test.go b/internal/watcher/diff/oauth_excluded_test.go index f5ad391358..8643f59447 100644 --- a/internal/watcher/diff/oauth_excluded_test.go +++ b/internal/watcher/diff/oauth_excluded_test.go @@ -3,7 +3,7 @@ package diff import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestSummarizeExcludedModels_NormalizesAndDedupes(t *testing.T) { diff --git a/internal/watcher/diff/oauth_model_alias.go b/internal/watcher/diff/oauth_model_alias.go index c5a17d2940..8c14089b9f 100644 --- a/internal/watcher/diff/oauth_model_alias.go +++ b/internal/watcher/diff/oauth_model_alias.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) type OAuthModelAliasSummary struct { diff --git a/internal/watcher/diff/openai_compat.go b/internal/watcher/diff/openai_compat.go index 541b35b3d1..31d0bcd99d 100644 --- a/internal/watcher/diff/openai_compat.go +++ b/internal/watcher/diff/openai_compat.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // DiffOpenAICompatibility produces human-readable change descriptions. diff --git a/internal/watcher/diff/openai_compat_test.go b/internal/watcher/diff/openai_compat_test.go index db33db1487..5683671ae4 100644 --- a/internal/watcher/diff/openai_compat_test.go +++ b/internal/watcher/diff/openai_compat_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestDiffOpenAICompatibility(t *testing.T) { diff --git a/internal/watcher/dispatcher.go b/internal/watcher/dispatcher.go index 3d7d7527b3..d0182e2c25 100644 --- a/internal/watcher/dispatcher.go +++ b/internal/watcher/dispatcher.go @@ -9,9 +9,9 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) var snapshotCoreAuthsFunc = snapshotCoreAuths diff --git a/internal/watcher/synthesizer/config.go b/internal/watcher/synthesizer/config.go index 8026b02fa9..1eea3dc112 100644 --- a/internal/watcher/synthesizer/config.go +++ b/internal/watcher/synthesizer/config.go @@ -5,8 +5,8 @@ import ( "strconv" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // ConfigSynthesizer generates Auth entries from configuration API keys. @@ -60,6 +60,10 @@ func (s *ConfigSynthesizer) synthesizeGeminiKeys(ctx *SynthesisContext) []*corea "source": fmt.Sprintf("config:gemini[%s]", token), "api_key": key, } + metadata := map[string]any{} + if entry.DisableCooling { + metadata["disable_cooling"] = true + } if entry.Priority != 0 { attrs["priority"] = strconv.Itoa(entry.Priority) } @@ -78,10 +82,14 @@ func (s *ConfigSynthesizer) synthesizeGeminiKeys(ctx *SynthesisContext) []*corea Status: coreauth.StatusActive, ProxyURL: proxyURL, Attributes: attrs, + Metadata: metadata, CreatedAt: now, UpdatedAt: now, } ApplyAuthExcludedModelsMeta(a, cfg, entry.ExcludedModels, "apikey") + if len(a.Metadata) == 0 { + a.Metadata = nil + } out = append(out, a) } return out @@ -107,6 +115,10 @@ func (s *ConfigSynthesizer) synthesizeClaudeKeys(ctx *SynthesisContext) []*corea "source": fmt.Sprintf("config:claude[%s]", token), "api_key": key, } + metadata := map[string]any{} + if ck.DisableCooling { + metadata["disable_cooling"] = true + } if ck.Priority != 0 { attrs["priority"] = strconv.Itoa(ck.Priority) } @@ -126,10 +138,14 @@ func (s *ConfigSynthesizer) synthesizeClaudeKeys(ctx *SynthesisContext) []*corea Status: coreauth.StatusActive, ProxyURL: proxyURL, Attributes: attrs, + Metadata: metadata, CreatedAt: now, UpdatedAt: now, } ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey") + if len(a.Metadata) == 0 { + a.Metadata = nil + } out = append(out, a) } return out @@ -154,6 +170,10 @@ func (s *ConfigSynthesizer) synthesizeCodexKeys(ctx *SynthesisContext) []*coreau "source": fmt.Sprintf("config:codex[%s]", token), "api_key": key, } + metadata := map[string]any{} + if ck.DisableCooling { + metadata["disable_cooling"] = true + } if ck.Priority != 0 { attrs["priority"] = strconv.Itoa(ck.Priority) } @@ -176,10 +196,14 @@ func (s *ConfigSynthesizer) synthesizeCodexKeys(ctx *SynthesisContext) []*coreau Status: coreauth.StatusActive, ProxyURL: proxyURL, Attributes: attrs, + Metadata: metadata, CreatedAt: now, UpdatedAt: now, } ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey") + if len(a.Metadata) == 0 { + a.Metadata = nil + } out = append(out, a) } return out @@ -203,6 +227,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor providerName = "openai-compatibility" } base := strings.TrimSpace(compat.BaseURL) + disableCooling := compat.DisableCooling // Handle new APIKeyEntries format (preferred) createdEntries := 0 @@ -218,6 +243,10 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor "compat_name": compat.Name, "provider_key": providerName, } + metadata := map[string]any{} + if disableCooling { + metadata["disable_cooling"] = true + } if compat.Priority != 0 { attrs["priority"] = strconv.Itoa(compat.Priority) } @@ -236,9 +265,13 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor Status: coreauth.StatusActive, ProxyURL: proxyURL, Attributes: attrs, + Metadata: metadata, CreatedAt: now, UpdatedAt: now, } + if len(a.Metadata) == 0 { + a.Metadata = nil + } out = append(out, a) createdEntries++ } @@ -252,6 +285,10 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor "compat_name": compat.Name, "provider_key": providerName, } + metadata := map[string]any{} + if disableCooling { + metadata["disable_cooling"] = true + } if compat.Priority != 0 { attrs["priority"] = strconv.Itoa(compat.Priority) } @@ -266,9 +303,13 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor Prefix: prefix, Status: coreauth.StatusActive, Attributes: attrs, + Metadata: metadata, CreatedAt: now, UpdatedAt: now, } + if len(a.Metadata) == 0 { + a.Metadata = nil + } out = append(out, a) } } diff --git a/internal/watcher/synthesizer/config_test.go b/internal/watcher/synthesizer/config_test.go index 437f18d11e..c8526a654a 100644 --- a/internal/watcher/synthesizer/config_test.go +++ b/internal/watcher/synthesizer/config_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestNewConfigSynthesizer(t *testing.T) { @@ -68,11 +68,26 @@ func TestConfigSynthesizer_GeminiKeys(t *testing.T) { if auths[0].Attributes["api_key"] != "test-key-123" { t.Errorf("expected api_key test-key-123, got %s", auths[0].Attributes["api_key"]) } + if auths[0].Metadata != nil { + t.Errorf("expected metadata to be nil when disable_cooling not set, got %v", auths[0].Metadata) + } if auths[0].Status != coreauth.StatusActive { t.Errorf("expected status active, got %s", auths[0].Status) } }, }, + { + name: "gemini key disable cooling", + geminiKeys: []config.GeminiKey{ + {APIKey: "test-key-123", Prefix: "team-a", DisableCooling: true}, + }, + wantLen: 1, + validate: func(t *testing.T, auths []*coreauth.Auth) { + if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { + t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) + } + }, + }, { name: "gemini key with base url and proxy", geminiKeys: []config.GeminiKey{ @@ -160,9 +175,10 @@ func TestConfigSynthesizer_ClaudeKeys(t *testing.T) { Config: &config.Config{ ClaudeKey: []config.ClaudeKey{ { - APIKey: "sk-ant-api-xxx", - Prefix: "main", - BaseURL: "https://api.anthropic.com", + APIKey: "sk-ant-api-xxx", + Prefix: "main", + BaseURL: "https://api.anthropic.com", + DisableCooling: true, Models: []config.ClaudeModel{ {Name: "claude-3-opus"}, {Name: "claude-3-sonnet"}, @@ -197,6 +213,9 @@ func TestConfigSynthesizer_ClaudeKeys(t *testing.T) { if _, ok := auths[0].Attributes["models_hash"]; !ok { t.Error("expected models_hash in attributes") } + if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { + t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) + } } func TestConfigSynthesizer_ClaudeKeys_SkipsEmptyAndHeaders(t *testing.T) { @@ -231,11 +250,12 @@ func TestConfigSynthesizer_CodexKeys(t *testing.T) { Config: &config.Config{ CodexKey: []config.CodexKey{ { - APIKey: "codex-key-123", - Prefix: "dev", - BaseURL: "https://api.openai.com", - ProxyURL: "http://proxy.local", - Websockets: true, + APIKey: "codex-key-123", + Prefix: "dev", + BaseURL: "https://api.openai.com", + ProxyURL: "http://proxy.local", + Websockets: true, + DisableCooling: true, }, }, }, @@ -263,6 +283,9 @@ func TestConfigSynthesizer_CodexKeys(t *testing.T) { if auths[0].Attributes["websockets"] != "true" { t.Errorf("expected websockets=true, got %s", auths[0].Attributes["websockets"]) } + if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { + t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) + } } func TestConfigSynthesizer_CodexKeys_SkipsEmptyAndHeaders(t *testing.T) { @@ -301,8 +324,9 @@ func TestConfigSynthesizer_OpenAICompat(t *testing.T) { name: "with APIKeyEntries", compat: []config.OpenAICompatibility{ { - Name: "CustomProvider", - BaseURL: "https://custom.api.com", + Name: "CustomProvider", + BaseURL: "https://custom.api.com", + DisableCooling: true, APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "key-1"}, {APIKey: "key-2"}, @@ -365,6 +389,13 @@ func TestConfigSynthesizer_OpenAICompat(t *testing.T) { if len(auths) != tt.wantLen { t.Fatalf("expected %d auths, got %d", tt.wantLen, len(auths)) } + if tt.name == "with APIKeyEntries" { + for i := range auths { + if v, ok := auths[i].Metadata["disable_cooling"].(bool); !ok || !v { + t.Fatalf("expected auth[%d].disable_cooling=true, got %v", i, auths[i].Metadata["disable_cooling"]) + } + } + } }) } } diff --git a/internal/watcher/synthesizer/context.go b/internal/watcher/synthesizer/context.go index d973289a3a..f92b41ddaf 100644 --- a/internal/watcher/synthesizer/context.go +++ b/internal/watcher/synthesizer/context.go @@ -3,7 +3,7 @@ package synthesizer import ( "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) // SynthesisContext provides the context needed for auth synthesis. diff --git a/internal/watcher/synthesizer/file.go b/internal/watcher/synthesizer/file.go index 49a635e7e8..47990bc154 100644 --- a/internal/watcher/synthesizer/file.go +++ b/internal/watcher/synthesizer/file.go @@ -10,9 +10,9 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/geminicli" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // FileSynthesizer generates Auth entries from OAuth JSON files. diff --git a/internal/watcher/synthesizer/file_test.go b/internal/watcher/synthesizer/file_test.go index f3e4497923..63b394aaf5 100644 --- a/internal/watcher/synthesizer/file_test.go +++ b/internal/watcher/synthesizer/file_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestNewFileSynthesizer(t *testing.T) { diff --git a/internal/watcher/synthesizer/helpers.go b/internal/watcher/synthesizer/helpers.go index 102dc77e22..19b4c896f1 100644 --- a/internal/watcher/synthesizer/helpers.go +++ b/internal/watcher/synthesizer/helpers.go @@ -7,9 +7,9 @@ import ( "sort" "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // StableIDGenerator generates stable, deterministic IDs for auth entries. diff --git a/internal/watcher/synthesizer/helpers_test.go b/internal/watcher/synthesizer/helpers_test.go index 46b9c8a053..69ba85d60d 100644 --- a/internal/watcher/synthesizer/helpers_test.go +++ b/internal/watcher/synthesizer/helpers_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestNewStableIDGenerator(t *testing.T) { diff --git a/internal/watcher/synthesizer/interface.go b/internal/watcher/synthesizer/interface.go index 1a9aedc965..e0962c11c9 100644 --- a/internal/watcher/synthesizer/interface.go +++ b/internal/watcher/synthesizer/interface.go @@ -5,7 +5,7 @@ package synthesizer import ( - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // AuthSynthesizer defines the interface for generating Auth entries from various sources. diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index cf890a4c46..c18cd84d08 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -10,11 +10,11 @@ import ( "time" "github.com/fsnotify/fsnotify" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "gopkg.in/yaml.v3" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/internal/watcher/watcher_test.go b/internal/watcher/watcher_test.go index 00a7a14360..bb3b557777 100644 --- a/internal/watcher/watcher_test.go +++ b/internal/watcher/watcher_test.go @@ -14,11 +14,11 @@ import ( "time" "github.com/fsnotify/fsnotify" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "gopkg.in/yaml.v3" ) diff --git a/sdk/api/handlers/claude/code_handlers.go b/sdk/api/handlers/claude/code_handlers.go index 074ffc0d07..464f385eb5 100644 --- a/sdk/api/handlers/claude/code_handlers.go +++ b/sdk/api/handlers/claude/code_handlers.go @@ -16,10 +16,10 @@ import ( "net/http" "github.com/gin-gonic/gin" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/sdk/api/handlers/gemini/gemini-cli_handlers.go b/sdk/api/handlers/gemini/gemini-cli_handlers.go index 4c5ddf80f9..de79f05b7c 100644 --- a/sdk/api/handlers/gemini/gemini-cli_handlers.go +++ b/sdk/api/handlers/gemini/gemini-cli_handlers.go @@ -15,10 +15,10 @@ import ( "time" "github.com/gin-gonic/gin" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) diff --git a/sdk/api/handlers/gemini/gemini_handlers.go b/sdk/api/handlers/gemini/gemini_handlers.go index e51ad19bc5..60aed26a55 100644 --- a/sdk/api/handlers/gemini/gemini_handlers.go +++ b/sdk/api/handlers/gemini/gemini_handlers.go @@ -13,10 +13,10 @@ import ( "time" "github.com/gin-gonic/gin" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" ) // GeminiAPIHandler contains the handlers for Gemini API endpoints. diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index e89227aa70..6e0adb6417 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -14,14 +14,14 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "golang.org/x/net/context" ) @@ -850,14 +850,22 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string resolvedModelName := modelName initialSuffix := thinking.ParseSuffix(modelName) if initialSuffix.ModelName == "auto" { - resolvedBase := util.ResolveAutoModel(initialSuffix.ModelName) - if initialSuffix.HasSuffix { - resolvedModelName = fmt.Sprintf("%s(%s)", resolvedBase, initialSuffix.RawSuffix) + if h != nil && h.AuthManager != nil && h.AuthManager.HomeEnabled() { + resolvedModelName = modelName } else { - resolvedModelName = resolvedBase + resolvedBase := util.ResolveAutoModel(initialSuffix.ModelName) + if initialSuffix.HasSuffix { + resolvedModelName = fmt.Sprintf("%s(%s)", resolvedBase, initialSuffix.RawSuffix) + } else { + resolvedModelName = resolvedBase + } } } else { - resolvedModelName = util.ResolveAutoModel(modelName) + if h != nil && h.AuthManager != nil && h.AuthManager.HomeEnabled() { + resolvedModelName = modelName + } else { + resolvedModelName = util.ResolveAutoModel(modelName) + } } parsed := thinking.ParseSuffix(resolvedModelName) @@ -870,6 +878,10 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string } } + if h != nil && h.AuthManager != nil && h.AuthManager.HomeEnabled() { + return []string{"home"}, resolvedModelName, nil + } + providers = util.GetProviderName(baseModel) // Fallback: if baseModel has no provider but differs from resolvedModelName, // try using the full model name. This handles edge cases where custom models diff --git a/sdk/api/handlers/handlers_error_response_test.go b/sdk/api/handlers/handlers_error_response_test.go index 917971c245..0c206e386f 100644 --- a/sdk/api/handlers/handlers_error_response_test.go +++ b/sdk/api/handlers/handlers_error_response_test.go @@ -9,9 +9,9 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestWriteErrorResponse_AddonHeadersDisabledByDefault(t *testing.T) { diff --git a/sdk/api/handlers/handlers_metadata_test.go b/sdk/api/handlers/handlers_metadata_test.go index 99af872dc0..c5e94f963e 100644 --- a/sdk/api/handlers/handlers_metadata_test.go +++ b/sdk/api/handlers/handlers_metadata_test.go @@ -3,7 +3,7 @@ package handlers import ( "testing" - coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" "golang.org/x/net/context" ) diff --git a/sdk/api/handlers/handlers_request_details_test.go b/sdk/api/handlers/handlers_request_details_test.go index c98580f224..3110cbc561 100644 --- a/sdk/api/handlers/handlers_request_details_test.go +++ b/sdk/api/handlers/handlers_request_details_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestGetRequestDetails_PreservesSuffix(t *testing.T) { diff --git a/sdk/api/handlers/handlers_stream_bootstrap_test.go b/sdk/api/handlers/handlers_stream_bootstrap_test.go index f357962f0a..551baac374 100644 --- a/sdk/api/handlers/handlers_stream_bootstrap_test.go +++ b/sdk/api/handlers/handlers_stream_bootstrap_test.go @@ -8,11 +8,11 @@ import ( "sync" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) type failOnceStreamExecutor struct { diff --git a/sdk/api/handlers/openai/openai_handlers.go b/sdk/api/handlers/openai/openai_handlers.go index 4b4a9833bd..29dc0ea0b1 100644 --- a/sdk/api/handlers/openai/openai_handlers.go +++ b/sdk/api/handlers/openai/openai_handlers.go @@ -14,11 +14,11 @@ import ( "sync" "github.com/gin-gonic/gin" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - responsesconverter "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + responsesconverter "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/openai/openai/responses" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/sdk/api/handlers/openai/openai_images_handlers.go b/sdk/api/handlers/openai/openai_images_handlers.go index 8d22a4f4ed..6e6e8ef6ff 100644 --- a/sdk/api/handlers/openai/openai_images_handlers.go +++ b/sdk/api/handlers/openai/openai_images_handlers.go @@ -14,9 +14,9 @@ import ( "time" "github.com/gin-gonic/gin" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/sdk/api/handlers/openai/openai_images_handlers_test.go b/sdk/api/handlers/openai/openai_images_handlers_test.go index ea65ca3a5d..7796599619 100644 --- a/sdk/api/handlers/openai/openai_images_handlers_test.go +++ b/sdk/api/handlers/openai/openai_images_handlers_test.go @@ -10,9 +10,9 @@ import ( "testing" "github.com/gin-gonic/gin" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" "github.com/tidwall/gjson" ) diff --git a/sdk/api/handlers/openai/openai_responses_compact_test.go b/sdk/api/handlers/openai/openai_responses_compact_test.go index dcfcc99a7c..48b7e3bbde 100644 --- a/sdk/api/handlers/openai/openai_responses_compact_test.go +++ b/sdk/api/handlers/openai/openai_responses_compact_test.go @@ -9,11 +9,11 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) type compactCaptureExecutor struct { diff --git a/sdk/api/handlers/openai/openai_responses_handlers.go b/sdk/api/handlers/openai/openai_responses_handlers.go index 8dd1a0a7b1..5b2c006a30 100644 --- a/sdk/api/handlers/openai/openai_responses_handlers.go +++ b/sdk/api/handlers/openai/openai_responses_handlers.go @@ -16,10 +16,10 @@ import ( "sort" "github.com/gin-gonic/gin" - . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + . "github.com/router-for-me/CLIProxyAPI/v7/internal/constant" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/sdk/api/handlers/openai/openai_responses_handlers_stream_error_test.go b/sdk/api/handlers/openai/openai_responses_handlers_stream_error_test.go index 771e46b88b..54d1467589 100644 --- a/sdk/api/handlers/openai/openai_responses_handlers_stream_error_test.go +++ b/sdk/api/handlers/openai/openai_responses_handlers_stream_error_test.go @@ -8,9 +8,9 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestForwardResponsesStreamTerminalErrorUsesResponsesErrorChunk(t *testing.T) { diff --git a/sdk/api/handlers/openai/openai_responses_handlers_stream_test.go b/sdk/api/handlers/openai/openai_responses_handlers_stream_test.go index 151da9a79f..0742b9b3d3 100644 --- a/sdk/api/handlers/openai/openai_responses_handlers_stream_test.go +++ b/sdk/api/handlers/openai/openai_responses_handlers_stream_test.go @@ -7,9 +7,9 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" "github.com/tidwall/gjson" ) diff --git a/sdk/api/handlers/openai/openai_responses_websocket.go b/sdk/api/handlers/openai/openai_responses_websocket.go index c617c94644..574338fd75 100644 --- a/sdk/api/handlers/openai/openai_responses_websocket.go +++ b/sdk/api/handlers/openai/openai_responses_websocket.go @@ -13,13 +13,13 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/gorilla/websocket" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -104,6 +104,15 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) { var lastRequest []byte lastResponseOutput := []byte("[]") pinnedAuthID := "" + sessionAuthByID := func(authID string) (*coreauth.Auth, bool) { + if h == nil || h.AuthManager == nil { + return nil, false + } + if auth, ok := h.AuthManager.GetExecutionSessionAuthByID(passthroughSessionID, authID); ok { + return auth, true + } + return h.AuthManager.GetByID(authID) + } forceTranscriptReplayNextRequest := false for { @@ -130,8 +139,8 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) { appendWebsocketTimelineEvent(&wsTimelineLog, "request", payload, time.Now()) allowIncrementalInputWithPreviousResponseID := false - if pinnedAuthID != "" && h != nil && h.AuthManager != nil { - if pinnedAuth, ok := h.AuthManager.GetByID(pinnedAuthID); ok && pinnedAuth != nil { + if pinnedAuthID != "" { + if pinnedAuth, ok := sessionAuthByID(pinnedAuthID); ok && pinnedAuth != nil { allowIncrementalInputWithPreviousResponseID = websocketUpstreamSupportsIncrementalInput(pinnedAuth.Attributes, pinnedAuth.Metadata) } } else { @@ -146,8 +155,8 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) { } allowCompactionReplayBypass := false - if pinnedAuthID != "" && h != nil && h.AuthManager != nil { - if pinnedAuth, ok := h.AuthManager.GetByID(pinnedAuthID); ok && pinnedAuth != nil { + if pinnedAuthID != "" { + if pinnedAuth, ok := sessionAuthByID(pinnedAuthID); ok && pinnedAuth != nil { allowCompactionReplayBypass = responsesWebsocketAuthSupportsCompactionReplay(pinnedAuth) } } else { @@ -228,7 +237,7 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) { if authID == "" || h == nil || h.AuthManager == nil { return } - selectedAuth, ok := h.AuthManager.GetByID(authID) + selectedAuth, ok := sessionAuthByID(authID) if !ok || selectedAuth == nil { return } diff --git a/sdk/api/handlers/openai/openai_responses_websocket_test.go b/sdk/api/handlers/openai/openai_responses_websocket_test.go index 319127f0e0..7ff58fa3c8 100644 --- a/sdk/api/handlers/openai/openai_responses_websocket_test.go +++ b/sdk/api/handlers/openai/openai_responses_websocket_test.go @@ -14,12 +14,12 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" "github.com/tidwall/gjson" ) @@ -662,6 +662,34 @@ func TestRepairResponsesWebsocketToolCallsInsertsCachedCallForOrphanOutput(t *te } } +func TestRepairResponsesWebsocketToolCallsInsertsCachedCallForPreviousResponseOutput(t *testing.T) { + outputCache := newWebsocketToolOutputCache(time.Minute, 10) + callCache := newWebsocketToolOutputCache(time.Minute, 10) + sessionKey := "session-1" + + callCache.record(sessionKey, "call-1", []byte(`{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"}`)) + + raw := []byte(`{"previous_response_id":"resp-latest","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1","output":"ok"},{"type":"message","id":"msg-1"}]}`) + repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw) + + if got := gjson.GetBytes(repaired, "previous_response_id").String(); got != "resp-latest" { + t.Fatalf("previous_response_id = %q, want resp-latest", got) + } + input := gjson.GetBytes(repaired, "input").Array() + if len(input) != 3 { + t.Fatalf("repaired input len = %d, want 3: %s", len(input), repaired) + } + if input[0].Get("type").String() != "function_call" || input[0].Get("call_id").String() != "call-1" { + t.Fatalf("missing inserted call: %s", input[0].Raw) + } + if input[1].Get("type").String() != "function_call_output" || input[1].Get("call_id").String() != "call-1" { + t.Fatalf("unexpected output item: %s", input[1].Raw) + } + if input[2].Get("type").String() != "message" || input[2].Get("id").String() != "msg-1" { + t.Fatalf("unexpected trailing item: %s", input[2].Raw) + } +} + func TestRepairResponsesWebsocketToolCallsDropsOrphanOutputWhenCallMissing(t *testing.T) { outputCache := newWebsocketToolOutputCache(time.Minute, 10) callCache := newWebsocketToolOutputCache(time.Minute, 10) diff --git a/sdk/api/handlers/openai/openai_responses_websocket_toolcall_repair.go b/sdk/api/handlers/openai/openai_responses_websocket_toolcall_repair.go index 1a5772ec70..c521bec049 100644 --- a/sdk/api/handlers/openai/openai_responses_websocket_toolcall_repair.go +++ b/sdk/api/handlers/openai/openai_responses_websocket_toolcall_repair.go @@ -300,11 +300,6 @@ func repairResponsesToolCallsArray(outputCache, callCache *websocketToolOutputCa continue } - if allowOrphanOutputs { - filtered = append(filtered, item) - continue - } - if _, ok := callPresent[callID]; ok { filtered = append(filtered, item) continue @@ -322,6 +317,11 @@ func repairResponsesToolCallsArray(outputCache, callCache *websocketToolOutputCa } } + if allowOrphanOutputs { + filtered = append(filtered, item) + continue + } + // Drop orphaned function_call_output items; upstream rejects transcripts with missing calls. continue } diff --git a/sdk/api/handlers/stream_forwarder.go b/sdk/api/handlers/stream_forwarder.go index 401baca8fa..63ddc31e43 100644 --- a/sdk/api/handlers/stream_forwarder.go +++ b/sdk/api/handlers/stream_forwarder.go @@ -5,7 +5,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" ) type StreamForwardOptions struct { diff --git a/sdk/api/management.go b/sdk/api/management.go index a5a1cfc490..689cda3dca 100644 --- a/sdk/api/management.go +++ b/sdk/api/management.go @@ -1,16 +1,21 @@ // Package api exposes helpers for embedding CLIProxyAPI. // -// It wraps internal management handler types so external projects can integrate -// management endpoints without importing internal packages. +// It wraps internal management handler types and helpers so external projects +// can integrate management endpoints without importing internal packages. package api import ( + "context" + "github.com/gin-gonic/gin" - internalmanagement "github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + internalmanagement "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) +// Handler re-exports the management handler used by the internal HTTP API. +type Handler = internalmanagement.Handler + // ManagementTokenRequester exposes a limited subset of management endpoints for requesting tokens. type ManagementTokenRequester interface { RequestAnthropicToken(*gin.Context) @@ -23,13 +28,23 @@ type ManagementTokenRequester interface { } type managementTokenRequester struct { - handler *internalmanagement.Handler + handler *Handler +} + +// NewHandler creates a management handler for SDK consumers. +func NewHandler(cfg *config.Config, configFilePath string, manager *coreauth.Manager) *Handler { + return internalmanagement.NewHandler(cfg, configFilePath, manager) +} + +// NewHandlerWithoutConfigFilePath creates a management handler that skips config file persistence. +func NewHandlerWithoutConfigFilePath(cfg *config.Config, manager *coreauth.Manager) *Handler { + return internalmanagement.NewHandlerWithoutConfigFilePath(cfg, manager) } // NewManagementTokenRequester creates a limited management handler exposing only token request endpoints. func NewManagementTokenRequester(cfg *config.Config, manager *coreauth.Manager) ManagementTokenRequester { return &managementTokenRequester{ - handler: internalmanagement.NewHandlerWithoutConfigFilePath(cfg, manager), + handler: NewHandlerWithoutConfigFilePath(cfg, manager), } } @@ -60,3 +75,63 @@ func (m *managementTokenRequester) GetAuthStatus(c *gin.Context) { func (m *managementTokenRequester) PostOAuthCallback(c *gin.Context) { m.handler.PostOAuthCallback(c) } + +// WriteConfig persists management configuration to disk. +func WriteConfig(path string, data []byte) error { + return internalmanagement.WriteConfig(path, data) +} + +// RegisterOAuthSession records a pending OAuth callback state. +func RegisterOAuthSession(state, provider string) { + internalmanagement.RegisterOAuthSession(state, provider) +} + +// SetOAuthSessionError stores an OAuth session error message. +func SetOAuthSessionError(state, message string) { + internalmanagement.SetOAuthSessionError(state, message) +} + +// CompleteOAuthSession marks a single OAuth session as completed. +func CompleteOAuthSession(state string) { + internalmanagement.CompleteOAuthSession(state) +} + +// CompleteOAuthSessionsByProvider removes all pending OAuth sessions for a provider. +func CompleteOAuthSessionsByProvider(provider string) int { + return internalmanagement.CompleteOAuthSessionsByProvider(provider) +} + +// GetOAuthSession returns the current OAuth session state. +func GetOAuthSession(state string) (provider string, status string, ok bool) { + return internalmanagement.GetOAuthSession(state) +} + +// IsOAuthSessionPending reports whether a provider/state pair is still pending. +func IsOAuthSessionPending(state, provider string) bool { + return internalmanagement.IsOAuthSessionPending(state, provider) +} + +// ValidateOAuthState validates an OAuth state token. +func ValidateOAuthState(state string) error { + return internalmanagement.ValidateOAuthState(state) +} + +// NormalizeOAuthProvider normalizes a provider name to its canonical form. +func NormalizeOAuthProvider(provider string) (string, error) { + return internalmanagement.NormalizeOAuthProvider(provider) +} + +// WriteOAuthCallbackFile writes an OAuth callback payload to disk. +func WriteOAuthCallbackFile(authDir, provider, state, code, errorMessage string) (string, error) { + return internalmanagement.WriteOAuthCallbackFile(authDir, provider, state, code, errorMessage) +} + +// WriteOAuthCallbackFileForPendingSession writes an OAuth callback payload for a pending session. +func WriteOAuthCallbackFileForPendingSession(authDir, provider, state, code, errorMessage string) (string, error) { + return internalmanagement.WriteOAuthCallbackFileForPendingSession(authDir, provider, state, code, errorMessage) +} + +// PopulateAuthContext copies auth metadata from a Gin context into a request context. +func PopulateAuthContext(ctx context.Context, c *gin.Context) context.Context { + return internalmanagement.PopulateAuthContext(ctx, c) +} diff --git a/sdk/api/options.go b/sdk/api/options.go index 8497884bf0..e2bbff78e9 100644 --- a/sdk/api/options.go +++ b/sdk/api/options.go @@ -8,10 +8,10 @@ import ( "time" "github.com/gin-gonic/gin" - internalapi "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/logging" + internalapi "github.com/router-for-me/CLIProxyAPI/v7/internal/api" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/logging" ) // ServerOption customises HTTP server construction. diff --git a/sdk/auth/antigravity.go b/sdk/auth/antigravity.go index d52bf1d259..0a947b20f0 100644 --- a/sdk/auth/antigravity.go +++ b/sdk/auth/antigravity.go @@ -8,12 +8,12 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/antigravity" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/antigravity" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/sdk/auth/claude.go b/sdk/auth/claude.go index d82a718b2d..726fa922ae 100644 --- a/sdk/auth/claude.go +++ b/sdk/auth/claude.go @@ -7,13 +7,13 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" // legacy client removed - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/sdk/auth/codex.go b/sdk/auth/codex.go index 269e3d8b21..be58c9c5a6 100644 --- a/sdk/auth/codex.go +++ b/sdk/auth/codex.go @@ -7,13 +7,13 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" // legacy client removed - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/sdk/auth/codex_device.go b/sdk/auth/codex_device.go index 10f59fb97b..d7ea4e1fe9 100644 --- a/sdk/auth/codex_device.go +++ b/sdk/auth/codex_device.go @@ -13,11 +13,11 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/sdk/auth/errors.go b/sdk/auth/errors.go index 78fe9a17bd..f950e925ff 100644 --- a/sdk/auth/errors.go +++ b/sdk/auth/errors.go @@ -3,7 +3,7 @@ package auth import ( "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces" ) // ProjectSelectionError indicates that the user must choose a specific project ID. diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go index f8f49f44ba..5675caac29 100644 --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -15,7 +15,7 @@ import ( "sync" "time" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // FileTokenStore persists token records and auth metadata using the filesystem as backing storage. @@ -72,6 +72,10 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str switch { case auth.Storage != nil: + if auth.Metadata == nil { + auth.Metadata = make(map[string]any) + } + auth.Metadata["disabled"] = auth.Disabled if setter, ok := auth.Storage.(metadataSetter); ok { setter.SetMetadata(auth.Metadata) } diff --git a/sdk/auth/filestore_disabled_test.go b/sdk/auth/filestore_disabled_test.go new file mode 100644 index 0000000000..665f9ebf1f --- /dev/null +++ b/sdk/auth/filestore_disabled_test.go @@ -0,0 +1,64 @@ +package auth + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" +) + +type testTokenStorage struct { + meta map[string]any +} + +func (s *testTokenStorage) SetMetadata(meta map[string]any) { s.meta = meta } + +func (s *testTokenStorage) SaveTokenToFile(authFilePath string) error { + raw, err := json.Marshal(s.meta) + if err != nil { + return err + } + return os.WriteFile(authFilePath, raw, 0o600) +} + +func TestFileTokenStore_Save_DisabledPersistsFlagForTokenStorage(t *testing.T) { + ctx := context.Background() + baseDir := t.TempDir() + path := filepath.Join(baseDir, "disabled.json") + + if err := os.WriteFile(path, []byte(`{"type":"test","disabled":true}`), 0o600); err != nil { + t.Fatalf("seed auth file: %v", err) + } + + store := NewFileTokenStore() + store.SetBaseDir(baseDir) + storage := &testTokenStorage{} + + auth := &cliproxyauth.Auth{ + ID: "disabled.json", + Provider: "test", + FileName: "disabled.json", + Disabled: true, + Storage: storage, + Metadata: map[string]any{"type": "test"}, + } + + if _, err := store.Save(ctx, auth); err != nil { + t.Fatalf("Save() error: %v", err) + } + + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read auth file: %v", err) + } + var meta map[string]any + if err := json.Unmarshal(raw, &meta); err != nil { + t.Fatalf("unmarshal auth file: %v", err) + } + if disabled, _ := meta["disabled"].(bool); !disabled { + t.Fatalf("disabled=%v, want true (raw=%s)", meta["disabled"], string(raw)) + } +} diff --git a/sdk/auth/gemini.go b/sdk/auth/gemini.go index 2b8f9c2b88..ba7c7728ad 100644 --- a/sdk/auth/gemini.go +++ b/sdk/auth/gemini.go @@ -5,10 +5,10 @@ import ( "fmt" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini" // legacy client removed - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // GeminiAuthenticator implements the login flow for Google Gemini CLI accounts. diff --git a/sdk/auth/interfaces.go b/sdk/auth/interfaces.go index 64cf8ed035..e5582a0cc5 100644 --- a/sdk/auth/interfaces.go +++ b/sdk/auth/interfaces.go @@ -5,8 +5,8 @@ import ( "errors" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported") diff --git a/sdk/auth/kimi.go b/sdk/auth/kimi.go index 12ae101e7d..4dbff1e87e 100644 --- a/sdk/auth/kimi.go +++ b/sdk/auth/kimi.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" - "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi" + "github.com/router-for-me/CLIProxyAPI/v7/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) diff --git a/sdk/auth/manager.go b/sdk/auth/manager.go index c6469a7d19..bceb5e196d 100644 --- a/sdk/auth/manager.go +++ b/sdk/auth/manager.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) // Manager aggregates authenticators and coordinates persistence via a token store. diff --git a/sdk/auth/refresh_registry.go b/sdk/auth/refresh_registry.go index ae60f56a64..fe25231507 100644 --- a/sdk/auth/refresh_registry.go +++ b/sdk/auth/refresh_registry.go @@ -3,7 +3,7 @@ package auth import ( "time" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func init() { diff --git a/sdk/auth/store_registry.go b/sdk/auth/store_registry.go index 760449f8cf..1971947bc8 100644 --- a/sdk/auth/store_registry.go +++ b/sdk/auth/store_registry.go @@ -3,7 +3,7 @@ package auth import ( "sync" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) var ( diff --git a/sdk/cliproxy/auth/antigravity_credits_test.go b/sdk/cliproxy/auth/antigravity_credits_test.go index 38c08dcfbc..34a475dc6a 100644 --- a/sdk/cliproxy/auth/antigravity_credits_test.go +++ b/sdk/cliproxy/auth/antigravity_credits_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type antigravityCreditsFallbackExecutor struct { diff --git a/sdk/cliproxy/auth/api_key_model_alias_test.go b/sdk/cliproxy/auth/api_key_model_alias_test.go index 70915d9e37..25da4df4ed 100644 --- a/sdk/cliproxy/auth/api_key_model_alias_test.go +++ b/sdk/cliproxy/auth/api_key_model_alias_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestLookupAPIKeyUpstreamModel(t *testing.T) { diff --git a/sdk/cliproxy/auth/auto_refresh_loop.go b/sdk/cliproxy/auth/auto_refresh_loop.go index 9767ee5803..35d69cfecf 100644 --- a/sdk/cliproxy/auth/auto_refresh_loop.go +++ b/sdk/cliproxy/auth/auto_refresh_loop.go @@ -336,7 +336,10 @@ func (l *authAutoRefreshLoop) remove(authID string) { } func nextRefreshCheckAt(now time.Time, auth *Auth, interval time.Duration) (time.Time, bool) { - if auth == nil || auth.Disabled { + if auth == nil { + return time.Time{}, false + } + if hasUnauthorizedAuthFailure(auth) { return time.Time{}, false } diff --git a/sdk/cliproxy/auth/auto_refresh_loop_test.go b/sdk/cliproxy/auth/auto_refresh_loop_test.go index 420aae237a..e4edb2df55 100644 --- a/sdk/cliproxy/auth/auto_refresh_loop_test.go +++ b/sdk/cliproxy/auth/auto_refresh_loop_test.go @@ -34,9 +34,31 @@ func setRefreshLeadFactory(t *testing.T, provider string, factory func() *time.D func TestNextRefreshCheckAt_DisabledUnschedule(t *testing.T) { now := time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC) - auth := &Auth{ID: "a1", Provider: "test", Disabled: true} - if _, ok := nextRefreshCheckAt(now, auth, 15*time.Minute); ok { - t.Fatalf("nextRefreshCheckAt() ok = true, want false") + expiry := now.Add(time.Hour) + lead := 10 * time.Minute + setRefreshLeadFactory(t, "disabled-schedule", func() *time.Duration { + d := lead + return &d + }) + + auth := &Auth{ + ID: "a1", + Provider: "disabled-schedule", + Disabled: true, + Status: StatusDisabled, + Metadata: map[string]any{ + "email": "x@example.com", + "expires_at": expiry.Format(time.RFC3339), + }, + } + + got, ok := nextRefreshCheckAt(now, auth, 15*time.Minute) + if !ok { + t.Fatalf("nextRefreshCheckAt() ok = false, want true") + } + want := expiry.Add(-lead) + if !got.Equal(want) { + t.Fatalf("nextRefreshCheckAt() = %s, want %s", got, want) } } diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index ab3eca4957..fca26a9c24 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -16,13 +16,14 @@ import ( "time" "github.com/google/uuid" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" log "github.com/sirupsen/logrus" ) @@ -50,6 +51,7 @@ type ExecutionSessionCloser interface { } const ( + homeAuthCountMetadataKey = "__cliproxy_home_auth_count" // CloseAllExecutionSessionsID asks an executor to release all active execution sessions. // Executors that do not support this marker may ignore it. CloseAllExecutionSessionsID = "__all_execution_sessions__" @@ -149,6 +151,9 @@ type Manager struct { mu sync.RWMutex auths map[string]*Auth scheduler *authScheduler + // homeRuntimeAuths caches auths returned by Home so websocket sessions can + // reuse an established upstream credential without dispatching every turn. + homeRuntimeAuths map[string]map[string]*Auth // providerOffsets tracks per-model provider rotation state for multi-provider routing. providerOffsets map[string]int @@ -193,6 +198,7 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager { selector: selector, hook: hook, auths: make(map[string]*Auth), + homeRuntimeAuths: make(map[string]map[string]*Auth), providerOffsets: make(map[string]int), modelPoolOffsets: make(map[string]int), } @@ -374,9 +380,21 @@ func (m *Manager) SetConfig(cfg *internalconfig.Config) { cfg = &internalconfig.Config{} } m.runtimeConfig.Store(cfg) + if !cfg.Home.Enabled { + m.clearHomeRuntimeAuths() + } m.rebuildAPIKeyModelAliasFromRuntimeConfig() } +// HomeEnabled reports whether the home control plane integration is enabled in the runtime config. +func (m *Manager) HomeEnabled() bool { + if m == nil { + return false + } + cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config) + return cfg != nil && cfg.Home.Enabled +} + func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) string { if m == nil { return "" @@ -522,6 +540,11 @@ func preserveRequestedModelSuffix(requestedModel, resolved string) string { } func (m *Manager) executionModelCandidates(auth *Auth, routeModel string) []string { + if auth != nil && auth.Attributes != nil { + if homeModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); homeModel != "" { + return []string{homeModel} + } + } requestedModel := rewriteModelForAuth(routeModel, auth) requestedModel = m.applyOAuthModelAlias(auth, requestedModel) if pool := m.resolveOpenAICompatUpstreamModelPool(auth, requestedModel); len(pool) > 0 { @@ -555,6 +578,14 @@ func (m *Manager) selectionModelKeyForAuth(auth *Auth, routeModel string) string } func (m *Manager) stateModelForExecution(auth *Auth, routeModel, upstreamModel string, pooled bool) string { + if auth != nil && auth.Attributes != nil { + if homeModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); homeModel != "" { + if resolved := strings.TrimSpace(upstreamModel); resolved != "" { + return resolved + } + return homeModel + } + } stateModel := executionResultModel(routeModel, upstreamModel, pooled) selectionModel := m.selectionModelForAuth(auth, routeModel) if canonicalModelKey(selectionModel) == canonicalModelKey(upstreamModel) && strings.TrimSpace(selectionModel) != "" { @@ -1293,19 +1324,25 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req } routeModel := req.Model opts = ensureRequestedModelMetadata(opts, routeModel) + homeMode := m.HomeEnabled() + homeAuthCount := 1 tried := make(map[string]struct{}) attempted := make(map[string]struct{}) var lastErr error for { - if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { + if !homeMode && maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { if lastErr != nil { return cliproxyexecutor.Response{}, lastErr } return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"} } - auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, opts, tried) + pickOpts := opts + if homeMode { + pickOpts = withHomeAuthCount(opts, homeAuthCount) + } + auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried) if errPick != nil { - if lastErr != nil { + if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) { return cliproxyexecutor.Response{}, lastErr } return cliproxyexecutor.Response{}, errPick @@ -1361,6 +1398,9 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req return cliproxyexecutor.Response{}, authErr } lastErr = authErr + if homeMode { + homeAuthCount++ + } continue } } @@ -1372,19 +1412,25 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, } routeModel := req.Model opts = ensureRequestedModelMetadata(opts, routeModel) + homeMode := m.HomeEnabled() + homeAuthCount := 1 tried := make(map[string]struct{}) attempted := make(map[string]struct{}) var lastErr error for { - if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { + if !homeMode && maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { if lastErr != nil { return cliproxyexecutor.Response{}, lastErr } return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"} } - auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, opts, tried) + pickOpts := opts + if homeMode { + pickOpts = withHomeAuthCount(opts, homeAuthCount) + } + auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried) if errPick != nil { - if lastErr != nil { + if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) { return cliproxyexecutor.Response{}, lastErr } return cliproxyexecutor.Response{}, errPick @@ -1440,6 +1486,9 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, return cliproxyexecutor.Response{}, authErr } lastErr = authErr + if homeMode { + homeAuthCount++ + } continue } } @@ -1451,19 +1500,25 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string } routeModel := req.Model opts = ensureRequestedModelMetadata(opts, routeModel) + homeMode := m.HomeEnabled() + homeAuthCount := 1 tried := make(map[string]struct{}) attempted := make(map[string]struct{}) var lastErr error for { - if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { + if !homeMode && maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials { if lastErr != nil { return nil, lastErr } return nil, &Error{Code: "auth_not_found", Message: "no auth available"} } - auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, opts, tried) + pickOpts := opts + if homeMode { + pickOpts = withHomeAuthCount(opts, homeAuthCount) + } + auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried) if errPick != nil { - if lastErr != nil { + if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) { return nil, lastErr } return nil, errPick @@ -1493,6 +1548,9 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string return nil, errStream } lastErr = errStream + if homeMode { + homeAuthCount++ + } continue } return streamResult, nil @@ -1520,6 +1578,40 @@ func ensureRequestedModelMetadata(opts cliproxyexecutor.Options, requestedModel return opts } +func withHomeAuthCount(opts cliproxyexecutor.Options, count int) cliproxyexecutor.Options { + if count <= 0 { + count = 1 + } + meta := make(map[string]any, len(opts.Metadata)+1) + for k, v := range opts.Metadata { + meta[k] = v + } + meta[homeAuthCountMetadataKey] = count + opts.Metadata = meta + return opts +} + +func homeAuthCountFromMetadata(meta map[string]any) int { + if len(meta) == 0 { + return 1 + } + switch value := meta[homeAuthCountMetadataKey].(type) { + case int: + if value > 0 { + return value + } + case int64: + if value > 0 { + return int(value) + } + case float64: + if value > 0 { + return int(value) + } + } + return 1 +} + func hasRequestedModelMetadata(meta map[string]any) bool { if len(meta) == 0 { return false @@ -2394,6 +2486,40 @@ func statusCodeFromError(err error) int { return 0 } +func isUnauthorizedError(err error) bool { + if err == nil { + return false + } + if statusCodeFromError(err) == http.StatusUnauthorized { + return true + } + raw := strings.ToLower(err.Error()) + return strings.Contains(raw, "status 401") || strings.Contains(raw, "401 unauthorized") +} + +func hasUnauthorizedAuthFailure(auth *Auth) bool { + if auth == nil || auth.LastError == nil { + return false + } + return auth.LastError.StatusCode() == http.StatusUnauthorized || strings.EqualFold(auth.LastError.Code, "unauthorized") +} + +func refreshErrorFromError(err error) *Error { + if err == nil { + return nil + } + statusCode := statusCodeFromError(err) + if statusCode == 0 && isUnauthorizedError(err) { + statusCode = http.StatusUnauthorized + } + authErr := &Error{Message: err.Error(), HTTPStatus: statusCode} + if statusCode == http.StatusUnauthorized { + authErr.Code = "unauthorized" + authErr.Retryable = false + } + return authErr +} + func retryAfterFromError(err error) *time.Duration { if err == nil { return nil @@ -2633,6 +2759,23 @@ func (m *Manager) GetByID(id string) (*Auth, bool) { return auth.Clone(), true } +// GetExecutionSessionAuthByID retrieves a Home runtime auth scoped to an execution session. +func (m *Manager) GetExecutionSessionAuthByID(sessionID string, authID string) (*Auth, bool) { + sessionID = strings.TrimSpace(sessionID) + authID = strings.TrimSpace(authID) + if m == nil || sessionID == "" || authID == "" { + return nil, false + } + m.mu.RLock() + defer m.mu.RUnlock() + sessionAuths := m.homeRuntimeAuths[sessionID] + auth := sessionAuths[authID] + if auth == nil { + return nil, false + } + return auth.Clone(), true +} + // Executor returns the registered provider executor for a provider key. func (m *Manager) Executor(provider string) (ProviderExecutor, bool) { if m == nil { @@ -2666,12 +2809,17 @@ func (m *Manager) CloseExecutionSession(sessionID string) { return } - m.mu.RLock() + m.mu.Lock() + if sessionID == CloseAllExecutionSessionsID { + m.clearHomeRuntimeAuthsLocked() + } else { + m.clearHomeRuntimeAuthsForSessionLocked(sessionID) + } executors := make([]ProviderExecutor, 0, len(m.executors)) for _, exec := range m.executors { executors = append(executors, exec) } - m.mu.RUnlock() + m.mu.Unlock() for i := range executors { if closer, ok := executors[i].(ExecutionSessionCloser); ok && closer != nil { @@ -2710,6 +2858,11 @@ func (m *Manager) routeAwareSelectionRequired(auth *Auth, routeModel string) boo } func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { + if m.HomeEnabled() { + auth, exec, _, err := m.pickNextViaHome(ctx, model, opts, tried) + return auth, exec, err + } + pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata) disallowFreeAuth := disallowFreeAuthFromMetadata(opts.Metadata) @@ -2779,6 +2932,11 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op } func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { + if m.HomeEnabled() { + auth, exec, _, err := m.pickNextViaHome(ctx, model, opts, tried) + return auth, exec, err + } + if !m.useSchedulerFastPath() { return m.pickNextLegacy(ctx, provider, model, opts, tried) } @@ -2836,6 +2994,10 @@ func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cli } func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { + if m.HomeEnabled() { + return m.pickNextViaHome(ctx, model, opts, tried) + } + pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata) disallowFreeAuth := disallowFreeAuthFromMetadata(opts.Metadata) @@ -2928,6 +3090,10 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m } func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { + if m.HomeEnabled() { + return m.pickNextViaHome(ctx, model, opts, tried) + } + if !m.useSchedulerFastPath() { return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) } @@ -3012,6 +3178,354 @@ func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model s } } +type homeErrorEnvelope struct { + Error *homeErrorDetail `json:"error"` +} + +type homeErrorDetail struct { + Type string `json:"type"` + Message string `json:"message"` + Code string `json:"code,omitempty"` +} + +const ( + homeUpstreamModelAttributeKey = "home_upstream_model" + homeRequestRetryExceededErrorCode = "request_retry_exceeded" +) + +func isHomeRequestRetryExceededError(err error) bool { + var authErr *Error + if !errors.As(err, &authErr) || authErr == nil { + return false + } + return strings.EqualFold(strings.TrimSpace(authErr.Code), homeRequestRetryExceededErrorCode) +} + +func shouldReturnLastErrorOnPickFailure(homeMode bool, lastErr error, errPick error) bool { + if lastErr == nil { + return false + } + if !homeMode { + return true + } + return isHomeRequestRetryExceededError(errPick) +} + +type homeAuthDispatchResponse struct { + Model string `json:"model"` + Provider string `json:"provider"` + AuthIndex string `json:"auth_index"` + UserAPIKey string `json:"user_api_key"` + Auth Auth `json:"auth"` +} + +func setHomeUserAPIKeyOnGinContext(ctx context.Context, apiKey string) { + apiKey = strings.TrimSpace(apiKey) + if apiKey == "" || ctx == nil { + return + } + ginCtx, ok := ctx.Value("gin").(interface{ Set(string, any) }) + if !ok || ginCtx == nil { + return + } + ginCtx.Set("userApiKey", apiKey) +} + +func homeDispatchHeaders(ctx context.Context, headers http.Header) http.Header { + apiKey, ok := homeQueryCredentialFromContext(ctx) + if !ok { + return headers + } + out := headers.Clone() + if out == nil { + out = http.Header{} + } + if out.Get("Authorization") != "" || out.Get("X-Goog-Api-Key") != "" || out.Get("X-Api-Key") != "" { + return out + } + out.Set("X-Goog-Api-Key", apiKey) + return out +} + +func homeQueryCredentialFromContext(ctx context.Context) (string, bool) { + if ctx == nil { + return "", false + } + if queryCtx, ok := ctx.Value("gin").(interface{ Query(string) string }); ok && queryCtx != nil { + if apiKey := strings.TrimSpace(queryCtx.Query("key")); apiKey != "" { + return apiKey, true + } + if apiKey := strings.TrimSpace(queryCtx.Query("auth_token")); apiKey != "" { + return apiKey, true + } + } + ginCtx, ok := ctx.Value("gin").(interface{ Get(string) (any, bool) }) + if !ok || ginCtx == nil { + return "", false + } + rawMetadata, ok := ginCtx.Get("accessMetadata") + if !ok { + return "", false + } + source := accessMetadataSource(rawMetadata) + if source != "query-key" && source != "query-auth-token" { + return "", false + } + rawAPIKey, ok := ginCtx.Get("userApiKey") + if !ok { + return "", false + } + apiKey := contextStringValue(rawAPIKey) + if apiKey == "" { + return "", false + } + return apiKey, true +} + +func accessMetadataSource(raw any) string { + switch v := raw.(type) { + case map[string]string: + return strings.TrimSpace(v["source"]) + case map[string]any: + return contextStringValue(v["source"]) + default: + return "" + } +} + +func contextStringValue(raw any) string { + switch v := raw.(type) { + case string: + return strings.TrimSpace(v) + case []byte: + return strings.TrimSpace(string(v)) + default: + return "" + } +} + +func homeExecutionSessionIDFromMetadata(meta map[string]any) string { + if len(meta) == 0 { + return "" + } + raw, ok := meta[cliproxyexecutor.ExecutionSessionMetadataKey] + if !ok || raw == nil { + return "" + } + switch value := raw.(type) { + case string: + return strings.TrimSpace(value) + case []byte: + return strings.TrimSpace(string(value)) + default: + return "" + } +} + +func (m *Manager) clearHomeRuntimeAuths() { + if m == nil { + return + } + m.mu.Lock() + m.clearHomeRuntimeAuthsLocked() + m.mu.Unlock() +} + +func (m *Manager) clearHomeRuntimeAuthsLocked() { + if m == nil { + return + } + m.homeRuntimeAuths = make(map[string]map[string]*Auth) +} + +func (m *Manager) clearHomeRuntimeAuthsForSessionLocked(sessionID string) { + sessionID = strings.TrimSpace(sessionID) + if m == nil || sessionID == "" { + return + } + delete(m.homeRuntimeAuths, sessionID) +} + +func (m *Manager) rememberHomeRuntimeAuth(sessionID string, auth *Auth) { + sessionID = strings.TrimSpace(sessionID) + authID := "" + if auth != nil { + authID = strings.TrimSpace(auth.ID) + } + if m == nil || auth == nil || sessionID == "" || authID == "" || !authWebsocketsEnabled(auth) { + return + } + m.mu.Lock() + if m.homeRuntimeAuths == nil { + m.homeRuntimeAuths = make(map[string]map[string]*Auth) + } + sessionAuths := m.homeRuntimeAuths[sessionID] + if sessionAuths == nil { + sessionAuths = make(map[string]*Auth) + m.homeRuntimeAuths[sessionID] = sessionAuths + } + sessionAuths[authID] = auth.Clone() + m.mu.Unlock() +} + +func (m *Manager) homeRuntimeAuthByID(sessionID string, authID string) (*Auth, ProviderExecutor, string, bool) { + sessionID = strings.TrimSpace(sessionID) + authID = strings.TrimSpace(authID) + if m == nil || sessionID == "" || authID == "" { + return nil, nil, "", false + } + m.mu.RLock() + sessionAuths := m.homeRuntimeAuths[sessionID] + auth := sessionAuths[authID] + m.mu.RUnlock() + if auth == nil || !authWebsocketsEnabled(auth) { + return nil, nil, "", false + } + providerKey := strings.ToLower(strings.TrimSpace(auth.Provider)) + if providerKey == "" { + return nil, nil, "", false + } + executor, ok := m.Executor(providerKey) + if !ok && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["base_url"]) != "" { + executor, ok = m.Executor("openai-compatibility") + if ok { + providerKey = "openai-compatibility" + } + } + if !ok { + return nil, nil, "", false + } + return auth.Clone(), executor, providerKey, true +} + +func (m *Manager) pickNextViaHome(ctx context.Context, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { + if m == nil { + return nil, nil, "", &Error{Code: "auth_not_found", Message: "no auth available"} + } + if ctx == nil { + ctx = context.Background() + } + executionSessionID := homeExecutionSessionIDFromMetadata(opts.Metadata) + count := homeAuthCountFromMetadata(opts.Metadata) + if cliproxyexecutor.DownstreamWebsocket(ctx) && executionSessionID != "" && count <= 1 { + if pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata); pinnedAuthID != "" { + _, alreadyTried := tried[pinnedAuthID] + if !alreadyTried { + if auth, executor, providerKey, ok := m.homeRuntimeAuthByID(executionSessionID, pinnedAuthID); ok { + return auth, executor, providerKey, nil + } + } + } + } + + client := home.Current() + if client == nil || !client.HeartbeatOK() { + return nil, nil, "", &Error{Code: "home_unavailable", Message: "home control center unavailable", HTTPStatus: http.StatusServiceUnavailable} + } + + requestedModel := requestedModelFromMetadata(opts.Metadata, model) + sessionID := ExtractSessionID(opts.Headers, opts.OriginalRequest, opts.Metadata) + dispatchHeaders := homeDispatchHeaders(ctx, opts.Headers) + + raw, err := client.RPopAuth(ctx, requestedModel, sessionID, dispatchHeaders, count) + if err != nil { + return nil, nil, "", &Error{Code: "auth_not_found", Message: err.Error(), HTTPStatus: http.StatusServiceUnavailable} + } + + var env homeErrorEnvelope + if errUnmarshal := json.Unmarshal(raw, &env); errUnmarshal == nil && env.Error != nil { + code := strings.TrimSpace(env.Error.Type) + if code == "" { + code = strings.TrimSpace(env.Error.Code) + } + msg := strings.TrimSpace(env.Error.Message) + if msg == "" { + msg = "home returned error" + } + status := http.StatusBadGateway + switch strings.ToLower(code) { + case "model_not_found": + status = http.StatusNotFound + case "authentication_error", "unauthorized": + status = http.StatusUnauthorized + } + return nil, nil, "", &Error{Code: code, Message: msg, HTTPStatus: status} + } + + var dispatch homeAuthDispatchResponse + if errUnmarshal := json.Unmarshal(raw, &dispatch); errUnmarshal != nil { + return nil, nil, "", &Error{Code: "invalid_auth", Message: "home returned invalid auth payload", HTTPStatus: http.StatusBadGateway} + } + setHomeUserAPIKeyOnGinContext(ctx, dispatch.UserAPIKey) + auth := dispatch.Auth + if strings.TrimSpace(auth.ID) == "" { + // Backward compatibility: older home instances returned the auth directly. + if errUnmarshal := json.Unmarshal(raw, &auth); errUnmarshal != nil { + return nil, nil, "", &Error{Code: "invalid_auth", Message: "home returned invalid auth payload", HTTPStatus: http.StatusBadGateway} + } + } + if upstreamModel := strings.TrimSpace(dispatch.Model); upstreamModel != "" { + if auth.Attributes == nil { + auth.Attributes = make(map[string]string, 1) + } + auth.Attributes[homeUpstreamModelAttributeKey] = upstreamModel + } + if strings.TrimSpace(auth.ID) == "" { + return nil, nil, "", &Error{Code: "invalid_auth", Message: "home returned auth without id", HTTPStatus: http.StatusBadGateway} + } + providerKey := strings.ToLower(strings.TrimSpace(auth.Provider)) + if providerKey == "" { + return nil, nil, "", &Error{Code: "invalid_auth", Message: "home returned auth without provider", HTTPStatus: http.StatusBadGateway} + } + + homeAuthIndex := strings.TrimSpace(dispatch.AuthIndex) + if homeAuthIndex != "" { + auth.Index = homeAuthIndex + auth.indexAssigned = true + } else { + auth.EnsureIndex() + } + + executor, ok := m.Executor(providerKey) + if !ok && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["base_url"]) != "" { + executor, ok = m.Executor("openai-compatibility") + if ok { + providerKey = "openai-compatibility" + } + } + if !ok { + return nil, nil, "", &Error{Code: "executor_not_found", Message: "executor not registered", HTTPStatus: http.StatusBadGateway} + } + + authCopy := auth.Clone() + if cliproxyexecutor.DownstreamWebsocket(ctx) && executionSessionID != "" && authWebsocketsEnabled(authCopy) { + m.rememberHomeRuntimeAuth(executionSessionID, authCopy) + } + return authCopy, executor, providerKey, nil +} + +func requestedModelFromMetadata(metadata map[string]any, fallback string) string { + if metadata != nil { + if v, ok := metadata[cliproxyexecutor.RequestedModelMetadataKey]; ok { + switch typed := v.(type) { + case string: + if trimmed := strings.TrimSpace(typed); trimmed != "" { + return trimmed + } + case []byte: + if trimmed := strings.TrimSpace(string(typed)); trimmed != "" { + return trimmed + } + } + } + } + fallback = strings.TrimSpace(fallback) + if fallback == "" { + return "unknown" + } + return fallback +} + func (m *Manager) findAllAntigravityCreditsCandidateAuths(routeModel string, opts cliproxyexecutor.Options) []creditsCandidateEntry { if m == nil { return nil @@ -3271,7 +3785,10 @@ func (m *Manager) queueRefreshReschedule(authID string) { } func (m *Manager) shouldRefresh(a *Auth, now time.Time) bool { - if a == nil || a.Disabled { + if a == nil { + return false + } + if hasUnauthorizedAuthFailure(a) { return false } if !a.NextRefreshAfter.IsZero() && now.Before(a.NextRefreshAfter) { @@ -3478,7 +3995,7 @@ func lookupMetadataTime(meta map[string]any, keys ...string) (time.Time, bool) { func (m *Manager) markRefreshPending(id string, now time.Time) bool { m.mu.Lock() auth, ok := m.auths[id] - if !ok || auth == nil || auth.Disabled { + if !ok || auth == nil { m.mu.Unlock() return false } @@ -3518,11 +4035,19 @@ func (m *Manager) refreshAuth(ctx context.Context, id string) { log.Debugf("refreshed %s, %s, %v", auth.Provider, auth.ID, err) now := time.Now() if err != nil { + unauthorized := isUnauthorizedError(err) shouldReschedule := false m.mu.Lock() if current := m.auths[id]; current != nil { - current.NextRefreshAfter = now.Add(refreshFailureBackoff) - current.LastError = &Error{Message: err.Error()} + current.LastError = refreshErrorFromError(err) + if unauthorized { + current.NextRefreshAfter = time.Time{} + current.Unavailable = true + current.Status = StatusError + current.StatusMessage = "unauthorized" + } else { + current.NextRefreshAfter = now.Add(refreshFailureBackoff) + } m.auths[id] = current shouldReschedule = true if m.scheduler != nil { diff --git a/sdk/cliproxy/auth/conductor_credits_candidates_test.go b/sdk/cliproxy/auth/conductor_credits_candidates_test.go index e66798acf6..f9487b0b9b 100644 --- a/sdk/cliproxy/auth/conductor_credits_candidates_test.go +++ b/sdk/cliproxy/auth/conductor_credits_candidates_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) func TestFindAllAntigravityCreditsCandidateAuths_PrefersKnownCreditsThenUnknown(t *testing.T) { diff --git a/sdk/cliproxy/auth/conductor_executor_replace_test.go b/sdk/cliproxy/auth/conductor_executor_replace_test.go index 2ee91a87c1..99ecf466a6 100644 --- a/sdk/cliproxy/auth/conductor_executor_replace_test.go +++ b/sdk/cliproxy/auth/conductor_executor_replace_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type replaceAwareExecutor struct { diff --git a/sdk/cliproxy/auth/conductor_oauth_alias_suspension_test.go b/sdk/cliproxy/auth/conductor_oauth_alias_suspension_test.go index b4b72204c8..ba8371dc61 100644 --- a/sdk/cliproxy/auth/conductor_oauth_alias_suspension_test.go +++ b/sdk/cliproxy/auth/conductor_oauth_alias_suspension_test.go @@ -7,10 +7,10 @@ import ( "testing" "time" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" ) type aliasRoutingExecutor struct { diff --git a/sdk/cliproxy/auth/conductor_overrides_test.go b/sdk/cliproxy/auth/conductor_overrides_test.go index f74621bec7..017602e362 100644 --- a/sdk/cliproxy/auth/conductor_overrides_test.go +++ b/sdk/cliproxy/auth/conductor_overrides_test.go @@ -8,9 +8,9 @@ import ( "time" "github.com/google/uuid" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) const requestScopedNotFoundMessage = "Item with id 'rs_0b5f3eb6f51f175c0169ca74e4a85881998539920821603a74' not found. Items are not persisted when `store` is set to false. Try again with `store` set to true, or remove this item from your input." diff --git a/sdk/cliproxy/auth/conductor_scheduler_refresh_test.go b/sdk/cliproxy/auth/conductor_scheduler_refresh_test.go index 5c6eff7805..8ccae636a5 100644 --- a/sdk/cliproxy/auth/conductor_scheduler_refresh_test.go +++ b/sdk/cliproxy/auth/conductor_scheduler_refresh_test.go @@ -5,9 +5,10 @@ import ( "errors" "net/http" "testing" + "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type schedulerProviderTestExecutor struct { @@ -36,6 +37,59 @@ func (e schedulerProviderTestExecutor) HttpRequest(ctx context.Context, auth *Au return nil, nil } +type unauthorizedRefreshTestExecutor struct { + schedulerProviderTestExecutor +} + +func (e unauthorizedRefreshTestExecutor) Refresh(ctx context.Context, auth *Auth) (*Auth, error) { + return nil, errors.New("token refresh failed with status 401: invalid_grant") +} + +func TestManager_RefreshAuthUnauthorizedFailureStopsAutoRefreshRetry(t *testing.T) { + ctx := context.Background() + manager := NewManager(nil, &RoundRobinSelector{}, nil) + manager.RegisterExecutor(unauthorizedRefreshTestExecutor{ + schedulerProviderTestExecutor: schedulerProviderTestExecutor{provider: "codex"}, + }) + + auth := &Auth{ + ID: "unauthorized-refresh", + Provider: "codex", + Metadata: map[string]any{ + "email": "x@example.com", + }, + } + if _, errRegister := manager.Register(ctx, auth); errRegister != nil { + t.Fatalf("register auth: %v", errRegister) + } + + manager.refreshAuth(ctx, auth.ID) + + updated, ok := manager.GetByID(auth.ID) + if !ok { + t.Fatalf("expected auth %q after refresh", auth.ID) + } + if updated.LastError == nil { + t.Fatal("expected unauthorized refresh failure to be recorded") + } + if got := updated.LastError.StatusCode(); got != http.StatusUnauthorized { + t.Fatalf("LastError.StatusCode() = %d, want %d", got, http.StatusUnauthorized) + } + if updated.LastError.Code != "unauthorized" { + t.Fatalf("LastError.Code = %q, want unauthorized", updated.LastError.Code) + } + if !updated.NextRefreshAfter.IsZero() { + t.Fatalf("NextRefreshAfter = %s, want zero for unauthorized refresh failure", updated.NextRefreshAfter) + } + now := time.Now() + if manager.shouldRefresh(updated, now) { + t.Fatal("expected unauthorized auth to stop refresh attempts") + } + if _, shouldSchedule := nextRefreshCheckAt(now, updated, time.Second); shouldSchedule { + t.Fatal("expected unauthorized auth to be removed from the auto-refresh schedule") + } +} + func TestManager_RefreshSchedulerEntry_RebuildsSupportedModelSetAfterModelRegistration(t *testing.T) { ctx := context.Background() diff --git a/sdk/cliproxy/auth/home_dispatch_headers_test.go b/sdk/cliproxy/auth/home_dispatch_headers_test.go new file mode 100644 index 0000000000..b4aef310d8 --- /dev/null +++ b/sdk/cliproxy/auth/home_dispatch_headers_test.go @@ -0,0 +1,87 @@ +package auth + +import ( + "context" + "net/http" + "testing" +) + +type homeDispatchTestGinContext struct { + values map[string]any + query map[string]string +} + +func (c homeDispatchTestGinContext) Get(key string) (any, bool) { + v, ok := c.values[key] + return v, ok +} + +func (c homeDispatchTestGinContext) Query(key string) string { + if c.query == nil { + return "" + } + return c.query[key] +} + +func TestHomeDispatchHeadersAddsQueryKeyCredential(t *testing.T) { + ginCtx := homeDispatchTestGinContext{query: map[string]string{"key": "12345"}} + ctx := context.WithValue(context.Background(), "gin", ginCtx) + headers := http.Header{"User-Agent": {"client"}} + + got := homeDispatchHeaders(ctx, headers) + + if got.Get("X-Goog-Api-Key") != "12345" { + t.Fatalf("X-Goog-Api-Key = %q, want %q", got.Get("X-Goog-Api-Key"), "12345") + } + if headers.Get("X-Goog-Api-Key") != "" { + t.Fatalf("original headers were mutated: %v", headers) + } +} + +func TestHomeDispatchHeadersAddsQueryCredentialFromAccessMetadata(t *testing.T) { + ginCtx := homeDispatchTestGinContext{values: map[string]any{ + "accessMetadata": map[string]string{"source": "query-key"}, + "userApiKey": "12345", + }} + ctx := context.WithValue(context.Background(), "gin", ginCtx) + headers := http.Header{"User-Agent": {"client"}} + + got := homeDispatchHeaders(ctx, headers) + + if got.Get("X-Goog-Api-Key") != "12345" { + t.Fatalf("X-Goog-Api-Key = %q, want %q", got.Get("X-Goog-Api-Key"), "12345") + } + if headers.Get("X-Goog-Api-Key") != "" { + t.Fatalf("original headers were mutated: %v", headers) + } +} + +func TestHomeDispatchHeadersKeepsExistingCredentialHeader(t *testing.T) { + ginCtx := homeDispatchTestGinContext{query: map[string]string{"key": "query-key"}} + ctx := context.WithValue(context.Background(), "gin", ginCtx) + headers := http.Header{"X-Goog-Api-Key": {"header-key"}} + + got := homeDispatchHeaders(ctx, headers) + + if got.Get("X-Goog-Api-Key") != "header-key" { + t.Fatalf("X-Goog-Api-Key = %q, want %q", got.Get("X-Goog-Api-Key"), "header-key") + } +} + +func TestHomeDispatchHeadersIgnoresHeaderCredentialSource(t *testing.T) { + ginCtx := homeDispatchTestGinContext{values: map[string]any{ + "accessMetadata": map[string]string{"source": "authorization"}, + "userApiKey": "12345", + }} + ctx := context.WithValue(context.Background(), "gin", ginCtx) + headers := http.Header{"Authorization": {"Bearer 12345"}} + + got := homeDispatchHeaders(ctx, headers) + + if got.Get("X-Goog-Api-Key") != "" { + t.Fatalf("X-Goog-Api-Key = %q, want empty", got.Get("X-Goog-Api-Key")) + } + if got.Get("Authorization") != "Bearer 12345" { + t.Fatalf("Authorization = %q, want %q", got.Get("Authorization"), "Bearer 12345") + } +} diff --git a/sdk/cliproxy/auth/home_websocket_reuse_test.go b/sdk/cliproxy/auth/home_websocket_reuse_test.go new file mode 100644 index 0000000000..28d4800429 --- /dev/null +++ b/sdk/cliproxy/auth/home_websocket_reuse_test.go @@ -0,0 +1,270 @@ +package auth + +import ( + "context" + "errors" + "net/http" + "testing" + + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" +) + +func TestPickNextViaHomeReusesPinnedWebsocketAuthWithoutHomeDispatch(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.RegisterExecutor(schedulerTestExecutor{}) + + auth := &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + Attributes: map[string]string{ + "websockets": "true", + homeUpstreamModelAttributeKey: "upstream-model", + }, + Metadata: map[string]any{"email": "home@example.com"}, + } + auth.EnsureIndex() + manager.rememberHomeRuntimeAuth("session-1", auth) + cachedAuth, ok := manager.GetExecutionSessionAuthByID("session-1", "home-auth-1") + if !ok || cachedAuth == nil || !authWebsocketsEnabled(cachedAuth) { + t.Fatalf("GetExecutionSessionAuthByID() did not expose remembered websocket home auth: auth=%#v ok=%v", cachedAuth, ok) + } + + ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) + opts := cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-1", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + Headers: http.Header{"Authorization": {"Bearer client-key"}}, + } + + got, executor, provider, errPick := manager.pickNextViaHome(ctx, "gpt-5.4", opts, nil) + if errPick != nil { + t.Fatalf("pickNextViaHome() error = %v", errPick) + } + if got == nil || got.ID != "home-auth-1" { + t.Fatalf("pickNextViaHome() auth = %#v, want home-auth-1", got) + } + if executor == nil { + t.Fatal("pickNextViaHome() executor is nil") + } + if provider != "test" { + t.Fatalf("pickNextViaHome() provider = %q, want test", provider) + } +} + +func TestPickNextViaHomeKeepsSameAuthIDPayloadSessionScoped(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.RegisterExecutor(schedulerTestExecutor{}) + + manager.rememberHomeRuntimeAuth("session-1", &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + Attributes: map[string]string{ + "websockets": "true", + homeUpstreamModelAttributeKey: "upstream-model-a", + }, + }) + manager.rememberHomeRuntimeAuth("session-2", &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + Attributes: map[string]string{ + "websockets": "true", + homeUpstreamModelAttributeKey: "upstream-model-b", + }, + }) + + ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) + optsSession1 := cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-1", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + } + optsSession2 := cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-2", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + } + + gotSession1, _, _, errSession1 := manager.pickNextViaHome(ctx, "gpt-5.4", optsSession1, nil) + if errSession1 != nil { + t.Fatalf("pickNextViaHome(session-1) error = %v", errSession1) + } + if got := gotSession1.Attributes[homeUpstreamModelAttributeKey]; got != "upstream-model-a" { + t.Fatalf("pickNextViaHome(session-1) upstream model = %q, want upstream-model-a", got) + } + + gotSession2, _, _, errSession2 := manager.pickNextViaHome(ctx, "gpt-5.4", optsSession2, nil) + if errSession2 != nil { + t.Fatalf("pickNextViaHome(session-2) error = %v", errSession2) + } + if got := gotSession2.Attributes[homeUpstreamModelAttributeKey]; got != "upstream-model-b" { + t.Fatalf("pickNextViaHome(session-2) upstream model = %q, want upstream-model-b", got) + } +} + +func TestPickNextViaHomeDoesNotReuseTriedPinnedWebsocketAuth(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.RegisterExecutor(schedulerTestExecutor{}) + + auth := &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + Attributes: map[string]string{ + "websockets": "true", + }, + } + manager.rememberHomeRuntimeAuth("session-1", auth) + + ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) + opts := cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-1", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + } + tried := map[string]struct{}{"home-auth-1": {}} + + got, executor, provider, errPick := manager.pickNextViaHome(ctx, "gpt-5.4", opts, tried) + if errPick == nil { + t.Fatal("pickNextViaHome() error is nil, want home unavailable error") + } + var authErr *Error + if !errors.As(errPick, &authErr) || authErr.Code != "home_unavailable" { + t.Fatalf("pickNextViaHome() error = %v, want home_unavailable", errPick) + } + if got != nil || executor != nil || provider != "" { + t.Fatalf("pickNextViaHome() reused tried auth: auth=%#v executor=%#v provider=%q", got, executor, provider) + } +} + +func TestPickNextViaHomeDoesNotReusePinnedWebsocketAuthAfterFirstHomeAttempt(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.RegisterExecutor(schedulerTestExecutor{}) + + auth := &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + Attributes: map[string]string{ + "websockets": "true", + }, + } + manager.rememberHomeRuntimeAuth("session-1", auth) + + ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) + opts := withHomeAuthCount(cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-1", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + }, 2) + + got, executor, provider, errPick := manager.pickNextViaHome(ctx, "gpt-5.4", opts, nil) + if errPick == nil { + t.Fatal("pickNextViaHome() error is nil, want home unavailable error") + } + var authErr *Error + if !errors.As(errPick, &authErr) || authErr.Code != "home_unavailable" { + t.Fatalf("pickNextViaHome() error = %v, want home_unavailable", errPick) + } + if got != nil || executor != nil || provider != "" { + t.Fatalf("pickNextViaHome() reused auth after first home attempt: auth=%#v executor=%#v provider=%q", got, executor, provider) + } +} + +func TestPickNextViaHomeDoesNotReusePinnedNonWebsocketAuth(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.RegisterExecutor(schedulerTestExecutor{}) + + manager.mu.Lock() + manager.homeRuntimeAuths["session-1"] = map[string]*Auth{ + "home-auth-1": &Auth{ + ID: "home-auth-1", + Provider: "test", + Status: StatusActive, + }, + } + manager.mu.Unlock() + + ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) + opts := cliproxyexecutor.Options{ + Metadata: map[string]any{ + cliproxyexecutor.ExecutionSessionMetadataKey: "session-1", + cliproxyexecutor.PinnedAuthMetadataKey: "home-auth-1", + }, + Headers: http.Header{"Authorization": {"Bearer client-key"}}, + } + + got, executor, provider, errPick := manager.pickNextViaHome(ctx, "gpt-5.4", opts, nil) + if errPick == nil { + t.Fatal("pickNextViaHome() error is nil, want home unavailable error") + } + var authErr *Error + if !errors.As(errPick, &authErr) || authErr.Code != "home_unavailable" { + t.Fatalf("pickNextViaHome() error = %v, want home_unavailable", errPick) + } + if got != nil || executor != nil || provider != "" { + t.Fatalf("pickNextViaHome() reused non-websocket auth: auth=%#v executor=%#v provider=%q", got, executor, provider) + } +} + +func TestHomeRuntimeAuthsClearWhenHomeDisabled(t *testing.T) { + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) + manager.rememberHomeRuntimeAuth("session-1", &Auth{ + ID: "home-auth-1", + Provider: "test", + Attributes: map[string]string{ + "websockets": "true", + }, + }) + + if _, ok := manager.GetExecutionSessionAuthByID("session-1", "home-auth-1"); !ok { + t.Fatal("expected remembered home auth before disabling home") + } + + manager.SetConfig(&internalconfig.Config{}) + if _, ok := manager.GetExecutionSessionAuthByID("session-1", "home-auth-1"); ok { + t.Fatal("remembered home auth was not cleared when home was disabled") + } +} + +func TestCloseExecutionSessionClearsHomeRuntimeAuthForSession(t *testing.T) { + manager := NewManager(nil, nil, nil) + auth := &Auth{ + ID: "home-auth-1", + Provider: "test", + Attributes: map[string]string{ + "websockets": "true", + }, + } + + manager.rememberHomeRuntimeAuth("session-1", auth) + manager.rememberHomeRuntimeAuth("session-2", auth) + + manager.CloseExecutionSession("session-1") + if _, ok := manager.GetExecutionSessionAuthByID("session-1", "home-auth-1"); ok { + t.Fatal("home auth for closed session was not cleared") + } + if _, ok := manager.GetExecutionSessionAuthByID("session-2", "home-auth-1"); !ok { + t.Fatal("home auth for another session was cleared") + } + + manager.CloseExecutionSession("session-2") + if _, ok := manager.GetExecutionSessionAuthByID("session-2", "home-auth-1"); ok { + t.Fatal("home auth was not cleared when its last session closed") + } +} diff --git a/sdk/cliproxy/auth/oauth_model_alias.go b/sdk/cliproxy/auth/oauth_model_alias.go index 46c82a9c53..7e6740d6bb 100644 --- a/sdk/cliproxy/auth/oauth_model_alias.go +++ b/sdk/cliproxy/auth/oauth_model_alias.go @@ -3,8 +3,8 @@ package auth import ( "strings" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" ) type modelAliasEntry interface { diff --git a/sdk/cliproxy/auth/oauth_model_alias_test.go b/sdk/cliproxy/auth/oauth_model_alias_test.go index 73ddbe675d..521e158e55 100644 --- a/sdk/cliproxy/auth/oauth_model_alias_test.go +++ b/sdk/cliproxy/auth/oauth_model_alias_test.go @@ -3,7 +3,7 @@ package auth import ( "testing" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { diff --git a/sdk/cliproxy/auth/openai_compat_pool_test.go b/sdk/cliproxy/auth/openai_compat_pool_test.go index ff2c4dd040..f052c486f4 100644 --- a/sdk/cliproxy/auth/openai_compat_pool_test.go +++ b/sdk/cliproxy/auth/openai_compat_pool_test.go @@ -7,9 +7,9 @@ import ( "sync" "testing" - internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type openAICompatPoolExecutor struct { diff --git a/sdk/cliproxy/auth/scheduler.go b/sdk/cliproxy/auth/scheduler.go index b5a3928286..9947f59c63 100644 --- a/sdk/cliproxy/auth/scheduler.go +++ b/sdk/cliproxy/auth/scheduler.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) // schedulerStrategy identifies which built-in routing semantics the scheduler should apply. diff --git a/sdk/cliproxy/auth/scheduler_benchmark_test.go b/sdk/cliproxy/auth/scheduler_benchmark_test.go index 050a7cbd1e..4d160276f2 100644 --- a/sdk/cliproxy/auth/scheduler_benchmark_test.go +++ b/sdk/cliproxy/auth/scheduler_benchmark_test.go @@ -6,8 +6,8 @@ import ( "net/http" "testing" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type schedulerBenchmarkExecutor struct { diff --git a/sdk/cliproxy/auth/scheduler_test.go b/sdk/cliproxy/auth/scheduler_test.go index 8caaa4735b..864fa938e9 100644 --- a/sdk/cliproxy/auth/scheduler_test.go +++ b/sdk/cliproxy/auth/scheduler_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) type schedulerTestExecutor struct{} diff --git a/sdk/cliproxy/auth/selector.go b/sdk/cliproxy/auth/selector.go index f0fe237c83..5e23c46f55 100644 --- a/sdk/cliproxy/auth/selector.go +++ b/sdk/cliproxy/auth/selector.go @@ -18,9 +18,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" - "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) // RoundRobinSelector provides a simple provider scoped round-robin selection strategy. diff --git a/sdk/cliproxy/auth/selector_test.go b/sdk/cliproxy/auth/selector_test.go index f6682c6fce..99231bdf78 100644 --- a/sdk/cliproxy/auth/selector_test.go +++ b/sdk/cliproxy/auth/selector_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) func TestFillFirstSelectorPick_Deterministic(t *testing.T) { diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 76f4c396c8..882c25eabd 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -7,12 +7,13 @@ import ( "encoding/json" "net/http" "net/url" + "path/filepath" "strconv" "strings" "sync" "time" - baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth" + baseauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth" ) // PostAuthHook defines a function that is called after an Auth record is created @@ -256,45 +257,65 @@ func (a *Auth) indexSeed() string { return "" } - if fileName := strings.TrimSpace(a.FileName); fileName != "" { - return "file:" + fileName - } - - providerKey := strings.ToLower(strings.TrimSpace(a.Provider)) + provider := strings.ToLower(strings.TrimSpace(a.Provider)) compatName := "" baseURL := "" apiKey := "" - source := "" + filePath := "" if a.Attributes != nil { - if value := strings.TrimSpace(a.Attributes["provider_key"]); value != "" { - providerKey = strings.ToLower(value) - } - compatName = strings.ToLower(strings.TrimSpace(a.Attributes["compat_name"])) + compatName = strings.TrimSpace(a.Attributes["compat_name"]) baseURL = strings.TrimSpace(a.Attributes["base_url"]) apiKey = strings.TrimSpace(a.Attributes["api_key"]) - source = strings.TrimSpace(a.Attributes["source"]) + filePath = strings.TrimSpace(a.Attributes["path"]) + if filePath == "" { + filePath = strings.TrimSpace(a.Attributes["source"]) + } } - proxyURL := strings.TrimSpace(a.ProxyURL) - hasCredentialIdentity := compatName != "" || baseURL != "" || proxyURL != "" || apiKey != "" || source != "" - if providerKey != "" && hasCredentialIdentity { - parts := []string{"provider=" + providerKey} - if compatName != "" { - parts = append(parts, "compat="+compatName) + if filePath == "" { + filePath = strings.TrimSpace(a.FileName) + } + if filePath == "" { + filePath = strings.TrimSpace(a.ID) + } + + if filePath != "" && strings.HasSuffix(strings.ToLower(filePath), ".json") { + abs, errAbs := filepath.Abs(filePath) + if errAbs == nil && strings.TrimSpace(abs) != "" { + filePath = abs } - if baseURL != "" { - parts = append(parts, "base="+baseURL) + filePath = filepath.Clean(filePath) + + authType := "" + if a.Metadata != nil { + if rawType, ok := a.Metadata["type"].(string); ok { + authType = strings.TrimSpace(rawType) + } } - if proxyURL != "" { - parts = append(parts, "proxy="+proxyURL) + if authType == "" { + authType = strings.TrimSpace(provider) } - if apiKey != "" { - parts = append(parts, "api_key="+apiKey) + authType = strings.ToLower(strings.TrimSpace(authType)) + if authType != "" { + return authType + ":" + filePath } - if source != "" { - parts = append(parts, "source="+source) + } + + apiPrefix := "" + if apiKey != "" { + switch { + case compatName != "" || strings.EqualFold(provider, "openai-compatibility"): + apiPrefix = "openai-compatibility" + case strings.EqualFold(provider, "gemini"): + apiPrefix = "gemini-api-key" + case strings.EqualFold(provider, "codex"): + apiPrefix = "codex-api-key" + case strings.EqualFold(provider, "claude"): + apiPrefix = "claude-api-key" } - return "config:" + strings.Join(parts, "\x00") + } + if apiPrefix != "" { + return apiPrefix + ":" + strings.TrimSpace(baseURL) + "+" + strings.TrimSpace(apiKey) } if id := strings.TrimSpace(a.ID); id != "" { @@ -355,19 +376,28 @@ func (a *Auth) ProxyInfo() string { return "via proxy" } -// DisableCoolingOverride returns the auth-file scoped disable_cooling override when present. +// DisableCoolingOverride returns the auth scoped disable_cooling override when present. // The value is read from metadata key "disable_cooling" (or legacy "disable-cooling"). +// +// NOTE: This override is intentionally "true-only". When the metadata value is false, it is treated +// as "not set" so the global disable-cooling flag can still take effect. func (a *Auth) DisableCoolingOverride() (bool, bool) { if a == nil || a.Metadata == nil { return false, false } if val, ok := a.Metadata["disable_cooling"]; ok { if parsed, okParse := parseBoolAny(val); okParse { + if !parsed { + return false, false + } return parsed, true } } if val, ok := a.Metadata["disable-cooling"]; ok { if parsed, okParse := parseBoolAny(val); okParse { + if !parsed { + return false, false + } return parsed, true } } diff --git a/sdk/cliproxy/auth/types_test.go b/sdk/cliproxy/auth/types_test.go index 06836da1f2..f579bfda2e 100644 --- a/sdk/cliproxy/auth/types_test.go +++ b/sdk/cliproxy/auth/types_test.go @@ -1,6 +1,8 @@ package auth import ( + "os" + "path/filepath" "strings" "testing" "time" @@ -96,8 +98,40 @@ func TestEnsureIndexUsesCredentialIdentity(t *testing.T) { if geminiIndex == altBaseIndex { t.Fatalf("same provider/key with different base_url produced duplicate auth_index %q", geminiIndex) } - if geminiIndex == duplicateIndex { - t.Fatalf("duplicate config entries should be separated by source-derived seed, got %q", geminiIndex) + if geminiIndex != duplicateIndex { + t.Fatalf("same provider/key with different source should share auth_index, got %q vs %q", geminiIndex, duplicateIndex) + } +} + +func TestEnsureIndexUsesOAuthTypeAndAbsolutePath(t *testing.T) { + t.Parallel() + + wd, errWd := os.Getwd() + if errWd != nil { + t.Fatalf("os.Getwd returned error: %v", errWd) + } + + relPath := "test-oauth.json" + absPath := filepath.Join(wd, relPath) + expectedSeed := "gemini:" + filepath.Clean(absPath) + expectedIndex := stableAuthIndex(expectedSeed) + + a := &Auth{ + Provider: "gemini-cli", + Attributes: map[string]string{ + "path": relPath, + }, + Metadata: map[string]any{ + "type": "gemini", + }, + } + + got := a.EnsureIndex() + if got == "" { + t.Fatal("auth index should not be empty") + } + if got != expectedIndex { + t.Fatalf("auth index = %q, want %q", got, expectedIndex) } } diff --git a/sdk/cliproxy/builder.go b/sdk/cliproxy/builder.go index b8cf991c14..c7e187ee6b 100644 --- a/sdk/cliproxy/builder.go +++ b/sdk/cliproxy/builder.go @@ -8,12 +8,12 @@ import ( "strings" "time" - configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) // Builder constructs a Service instance with customizable providers. @@ -214,7 +214,7 @@ func (b *Builder) Build() (*Service, error) { if b.cfg != nil { strategy = strings.ToLower(strings.TrimSpace(b.cfg.Routing.Strategy)) // Support both legacy ClaudeCodeSessionAffinity and new universal SessionAffinity - sessionAffinity = b.cfg.Routing.ClaudeCodeSessionAffinity || b.cfg.Routing.SessionAffinity + sessionAffinity = b.cfg.Routing.SessionAffinity if ttlStr := strings.TrimSpace(b.cfg.Routing.SessionAffinityTTL); ttlStr != "" { if parsed, err := time.ParseDuration(ttlStr); err == nil && parsed > 0 { sessionAffinityTTL = parsed diff --git a/sdk/cliproxy/executor/types.go b/sdk/cliproxy/executor/types.go index c8bb917d03..fd1da2e537 100644 --- a/sdk/cliproxy/executor/types.go +++ b/sdk/cliproxy/executor/types.go @@ -4,7 +4,7 @@ import ( "net/http" "net/url" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) // RequestedModelMetadataKey stores the client-requested model name in Options.Metadata. diff --git a/sdk/cliproxy/model_registry.go b/sdk/cliproxy/model_registry.go index 01cea5b715..9cb928c98a 100644 --- a/sdk/cliproxy/model_registry.go +++ b/sdk/cliproxy/model_registry.go @@ -1,6 +1,6 @@ package cliproxy -import "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" +import "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" // ModelInfo re-exports the registry model info structure. type ModelInfo = registry.ModelInfo diff --git a/sdk/cliproxy/pipeline/context.go b/sdk/cliproxy/pipeline/context.go index fc6754eb97..4cffb0b4d9 100644 --- a/sdk/cliproxy/pipeline/context.go +++ b/sdk/cliproxy/pipeline/context.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) // Context encapsulates execution state shared across middleware, translators, and executors. diff --git a/sdk/cliproxy/pprof_server.go b/sdk/cliproxy/pprof_server.go index 3fafef4cd4..ec30b4bef3 100644 --- a/sdk/cliproxy/pprof_server.go +++ b/sdk/cliproxy/pprof_server.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" log "github.com/sirupsen/logrus" ) diff --git a/sdk/cliproxy/providers.go b/sdk/cliproxy/providers.go index 7ce89f76fe..542b2d9d6a 100644 --- a/sdk/cliproxy/providers.go +++ b/sdk/cliproxy/providers.go @@ -3,8 +3,8 @@ package cliproxy import ( "context" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) // NewFileTokenClientProvider returns the default token-backed client loader. diff --git a/sdk/cliproxy/rtprovider.go b/sdk/cliproxy/rtprovider.go index 5c4f579a85..d07b4cb4f9 100644 --- a/sdk/cliproxy/rtprovider.go +++ b/sdk/cliproxy/rtprovider.go @@ -5,8 +5,8 @@ import ( "strings" "sync" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" ) diff --git a/sdk/cliproxy/rtprovider_test.go b/sdk/cliproxy/rtprovider_test.go index f907081e29..6ea08432c1 100644 --- a/sdk/cliproxy/rtprovider_test.go +++ b/sdk/cliproxy/rtprovider_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestRoundTripperForDirectBypassesProxy(t *testing.T) { diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 9f195f5679..8685872e0f 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -12,17 +12,20 @@ import ( "sync" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" - "github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay" - sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api" + "github.com/router-for-me/CLIProxyAPI/v7/internal/home" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor" + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff" + "github.com/router-for-me/CLIProxyAPI/v7/internal/wsrelay" + sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" log "github.com/sirupsen/logrus" ) @@ -36,6 +39,9 @@ type Service struct { // cfgMu protects concurrent access to the configuration. cfgMu sync.RWMutex + // configUpdateMu serializes config updates across watcher + home. + configUpdateMu sync.Mutex + // configPath is the path to the configuration file. configPath string @@ -89,6 +95,9 @@ type Service struct { // wsGateway manages websocket Gemini providers. wsGateway *wsrelay.Manager + + homeClient *home.Client + homeCancel context.CancelFunc } // RegisterUsagePlugin registers a usage plugin on the global usage manager. @@ -462,6 +471,270 @@ func (s *Service) rebindExecutors() { } } +func (s *Service) applyConfigUpdate(newCfg *config.Config) { + if s == nil { + return + } + + s.configUpdateMu.Lock() + defer s.configUpdateMu.Unlock() + + previousStrategy := "" + var previousSessionAffinity bool + var previousSessionAffinityTTL string + s.cfgMu.RLock() + if s.cfg != nil { + previousStrategy = strings.ToLower(strings.TrimSpace(s.cfg.Routing.Strategy)) + previousSessionAffinity = s.cfg.Routing.SessionAffinity + previousSessionAffinityTTL = s.cfg.Routing.SessionAffinityTTL + } + s.cfgMu.RUnlock() + + if newCfg == nil { + s.cfgMu.RLock() + newCfg = s.cfg + s.cfgMu.RUnlock() + } + if newCfg == nil { + return + } + + nextStrategy := strings.ToLower(strings.TrimSpace(newCfg.Routing.Strategy)) + normalizeStrategy := func(strategy string) string { + switch strategy { + case "fill-first", "fillfirst", "ff": + return "fill-first" + default: + return "round-robin" + } + } + previousStrategy = normalizeStrategy(previousStrategy) + nextStrategy = normalizeStrategy(nextStrategy) + + nextSessionAffinity := newCfg.Routing.SessionAffinity + nextSessionAffinityTTL := newCfg.Routing.SessionAffinityTTL + + selectorChanged := previousStrategy != nextStrategy || + previousSessionAffinity != nextSessionAffinity || + previousSessionAffinityTTL != nextSessionAffinityTTL + + if s.coreManager != nil && selectorChanged { + var selector coreauth.Selector + switch nextStrategy { + case "fill-first": + selector = &coreauth.FillFirstSelector{} + default: + selector = &coreauth.RoundRobinSelector{} + } + + if nextSessionAffinity { + ttl := time.Hour + if ttlStr := strings.TrimSpace(nextSessionAffinityTTL); ttlStr != "" { + if parsed, err := time.ParseDuration(ttlStr); err == nil && parsed > 0 { + ttl = parsed + } + } + selector = coreauth.NewSessionAffinitySelectorWithConfig(coreauth.SessionAffinityConfig{ + Fallback: selector, + TTL: ttl, + }) + } + + s.coreManager.SetSelector(selector) + } + + s.applyRetryConfig(newCfg) + s.applyPprofConfig(newCfg) + if s.server != nil { + s.server.UpdateClients(newCfg) + } + s.cfgMu.Lock() + s.cfg = newCfg + s.cfgMu.Unlock() + if s.coreManager != nil { + s.coreManager.SetConfig(newCfg) + s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias) + } + s.rebindExecutors() +} + +func forceHomeRuntimeConfig(cfg *config.Config) { + if cfg == nil { + return + } + cfg.APIKeys = nil + cfg.UsageStatisticsEnabled = true + cfg.DisableCooling = true + cfg.WebsocketAuth = false + cfg.EnableGeminiCLIEndpoint = false + cfg.RemoteManagement.AllowRemote = false + cfg.RemoteManagement.DisableControlPanel = true +} + +func (s *Service) registerHomeExecutors() { + if s == nil || s.coreManager == nil || s.cfg == nil { + return + } + + // Register baseline executors so home-dispatched auth entries can execute without + // requiring any local auth-dir credentials. + s.coreManager.RegisterExecutor(executor.NewCodexAutoExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewGeminiVertexExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(s.cfg, "", s.wsGateway)) + s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg)) + s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility", s.cfg)) +} + +func (s *Service) applyHomeOverlay(remoteCfg *config.Config) { + if s == nil || remoteCfg == nil { + return + } + + s.cfgMu.RLock() + baseCfg := s.cfg + s.cfgMu.RUnlock() + if baseCfg == nil { + return + } + + merged := *remoteCfg + merged.Host = baseCfg.Host + merged.Port = baseCfg.Port + merged.TLS = baseCfg.TLS + merged.Home = baseCfg.Home + forceHomeRuntimeConfig(&merged) + + logHomeConfigChanges(baseCfg, &merged) + s.applyConfigUpdate(&merged) +} + +func logHomeConfigChanges(oldCfg, newCfg *config.Config) { + if oldCfg == nil || newCfg == nil || !newCfg.Home.Enabled || (!oldCfg.Debug && !newCfg.Debug) { + return + } + + details := diff.BuildConfigChangeDetails(oldCfg, newCfg) + if len(details) == 0 { + return + } + + if newCfg.Debug && !log.IsLevelEnabled(log.DebugLevel) { + util.SetLogLevel(newCfg) + } + + log.Debugf("home config changes detected:") + for _, detail := range details { + log.Debugf(" %s", detail) + } +} + +func (s *Service) startHomeUsageForwarder(ctx context.Context, client *home.Client) { + if s == nil || client == nil { + return + } + if ctx == nil { + ctx = context.Background() + } + + sleep := func(d time.Duration) bool { + if d <= 0 { + return true + } + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-ctx.Done(): + return false + case <-timer.C: + return true + } + } + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + } + + if !client.HeartbeatOK() { + if !sleep(time.Second) { + return + } + continue + } + + items := redisqueue.PopOldest(64) + if len(items) == 0 { + if !sleep(500 * time.Millisecond) { + return + } + continue + } + + for i := range items { + if errPush := client.LPushUsage(ctx, items[i]); errPush != nil { + for j := i; j < len(items); j++ { + redisqueue.Enqueue(items[j]) + } + if !sleep(time.Second) { + return + } + break + } + } + } + }() +} + +func (s *Service) startHomeSubscriber(ctx context.Context) { + if s == nil { + return + } + s.cfgMu.RLock() + cfg := s.cfg + s.cfgMu.RUnlock() + if cfg == nil || !cfg.Home.Enabled { + return + } + + if s.homeCancel != nil { + s.homeCancel() + s.homeCancel = nil + } + if s.homeClient != nil { + s.homeClient.Close() + s.homeClient = nil + } + + homeCtx := ctx + if homeCtx == nil { + homeCtx = context.Background() + } + homeCtx, cancel := context.WithCancel(homeCtx) + s.homeCancel = cancel + + client := home.New(cfg.Home) + s.homeClient = client + home.SetCurrent(client) + + go client.StartConfigSubscriber(homeCtx, func(raw []byte) error { + parsed, err := config.ParseConfigBytes(raw) + if err != nil { + log.Warnf("failed to parse home config payload: %v", err) + return err + } + s.applyHomeOverlay(parsed) + return nil + }) + s.startHomeUsageForwarder(homeCtx, client) +} + // Run starts the service and blocks until the context is cancelled or the server stops. // It initializes all components including authentication, file watching, HTTP server, // and starts processing requests. The method blocks until the context is cancelled. @@ -480,6 +753,11 @@ func (s *Service) Run(ctx context.Context) error { } usage.StartDefault(ctx) + homeEnabled := s.cfg != nil && s.cfg.Home.Enabled + if homeEnabled { + forceHomeRuntimeConfig(s.cfg) + redisqueue.SetUsageStatisticsEnabled(true) + } shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() @@ -489,32 +767,36 @@ func (s *Service) Run(ctx context.Context) error { } }() - if err := s.ensureAuthDir(); err != nil { - return err + if !homeEnabled { + if errEnsureAuthDir := s.ensureAuthDir(); errEnsureAuthDir != nil { + return errEnsureAuthDir + } } s.applyRetryConfig(s.cfg) - if s.coreManager != nil { + if s.coreManager != nil && !homeEnabled { if errLoad := s.coreManager.Load(ctx); errLoad != nil { log.Warnf("failed to load auth store: %v", errLoad) } } - tokenResult, err := s.tokenProvider.Load(ctx, s.cfg) - if err != nil && !errors.Is(err, context.Canceled) { - return err - } - if tokenResult == nil { - tokenResult = &TokenClientResult{} - } + if !homeEnabled { + tokenResult, err := s.tokenProvider.Load(ctx, s.cfg) + if err != nil && !errors.Is(err, context.Canceled) { + return err + } + if tokenResult == nil { + tokenResult = &TokenClientResult{} + } - apiKeyResult, err := s.apiKeyProvider.Load(ctx, s.cfg) - if err != nil && !errors.Is(err, context.Canceled) { - return err - } - if apiKeyResult == nil { - apiKeyResult = &APIKeyClientResult{} + apiKeyResult, err := s.apiKeyProvider.Load(ctx, s.cfg) + if err != nil && !errors.Is(err, context.Canceled) { + return err + } + if apiKeyResult == nil { + apiKeyResult = &APIKeyClientResult{} + } } // legacy clients removed; no caches to refresh @@ -526,6 +808,10 @@ func (s *Service) Run(ctx context.Context) error { s.authManager = newDefaultAuthManager() } + if homeEnabled { + s.startHomeSubscriber(ctx) + } + s.ensureWebsocketGateway() if s.server != nil && s.wsGateway != nil { s.server.AttachWebsocketRoute(s.wsGateway.Path(), s.wsGateway.Handler()) @@ -547,6 +833,12 @@ func (s *Service) Run(ctx context.Context) error { }) } + if homeEnabled { + s.registerHomeExecutors() + // Home mode does not expose in-process Redis RESP usage output; usage is forwarded to home instead. + redisqueue.SetEnabled(true) + } + if s.hooks.OnBeforeStart != nil { s.hooks.OnBeforeStart(s.cfg) } @@ -607,107 +899,31 @@ func (s *Service) Run(ctx context.Context) error { s.hooks.OnAfterStart(s) } - var watcherWrapper *WatcherWrapper - reloadCallback := func(newCfg *config.Config) { - previousStrategy := "" - var previousSessionAffinity bool - var previousSessionAffinityTTL string - s.cfgMu.RLock() - if s.cfg != nil { - previousStrategy = strings.ToLower(strings.TrimSpace(s.cfg.Routing.Strategy)) - previousSessionAffinity = s.cfg.Routing.ClaudeCodeSessionAffinity || s.cfg.Routing.SessionAffinity - previousSessionAffinityTTL = s.cfg.Routing.SessionAffinityTTL - } - s.cfgMu.RUnlock() + if !homeEnabled { + var watcherWrapper *WatcherWrapper + reloadCallback := func(newCfg *config.Config) { s.applyConfigUpdate(newCfg) } - if newCfg == nil { - s.cfgMu.RLock() - newCfg = s.cfg - s.cfgMu.RUnlock() + watcherWrapper, errCreate := s.watcherFactory(s.configPath, s.cfg.AuthDir, reloadCallback) + if errCreate != nil { + return fmt.Errorf("cliproxy: failed to create watcher: %w", errCreate) } - if newCfg == nil { - return + s.watcher = watcherWrapper + s.ensureAuthUpdateQueue(ctx) + if s.authUpdates != nil { + watcherWrapper.SetAuthUpdateQueue(s.authUpdates) } + watcherWrapper.SetConfig(s.cfg) - nextStrategy := strings.ToLower(strings.TrimSpace(newCfg.Routing.Strategy)) - normalizeStrategy := func(strategy string) string { - switch strategy { - case "fill-first", "fillfirst", "ff": - return "fill-first" - default: - return "round-robin" - } + watcherCtx, watcherCancel := context.WithCancel(context.Background()) + s.watcherCancel = watcherCancel + if errStart := watcherWrapper.Start(watcherCtx); errStart != nil { + return fmt.Errorf("cliproxy: failed to start watcher: %w", errStart) } - previousStrategy = normalizeStrategy(previousStrategy) - nextStrategy = normalizeStrategy(nextStrategy) - - nextSessionAffinity := newCfg.Routing.ClaudeCodeSessionAffinity || newCfg.Routing.SessionAffinity - nextSessionAffinityTTL := newCfg.Routing.SessionAffinityTTL - - selectorChanged := previousStrategy != nextStrategy || - previousSessionAffinity != nextSessionAffinity || - previousSessionAffinityTTL != nextSessionAffinityTTL - - if s.coreManager != nil && selectorChanged { - var selector coreauth.Selector - switch nextStrategy { - case "fill-first": - selector = &coreauth.FillFirstSelector{} - default: - selector = &coreauth.RoundRobinSelector{} - } - - if nextSessionAffinity { - ttl := time.Hour - if ttlStr := strings.TrimSpace(nextSessionAffinityTTL); ttlStr != "" { - if parsed, err := time.ParseDuration(ttlStr); err == nil && parsed > 0 { - ttl = parsed - } - } - selector = coreauth.NewSessionAffinitySelectorWithConfig(coreauth.SessionAffinityConfig{ - Fallback: selector, - TTL: ttl, - }) - } - - s.coreManager.SetSelector(selector) - } - - s.applyRetryConfig(newCfg) - s.applyPprofConfig(newCfg) - if s.server != nil { - s.server.UpdateClients(newCfg) - } - s.cfgMu.Lock() - s.cfg = newCfg - s.cfgMu.Unlock() - if s.coreManager != nil { - s.coreManager.SetConfig(newCfg) - s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias) - } - s.rebindExecutors() + log.Info("file watcher started for config and auth directory changes") } - watcherWrapper, err = s.watcherFactory(s.configPath, s.cfg.AuthDir, reloadCallback) - if err != nil { - return fmt.Errorf("cliproxy: failed to create watcher: %w", err) - } - s.watcher = watcherWrapper - s.ensureAuthUpdateQueue(ctx) - if s.authUpdates != nil { - watcherWrapper.SetAuthUpdateQueue(s.authUpdates) - } - watcherWrapper.SetConfig(s.cfg) - - watcherCtx, watcherCancel := context.WithCancel(context.Background()) - s.watcherCancel = watcherCancel - if err = watcherWrapper.Start(watcherCtx); err != nil { - return fmt.Errorf("cliproxy: failed to start watcher: %w", err) - } - log.Info("file watcher started for config and auth directory changes") - // Prefer core auth manager auto refresh if available. - if s.coreManager != nil { + if s.coreManager != nil && !homeEnabled { interval := 15 * time.Minute s.coreManager.StartAutoRefresh(context.Background(), interval) log.Infof("core auth auto-refresh started (interval=%s)", interval) @@ -717,8 +933,8 @@ func (s *Service) Run(ctx context.Context) error { case <-ctx.Done(): log.Debug("service context cancelled, shutting down...") return ctx.Err() - case err = <-s.serverErr: - return err + case errServer := <-s.serverErr: + return errServer } } @@ -741,6 +957,16 @@ func (s *Service) Shutdown(ctx context.Context) error { ctx = context.Background() } + if s.homeCancel != nil { + s.homeCancel() + s.homeCancel = nil + } + if s.homeClient != nil { + s.homeClient.Close() + s.homeClient = nil + } + home.ClearCurrent() + // legacy refresh loop removed; only stopping core auth manager below if s.watcherCancel != nil { diff --git a/sdk/cliproxy/service_codex_executor_binding_test.go b/sdk/cliproxy/service_codex_executor_binding_test.go index bb4fc84e10..20a9cd7c86 100644 --- a/sdk/cliproxy/service_codex_executor_binding_test.go +++ b/sdk/cliproxy/service_codex_executor_binding_test.go @@ -3,8 +3,8 @@ package cliproxy import ( "testing" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestEnsureExecutorsForAuth_CodexDoesNotReplaceInNormalMode(t *testing.T) { diff --git a/sdk/cliproxy/service_excluded_models_test.go b/sdk/cliproxy/service_excluded_models_test.go index 198a5bed73..fc16c09561 100644 --- a/sdk/cliproxy/service_excluded_models_test.go +++ b/sdk/cliproxy/service_excluded_models_test.go @@ -4,8 +4,8 @@ import ( "strings" "testing" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestRegisterModelsForAuth_UsesPreMergedExcludedModelsAttribute(t *testing.T) { diff --git a/sdk/cliproxy/service_oauth_model_alias_test.go b/sdk/cliproxy/service_oauth_model_alias_test.go index 2caf7a178f..7405f7caca 100644 --- a/sdk/cliproxy/service_oauth_model_alias_test.go +++ b/sdk/cliproxy/service_oauth_model_alias_test.go @@ -3,7 +3,7 @@ package cliproxy import ( "testing" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestApplyOAuthModelAlias_Rename(t *testing.T) { diff --git a/sdk/cliproxy/service_stale_state_test.go b/sdk/cliproxy/service_stale_state_test.go index 010218d966..53849eb349 100644 --- a/sdk/cliproxy/service_stale_state_test.go +++ b/sdk/cliproxy/service_stale_state_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func TestServiceApplyCoreAuthAddOrUpdate_DeleteReAddDoesNotInheritStaleRuntimeState(t *testing.T) { @@ -99,3 +99,32 @@ func TestServiceApplyCoreAuthAddOrUpdate_DeleteReAddDoesNotInheritStaleRuntimeSt t.Fatalf("expected re-added auth to re-register models in global registry") } } + +func TestForceHomeRuntimeConfigEnablesUsageStatistics(t *testing.T) { + cfg := &config.Config{ + UsageStatisticsEnabled: false, + } + + forceHomeRuntimeConfig(cfg) + + if !cfg.UsageStatisticsEnabled { + t.Fatal("expected home runtime config to force usage statistics enabled") + } +} + +func TestApplyHomeOverlayForcesUsageStatisticsEnabled(t *testing.T) { + baseCfg := &config.Config{} + baseCfg.Home.Enabled = true + service := &Service{cfg: baseCfg} + + service.applyHomeOverlay(&config.Config{ + UsageStatisticsEnabled: false, + }) + + if service.cfg == nil || !service.cfg.UsageStatisticsEnabled { + t.Fatal("expected home overlay to force usage statistics enabled") + } + if !service.cfg.Home.Enabled { + t.Fatal("expected home overlay to preserve local home settings") + } +} diff --git a/sdk/cliproxy/types.go b/sdk/cliproxy/types.go index 1521dffee4..c30b712bdd 100644 --- a/sdk/cliproxy/types.go +++ b/sdk/cliproxy/types.go @@ -6,9 +6,9 @@ package cliproxy import ( "context" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) // TokenClientProvider loads clients backed by stored authentication tokens. diff --git a/sdk/cliproxy/usage/manager.go b/sdk/cliproxy/usage/manager.go index 72405d7587..7bc73114e8 100644 --- a/sdk/cliproxy/usage/manager.go +++ b/sdk/cliproxy/usage/manager.go @@ -22,16 +22,25 @@ type Record struct { RequestedAt time.Time Latency time.Duration Failed bool + Fail Failure Detail Detail } +// Failure holds HTTP failure metadata for an upstream request attempt. +type Failure struct { + StatusCode int + Body string +} + // Detail holds the token usage breakdown. type Detail struct { - InputTokens int64 - OutputTokens int64 - ReasoningTokens int64 - CachedTokens int64 - TotalTokens int64 + InputTokens int64 + OutputTokens int64 + ReasoningTokens int64 + CachedTokens int64 + CacheReadTokens int64 + CacheCreationTokens int64 + TotalTokens int64 } type requestedModelAliasContextKey struct{} diff --git a/sdk/cliproxy/watcher.go b/sdk/cliproxy/watcher.go index caeadf19b9..e4a9081b41 100644 --- a/sdk/cliproxy/watcher.go +++ b/sdk/cliproxy/watcher.go @@ -3,9 +3,9 @@ package cliproxy import ( "context" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher" + coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" ) func defaultWatcherFactory(configPath, authDir string, reload func(*config.Config)) (*WatcherWrapper, error) { diff --git a/sdk/config/config.go b/sdk/config/config.go index 14163418f7..d39e512de1 100644 --- a/sdk/config/config.go +++ b/sdk/config/config.go @@ -4,7 +4,7 @@ // embed CLIProxyAPI without importing internal packages. package config -import internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" +import internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" type SDKConfig = internalconfig.SDKConfig @@ -41,6 +41,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { return internalconfig.LoadConfigOptional(configFile, optional) } +func ParseConfigBytes(data []byte) (*Config, error) { return internalconfig.ParseConfigBytes(data) } + func SaveConfigPreserveComments(configFile string, cfg *Config) error { return internalconfig.SaveConfigPreserveComments(configFile, cfg) } diff --git a/sdk/logging/request_logger.go b/sdk/logging/request_logger.go index ddbda6b8b0..5f8cf754e1 100644 --- a/sdk/logging/request_logger.go +++ b/sdk/logging/request_logger.go @@ -1,7 +1,7 @@ // Package logging re-exports request logging primitives for SDK consumers. package logging -import internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" +import internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" const defaultErrorLogsMaxFiles = 10 diff --git a/sdk/translator/builtin/builtin.go b/sdk/translator/builtin/builtin.go index 798e43f1a9..f95e65870f 100644 --- a/sdk/translator/builtin/builtin.go +++ b/sdk/translator/builtin/builtin.go @@ -2,9 +2,9 @@ package builtin import ( - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" ) // Registry exposes the default registry populated with all built-in translators. diff --git a/test/amp_management_test.go b/test/amp_management_test.go index e384ef0e8b..6c694db6fa 100644 --- a/test/amp_management_test.go +++ b/test/amp_management_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func init() { diff --git a/test/builtin_tools_translation_test.go b/test/builtin_tools_translation_test.go index 07d7671544..70ee0ac1b9 100644 --- a/test/builtin_tools_translation_test.go +++ b/test/builtin_tools_translation_test.go @@ -3,9 +3,9 @@ package test import ( "testing" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 51671a9c5f..9173aa0194 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -5,20 +5,20 @@ import ( "testing" "time" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" // Import provider packages to trigger init() registration of ProviderAppliers - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/claude" - _ "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/kimi" - _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/openai" - - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/antigravity" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/claude" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/codex" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/gemini" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/geminicli" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/openai" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/test/usage_logging_test.go b/test/usage_logging_test.go index ee03c4d79c..bcf6d19254 100644 --- a/test/usage_logging_test.go +++ b/test/usage_logging_test.go @@ -9,12 +9,12 @@ import ( "testing" "time" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" - runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" - cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" - sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue" + runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) func TestGeminiExecutorRecordsSuccessfulZeroUsageInQueue(t *testing.T) {