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