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"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
sdkauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
sdkauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
+109 -32
View File
@@ -10,28 +10,31 @@ import (
"fmt"
"io"
"io/fs"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/store"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
"github.com/router-for-me/CLIProxyAPI/v6/internal/tui"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v7/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/store"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/tui"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
@@ -70,6 +73,8 @@ func main() {
var vertexImportPrefix string
var configPath string
var password string
var homeAddr string
var homePassword string
var tuiMode bool
var standalone bool
var localModel bool
@@ -88,6 +93,8 @@ func main() {
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)")
flag.StringVar(&password, "password", "", "")
flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port format (loads config from home and skips local config file)")
flag.StringVar(&homePassword, "home-password", "", "Home control plane password (Redis AUTH)")
flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI")
flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server")
flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching")
@@ -126,6 +133,7 @@ func main() {
var err error
var cfg *config.Config
var isCloudDeploy bool
var configLoadedFromHome bool
var (
usePostgresStore bool
pgStoreDSN string
@@ -236,7 +244,68 @@ func main() {
// Determine and load the configuration file.
// Prefer the Postgres store when configured, otherwise fallback to git or local files.
var configFilePath string
if usePostgresStore {
if strings.TrimSpace(homeAddr) != "" {
configLoadedFromHome = true
trimmedHomePassword := strings.TrimSpace(homePassword)
host, portStr, errSplit := net.SplitHostPort(strings.TrimSpace(homeAddr))
if errSplit != nil {
log.Errorf("invalid -home address %q (expected host:port): %v", homeAddr, errSplit)
return
}
host = strings.TrimSpace(host)
if host == "" {
log.Errorf("invalid -home address %q: host is empty", homeAddr)
return
}
port, errPort := strconv.Atoi(strings.TrimSpace(portStr))
if errPort != nil || port <= 0 {
log.Errorf("invalid -home address %q: invalid port %q", homeAddr, portStr)
return
}
homeCfg := config.HomeConfig{
Enabled: true,
Host: host,
Port: port,
Password: trimmedHomePassword,
}
homeClient := home.New(homeCfg)
defer homeClient.Close()
ctxHome, cancelHome := context.WithTimeout(context.Background(), 30*time.Second)
raw, errGetConfig := homeClient.GetConfig(ctxHome)
cancelHome()
if errGetConfig != nil {
log.Errorf("failed to fetch config from home: %v", errGetConfig)
return
}
parsed, errParseConfig := config.ParseConfigBytes(raw)
if errParseConfig != nil {
log.Errorf("failed to parse config payload from home: %v", errParseConfig)
return
}
if parsed == nil {
parsed = &config.Config{}
}
parsed.Home = homeCfg
parsed.Port = 8317 // Default to 8317 for home mode, can be overridden by home config
parsed.UsageStatisticsEnabled = true
cfg = parsed
// Keep a non-empty config path for downstream components (log paths, management assets, etc),
// but do not require the file to exist when loading config from home.
if strings.TrimSpace(configPath) != "" {
configFilePath = configPath
} else {
configFilePath = filepath.Join(wd, "config.yaml")
}
// Local stores are intentionally disabled when config is loaded from home.
usePostgresStore = false
useObjectStore = false
useGitStore = false
} else if usePostgresStore {
if pgStoreLocalPath == "" {
pgStoreLocalPath = wd
}
@@ -400,21 +469,25 @@ func main() {
// In cloud deploy mode, check if we have a valid configuration
var configFileExists bool
if isCloudDeploy {
if info, errStat := os.Stat(configFilePath); errStat != nil {
// Don't mislead: API server will not start until configuration is provided.
log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration")
configFileExists = false
} else if info.IsDir() {
log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration")
configFileExists = false
} else if cfg.Port == 0 {
// LoadConfigOptional returns empty config when file is empty or invalid.
// Config file exists but is empty or invalid; treat as missing config
log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration")
configFileExists = false
if configLoadedFromHome && cfg != nil {
configFileExists = cfg.Port != 0
} else {
log.Info("Cloud deploy mode: Configuration file detected; starting service")
configFileExists = true
if info, errStat := os.Stat(configFilePath); errStat != nil {
// Don't mislead: API server will not start until configuration is provided.
log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration")
configFileExists = false
} else if info.IsDir() {
log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration")
configFileExists = false
} else if cfg.Port == 0 {
// LoadConfigOptional returns empty config when file is empty or invalid.
// Config file exists but is empty or invalid; treat as missing config
log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration")
configFileExists = false
} else {
log.Info("Cloud deploy mode: Configuration file detected; starting service")
configFileExists = true
}
}
}
redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled)
@@ -496,8 +569,10 @@ func main() {
// Standalone mode: start an embedded local server and connect TUI client to it.
managementasset.StartAutoUpdater(context.Background(), configFilePath)
misc.StartAntigravityVersionUpdater(context.Background())
if !localModel {
if !localModel && !cfg.Home.Enabled {
registry.StartModelsUpdater(context.Background())
} else if cfg.Home.Enabled {
log.Info("Home mode: remote model updates disabled")
}
hook := tui.NewLogHook(2000)
hook.SetFormatter(&logging.LogFormatter{})
@@ -572,8 +647,10 @@ func main() {
// Start the main proxy service
managementasset.StartAutoUpdater(context.Background(), configFilePath)
misc.StartAntigravityVersionUpdater(context.Background())
if !localModel {
if !localModel && !cfg.Home.Enabled {
registry.StartModelsUpdater(context.Background())
} else if cfg.Home.Enabled {
log.Info("Home mode: remote model updates disabled")
}
cmd.StartService(cfg, configFilePath, password)
}
+12
View File
@@ -11,6 +11,13 @@ tls:
cert: ""
key: ""
# Optional "home" control plane integration over Redis protocol.
home:
enabled: false
host: "127.0.0.1"
port: 6379
password: ""
# Management API settings
remote-management:
# Whether to allow remote (non-localhost) management access.
@@ -67,6 +74,7 @@ error-logs-max-files: 10
usage-statistics-enabled: false
# How long (in seconds) Redis usage queue items are retained in memory for the RESP interface (LPOP/RPOP).
# Note: the in-process Redis RESP usage output is disabled when home.enabled is true.
# Default: 60. Max: 3600.
redis-usage-queue-retention-seconds: 60
@@ -149,6 +157,7 @@ nonstream-keepalive-interval: 0
# gemini-api-key:
# - api-key: "AIzaSy...01"
# prefix: "test" # optional: require calls like "test/gemini-3-pro-preview" to target this credential
# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling
# base-url: "https://generativelanguage.googleapis.com"
# headers:
# X-Custom-Header: "custom-value"
@@ -168,6 +177,7 @@ nonstream-keepalive-interval: 0
# codex-api-key:
# - api-key: "sk-atSM..."
# prefix: "test" # optional: require calls like "test/gpt-5-codex" to target this credential
# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling
# base-url: "https://www.example.com" # use the custom codex API endpoint
# headers:
# X-Custom-Header: "custom-value"
@@ -187,6 +197,7 @@ nonstream-keepalive-interval: 0
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
# - api-key: "sk-atSM..."
# prefix: "test" # optional: require calls like "test/claude-sonnet-latest" to target this credential
# disable-cooling: false # optional: per-auth override for auth/model cooldown scheduling
# base-url: "https://www.example.com" # use the custom claude API endpoint
# headers:
# X-Custom-Header: "custom-value"
@@ -242,6 +253,7 @@ nonstream-keepalive-interval: 0
# disabled: false # optional: set to true to disable this provider without removing it
# prefix: "test" # optional: require calls like "test/kimi-k2" to target this provider's credentials
# base-url: "https://openrouter.ai/api/v1" # The base URL of the provider.
# disable-cooling: false # optional: per-provider override for auth/model cooldown scheduling
# headers:
# X-Custom-Header: "custom-value"
# api-key-entries:
+8 -8
View File
@@ -24,14 +24,14 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/logging"
sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/logging"
sdktr "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
const (
+2 -2
View File
@@ -16,8 +16,8 @@ import (
"strings"
"time"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
_ "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator/builtin"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
_ "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator/builtin"
)
func main() {
+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
@@ -31,6 +31,12 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/redis/go-redis/v9 v9.19.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
)
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
+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/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@@ -158,6 +160,8 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -203,6 +207,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"net/http"
"strings"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
)
// Register ensures the config-access provider is available to the access manager.
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"sort"
"strings"
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
configaccess "github.com/router-for-me/CLIProxyAPI/v7/internal/access/config_access"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
log "github.com/sirupsen/logrus"
)
@@ -6,7 +6,7 @@ import (
"time"
"github.com/gin-gonic/gin"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
type apiKeyUsageEntry struct {
@@ -8,8 +8,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) {
@@ -11,10 +11,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/geminicli"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
@@ -5,9 +5,9 @@ import (
"net/http"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
)
func TestAPICallTransportDirectBypassesGlobalProxy(t *testing.T) {
+11 -11
View File
@@ -22,17 +22,17 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/antigravity"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/antigravity"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
geminiAuth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"golang.org/x/oauth2"
@@ -12,8 +12,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestUploadAuthFile_BatchMultipart(t *testing.T) {
@@ -11,8 +11,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestDeleteAuthFile_UsesAuthPathFromManager(t *testing.T) {
@@ -9,7 +9,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestDownloadAuthFile_ReturnsFile(t *testing.T) {
@@ -11,7 +11,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestDownloadAuthFile_PreventsWindowsSlashTraversal(t *testing.T) {
@@ -9,8 +9,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestPatchAuthFileFields_MergeHeadersAndDeleteEmptyValues(t *testing.T) {
@@ -8,8 +8,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) {
@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer"
)
type geminiKeyWithAuthIndex struct {
@@ -11,9 +11,9 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
@@ -6,7 +6,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
// Generic helpers for list[string]
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func writeTestConfigFile(t *testing.T) string {
+4 -4
View File
@@ -13,10 +13,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"golang.org/x/crypto/bcrypt"
)
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestAuthenticateManagementKey_LocalhostIPBan_BlocksCorrectKeyDuringBan(t *testing.T) {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
)
const (
@@ -5,7 +5,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
// GetStaticModelDefinitions returns static model metadata for a given channel.
@@ -4,7 +4,7 @@ import (
"context"
"sync"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
type memoryAuthStore struct {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
)
type usageQueueRecord []byte
@@ -7,7 +7,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
)
func TestGetUsageQueuePopsRequestedRecords(t *testing.T) {
@@ -9,8 +9,8 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// ImportVertexCredential handles uploading a Vertex service account JSON and saving it as an auth record.
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
)
const maxErrorOnlyCapturedRequestBodyBytes int64 = 1 << 20 // 1 MiB
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
)
const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE"
@@ -7,8 +7,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
)
func TestExtractRequestBodyPrefersOverride(t *testing.T) {
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"sync"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
log "github.com/sirupsen/logrus"
)
+4 -4
View File
@@ -9,10 +9,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
)
func TestAmpModule_Name(t *testing.T) {
@@ -8,8 +8,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -9,8 +9,8 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
func TestFallbackHandler_ModelMapping_PreservesThinkingSuffixAndRewritesResponse(t *testing.T) {
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"strings"
"sync"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
)
@@ -3,8 +3,8 @@ package amp
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
func TestNewModelMapper(t *testing.T) {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
// Helper: compress data with gzip
+7 -7
View File
@@ -9,11 +9,11 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai"
log "github.com/sirupsen/logrus"
)
@@ -21,12 +21,12 @@ import (
// from gin.Context to the request context for SecretSource lookup.
type clientAPIKeyContextKey struct{}
// clientAPIKeyMiddleware injects the authenticated client API key from gin.Context["apiKey"]
// clientAPIKeyMiddleware injects the authenticated client API key from gin.Context["userApiKey"]
// into the request context so that SecretSource can look it up for per-client upstream routing.
func clientAPIKeyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Extract the client API key from gin context (set by AuthMiddleware)
if apiKey, exists := c.Get("apiKey"); exists {
if apiKey, exists := c.Get("userApiKey"); exists {
if keyStr, ok := apiKey.(string); ok && keyStr != "" {
// Inject into request context for SecretSource.Get(ctx) to read
ctx := context.WithValue(c.Request.Context(), clientAPIKeyContextKey{}, keyStr)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
)
func TestRegisterManagementRoutes(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
)
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
)
// Context encapsulates the dependencies exposed to routing modules during
+6
View File
@@ -83,6 +83,12 @@ func (s *Server) acceptMuxConnections(listener net.Listener, httpListener *muxLi
}
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 errClose := conn.Close(); errClose != nil {
log.Errorf("failed to close redis connection while management is disabled: %v", errClose)
+7 -1
View File
@@ -10,7 +10,7 @@ import (
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
log "github.com/sirupsen/logrus"
)
@@ -45,6 +45,12 @@ func (s *Server) handleRedisConnection(conn net.Conn, reader *bufio.Reader) {
return true
}
if s.cfg != nil && s.cfg.Home.Enabled {
_ = writeRedisError(writer, "ERR redis usage output disabled in home mode")
_ = writer.Flush()
return
}
for {
if !s.managementRoutesEnabled.Load() {
return
@@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
)
type remoteAddrConn struct {
@@ -204,6 +204,43 @@ func TestRedisProtocol_ManagementDisabled_RejectsConnection(t *testing.T) {
}
}
func TestRedisProtocol_HomeEnabled_DisablesConnection(t *testing.T) {
t.Setenv("MANAGEMENT_PASSWORD", "test-management-password")
redisqueue.SetEnabled(false)
t.Cleanup(func() { redisqueue.SetEnabled(false) })
server := newTestServer(t)
if !server.managementRoutesEnabled.Load() {
t.Fatalf("expected managementRoutesEnabled to be true")
}
if server.cfg == nil {
t.Fatalf("expected server cfg to be non-nil")
}
server.cfg.Home.Enabled = true
redisqueue.SetEnabled(true)
addr, stop := startRedisMuxListener(t, server)
t.Cleanup(stop)
conn, errDial := net.DialTimeout("tcp", addr, time.Second)
if errDial != nil {
t.Fatalf("failed to dial redis listener: %v", errDial)
}
t.Cleanup(func() { _ = conn.Close() })
_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
_ = writeTestRESPCommand(conn, "PING")
buf := make([]byte, 1)
_, errRead := conn.Read(buf)
if errRead == nil {
t.Fatalf("expected connection to be closed when home mode is enabled")
}
if ne, ok := errRead.(net.Error); ok && ne.Timeout() {
t.Fatalf("expected connection to be closed when home mode is enabled, got timeout: %v", errRead)
}
}
func TestRedisProtocol_AUTH_And_PopContracts(t *testing.T) {
const managementPassword = "test-management-password"
+244 -27
View File
@@ -8,6 +8,7 @@ import (
"context"
"crypto/subtle"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
@@ -15,30 +16,32 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/access"
managementHandlers "github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/middleware"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules"
ampmodule "github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules/amp"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/access"
managementHandlers "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api/middleware"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules"
ampmodule "github.com/router-for-me/CLIProxyAPI/v7/internal/api/modules/amp"
"github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/claude"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/gemini"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"gopkg.in/yaml.v3"
@@ -64,7 +67,9 @@ type ServerOption func(*serverOptionConfig)
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
configDir := filepath.Dir(configPath)
logsDir := logging.ResolveLogDirectory(cfg)
return logging.NewFileRequestLogger(cfg.RequestLog, logsDir, configDir, cfg.ErrorLogsMaxFiles)
logger := logging.NewFileRequestLogger(cfg.RequestLog, logsDir, configDir, cfg.ErrorLogsMaxFiles)
logger.SetHomeEnabled(cfg != nil && cfg.Home.Enabled)
return logger
}
// WithMiddleware appends additional Gin middleware during server construction.
@@ -284,6 +289,10 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
}
s.localPassword = optionState.localPassword
// Home heartbeat gate: when home is enabled, block all endpoints with 503 until the
// subscribe-config heartbeat connection is healthy.
engine.Use(s.homeHeartbeatMiddleware())
// Setup routes
s.setupRoutes()
@@ -308,7 +317,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
// or when a local management password is provided (e.g. TUI mode).
hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != ""
s.managementRoutesEnabled.Store(hasManagementSecret)
redisqueue.SetEnabled(hasManagementSecret)
redisqueue.SetEnabled(hasManagementSecret || (cfg != nil && cfg.Home.Enabled))
if hasManagementSecret {
s.registerManagementRoutes()
}
@@ -326,6 +335,28 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
return s
}
func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if s == nil || s.cfg == nil || !s.cfg.Home.Enabled {
c.Next()
return
}
if c != nil && c.Request != nil {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/v0/management/") || path == "/v0/management" || path == "/management.html" {
c.Next()
return
}
}
client := home.Current()
if client == nil || !client.HeartbeatOK() {
c.AbortWithStatus(http.StatusServiceUnavailable)
return
}
c.Next()
}
}
// setupRoutes configures the API routes for the server.
// It defines the endpoints and associates them with their respective handlers.
func (s *Server) setupRoutes() {
@@ -661,6 +692,14 @@ func (s *Server) registerManagementRoutes() {
func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if s == nil || s.cfg == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
if s.cfg.Home.Enabled {
c.AbortWithStatus(http.StatusNotFound)
return
}
if !s.managementRoutesEnabled.Load() {
c.AbortWithStatus(http.StatusNotFound)
return
@@ -671,7 +710,7 @@ func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
func (s *Server) serveManagementControlPanel(c *gin.Context) {
cfg := s.cfg
if cfg == nil || cfg.RemoteManagement.DisableControlPanel {
if cfg == nil || cfg.Home.Enabled || cfg.RemoteManagement.DisableControlPanel {
c.AbortWithStatus(http.StatusNotFound)
return
}
@@ -783,6 +822,11 @@ func (s *Server) watchKeepAlive() {
// otherwise it routes to OpenAI handler.
func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, claudeHandler *claude.ClaudeCodeAPIHandler) gin.HandlerFunc {
return func(c *gin.Context) {
if s != nil && s.cfg != nil && s.cfg.Home.Enabled {
s.handleHomeModels(c)
return
}
userAgent := c.GetHeader("User-Agent")
// Route to Claude handler if User-Agent starts with "claude-cli"
@@ -796,6 +840,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.
// 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 err := logging.ConfigureLogOutput(cfg); err != nil {
log.Errorf("failed to reconfigure log output: %v", err)
@@ -1061,7 +1275,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
s.managementRoutesEnabled.Store(!newSecretEmpty)
}
}
redisqueue.SetEnabled(s.managementRoutesEnabled.Load())
redisqueue.SetEnabled(s.managementRoutesEnabled.Load() || (cfg != nil && cfg.Home.Enabled))
s.applyAccessConfig(oldCfg, cfg)
s.cfg = cfg
@@ -1094,11 +1308,14 @@ func (s *Server) UpdateClients(cfg *config.Config) {
}
// Count client sources from configuration and auth store.
tokenStore := sdkAuth.GetTokenStore()
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir)
authEntries := 0
if cfg != nil && !cfg.Home.Enabled {
tokenStore := sdkAuth.GetTokenStore()
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir)
}
authEntries = util.CountAuthFiles(context.Background(), tokenStore)
}
authEntries := util.CountAuthFiles(context.Background(), tokenStore)
geminiAPIKeyCount := len(cfg.GeminiKey)
claudeAPIKeyCount := len(cfg.ClaudeKey)
codexAPIKeyCount := len(cfg.CodexKey)
@@ -1146,7 +1363,7 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc {
result, err := manager.Authenticate(c.Request.Context(), c.Request)
if err == nil {
if result != nil {
c.Set("apiKey", result.Principal)
c.Set("userApiKey", result.Principal)
c.Set("accessProvider", result.Provider)
if len(result.Metadata) > 0 {
c.Set("accessMetadata", result.Metadata)
+32 -6
View File
@@ -11,12 +11,12 @@ import (
"time"
gin "github.com/gin-gonic/gin"
proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
proxyconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
)
func newTestServer(t *testing.T) *Server {
@@ -147,6 +147,32 @@ func TestManagementUsageRequiresManagementAuthAndPopsArray(t *testing.T) {
}
}
func TestHomeEnabledHidesManagementEndpointsAndControlPanel(t *testing.T) {
t.Setenv("MANAGEMENT_PASSWORD", "test-management-key")
server := newTestServer(t)
server.cfg.Home.Enabled = true
t.Run("management endpoints return 404", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v0/management/config", nil)
req.Header.Set("Authorization", "Bearer test-management-key")
rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusNotFound, rr.Body.String())
}
})
t.Run("management control panel returns 404", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/management.html", nil)
rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusNotFound, rr.Body.String())
}
})
}
func TestAmpProviderModelRoutes(t *testing.T) {
testCases := []struct {
name string
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
)
@@ -3,7 +3,7 @@ package claude
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"golang.org/x/net/proxy"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os"
"path/filepath"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
)
// ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication.
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"sync"
tls "github.com/refraction-networking/utls"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/proxy"
+2 -2
View File
@@ -14,8 +14,8 @@ import (
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os"
"path/filepath"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
)
// CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication.
+6 -6
View File
@@ -13,12 +13,12 @@ import (
"net/http"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/browser"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus"
)
+2 -2
View File
@@ -15,8 +15,8 @@ import (
"time"
"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"net/http"
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"path/filepath"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
)
// KimiTokenStorage stores OAuth2 token information for Kimi API authentication.
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os"
"path/filepath"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus"
)
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt"
"os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus"
)
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package cmd
import (
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
)
// newAuthManager creates a new authentication manager instance with all supported
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus"
)
+6 -6
View File
@@ -17,12 +17,12 @@ import (
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt"
"os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus"
)
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"fmt"
"os"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
log "github.com/sirupsen/logrus"
)
+3 -3
View File
@@ -10,9 +10,9 @@ import (
"syscall"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy"
log "github.com/sirupsen/logrus"
)
+5 -5
View File
@@ -9,11 +9,11 @@ import (
"os"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/vertex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
+16 -7
View File
@@ -13,7 +13,7 @@ import (
"strings"
"syscall"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
@@ -37,6 +37,9 @@ type Config struct {
// TLS config controls HTTPS server settings.
TLS TLSConfig `yaml:"tls" json:"tls"`
// Home config enables the Redis-based control plane integration.
Home HomeConfig `yaml:"home" json:"-"`
// RemoteManagement nests management-related options under 'remote-management'.
RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"`
@@ -224,12 +227,6 @@ type RoutingConfig struct {
// Supported values: "round-robin" (default), "fill-first".
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
// ClaudeCodeSessionAffinity enables session-sticky routing for Claude Code clients.
// When enabled, requests with the same session ID (extracted from metadata.user_id)
// are routed to the same auth credential when available.
// Deprecated: Use SessionAffinity instead for universal session support.
ClaudeCodeSessionAffinity bool `yaml:"claude-code-session-affinity,omitempty" json:"claude-code-session-affinity,omitempty"`
// SessionAffinity enables universal session-sticky routing for all clients.
// Session IDs are extracted from multiple sources:
// metadata.user_id (Claude Code session format), X-Session-ID, Session_id (Codex),
@@ -401,6 +398,9 @@ type ClaudeKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"`
// DisableCooling disables auth/model cooldown scheduling for this credential when true.
DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"`
// Cloak configures request cloaking for non-Claude-Code clients.
Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"`
@@ -456,6 +456,9 @@ type CodexKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"`
// DisableCooling disables auth/model cooldown scheduling for this credential when true.
DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"`
}
func (k CodexKey) GetAPIKey() string { return k.APIKey }
@@ -500,6 +503,9 @@ type GeminiKey struct {
// ExcludedModels lists model IDs that should be excluded for this provider.
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"`
// DisableCooling disables auth/model cooldown scheduling for this credential when true.
DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"`
}
func (k GeminiKey) GetAPIKey() string { return k.APIKey }
@@ -544,6 +550,9 @@ type OpenAICompatibility struct {
// Headers optionally adds extra HTTP headers for requests sent to this provider.
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
// DisableCooling disables auth/model cooldown scheduling for this provider when true.
DisableCooling bool `yaml:"disable-cooling,omitempty" json:"disable-cooling,omitempty"`
}
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
+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.
package interfaces
import sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
import sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
// Backwards compatible aliases for translator function types.
type TranslateRequestFunc = sdktranslator.RequestTransform
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
)
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"sync"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
+279 -3
View File
@@ -8,6 +8,8 @@ import (
"bytes"
"compress/flate"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"os"
@@ -22,13 +24,23 @@ import (
"github.com/klauspost/compress/zstd"
log "github.com/sirupsen/logrus"
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
)
var requestLogID atomic.Uint64
type homeRequestLogClient interface {
HeartbeatOK() bool
RPushRequestLog(ctx context.Context, payload []byte) error
}
var currentHomeRequestLogClient = func() homeRequestLogClient {
return home.Current()
}
// RequestLogger defines the interface for logging HTTP requests and responses.
// It provides methods for logging both regular and streaming HTTP request/response cycles.
type RequestLogger interface {
@@ -148,6 +160,58 @@ type FileRequestLogger struct {
// errorLogsMaxFiles limits the number of error log files retained.
errorLogsMaxFiles int
homeEnabled bool
}
type homeRequestLogPayload struct {
Headers map[string][]string `json:"headers,omitempty"`
RequestLog string `json:"request_log,omitempty"`
}
func cloneHeaders(headers map[string][]string) map[string][]string {
if len(headers) == 0 {
return nil
}
out := make(map[string][]string, len(headers))
for key, values := range headers {
if strings.TrimSpace(key) == "" {
continue
}
if values == nil {
out[key] = nil
continue
}
copied := make([]string, len(values))
copy(copied, values)
out[key] = copied
}
if len(out) == 0 {
return nil
}
return out
}
func (l *FileRequestLogger) forwardRequestLogToHome(ctx context.Context, headers map[string][]string, logText string) error {
if l == nil || !l.homeEnabled {
return nil
}
client := currentHomeRequestLogClient()
if client == nil || !client.HeartbeatOK() {
return nil
}
payload := homeRequestLogPayload{
Headers: cloneHeaders(headers),
RequestLog: logText,
}
raw, errMarshal := json.Marshal(&payload)
if errMarshal != nil {
return errMarshal
}
if ctx == nil {
ctx = context.Background()
}
return client.RPushRequestLog(ctx, raw)
}
// NewFileRequestLogger creates a new file-based request logger.
@@ -173,9 +237,19 @@ func NewFileRequestLogger(enabled bool, logsDir string, configDir string, errorL
enabled: enabled,
logsDir: logsDir,
errorLogsMaxFiles: errorLogsMaxFiles,
homeEnabled: false,
}
}
// SetHomeEnabled toggles home request-log forwarding.
// When enabled, request logs are not written to disk and are instead forwarded to home via Redis RESP.
func (l *FileRequestLogger) SetHomeEnabled(enabled bool) {
if l == nil {
return
}
l.homeEnabled = enabled
}
// IsEnabled returns whether request logging is currently enabled.
//
// Returns:
@@ -231,6 +305,38 @@ func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[st
return nil
}
if l.homeEnabled && l.enabled {
responseToWrite, decompressErr := l.decompressResponse(responseHeaders, response)
if decompressErr != nil {
responseToWrite = response
}
var buf bytes.Buffer
writeErr := l.writeNonStreamingLog(
&buf,
url,
method,
requestHeaders,
body,
"",
websocketTimeline,
apiRequest,
apiResponse,
apiWebsocketTimeline,
apiResponseErrors,
statusCode,
responseHeaders,
responseToWrite,
decompressErr,
requestTimestamp,
apiResponseTimestamp,
)
if writeErr != nil {
return fmt.Errorf("failed to build request log content: %w", writeErr)
}
return l.forwardRequestLogToHome(context.Background(), requestHeaders, buf.String())
}
// Ensure logs directory exists
if errEnsure := l.ensureLogsDir(); errEnsure != nil {
return fmt.Errorf("failed to create logs directory: %w", errEnsure)
@@ -321,6 +427,14 @@ func (l *FileRequestLogger) LogStreamingRequest(url, method string, headers map[
return &NoOpStreamingLogWriter{}, nil
}
if l.homeEnabled {
client := home.Current()
if client == nil || !client.HeartbeatOK() {
return &NoOpStreamingLogWriter{}, nil
}
return newHomeStreamingLogWriter(url, method, headers, body, requestID), nil
}
// Ensure logs directory exists
if err := l.ensureLogsDir(); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %w", err)
@@ -1498,3 +1612,165 @@ func (w *NoOpStreamingLogWriter) SetFirstChunkTimestamp(_ time.Time) {}
// Returns:
// - error: Always returns nil
func (w *NoOpStreamingLogWriter) Close() error { return nil }
type homeStreamingLogWriter struct {
url string
method string
timestamp time.Time
requestHeaders map[string][]string
requestBody []byte
chunkChan chan []byte
doneChan chan struct{}
responseStatus int
statusWritten bool
responseHeaders map[string][]string
responseBody bytes.Buffer
apiRequest []byte
apiResponse []byte
apiWebsocketTime []byte
apiResponseTS time.Time
firstChunkTS time.Time
}
func newHomeStreamingLogWriter(url, method string, headers map[string][]string, body []byte, _ string) *homeStreamingLogWriter {
requestHeaders := make(map[string][]string, len(headers))
for key, values := range headers {
headerValues := make([]string, len(values))
copy(headerValues, values)
requestHeaders[key] = headerValues
}
writer := &homeStreamingLogWriter{
url: url,
method: method,
timestamp: time.Now(),
requestHeaders: requestHeaders,
requestBody: append([]byte(nil), body...),
chunkChan: make(chan []byte, 100),
doneChan: make(chan struct{}),
}
go writer.asyncWriter()
return writer
}
func (w *homeStreamingLogWriter) asyncWriter() {
defer close(w.doneChan)
for chunk := range w.chunkChan {
if len(chunk) == 0 {
continue
}
_, _ = w.responseBody.Write(chunk)
}
}
func (w *homeStreamingLogWriter) WriteChunkAsync(chunk []byte) {
if w == nil || w.chunkChan == nil || len(chunk) == 0 {
return
}
select {
case w.chunkChan <- append([]byte(nil), chunk...):
default:
}
}
func (w *homeStreamingLogWriter) WriteStatus(status int, headers map[string][]string) error {
if w == nil || status == 0 {
return nil
}
w.responseStatus = status
w.statusWritten = true
if headers != nil {
w.responseHeaders = make(map[string][]string, len(headers))
for key, values := range headers {
copied := make([]string, len(values))
copy(copied, values)
w.responseHeaders[key] = copied
}
}
return nil
}
func (w *homeStreamingLogWriter) WriteAPIRequest(apiRequest []byte) error {
if w == nil || len(apiRequest) == 0 {
return nil
}
w.apiRequest = bytes.Clone(apiRequest)
return nil
}
func (w *homeStreamingLogWriter) WriteAPIResponse(apiResponse []byte) error {
if w == nil || len(apiResponse) == 0 {
return nil
}
w.apiResponse = bytes.Clone(apiResponse)
return nil
}
func (w *homeStreamingLogWriter) WriteAPIWebsocketTimeline(apiWebsocketTimeline []byte) error {
if w == nil || len(apiWebsocketTimeline) == 0 {
return nil
}
w.apiWebsocketTime = bytes.Clone(apiWebsocketTimeline)
return nil
}
func (w *homeStreamingLogWriter) SetFirstChunkTimestamp(timestamp time.Time) {
if w == nil {
return
}
if !timestamp.IsZero() {
w.firstChunkTS = timestamp
w.apiResponseTS = timestamp
}
}
func (w *homeStreamingLogWriter) Close() error {
if w == nil {
return nil
}
client := currentHomeRequestLogClient()
if client == nil || !client.HeartbeatOK() {
return nil
}
if w.chunkChan != nil {
close(w.chunkChan)
<-w.doneChan
w.chunkChan = nil
}
responsePayload := w.responseBody.Bytes()
var buf bytes.Buffer
upstreamTransport := inferUpstreamTransport(w.apiRequest, w.apiResponse, w.apiWebsocketTime, nil)
if errWrite := writeRequestInfoWithBody(&buf, w.url, w.method, w.requestHeaders, w.requestBody, "", w.timestamp, "http", upstreamTransport, true); errWrite != nil {
return errWrite
}
if errWrite := writeAPISection(&buf, "=== API WEBSOCKET TIMELINE ===\n", "=== API WEBSOCKET TIMELINE", w.apiWebsocketTime, time.Time{}); errWrite != nil {
return errWrite
}
if errWrite := writeAPISection(&buf, "=== API REQUEST ===\n", "=== API REQUEST", w.apiRequest, time.Time{}); errWrite != nil {
return errWrite
}
if errWrite := writeAPISection(&buf, "=== API RESPONSE ===\n", "=== API RESPONSE", w.apiResponse, w.apiResponseTS); errWrite != nil {
return errWrite
}
if errWrite := writeResponseSection(&buf, w.responseStatus, w.statusWritten, w.responseHeaders, bytes.NewReader(responsePayload), nil, false); errWrite != nil {
return errWrite
}
payload := homeRequestLogPayload{
Headers: cloneHeaders(w.requestHeaders),
RequestLog: buf.String(),
}
raw, errMarshal := json.Marshal(&payload)
if errMarshal != nil {
return errMarshal
}
return client.RPushRequestLog(context.Background(), raw)
}
@@ -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"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
)
+27 -2
View File
@@ -6,8 +6,8 @@ import (
"strings"
"time"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
)
func init() {
@@ -66,6 +66,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
if !failed {
failed = !resolveSuccess(ctx)
}
fail := resolveFail(ctx, record, failed)
detail := requestDetail{
Timestamp: timestamp,
@@ -74,6 +75,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
AuthIndex: record.AuthIndex,
Tokens: tokens,
Failed: failed,
Fail: fail,
}
payload, err := json.Marshal(queuedUsageDetail{
@@ -110,6 +112,7 @@ type requestDetail struct {
AuthIndex string `json:"auth_index"`
Tokens tokenStats `json:"tokens"`
Failed bool `json:"failed"`
Fail failDetail `json:"fail"`
}
type tokenStats struct {
@@ -120,6 +123,28 @@ type tokenStats struct {
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 {
status := internallogging.GetResponseStatus(ctx)
if status == 0 {
+43 -2
View File
@@ -9,8 +9,8 @@ import (
"time"
"github.com/gin-gonic/gin"
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
)
func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
@@ -44,8 +44,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
requireStringField(t, payload, "alias", "client-gpt")
requireStringField(t, payload, "endpoint", "POST /v1/chat/completions")
requireStringField(t, payload, "auth_type", "apikey")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "ctx-request-id")
requireBoolField(t, payload, "failed", false)
requireFailField(t, payload, http.StatusOK, "")
})
}
@@ -67,6 +69,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t
Source: "user@example.com",
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
Latency: 2500 * time.Millisecond,
Fail: coreusage.Failure{
StatusCode: http.StatusInternalServerError,
Body: "upstream failed",
},
Detail: coreusage.Detail{
InputTokens: 10,
OutputTokens: 20,
@@ -80,8 +86,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t
requireStringField(t, payload, "alias", "client-mini")
requireStringField(t, payload, "endpoint", "GET /v1/responses")
requireStringField(t, payload, "auth_type", "apikey")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "gin-request-id")
requireBoolField(t, payload, "failed", true)
requireFailField(t, payload, http.StatusInternalServerError, "upstream failed")
})
}
@@ -113,6 +121,10 @@ func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) {
Source: "user@example.com",
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
Latency: 1500 * time.Millisecond,
Fail: coreusage.Failure{
StatusCode: http.StatusBadGateway,
Body: "bad gateway",
},
Detail: coreusage.Detail{
InputTokens: 10,
OutputTokens: 20,
@@ -123,8 +135,10 @@ func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) {
payload := waitForSinglePayload(t, 2*time.Second)
requireStringField(t, payload, "endpoint", "POST /v1/chat/completions")
requireStringField(t, payload, "alias", "client-gpt")
requireMissingField(t, payload, "user_api_key")
requireStringField(t, payload, "request_id", "ctx-request-id")
requireBoolField(t, payload, "failed", true)
requireFailField(t, payload, http.StatusBadGateway, "bad gateway")
})
}
@@ -214,6 +228,14 @@ func requireStringField(t *testing.T, payload map[string]json.RawMessage, key, w
}
}
func requireMissingField(t *testing.T, payload map[string]json.RawMessage, key string) {
t.Helper()
if _, ok := payload[key]; ok {
t.Fatalf("payload unexpectedly contains %q", key)
}
}
type pluginFunc func(context.Context, coreusage.Record)
func (fn pluginFunc) HandleUsage(ctx context.Context, record coreusage.Record) {
@@ -235,3 +257,22 @@ func requireBoolField(t *testing.T, payload map[string]json.RawMessage, key stri
t.Fatalf("%s = %t, want %t", key, got, want)
}
}
func requireFailField(t *testing.T, payload map[string]json.RawMessage, wantStatus int, wantBody string) {
t.Helper()
raw, ok := payload["fail"]
if !ok {
t.Fatalf("payload missing %q", "fail")
}
var got struct {
StatusCode int `json:"status_code"`
Body string `json:"body"`
}
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatalf("unmarshal fail: %v", err)
}
if got.StatusCode != wantStatus || got.Body != wantBody {
t.Fatalf("fail = {status_code:%d body:%q}, want {status_code:%d body:%q}", got.StatusCode, got.Body, wantStatus, wantBody)
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"sync"
"time"
misc "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
misc "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus"
)
+14 -11
View File
@@ -13,14 +13,14 @@ import (
"net/url"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/wsrelay"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -284,7 +284,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
processEvent := func(event wsrelay.StreamEvent) bool {
if event.Err != nil {
helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, event.Err)
select {
case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}:
case <-ctx.Done():
@@ -336,7 +336,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
return false
case wsrelay.MessageTypeError:
helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, event.Err)
select {
case out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}:
case <-ctx.Done():
@@ -414,7 +414,10 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
}
// Refresh refreshes the authentication credentials (no-op for AI Studio).
func (e *AIStudioExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
func (e *AIStudioExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
return refreshed, err
}
return auth, nil
}
@@ -23,18 +23,18 @@ import (
"time"
"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
antigravityclaude "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
antigravityclaude "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/antigravity/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -898,7 +898,7 @@ attemptLoop:
}
if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, errScan)
out <- cliproxyexecutor.StreamChunk{Err: errScan}
} else {
reporter.EnsurePublished(ctx)
@@ -1374,7 +1374,7 @@ attemptLoop:
}
if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, errScan)
select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done():
@@ -1402,6 +1402,9 @@ attemptLoop:
// Refresh refreshes the authentication credentials using the refresh token.
func (e *AntigravityExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
return refreshed, err
}
if auth == nil {
return auth, nil
}
@@ -1589,6 +1592,18 @@ func (e *AntigravityExecutor) ensureAccessToken(ctx context.Context, auth *clipr
refreshCtx = context.WithValue(refreshCtx, "cliproxy.roundtripper", rt)
}
}
if refreshed, handled, err := helps.RefreshAuthViaHome(refreshCtx, e.cfg, auth); handled {
if err != nil {
return "", nil, err
}
token := metaStringValue(refreshed.Metadata, "access_token")
if strings.TrimSpace(token) == "" {
return "", nil, statusErr{code: http.StatusUnauthorized, msg: "missing access token"}
}
e.maybeRefreshAntigravityCreditsHint(ctx, refreshed, token)
return token, refreshed, nil
}
updated, errRefresh := e.refreshToken(refreshCtx, auth.Clone())
if errRefresh != nil {
return "", nil, errRefresh
@@ -6,7 +6,7 @@ import (
"io"
"testing"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestAntigravityBuildRequest_SanitizesGeminiToolSchema(t *testing.T) {
@@ -10,10 +10,10 @@ import (
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
func resetAntigravityCreditsRetryState() {
@@ -10,10 +10,10 @@ import (
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
func testGeminiSignaturePayload() string {
+15 -12
View File
@@ -17,16 +17,16 @@ import (
"github.com/andybalholm/brotli"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -472,7 +472,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
}
if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, errScan)
select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done():
@@ -512,7 +512,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
}
if errScan := scanner.Err(); errScan != nil {
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.PublishFailure(ctx)
reporter.PublishFailure(ctx, errScan)
select {
case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
case <-ctx.Done():
@@ -691,6 +691,9 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
log.Debugf("claude executor: refresh called")
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
return refreshed, err
}
if auth == nil {
return nil, fmt.Errorf("claude executor: auth is nil")
}
@@ -17,12 +17,12 @@ import (
"github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd"
xxHash64 "github.com/pierrec/xxHash/xxHash64"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

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