Merge branch 'v7' into dev
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user