Merge branch 'v7' into dev

This commit is contained in:
Luis Pater
2026-05-10 02:33:42 +08:00
324 changed files with 3634 additions and 1148 deletions
+5 -5
View File
@@ -25,11 +25,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
sdkauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
+95 -18
View File
@@ -10,28 +10,31 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"net"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cmd" "github.com/router-for-me/CLIProxyAPI/v7/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v6/internal/store" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" "github.com/router-for-me/CLIProxyAPI/v7/internal/store"
"github.com/router-for-me/CLIProxyAPI/v6/internal/tui" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/tui"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
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" log "github.com/sirupsen/logrus"
) )
@@ -70,6 +73,8 @@ func main() {
var vertexImportPrefix string var vertexImportPrefix string
var configPath string var configPath string
var password string var password string
var homeAddr string
var homePassword string
var tuiMode bool var tuiMode bool
var standalone bool var standalone bool
var localModel bool var localModel bool
@@ -88,6 +93,8 @@ func main() {
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file") 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(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)")
flag.StringVar(&password, "password", "", "") 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(&tuiMode, "tui", false, "Start with terminal management UI")
flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server") 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") 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 err error
var cfg *config.Config var cfg *config.Config
var isCloudDeploy bool var isCloudDeploy bool
var configLoadedFromHome bool
var ( var (
usePostgresStore bool usePostgresStore bool
pgStoreDSN string pgStoreDSN string
@@ -236,7 +244,68 @@ func main() {
// Determine and load the configuration file. // Determine and load the configuration file.
// Prefer the Postgres store when configured, otherwise fallback to git or local files. // Prefer the Postgres store when configured, otherwise fallback to git or local files.
var configFilePath string 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 == "" { if pgStoreLocalPath == "" {
pgStoreLocalPath = wd pgStoreLocalPath = wd
} }
@@ -400,6 +469,9 @@ func main() {
// In cloud deploy mode, check if we have a valid configuration // In cloud deploy mode, check if we have a valid configuration
var configFileExists bool var configFileExists bool
if isCloudDeploy { if isCloudDeploy {
if configLoadedFromHome && cfg != nil {
configFileExists = cfg.Port != 0
} else {
if info, errStat := os.Stat(configFilePath); errStat != nil { if info, errStat := os.Stat(configFilePath); errStat != nil {
// Don't mislead: API server will not start until configuration is provided. // 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") log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration")
@@ -417,6 +489,7 @@ func main() {
configFileExists = true configFileExists = true
} }
} }
}
redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled) redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled)
redisqueue.SetRetentionSeconds(cfg.RedisUsageQueueRetentionSeconds) redisqueue.SetRetentionSeconds(cfg.RedisUsageQueueRetentionSeconds)
coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling) coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling)
@@ -496,8 +569,10 @@ func main() {
// Standalone mode: start an embedded local server and connect TUI client to it. // Standalone mode: start an embedded local server and connect TUI client to it.
managementasset.StartAutoUpdater(context.Background(), configFilePath) managementasset.StartAutoUpdater(context.Background(), configFilePath)
misc.StartAntigravityVersionUpdater(context.Background()) misc.StartAntigravityVersionUpdater(context.Background())
if !localModel { if !localModel && !cfg.Home.Enabled {
registry.StartModelsUpdater(context.Background()) registry.StartModelsUpdater(context.Background())
} else if cfg.Home.Enabled {
log.Info("Home mode: remote model updates disabled")
} }
hook := tui.NewLogHook(2000) hook := tui.NewLogHook(2000)
hook.SetFormatter(&logging.LogFormatter{}) hook.SetFormatter(&logging.LogFormatter{})
@@ -572,8 +647,10 @@ func main() {
// Start the main proxy service // Start the main proxy service
managementasset.StartAutoUpdater(context.Background(), configFilePath) managementasset.StartAutoUpdater(context.Background(), configFilePath)
misc.StartAntigravityVersionUpdater(context.Background()) misc.StartAntigravityVersionUpdater(context.Background())
if !localModel { if !localModel && !cfg.Home.Enabled {
registry.StartModelsUpdater(context.Background()) registry.StartModelsUpdater(context.Background())
} else if cfg.Home.Enabled {
log.Info("Home mode: remote model updates disabled")
} }
cmd.StartService(cfg, configFilePath, password) cmd.StartService(cfg, configFilePath, password)
} }
+12
View File
@@ -11,6 +11,13 @@ tls:
cert: "" cert: ""
key: "" key: ""
# Optional "home" control plane integration over Redis protocol.
home:
enabled: false
host: "127.0.0.1"
port: 6379
password: ""
# Management API settings # Management API settings
remote-management: remote-management:
# Whether to allow remote (non-localhost) management access. # Whether to allow remote (non-localhost) management access.
@@ -67,6 +74,7 @@ error-logs-max-files: 10
usage-statistics-enabled: false usage-statistics-enabled: false
# How long (in seconds) Redis usage queue items are retained in memory for the RESP interface (LPOP/RPOP). # 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. # Default: 60. Max: 3600.
redis-usage-queue-retention-seconds: 60 redis-usage-queue-retention-seconds: 60
@@ -149,6 +157,7 @@ nonstream-keepalive-interval: 0
# gemini-api-key: # gemini-api-key:
# - api-key: "AIzaSy...01" # - api-key: "AIzaSy...01"
# prefix: "test" # optional: require calls like "test/gemini-3-pro-preview" to target this credential # 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" # base-url: "https://generativelanguage.googleapis.com"
# headers: # headers:
# X-Custom-Header: "custom-value" # X-Custom-Header: "custom-value"
@@ -168,6 +177,7 @@ nonstream-keepalive-interval: 0
# codex-api-key: # codex-api-key:
# - api-key: "sk-atSM..." # - api-key: "sk-atSM..."
# prefix: "test" # optional: require calls like "test/gpt-5-codex" to target this credential # 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 # base-url: "https://www.example.com" # use the custom codex API endpoint
# headers: # headers:
# X-Custom-Header: "custom-value" # 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..." # use the official claude API key, no need to set the base url
# - api-key: "sk-atSM..." # - api-key: "sk-atSM..."
# prefix: "test" # optional: require calls like "test/claude-sonnet-latest" to target this credential # 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 # base-url: "https://www.example.com" # use the custom claude API endpoint
# headers: # headers:
# X-Custom-Header: "custom-value" # 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 # 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 # 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. # 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: # headers:
# X-Custom-Header: "custom-value" # X-Custom-Header: "custom-value"
# api-key-entries: # api-key-entries:
+8 -8
View File
@@ -24,14 +24,14 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config" "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/logging" "github.com/router-for-me/CLIProxyAPI/v7/sdk/logging"
sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktr "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
) )
const ( const (
+2 -2
View File
@@ -16,8 +16,8 @@ import (
"strings" "strings"
"time" "time"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
_ "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator/builtin" _ "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator/builtin"
) )
func main() { func main() {
+7 -1
View File
@@ -1,4 +1,4 @@
module github.com/router-for-me/CLIProxyAPI/v6 module github.com/router-for-me/CLIProxyAPI/v7
go 1.26.0 go 1.26.0
@@ -31,6 +31,12 @@ require (
gopkg.in/yaml.v3 v3.0.1 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 ( require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
+6
View File
@@ -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 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 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 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 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= 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= 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/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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 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.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 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"strings" "strings"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
) )
// Register ensures the config-access provider is available to the access manager. // Register ensures the config-access provider is available to the access manager.
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"sort" "sort"
"strings" "strings"
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "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 { type apiKeyUsageEntry struct {
@@ -8,8 +8,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
) )
func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) { func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) {
@@ -11,10 +11,10 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/geminicli"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
@@ -5,9 +5,9 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
) )
func TestAPICallTransportDirectBypassesGlobalProxy(t *testing.T) { func TestAPICallTransportDirectBypassesGlobalProxy(t *testing.T) {
+11 -11
View File
@@ -22,17 +22,17 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/antigravity" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/antigravity"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" geminiAuth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -12,8 +12,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
) )
func TestUploadAuthFile_BatchMultipart(t *testing.T) { func TestUploadAuthFile_BatchMultipart(t *testing.T) {
@@ -11,8 +11,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
) )
func TestDeleteAuthFile_UsesAuthPathFromManager(t *testing.T) { func TestDeleteAuthFile_UsesAuthPathFromManager(t *testing.T) {
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "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) { func TestDownloadAuthFile_ReturnsFile(t *testing.T) {
@@ -11,7 +11,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "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) { func TestDownloadAuthFile_PreventsWindowsSlashTraversal(t *testing.T) {
@@ -9,8 +9,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
) )
func TestPatchAuthFileFields_MergeHeadersAndDeleteEmptyValues(t *testing.T) { func TestPatchAuthFileFields_MergeHeadersAndDeleteEmptyValues(t *testing.T) {
@@ -8,8 +8,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
) )
func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) { func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) {
@@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer"
) )
type geminiKeyWithAuthIndex struct { type geminiKeyWithAuthIndex struct {
@@ -11,9 +11,9 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -6,7 +6,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "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] // Generic helpers for list[string]
@@ -8,7 +8,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "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 { func writeTestConfigFile(t *testing.T) string {
+4 -4
View File
@@ -13,10 +13,10 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "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) { func TestAuthenticateManagementKey_LocalhostIPBan_BlocksCorrectKeyDuringBan(t *testing.T) {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
) )
const ( const (
@@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "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. // GetStaticModelDefinitions returns static model metadata for a given channel.
@@ -4,7 +4,7 @@ import (
"context" "context"
"sync" "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 { type memoryAuthStore struct {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "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 type usageQueueRecord []byte
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "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) { func TestGetUsageQueuePopsRequestedRecords(t *testing.T) {
@@ -9,8 +9,8 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 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. // ImportVertexCredential handles uploading a Vertex service account JSON and saving it as an auth record.
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
) )
const maxErrorOnlyCapturedRequestBodyBytes int64 = 1 << 20 // 1 MiB const maxErrorOnlyCapturedRequestBodyBytes int64 = 1 << 20 // 1 MiB
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
) )
const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE" const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE"
@@ -7,8 +7,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
) )
func TestExtractRequestBodyPrefersOverride(t *testing.T) { func TestExtractRequestBodyPrefersOverride(t *testing.T) {
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"sync" "sync"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+4 -4
View File
@@ -9,10 +9,10 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
) )
func TestAmpModule_Name(t *testing.T) { func TestAmpModule_Name(t *testing.T) {
@@ -8,8 +8,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
@@ -9,8 +9,8 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
) )
func TestFallbackHandler_ModelMapping_PreservesThinkingSuffixAndRewritesResponse(t *testing.T) { func TestFallbackHandler_ModelMapping_PreservesThinkingSuffixAndRewritesResponse(t *testing.T) {
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -3,8 +3,8 @@ package amp
import ( import (
"testing" "testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
) )
func TestNewModelMapper(t *testing.T) { func TestNewModelMapper(t *testing.T) {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "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" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
) )
// Helper: compress data with gzip // Helper: compress data with gzip
+7 -7
View File
@@ -9,11 +9,11 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -21,12 +21,12 @@ import (
// from gin.Context to the request context for SecretSource lookup. // from gin.Context to the request context for SecretSource lookup.
type clientAPIKeyContextKey struct{} 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. // into the request context so that SecretSource can look it up for per-client upstream routing.
func clientAPIKeyMiddleware() gin.HandlerFunc { func clientAPIKeyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// Extract the client API key from gin context (set by AuthMiddleware) // 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 != "" { if keyStr, ok := apiKey.(string); ok && keyStr != "" {
// Inject into request context for SecretSource.Get(ctx) to read // Inject into request context for SecretSource.Get(ctx) to read
ctx := context.WithValue(c.Request.Context(), clientAPIKeyContextKey{}, keyStr) ctx := context.WithValue(c.Request.Context(), clientAPIKeyContextKey{}, keyStr)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "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) { func TestRegisterManagementRoutes(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test" "github.com/sirupsen/logrus/hooks/test"
) )
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
) )
// Context encapsulates the dependencies exposed to routing modules during // Context encapsulates the dependencies exposed to routing modules during
+6
View File
@@ -83,6 +83,12 @@ func (s *Server) acceptMuxConnections(listener net.Listener, httpListener *muxLi
} }
if isRedisRESPPrefix(prefix[0]) { 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)
}
continue
}
if !s.managementRoutesEnabled.Load() { if !s.managementRoutesEnabled.Load() {
if errClose := conn.Close(); errClose != nil { if errClose := conn.Close(); errClose != nil {
log.Errorf("failed to close redis connection while management is disabled: %v", errClose) log.Errorf("failed to close redis connection while management is disabled: %v", errClose)
+7 -1
View File
@@ -10,7 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -45,6 +45,12 @@ func (s *Server) handleRedisConnection(conn net.Conn, reader *bufio.Reader) {
return true return true
} }
if s.cfg != nil && s.cfg.Home.Enabled {
_ = writeRedisError(writer, "ERR redis usage output disabled in home mode")
_ = writer.Flush()
return
}
for { for {
if !s.managementRoutesEnabled.Load() { if !s.managementRoutesEnabled.Load() {
return return
@@ -12,7 +12,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
) )
type remoteAddrConn struct { 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) { func TestRedisProtocol_AUTH_And_PopContracts(t *testing.T) {
const managementPassword = "test-management-password" const managementPassword = "test-management-password"
+241 -24
View File
@@ -8,6 +8,7 @@ import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@@ -15,30 +16,32 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/access" "github.com/router-for-me/CLIProxyAPI/v7/internal/access"
managementHandlers "github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management" managementHandlers "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/middleware" "github.com/router-for-me/CLIProxyAPI/v7/internal/api/middleware"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules" "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
ampmodule "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules/amp" ampmodule "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules/amp"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache" "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 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" log "github.com/sirupsen/logrus"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -64,7 +67,9 @@ type ServerOption func(*serverOptionConfig)
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger { func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
configDir := filepath.Dir(configPath) configDir := filepath.Dir(configPath)
logsDir := logging.ResolveLogDirectory(cfg) 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. // 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 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 // Setup routes
s.setupRoutes() 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). // or when a local management password is provided (e.g. TUI mode).
hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != "" hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != ""
s.managementRoutesEnabled.Store(hasManagementSecret) s.managementRoutesEnabled.Store(hasManagementSecret)
redisqueue.SetEnabled(hasManagementSecret) redisqueue.SetEnabled(hasManagementSecret || (cfg != nil && cfg.Home.Enabled))
if hasManagementSecret { if hasManagementSecret {
s.registerManagementRoutes() s.registerManagementRoutes()
} }
@@ -326,6 +335,28 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
return s 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. // setupRoutes configures the API routes for the server.
// It defines the endpoints and associates them with their respective handlers. // It defines the endpoints and associates them with their respective handlers.
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
@@ -661,6 +692,14 @@ func (s *Server) registerManagementRoutes() {
func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc { func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { 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() { if !s.managementRoutesEnabled.Load() {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
return return
@@ -671,7 +710,7 @@ func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
func (s *Server) serveManagementControlPanel(c *gin.Context) { func (s *Server) serveManagementControlPanel(c *gin.Context) {
cfg := s.cfg cfg := s.cfg
if cfg == nil || cfg.RemoteManagement.DisableControlPanel { if cfg == nil || cfg.Home.Enabled || cfg.RemoteManagement.DisableControlPanel {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
return return
} }
@@ -783,6 +822,11 @@ func (s *Server) watchKeepAlive() {
// otherwise it routes to OpenAI handler. // otherwise it routes to OpenAI handler.
func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, claudeHandler *claude.ClaudeCodeAPIHandler) gin.HandlerFunc { func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, claudeHandler *claude.ClaudeCodeAPIHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if s != nil && s.cfg != nil && s.cfg.Home.Enabled {
s.handleHomeModels(c)
return
}
userAgent := c.GetHeader("User-Agent") userAgent := c.GetHeader("User-Agent")
// Route to Claude handler if User-Agent starts with "claude-cli" // Route to Claude handler if User-Agent starts with "claude-cli"
@@ -796,6 +840,170 @@ func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, cl
} }
} }
type homeModelEntry struct {
id string
created int64
ownedBy string
displayName string
}
func (s *Server) handleHomeModels(c *gin.Context) {
if s == nil || c == nil || c.Request == nil {
return
}
client := home.Current()
if client == nil {
c.JSON(http.StatusServiceUnavailable, handlers.ErrorResponse{
Error: handlers.ErrorDetail{
Message: "home control center unavailable",
Type: "server_error",
},
})
return
}
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
}
entries, errDecode := decodeHomeModels(raw)
if errDecode != nil {
c.JSON(http.StatusBadGateway, handlers.ErrorResponse{
Error: handlers.ErrorDetail{
Message: errDecode.Error(),
Type: "server_error",
},
})
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, ok := out[0]["id"].(string); ok {
firstID = id
}
if id, ok := out[len(out)-1]["id"].(string); ok {
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 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 == "" {
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)
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. // Start begins listening for and serving HTTP or HTTPS requests.
// It's a blocking call and will only return on an unrecoverable error. // It's a blocking call and will only return on an unrecoverable error.
// //
@@ -991,6 +1199,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 oldCfg == nil || oldCfg.LoggingToFile != cfg.LoggingToFile || oldCfg.LogsMaxTotalSizeMB != cfg.LogsMaxTotalSizeMB {
if err := logging.ConfigureLogOutput(cfg); err != nil { if err := logging.ConfigureLogOutput(cfg); err != nil {
log.Errorf("failed to reconfigure log output: %v", err) log.Errorf("failed to reconfigure log output: %v", err)
@@ -1061,7 +1275,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
s.managementRoutesEnabled.Store(!newSecretEmpty) s.managementRoutesEnabled.Store(!newSecretEmpty)
} }
} }
redisqueue.SetEnabled(s.managementRoutesEnabled.Load()) redisqueue.SetEnabled(s.managementRoutesEnabled.Load() || (cfg != nil && cfg.Home.Enabled))
s.applyAccessConfig(oldCfg, cfg) s.applyAccessConfig(oldCfg, cfg)
s.cfg = cfg s.cfg = cfg
@@ -1094,11 +1308,14 @@ func (s *Server) UpdateClients(cfg *config.Config) {
} }
// Count client sources from configuration and auth store. // Count client sources from configuration and auth store.
authEntries := 0
if cfg != nil && !cfg.Home.Enabled {
tokenStore := sdkAuth.GetTokenStore() tokenStore := sdkAuth.GetTokenStore()
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok { if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir) dirSetter.SetBaseDir(cfg.AuthDir)
} }
authEntries := util.CountAuthFiles(context.Background(), tokenStore) authEntries = util.CountAuthFiles(context.Background(), tokenStore)
}
geminiAPIKeyCount := len(cfg.GeminiKey) geminiAPIKeyCount := len(cfg.GeminiKey)
claudeAPIKeyCount := len(cfg.ClaudeKey) claudeAPIKeyCount := len(cfg.ClaudeKey)
codexAPIKeyCount := len(cfg.CodexKey) codexAPIKeyCount := len(cfg.CodexKey)
@@ -1146,7 +1363,7 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc {
result, err := manager.Authenticate(c.Request.Context(), c.Request) result, err := manager.Authenticate(c.Request.Context(), c.Request)
if err == nil { if err == nil {
if result != nil { if result != nil {
c.Set("apiKey", result.Principal) c.Set("userApiKey", result.Principal)
c.Set("accessProvider", result.Provider) c.Set("accessProvider", result.Provider)
if len(result.Metadata) > 0 { if len(result.Metadata) > 0 {
c.Set("accessMetadata", result.Metadata) c.Set("accessMetadata", result.Metadata)
+32 -6
View File
@@ -11,12 +11,12 @@ import (
"time" "time"
gin "github.com/gin-gonic/gin" gin "github.com/gin-gonic/gin"
proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" proxyconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
) )
func newTestServer(t *testing.T) *Server { 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) { func TestAmpProviderModelRoutes(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
@@ -3,7 +3,7 @@ package claude
import ( import (
"testing" "testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
) )
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os" "os"
"path/filepath" "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. // ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication.
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"sync" "sync"
tls "github.com/refraction-networking/utls" tls "github.com/refraction-networking/utls"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config" "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
+2 -2
View File
@@ -14,8 +14,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sync/atomic" "sync/atomic"
"testing" "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) type roundTripFunc func(*http.Request) (*http.Response, error)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os" "os"
"path/filepath" "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. // CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication.
+6 -6
View File
@@ -13,12 +13,12 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser" "github.com/router-for-me/CLIProxyAPI/v7/internal/browser"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+2 -2
View File
@@ -15,8 +15,8 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"testing" "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) { func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"time" "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. // KimiTokenStorage stores OAuth2 token information for Kimi API authentication.
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os" "os"
"path/filepath" "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" log "github.com/sirupsen/logrus"
) )
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package cmd package cmd
import ( 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 // newAuthManager creates a new authentication manager instance with all supported
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+6 -6
View File
@@ -17,12 +17,12 @@ import (
"strings" "strings"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+3 -3
View File
@@ -10,9 +10,9 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api" "github.com/router-for-me/CLIProxyAPI/v7/internal/api"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+5 -5
View File
@@ -9,11 +9,11 @@ import (
"os" "os"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+16 -7
View File
@@ -13,7 +13,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -37,6 +37,9 @@ type Config struct {
// TLS config controls HTTPS server settings. // TLS config controls HTTPS server settings.
TLS TLSConfig `yaml:"tls" json:"tls"` 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 nests management-related options under 'remote-management'.
RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"` RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"`
@@ -224,12 +227,6 @@ type RoutingConfig struct {
// Supported values: "round-robin" (default), "fill-first". // Supported values: "round-robin" (default), "fill-first".
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` 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. // SessionAffinity enables universal session-sticky routing for all clients.
// Session IDs are extracted from multiple sources: // Session IDs are extracted from multiple sources:
// metadata.user_id (Claude Code session format), X-Session-ID, Session_id (Codex), // metadata.user_id (Claude Code session format), X-Session-ID, Session_id (Codex),
@@ -401,6 +398,9 @@ type ClaudeKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider. // ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` 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 configures request cloaking for non-Claude-Code clients.
Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"` Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"`
@@ -456,6 +456,9 @@ type CodexKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider. // ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` 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 } func (k CodexKey) GetAPIKey() string { return k.APIKey }
@@ -500,6 +503,9 @@ type GeminiKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider. // ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` 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 } func (k GeminiKey) GetAPIKey() string { return k.APIKey }
@@ -544,6 +550,9 @@ type OpenAICompatibility struct {
// Headers optionally adds extra HTTP headers for requests sent to this provider. // Headers optionally adds extra HTTP headers for requests sent to this provider.
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` 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. // OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
+9
View File
@@ -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:"-"`
}
+89
View File
@@ -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
}
+393
View File
@@ -0,0 +1,393 @@
package home
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"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
)
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 Client struct {
homeCfg config.HomeConfig
cmd *redis.Client
sub *redis.Client
heartbeatOK atomic.Bool
}
func New(homeCfg config.HomeConfig) *Client {
return &Client{homeCfg: homeCfg}
}
func (c *Client) Enabled() bool {
if c == nil {
return false
}
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)
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
}
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
}
addr, ok := c.addr()
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) Ping(ctx context.Context) error {
if err := c.ensureClients(); err != nil {
return err
}
if c.cmd == nil {
return ErrNotConnected
}
return c.cmd.Ping(ctx).Err()
}
func (c *Client) GetConfig(ctx context.Context) ([]byte, error) {
if err := c.ensureClients(); err != nil {
return nil, err
}
raw, err := c.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) {
if err := c.ensureClients(); err != nil {
return nil, err
}
raw, err := c.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) {
if err := c.ensureClients(); err != nil {
return nil, err
}
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 := c.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) {
if err := c.ensureClients(); err != nil {
return nil, err
}
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 := c.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 {
if err := c.ensureClients(); err != nil {
return err
}
if len(payload) == 0 {
return nil
}
return c.cmd.LPush(ctx, redisKeyUsage, payload).Err()
}
func (c *Client) RPushRequestLog(ctx context.Context, payload []byte) error {
if err := c.ensureClients(); err != nil {
return err
}
if len(payload) == 0 {
return nil
}
return c.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")
sleepWithContext(ctx, homeReconnectInterval)
continue
}
if errPing := c.Ping(ctx); errPing != nil {
log.Warn("unable to connect to home control center, retrying in 1 second")
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")
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
}
if c.sub == nil {
sleepWithContext(ctx, homeReconnectInterval)
continue
}
pubsub := c.sub.Subscribe(ctx, redisChannelConfig)
if pubsub == nil {
sleepWithContext(ctx, homeReconnectInterval)
continue
}
// Ensure the subscription is established before marking heartbeat OK.
if _, errReceive := pubsub.Receive(ctx); errReceive != nil {
_ = pubsub.Close()
sleepWithContext(ctx, homeReconnectInterval)
continue
}
c.heartbeatOK.Store(true)
for {
msg, errMsg := pubsub.ReceiveMessage(ctx)
if errMsg != nil {
_ = pubsub.Close()
c.heartbeatOK.Store(false)
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
}
}
+32
View File
@@ -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)
}
}
+25
View File
@@ -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))
}
+14
View File
@@ -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"`
}
+1 -1
View File
@@ -3,7 +3,7 @@
// transformation operations, maintaining compatibility with the SDK translator package. // transformation operations, maintaining compatibility with the SDK translator package.
package interfaces 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. // Backwards compatible aliases for translator function types.
type TranslateRequestFunc = sdktranslator.RequestTransform type TranslateRequestFunc = sdktranslator.RequestTransform
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "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" log "github.com/sirupsen/logrus"
) )
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"sync" "sync"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
+279 -3
View File
@@ -8,6 +8,8 @@ import (
"bytes" "bytes"
"compress/flate" "compress/flate"
"compress/gzip" "compress/gzip"
"context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -22,13 +24,23 @@ import (
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
) )
var requestLogID atomic.Uint64 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. // RequestLogger defines the interface for logging HTTP requests and responses.
// It provides methods for logging both regular and streaming HTTP request/response cycles. // It provides methods for logging both regular and streaming HTTP request/response cycles.
type RequestLogger interface { type RequestLogger interface {
@@ -148,6 +160,58 @@ type FileRequestLogger struct {
// errorLogsMaxFiles limits the number of error log files retained. // errorLogsMaxFiles limits the number of error log files retained.
errorLogsMaxFiles int 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. // NewFileRequestLogger creates a new file-based request logger.
@@ -173,9 +237,19 @@ func NewFileRequestLogger(enabled bool, logsDir string, configDir string, errorL
enabled: enabled, enabled: enabled,
logsDir: logsDir, logsDir: logsDir,
errorLogsMaxFiles: errorLogsMaxFiles, 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. // IsEnabled returns whether request logging is currently enabled.
// //
// Returns: // Returns:
@@ -231,6 +305,38 @@ func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[st
return nil 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 // Ensure logs directory exists
if errEnsure := l.ensureLogsDir(); errEnsure != nil { if errEnsure := l.ensureLogsDir(); errEnsure != nil {
return fmt.Errorf("failed to create logs directory: %w", errEnsure) 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 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 // Ensure logs directory exists
if err := l.ensureLogsDir(); err != nil { if err := l.ensureLogsDir(); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %w", err) return nil, fmt.Errorf("failed to create logs directory: %w", err)
@@ -1498,3 +1612,165 @@ func (w *NoOpStreamingLogWriter) SetFirstChunkTimestamp(_ time.Time) {}
// Returns: // Returns:
// - error: Always returns nil // - error: Always returns nil
func (w *NoOpStreamingLogWriter) Close() error { return 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)
}
@@ -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")
}
}
+3 -3
View File
@@ -17,9 +17,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
+27 -2
View File
@@ -6,8 +6,8 @@ import (
"strings" "strings"
"time" "time"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
) )
func init() { func init() {
@@ -66,6 +66,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
if !failed { if !failed {
failed = !resolveSuccess(ctx) failed = !resolveSuccess(ctx)
} }
fail := resolveFail(ctx, record, failed)
detail := requestDetail{ detail := requestDetail{
Timestamp: timestamp, Timestamp: timestamp,
@@ -74,6 +75,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
AuthIndex: record.AuthIndex, AuthIndex: record.AuthIndex,
Tokens: tokens, Tokens: tokens,
Failed: failed, Failed: failed,
Fail: fail,
} }
payload, err := json.Marshal(queuedUsageDetail{ payload, err := json.Marshal(queuedUsageDetail{
@@ -110,6 +112,7 @@ type requestDetail struct {
AuthIndex string `json:"auth_index"` AuthIndex string `json:"auth_index"`
Tokens tokenStats `json:"tokens"` Tokens tokenStats `json:"tokens"`
Failed bool `json:"failed"` Failed bool `json:"failed"`
Fail failDetail `json:"fail"`
} }
type tokenStats struct { type tokenStats struct {
@@ -120,6 +123,28 @@ type tokenStats struct {
TotalTokens int64 `json:"total_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 { func resolveSuccess(ctx context.Context) bool {
status := internallogging.GetResponseStatus(ctx) status := internallogging.GetResponseStatus(ctx)
if status == 0 { if status == 0 {
+43 -2
View File
@@ -9,8 +9,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
) )
func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) { func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
@@ -44,8 +44,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
requireStringField(t, payload, "alias", "client-gpt") requireStringField(t, payload, "alias", "client-gpt")
requireStringField(t, payload, "endpoint", "POST /v1/chat/completions") requireStringField(t, payload, "endpoint", "POST /v1/chat/completions")
requireStringField(t, payload, "auth_type", "apikey") requireStringField(t, payload, "auth_type", "apikey")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "ctx-request-id") requireStringField(t, payload, "request_id", "ctx-request-id")
requireBoolField(t, payload, "failed", false) requireBoolField(t, payload, "failed", false)
requireFailField(t, payload, http.StatusOK, "")
}) })
} }
@@ -67,6 +69,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t
Source: "user@example.com", Source: "user@example.com",
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC), RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
Latency: 2500 * time.Millisecond, Latency: 2500 * time.Millisecond,
Fail: coreusage.Failure{
StatusCode: http.StatusInternalServerError,
Body: "upstream failed",
},
Detail: coreusage.Detail{ Detail: coreusage.Detail{
InputTokens: 10, InputTokens: 10,
OutputTokens: 20, OutputTokens: 20,
@@ -80,8 +86,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t
requireStringField(t, payload, "alias", "client-mini") requireStringField(t, payload, "alias", "client-mini")
requireStringField(t, payload, "endpoint", "GET /v1/responses") requireStringField(t, payload, "endpoint", "GET /v1/responses")
requireStringField(t, payload, "auth_type", "apikey") requireStringField(t, payload, "auth_type", "apikey")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "gin-request-id") requireStringField(t, payload, "request_id", "gin-request-id")
requireBoolField(t, payload, "failed", true) 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", Source: "user@example.com",
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC), RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
Latency: 1500 * time.Millisecond, Latency: 1500 * time.Millisecond,
Fail: coreusage.Failure{
StatusCode: http.StatusBadGateway,
Body: "bad gateway",
},
Detail: coreusage.Detail{ Detail: coreusage.Detail{
InputTokens: 10, InputTokens: 10,
OutputTokens: 20, OutputTokens: 20,
@@ -123,8 +135,10 @@ func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) {
payload := waitForSinglePayload(t, 2*time.Second) payload := waitForSinglePayload(t, 2*time.Second)
requireStringField(t, payload, "endpoint", "POST /v1/chat/completions") requireStringField(t, payload, "endpoint", "POST /v1/chat/completions")
requireStringField(t, payload, "alias", "client-gpt") requireStringField(t, payload, "alias", "client-gpt")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "ctx-request-id") requireStringField(t, payload, "request_id", "ctx-request-id")
requireBoolField(t, payload, "failed", true) 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) type pluginFunc func(context.Context, coreusage.Record)
func (fn pluginFunc) HandleUsage(ctx context.Context, record 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) 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)
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"sync" "sync"
"time" "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" log "github.com/sirupsen/logrus"
) )
+14 -11
View File
@@ -13,14 +13,14 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay" "github.com/router-for-me/CLIProxyAPI/v7/internal/wsrelay"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
@@ -284,7 +284,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
processEvent := func(event wsrelay.StreamEvent) bool { processEvent := func(event wsrelay.StreamEvent) bool {
if event.Err != nil { if event.Err != nil {
helps.RecordAPIResponseError(ctx, e.cfg, event.Err) helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, event.Err)
select { select {
case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}: case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}:
case <-ctx.Done(): case <-ctx.Done():
@@ -336,7 +336,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
return false return false
case wsrelay.MessageTypeError: case wsrelay.MessageTypeError:
helps.RecordAPIResponseError(ctx, e.cfg, event.Err) helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, event.Err)
select { select {
case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}: case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}:
case <-ctx.Done(): 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). // 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 return auth, nil
} }
@@ -23,18 +23,18 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache" "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
antigravityclaude "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude" antigravityclaude "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
@@ -898,7 +898,7 @@ attemptLoop:
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, errScan)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} else { } else {
reporter.EnsurePublished(ctx) reporter.EnsurePublished(ctx)
@@ -1374,7 +1374,7 @@ attemptLoop:
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, errScan)
select { select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done(): case <-ctx.Done():
@@ -1402,6 +1402,9 @@ attemptLoop:
// Refresh refreshes the authentication credentials using the refresh token. // Refresh refreshes the authentication credentials using the refresh token.
func (e *AntigravityExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { 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 { if auth == nil {
return 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) 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()) updated, errRefresh := e.refreshToken(refreshCtx, auth.Clone())
if errRefresh != nil { if errRefresh != nil {
return "", nil, errRefresh return "", nil, errRefresh
@@ -6,7 +6,7 @@ import (
"io" "io"
"testing" "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) { func TestAntigravityBuildRequest_SanitizesGeminiToolSchema(t *testing.T) {
@@ -10,10 +10,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
) )
func resetAntigravityCreditsRetryState() { func resetAntigravityCreditsRetryState() {
@@ -10,10 +10,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache" "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
) )
func testGeminiSignaturePayload() string { func testGeminiSignaturePayload() string {
+15 -12
View File
@@ -17,16 +17,16 @@ import (
"github.com/andybalholm/brotli" "github.com/andybalholm/brotli"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
@@ -472,7 +472,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, errScan)
select { select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done(): case <-ctx.Done():
@@ -512,7 +512,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx) reporter.PublishFailure(ctx, errScan)
select { select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done(): 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) { func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
log.Debugf("claude executor: refresh called") log.Debugf("claude executor: refresh called")
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
return refreshed, err
}
if auth == nil { if auth == nil {
return nil, fmt.Errorf("claude executor: auth is nil") return nil, fmt.Errorf("claude executor: auth is nil")
} }
@@ -17,12 +17,12 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
xxHash64 "github.com/pierrec/xxHash/xxHash64" xxHash64 "github.com/pierrec/xxHash/xxHash64"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )

Some files were not shown because too many files have changed in this diff Show More