mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 12:50:51 +08:00
feat(config): add support for model prefixes and prefix normalization
Refactor model management to include an optional `prefix` field for model credentials, enabling better namespace handling. Update affected configuration files, APIs, and handlers to support prefix normalization and routing. Remove unused OpenAI compatibility provider logic to simplify processing.
This commit is contained in:
@@ -48,6 +48,9 @@ usage-statistics-enabled: false
|
|||||||
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
|
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
|
||||||
proxy-url: ""
|
proxy-url: ""
|
||||||
|
|
||||||
|
# When true, unprefixed model requests only use credentials without a prefix (except when prefix == model name).
|
||||||
|
force-model-prefix: false
|
||||||
|
|
||||||
# Number of times to retry a request. Retries will occur if the HTTP response code is 403, 408, 500, 502, 503, or 504.
|
# Number of times to retry a request. Retries will occur if the HTTP response code is 403, 408, 500, 502, 503, or 504.
|
||||||
request-retry: 3
|
request-retry: 3
|
||||||
|
|
||||||
@@ -65,6 +68,7 @@ ws-auth: false
|
|||||||
# Gemini API keys
|
# Gemini API keys
|
||||||
# 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
|
||||||
# base-url: "https://generativelanguage.googleapis.com"
|
# base-url: "https://generativelanguage.googleapis.com"
|
||||||
# headers:
|
# headers:
|
||||||
# X-Custom-Header: "custom-value"
|
# X-Custom-Header: "custom-value"
|
||||||
@@ -79,6 +83,7 @@ ws-auth: false
|
|||||||
# Codex API keys
|
# Codex API keys
|
||||||
# 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
|
||||||
# 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"
|
||||||
@@ -93,6 +98,7 @@ ws-auth: false
|
|||||||
# claude-api-key:
|
# claude-api-key:
|
||||||
# - 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
|
||||||
# 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"
|
||||||
@@ -109,6 +115,7 @@ ws-auth: false
|
|||||||
# OpenAI compatibility providers
|
# OpenAI compatibility providers
|
||||||
# openai-compatibility:
|
# openai-compatibility:
|
||||||
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
||||||
|
# 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.
|
||||||
# headers:
|
# headers:
|
||||||
# X-Custom-Header: "custom-value"
|
# X-Custom-Header: "custom-value"
|
||||||
@@ -123,6 +130,7 @@ ws-auth: false
|
|||||||
# Vertex API keys (Vertex-compatible endpoints, use API key + base URL)
|
# Vertex API keys (Vertex-compatible endpoints, use API key + base URL)
|
||||||
# vertex-api-key:
|
# vertex-api-key:
|
||||||
# - api-key: "vk-123..." # x-goog-api-key header
|
# - api-key: "vk-123..." # x-goog-api-key header
|
||||||
|
# prefix: "test" # optional: require calls like "test/vertex-pro" to target this credential
|
||||||
# base-url: "https://example.com/api" # e.g. https://zenmux.ai/api
|
# base-url: "https://example.com/api" # e.g. https://zenmux.ai/api
|
||||||
# proxy-url: "socks5://proxy.example.com:1080" # optional per-key proxy override
|
# proxy-url: "socks5://proxy.example.com:1080" # optional per-key proxy override
|
||||||
# headers:
|
# headers:
|
||||||
|
|||||||
@@ -230,13 +230,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
|
|||||||
envManagementSecret := envAdminPasswordSet && envAdminPassword != ""
|
envManagementSecret := envAdminPasswordSet && envAdminPassword != ""
|
||||||
|
|
||||||
// Create server instance
|
// Create server instance
|
||||||
providerNames := make([]string, 0, len(cfg.OpenAICompatibility))
|
|
||||||
for _, p := range cfg.OpenAICompatibility {
|
|
||||||
providerNames = append(providerNames, p.Name)
|
|
||||||
}
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
engine: engine,
|
engine: engine,
|
||||||
handlers: handlers.NewBaseAPIHandlers(&cfg.SDKConfig, authManager, providerNames),
|
handlers: handlers.NewBaseAPIHandlers(&cfg.SDKConfig, authManager),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
accessManager: accessManager,
|
accessManager: accessManager,
|
||||||
requestLogger: requestLogger,
|
requestLogger: requestLogger,
|
||||||
@@ -919,12 +915,6 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
// Save YAML snapshot for next comparison
|
// Save YAML snapshot for next comparison
|
||||||
s.oldConfigYaml, _ = yaml.Marshal(cfg)
|
s.oldConfigYaml, _ = yaml.Marshal(cfg)
|
||||||
|
|
||||||
providerNames := make([]string, 0, len(cfg.OpenAICompatibility))
|
|
||||||
for _, p := range cfg.OpenAICompatibility {
|
|
||||||
providerNames = append(providerNames, p.Name)
|
|
||||||
}
|
|
||||||
s.handlers.OpenAICompatProviders = providerNames
|
|
||||||
|
|
||||||
s.handlers.UpdateClients(&cfg.SDKConfig)
|
s.handlers.UpdateClients(&cfg.SDKConfig)
|
||||||
|
|
||||||
if !cfg.RemoteManagement.DisableControlPanel {
|
if !cfg.RemoteManagement.DisableControlPanel {
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ type ClaudeKey struct {
|
|||||||
// APIKey is the authentication key for accessing Claude API services.
|
// APIKey is the authentication key for accessing Claude API services.
|
||||||
APIKey string `yaml:"api-key" json:"api-key"`
|
APIKey string `yaml:"api-key" json:"api-key"`
|
||||||
|
|
||||||
|
// Prefix optionally namespaces models for this credential (e.g., "teamA/claude-sonnet-4").
|
||||||
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
|
||||||
|
|
||||||
// BaseURL is the base URL for the Claude API endpoint.
|
// BaseURL is the base URL for the Claude API endpoint.
|
||||||
// If empty, the default Claude API URL will be used.
|
// If empty, the default Claude API URL will be used.
|
||||||
BaseURL string `yaml:"base-url" json:"base-url"`
|
BaseURL string `yaml:"base-url" json:"base-url"`
|
||||||
@@ -219,6 +222,9 @@ type CodexKey struct {
|
|||||||
// APIKey is the authentication key for accessing Codex API services.
|
// APIKey is the authentication key for accessing Codex API services.
|
||||||
APIKey string `yaml:"api-key" json:"api-key"`
|
APIKey string `yaml:"api-key" json:"api-key"`
|
||||||
|
|
||||||
|
// Prefix optionally namespaces models for this credential (e.g., "teamA/gpt-5-codex").
|
||||||
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
|
||||||
|
|
||||||
// BaseURL is the base URL for the Codex API endpoint.
|
// BaseURL is the base URL for the Codex API endpoint.
|
||||||
// If empty, the default Codex API URL will be used.
|
// If empty, the default Codex API URL will be used.
|
||||||
BaseURL string `yaml:"base-url" json:"base-url"`
|
BaseURL string `yaml:"base-url" json:"base-url"`
|
||||||
@@ -239,6 +245,9 @@ type GeminiKey struct {
|
|||||||
// APIKey is the authentication key for accessing Gemini API services.
|
// APIKey is the authentication key for accessing Gemini API services.
|
||||||
APIKey string `yaml:"api-key" json:"api-key"`
|
APIKey string `yaml:"api-key" json:"api-key"`
|
||||||
|
|
||||||
|
// Prefix optionally namespaces models for this credential (e.g., "teamA/gemini-3-pro-preview").
|
||||||
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
|
||||||
|
|
||||||
// BaseURL optionally overrides the Gemini API endpoint.
|
// BaseURL optionally overrides the Gemini API endpoint.
|
||||||
BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"`
|
BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"`
|
||||||
|
|
||||||
@@ -258,6 +267,9 @@ type OpenAICompatibility struct {
|
|||||||
// Name is the identifier for this OpenAI compatibility configuration.
|
// Name is the identifier for this OpenAI compatibility configuration.
|
||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
|
|
||||||
|
// Prefix optionally namespaces model aliases for this provider (e.g., "teamA/kimi-k2").
|
||||||
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
|
||||||
|
|
||||||
// BaseURL is the base URL for the external OpenAI-compatible API endpoint.
|
// BaseURL is the base URL for the external OpenAI-compatible API endpoint.
|
||||||
BaseURL string `yaml:"base-url" json:"base-url"`
|
BaseURL string `yaml:"base-url" json:"base-url"`
|
||||||
|
|
||||||
@@ -422,6 +434,7 @@ func (cfg *Config) SanitizeOpenAICompatibility() {
|
|||||||
for i := range cfg.OpenAICompatibility {
|
for i := range cfg.OpenAICompatibility {
|
||||||
e := cfg.OpenAICompatibility[i]
|
e := cfg.OpenAICompatibility[i]
|
||||||
e.Name = strings.TrimSpace(e.Name)
|
e.Name = strings.TrimSpace(e.Name)
|
||||||
|
e.Prefix = normalizeModelPrefix(e.Prefix)
|
||||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||||
e.Headers = NormalizeHeaders(e.Headers)
|
e.Headers = NormalizeHeaders(e.Headers)
|
||||||
if e.BaseURL == "" {
|
if e.BaseURL == "" {
|
||||||
@@ -442,6 +455,7 @@ func (cfg *Config) SanitizeCodexKeys() {
|
|||||||
out := make([]CodexKey, 0, len(cfg.CodexKey))
|
out := make([]CodexKey, 0, len(cfg.CodexKey))
|
||||||
for i := range cfg.CodexKey {
|
for i := range cfg.CodexKey {
|
||||||
e := cfg.CodexKey[i]
|
e := cfg.CodexKey[i]
|
||||||
|
e.Prefix = normalizeModelPrefix(e.Prefix)
|
||||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||||
e.Headers = NormalizeHeaders(e.Headers)
|
e.Headers = NormalizeHeaders(e.Headers)
|
||||||
e.ExcludedModels = NormalizeExcludedModels(e.ExcludedModels)
|
e.ExcludedModels = NormalizeExcludedModels(e.ExcludedModels)
|
||||||
@@ -460,6 +474,7 @@ func (cfg *Config) SanitizeClaudeKeys() {
|
|||||||
}
|
}
|
||||||
for i := range cfg.ClaudeKey {
|
for i := range cfg.ClaudeKey {
|
||||||
entry := &cfg.ClaudeKey[i]
|
entry := &cfg.ClaudeKey[i]
|
||||||
|
entry.Prefix = normalizeModelPrefix(entry.Prefix)
|
||||||
entry.Headers = NormalizeHeaders(entry.Headers)
|
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||||
entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
|
entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
|
||||||
}
|
}
|
||||||
@@ -479,6 +494,7 @@ func (cfg *Config) SanitizeGeminiKeys() {
|
|||||||
if entry.APIKey == "" {
|
if entry.APIKey == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
entry.Prefix = normalizeModelPrefix(entry.Prefix)
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||||
entry.Headers = NormalizeHeaders(entry.Headers)
|
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||||
@@ -492,6 +508,18 @@ func (cfg *Config) SanitizeGeminiKeys() {
|
|||||||
cfg.GeminiKey = out
|
cfg.GeminiKey = out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeModelPrefix(prefix string) string {
|
||||||
|
trimmed := strings.TrimSpace(prefix)
|
||||||
|
trimmed = strings.Trim(trimmed, "/")
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.Contains(trimmed, "/") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
func syncInlineAccessProvider(cfg *Config) {
|
func syncInlineAccessProvider(cfg *Config) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ type VertexCompatKey struct {
|
|||||||
// Maps to the x-goog-api-key header.
|
// Maps to the x-goog-api-key header.
|
||||||
APIKey string `yaml:"api-key" json:"api-key"`
|
APIKey string `yaml:"api-key" json:"api-key"`
|
||||||
|
|
||||||
|
// Prefix optionally namespaces model aliases for this credential (e.g., "teamA/vertex-pro").
|
||||||
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
|
||||||
|
|
||||||
// BaseURL is the base URL for the Vertex-compatible API endpoint.
|
// BaseURL is the base URL for the Vertex-compatible API endpoint.
|
||||||
// The executor will append "/v1/publishers/google/models/{model}:action" to this.
|
// The executor will append "/v1/publishers/google/models/{model}:action" to this.
|
||||||
// Example: "https://zenmux.ai/api" becomes "https://zenmux.ai/api/v1/publishers/google/models/..."
|
// Example: "https://zenmux.ai/api" becomes "https://zenmux.ai/api/v1/publishers/google/models/..."
|
||||||
@@ -53,6 +56,7 @@ func (cfg *Config) SanitizeVertexCompatKeys() {
|
|||||||
if entry.APIKey == "" {
|
if entry.APIKey == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
entry.Prefix = normalizeModelPrefix(entry.Prefix)
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
if entry.BaseURL == "" {
|
if entry.BaseURL == "" {
|
||||||
// BaseURL is required for Vertex API key entries
|
// BaseURL is required for Vertex API key entries
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func (w *Watcher) Start(ctx context.Context) error {
|
|||||||
go w.processEvents(ctx)
|
go w.processEvents(ctx)
|
||||||
|
|
||||||
// Perform an initial full reload based on current config and auth dir
|
// Perform an initial full reload based on current config and auth dir
|
||||||
w.reloadClients(true, nil)
|
w.reloadClients(true, nil, false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ func (w *Watcher) DispatchRuntimeAuthUpdate(update AuthUpdate) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) refreshAuthState() {
|
func (w *Watcher) refreshAuthState(force bool) {
|
||||||
auths := w.SnapshotCoreAuths()
|
auths := w.SnapshotCoreAuths()
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
if len(w.runtimeAuths) > 0 {
|
if len(w.runtimeAuths) > 0 {
|
||||||
@@ -286,12 +286,12 @@ func (w *Watcher) refreshAuthState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updates := w.prepareAuthUpdatesLocked(auths)
|
updates := w.prepareAuthUpdatesLocked(auths, force)
|
||||||
w.clientsMutex.Unlock()
|
w.clientsMutex.Unlock()
|
||||||
w.dispatchAuthUpdates(updates)
|
w.dispatchAuthUpdates(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) prepareAuthUpdatesLocked(auths []*coreauth.Auth) []AuthUpdate {
|
func (w *Watcher) prepareAuthUpdatesLocked(auths []*coreauth.Auth, force bool) []AuthUpdate {
|
||||||
newState := make(map[string]*coreauth.Auth, len(auths))
|
newState := make(map[string]*coreauth.Auth, len(auths))
|
||||||
for _, auth := range auths {
|
for _, auth := range auths {
|
||||||
if auth == nil || auth.ID == "" {
|
if auth == nil || auth.ID == "" {
|
||||||
@@ -318,7 +318,7 @@ func (w *Watcher) prepareAuthUpdatesLocked(auths []*coreauth.Auth) []AuthUpdate
|
|||||||
for id, auth := range newState {
|
for id, auth := range newState {
|
||||||
if existing, ok := w.currentAuths[id]; !ok {
|
if existing, ok := w.currentAuths[id]; !ok {
|
||||||
updates = append(updates, AuthUpdate{Action: AuthUpdateActionAdd, ID: id, Auth: auth.Clone()})
|
updates = append(updates, AuthUpdate{Action: AuthUpdateActionAdd, ID: id, Auth: auth.Clone()})
|
||||||
} else if !authEqual(existing, auth) {
|
} else if force || !authEqual(existing, auth) {
|
||||||
updates = append(updates, AuthUpdate{Action: AuthUpdateActionModify, ID: id, Auth: auth.Clone()})
|
updates = append(updates, AuthUpdate{Action: AuthUpdateActionModify, ID: id, Auth: auth.Clone()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,15 +949,16 @@ func (w *Watcher) reloadConfig() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir
|
authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir
|
||||||
|
forceAuthRefresh := oldConfig != nil && oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix
|
||||||
|
|
||||||
log.Infof("config successfully reloaded, triggering client reload")
|
log.Infof("config successfully reloaded, triggering client reload")
|
||||||
// Reload clients with new config
|
// Reload clients with new config
|
||||||
w.reloadClients(authDirChanged, affectedOAuthProviders)
|
w.reloadClients(authDirChanged, affectedOAuthProviders, forceAuthRefresh)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// reloadClients performs a full scan and reload of all clients.
|
// reloadClients performs a full scan and reload of all clients.
|
||||||
func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string) {
|
func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string, forceAuthRefresh bool) {
|
||||||
log.Debugf("starting full client load process")
|
log.Debugf("starting full client load process")
|
||||||
|
|
||||||
w.clientsMutex.RLock()
|
w.clientsMutex.RLock()
|
||||||
@@ -1048,7 +1049,7 @@ func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string
|
|||||||
w.reloadCallback(cfg)
|
w.reloadCallback(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.refreshAuthState()
|
w.refreshAuthState(forceAuthRefresh)
|
||||||
|
|
||||||
log.Infof("full client load complete - %d clients (%d auth files + %d Gemini API keys + %d Vertex API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
log.Infof("full client load complete - %d clients (%d auth files + %d Gemini API keys + %d Vertex API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
||||||
totalNewClients,
|
totalNewClients,
|
||||||
@@ -1099,7 +1100,7 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
|
|
||||||
w.clientsMutex.Unlock() // Unlock before the callback
|
w.clientsMutex.Unlock() // Unlock before the callback
|
||||||
|
|
||||||
w.refreshAuthState()
|
w.refreshAuthState(false)
|
||||||
|
|
||||||
if w.reloadCallback != nil {
|
if w.reloadCallback != nil {
|
||||||
log.Debugf("triggering server update callback after add/update")
|
log.Debugf("triggering server update callback after add/update")
|
||||||
@@ -1118,7 +1119,7 @@ func (w *Watcher) removeClient(path string) {
|
|||||||
|
|
||||||
w.clientsMutex.Unlock() // Release the lock before the callback
|
w.clientsMutex.Unlock() // Release the lock before the callback
|
||||||
|
|
||||||
w.refreshAuthState()
|
w.refreshAuthState(false)
|
||||||
|
|
||||||
if w.reloadCallback != nil {
|
if w.reloadCallback != nil {
|
||||||
log.Debugf("triggering server update callback after removal")
|
log.Debugf("triggering server update callback after removal")
|
||||||
@@ -1147,6 +1148,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
prefix := strings.TrimSpace(entry.Prefix)
|
||||||
base := strings.TrimSpace(entry.BaseURL)
|
base := strings.TrimSpace(entry.BaseURL)
|
||||||
proxyURL := strings.TrimSpace(entry.ProxyURL)
|
proxyURL := strings.TrimSpace(entry.ProxyURL)
|
||||||
id, token := idGen.next("gemini:apikey", key, base)
|
id, token := idGen.next("gemini:apikey", key, base)
|
||||||
@@ -1162,6 +1164,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: "gemini",
|
Provider: "gemini",
|
||||||
Label: "gemini-apikey",
|
Label: "gemini-apikey",
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
@@ -1179,6 +1182,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
prefix := strings.TrimSpace(ck.Prefix)
|
||||||
base := strings.TrimSpace(ck.BaseURL)
|
base := strings.TrimSpace(ck.BaseURL)
|
||||||
id, token := idGen.next("claude:apikey", key, base)
|
id, token := idGen.next("claude:apikey", key, base)
|
||||||
attrs := map[string]string{
|
attrs := map[string]string{
|
||||||
@@ -1197,6 +1201,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: "claude",
|
Provider: "claude",
|
||||||
Label: "claude-apikey",
|
Label: "claude-apikey",
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
@@ -1213,6 +1218,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
prefix := strings.TrimSpace(ck.Prefix)
|
||||||
id, token := idGen.next("codex:apikey", key, ck.BaseURL)
|
id, token := idGen.next("codex:apikey", key, ck.BaseURL)
|
||||||
attrs := map[string]string{
|
attrs := map[string]string{
|
||||||
"source": fmt.Sprintf("config:codex[%s]", token),
|
"source": fmt.Sprintf("config:codex[%s]", token),
|
||||||
@@ -1227,6 +1233,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: "codex",
|
Provider: "codex",
|
||||||
Label: "codex-apikey",
|
Label: "codex-apikey",
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
@@ -1238,6 +1245,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
}
|
}
|
||||||
for i := range cfg.OpenAICompatibility {
|
for i := range cfg.OpenAICompatibility {
|
||||||
compat := &cfg.OpenAICompatibility[i]
|
compat := &cfg.OpenAICompatibility[i]
|
||||||
|
prefix := strings.TrimSpace(compat.Prefix)
|
||||||
providerName := strings.ToLower(strings.TrimSpace(compat.Name))
|
providerName := strings.ToLower(strings.TrimSpace(compat.Name))
|
||||||
if providerName == "" {
|
if providerName == "" {
|
||||||
providerName = "openai-compatibility"
|
providerName = "openai-compatibility"
|
||||||
@@ -1269,6 +1277,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
Label: compat.Name,
|
Label: compat.Name,
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
@@ -1295,6 +1304,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
Label: compat.Name,
|
Label: compat.Name,
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -1312,6 +1322,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
base := strings.TrimSpace(compat.BaseURL)
|
base := strings.TrimSpace(compat.BaseURL)
|
||||||
|
|
||||||
key := strings.TrimSpace(compat.APIKey)
|
key := strings.TrimSpace(compat.APIKey)
|
||||||
|
prefix := strings.TrimSpace(compat.Prefix)
|
||||||
proxyURL := strings.TrimSpace(compat.ProxyURL)
|
proxyURL := strings.TrimSpace(compat.ProxyURL)
|
||||||
idKind := fmt.Sprintf("vertex:apikey:%s", base)
|
idKind := fmt.Sprintf("vertex:apikey:%s", base)
|
||||||
id, token := idGen.next(idKind, key, base, proxyURL)
|
id, token := idGen.next(idKind, key, base, proxyURL)
|
||||||
@@ -1331,6 +1342,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
Label: "vertex-apikey",
|
Label: "vertex-apikey",
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
@@ -1383,10 +1395,20 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
proxyURL = p
|
proxyURL = p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix := ""
|
||||||
|
if rawPrefix, ok := metadata["prefix"].(string); ok {
|
||||||
|
trimmed := strings.TrimSpace(rawPrefix)
|
||||||
|
trimmed = strings.Trim(trimmed, "/")
|
||||||
|
if trimmed != "" && !strings.Contains(trimmed, "/") {
|
||||||
|
prefix = trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Label: label,
|
Label: label,
|
||||||
|
Prefix: prefix,
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
Attributes: map[string]string{
|
Attributes: map[string]string{
|
||||||
"source": full,
|
"source": full,
|
||||||
@@ -1473,6 +1495,7 @@ func synthesizeGeminiVirtualAuths(primary *coreauth.Auth, metadata map[string]an
|
|||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
Metadata: metadataCopy,
|
Metadata: metadataCopy,
|
||||||
ProxyURL: primary.ProxyURL,
|
ProxyURL: primary.ProxyURL,
|
||||||
|
Prefix: primary.Prefix,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
Runtime: geminicli.NewVirtualCredential(projectID, shared),
|
Runtime: geminicli.NewVirtualCredential(projectID, shared),
|
||||||
@@ -1742,6 +1765,9 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
if oldCfg.WebsocketAuth != newCfg.WebsocketAuth {
|
if oldCfg.WebsocketAuth != newCfg.WebsocketAuth {
|
||||||
changes = append(changes, fmt.Sprintf("ws-auth: %t -> %t", oldCfg.WebsocketAuth, newCfg.WebsocketAuth))
|
changes = append(changes, fmt.Sprintf("ws-auth: %t -> %t", oldCfg.WebsocketAuth, newCfg.WebsocketAuth))
|
||||||
}
|
}
|
||||||
|
if oldCfg.ForceModelPrefix != newCfg.ForceModelPrefix {
|
||||||
|
changes = append(changes, fmt.Sprintf("force-model-prefix: %t -> %t", oldCfg.ForceModelPrefix, newCfg.ForceModelPrefix))
|
||||||
|
}
|
||||||
|
|
||||||
// Quota-exceeded behavior
|
// Quota-exceeded behavior
|
||||||
if oldCfg.QuotaExceeded.SwitchProject != newCfg.QuotaExceeded.SwitchProject {
|
if oldCfg.QuotaExceeded.SwitchProject != newCfg.QuotaExceeded.SwitchProject {
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ type BaseAPIHandler struct {
|
|||||||
|
|
||||||
// Cfg holds the current application configuration.
|
// Cfg holds the current application configuration.
|
||||||
Cfg *config.SDKConfig
|
Cfg *config.SDKConfig
|
||||||
|
|
||||||
// OpenAICompatProviders is a list of provider names for OpenAI compatibility.
|
|
||||||
OpenAICompatProviders []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBaseAPIHandlers creates a new API handlers instance.
|
// NewBaseAPIHandlers creates a new API handlers instance.
|
||||||
@@ -63,11 +60,10 @@ type BaseAPIHandler struct {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *BaseAPIHandler: A new API handlers instance
|
// - *BaseAPIHandler: A new API handlers instance
|
||||||
func NewBaseAPIHandlers(cfg *config.SDKConfig, authManager *coreauth.Manager, openAICompatProviders []string) *BaseAPIHandler {
|
func NewBaseAPIHandlers(cfg *config.SDKConfig, authManager *coreauth.Manager) *BaseAPIHandler {
|
||||||
return &BaseAPIHandler{
|
return &BaseAPIHandler{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
AuthManager: authManager,
|
AuthManager: authManager,
|
||||||
OpenAICompatProviders: openAICompatProviders,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,20 +338,10 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string
|
|||||||
// Resolve "auto" model to an actual available model first
|
// Resolve "auto" model to an actual available model first
|
||||||
resolvedModelName := util.ResolveAutoModel(modelName)
|
resolvedModelName := util.ResolveAutoModel(modelName)
|
||||||
|
|
||||||
providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName)
|
|
||||||
|
|
||||||
targetModelName := resolvedModelName
|
|
||||||
if isDynamic {
|
|
||||||
targetModelName = extractedModelName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the model name to handle dynamic thinking suffixes before determining the provider.
|
// Normalize the model name to handle dynamic thinking suffixes before determining the provider.
|
||||||
normalizedModel, metadata = normalizeModelMetadata(targetModelName)
|
normalizedModel, metadata = normalizeModelMetadata(resolvedModelName)
|
||||||
|
|
||||||
if isDynamic {
|
// Use the normalizedModel to get the provider name.
|
||||||
providers = []string{providerName}
|
|
||||||
} else {
|
|
||||||
// For non-dynamic models, use the normalizedModel to get the provider name.
|
|
||||||
providers = util.GetProviderName(normalizedModel)
|
providers = util.GetProviderName(normalizedModel)
|
||||||
if len(providers) == 0 && metadata != nil {
|
if len(providers) == 0 && metadata != nil {
|
||||||
if originalRaw, ok := metadata[util.ThinkingOriginalModelMetadataKey]; ok {
|
if originalRaw, ok := metadata[util.ThinkingOriginalModelMetadataKey]; ok {
|
||||||
@@ -370,7 +356,6 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(providers) == 0 {
|
if len(providers) == 0 {
|
||||||
return nil, "", nil, &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)}
|
return nil, "", nil, &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)}
|
||||||
@@ -383,30 +368,6 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string
|
|||||||
return providers, normalizedModel, metadata, nil
|
return providers, normalizedModel, metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BaseAPIHandler) parseDynamicModel(modelName string) (providerName, model string, isDynamic bool) {
|
|
||||||
var providerPart, modelPart string
|
|
||||||
for _, sep := range []string{"://"} {
|
|
||||||
if parts := strings.SplitN(modelName, sep, 2); len(parts) == 2 {
|
|
||||||
providerPart = parts[0]
|
|
||||||
modelPart = parts[1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if providerPart == "" {
|
|
||||||
return "", modelName, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the provider is a configured openai-compatibility provider
|
|
||||||
for _, pName := range h.OpenAICompatProviders {
|
|
||||||
if pName == providerPart {
|
|
||||||
return providerPart, modelPart, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", modelName, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneBytes(src []byte) []byte {
|
func cloneBytes(src []byte) []byte {
|
||||||
if len(src) == 0 {
|
if len(src) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -363,10 +363,11 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req
|
|||||||
if provider == "" {
|
if provider == "" {
|
||||||
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
||||||
}
|
}
|
||||||
|
routeModel := req.Model
|
||||||
tried := make(map[string]struct{})
|
tried := make(map[string]struct{})
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for {
|
for {
|
||||||
auth, executor, errPick := m.pickNext(ctx, provider, req.Model, opts, tried)
|
auth, executor, errPick := m.pickNext(ctx, provider, routeModel, opts, tried)
|
||||||
if errPick != nil {
|
if errPick != nil {
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return cliproxyexecutor.Response{}, lastErr
|
return cliproxyexecutor.Response{}, lastErr
|
||||||
@@ -396,8 +397,10 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req
|
|||||||
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
||||||
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
||||||
}
|
}
|
||||||
resp, errExec := executor.Execute(execCtx, auth, req, opts)
|
execReq := req
|
||||||
result := Result{AuthID: auth.ID, Provider: provider, Model: req.Model, Success: errExec == nil}
|
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
|
||||||
|
resp, errExec := executor.Execute(execCtx, auth, execReq, opts)
|
||||||
|
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
|
||||||
if errExec != nil {
|
if errExec != nil {
|
||||||
result.Error = &Error{Message: errExec.Error()}
|
result.Error = &Error{Message: errExec.Error()}
|
||||||
var se cliproxyexecutor.StatusError
|
var se cliproxyexecutor.StatusError
|
||||||
@@ -420,10 +423,11 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string,
|
|||||||
if provider == "" {
|
if provider == "" {
|
||||||
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
||||||
}
|
}
|
||||||
|
routeModel := req.Model
|
||||||
tried := make(map[string]struct{})
|
tried := make(map[string]struct{})
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for {
|
for {
|
||||||
auth, executor, errPick := m.pickNext(ctx, provider, req.Model, opts, tried)
|
auth, executor, errPick := m.pickNext(ctx, provider, routeModel, opts, tried)
|
||||||
if errPick != nil {
|
if errPick != nil {
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return cliproxyexecutor.Response{}, lastErr
|
return cliproxyexecutor.Response{}, lastErr
|
||||||
@@ -453,8 +457,10 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string,
|
|||||||
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
||||||
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
||||||
}
|
}
|
||||||
resp, errExec := executor.CountTokens(execCtx, auth, req, opts)
|
execReq := req
|
||||||
result := Result{AuthID: auth.ID, Provider: provider, Model: req.Model, Success: errExec == nil}
|
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
|
||||||
|
resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts)
|
||||||
|
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
|
||||||
if errExec != nil {
|
if errExec != nil {
|
||||||
result.Error = &Error{Message: errExec.Error()}
|
result.Error = &Error{Message: errExec.Error()}
|
||||||
var se cliproxyexecutor.StatusError
|
var se cliproxyexecutor.StatusError
|
||||||
@@ -477,10 +483,11 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string
|
|||||||
if provider == "" {
|
if provider == "" {
|
||||||
return nil, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
return nil, &Error{Code: "provider_not_found", Message: "provider identifier is empty"}
|
||||||
}
|
}
|
||||||
|
routeModel := req.Model
|
||||||
tried := make(map[string]struct{})
|
tried := make(map[string]struct{})
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for {
|
for {
|
||||||
auth, executor, errPick := m.pickNext(ctx, provider, req.Model, opts, tried)
|
auth, executor, errPick := m.pickNext(ctx, provider, routeModel, opts, tried)
|
||||||
if errPick != nil {
|
if errPick != nil {
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return nil, lastErr
|
return nil, lastErr
|
||||||
@@ -510,14 +517,16 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string
|
|||||||
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
|
||||||
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
|
||||||
}
|
}
|
||||||
chunks, errStream := executor.ExecuteStream(execCtx, auth, req, opts)
|
execReq := req
|
||||||
|
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
|
||||||
|
chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts)
|
||||||
if errStream != nil {
|
if errStream != nil {
|
||||||
rerr := &Error{Message: errStream.Error()}
|
rerr := &Error{Message: errStream.Error()}
|
||||||
var se cliproxyexecutor.StatusError
|
var se cliproxyexecutor.StatusError
|
||||||
if errors.As(errStream, &se) && se != nil {
|
if errors.As(errStream, &se) && se != nil {
|
||||||
rerr.HTTPStatus = se.StatusCode()
|
rerr.HTTPStatus = se.StatusCode()
|
||||||
}
|
}
|
||||||
result := Result{AuthID: auth.ID, Provider: provider, Model: req.Model, Success: false, Error: rerr}
|
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: false, Error: rerr}
|
||||||
result.RetryAfter = retryAfterFromError(errStream)
|
result.RetryAfter = retryAfterFromError(errStream)
|
||||||
m.MarkResult(execCtx, result)
|
m.MarkResult(execCtx, result)
|
||||||
lastErr = errStream
|
lastErr = errStream
|
||||||
@@ -535,18 +544,66 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string
|
|||||||
if errors.As(chunk.Err, &se) && se != nil {
|
if errors.As(chunk.Err, &se) && se != nil {
|
||||||
rerr.HTTPStatus = se.StatusCode()
|
rerr.HTTPStatus = se.StatusCode()
|
||||||
}
|
}
|
||||||
m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: req.Model, Success: false, Error: rerr})
|
m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: routeModel, Success: false, Error: rerr})
|
||||||
}
|
}
|
||||||
out <- chunk
|
out <- chunk
|
||||||
}
|
}
|
||||||
if !failed {
|
if !failed {
|
||||||
m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: req.Model, Success: true})
|
m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: routeModel, Success: true})
|
||||||
}
|
}
|
||||||
}(execCtx, auth.Clone(), provider, chunks)
|
}(execCtx, auth.Clone(), provider, chunks)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rewriteModelForAuth(model string, metadata map[string]any, auth *Auth) (string, map[string]any) {
|
||||||
|
if auth == nil || model == "" {
|
||||||
|
return model, metadata
|
||||||
|
}
|
||||||
|
prefix := strings.TrimSpace(auth.Prefix)
|
||||||
|
if prefix == "" {
|
||||||
|
return model, metadata
|
||||||
|
}
|
||||||
|
needle := prefix + "/"
|
||||||
|
if !strings.HasPrefix(model, needle) {
|
||||||
|
return model, metadata
|
||||||
|
}
|
||||||
|
rewritten := strings.TrimPrefix(model, needle)
|
||||||
|
return rewritten, stripPrefixFromMetadata(metadata, needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripPrefixFromMetadata(metadata map[string]any, needle string) map[string]any {
|
||||||
|
if len(metadata) == 0 || needle == "" {
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
keys := []string{
|
||||||
|
util.ThinkingOriginalModelMetadataKey,
|
||||||
|
util.GeminiOriginalModelMetadataKey,
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
for _, key := range keys {
|
||||||
|
raw, ok := metadata[key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, okStr := raw.(string)
|
||||||
|
if !okStr || !strings.HasPrefix(value, needle) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = make(map[string]any, len(metadata))
|
||||||
|
for k, v := range metadata {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[key] = strings.TrimPrefix(value, needle)
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) normalizeProviders(providers []string) []string {
|
func (m *Manager) normalizeProviders(providers []string) []string {
|
||||||
if len(providers) == 0 {
|
if len(providers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type Auth struct {
|
|||||||
Index uint64 `json:"-"`
|
Index uint64 `json:"-"`
|
||||||
// Provider is the upstream provider key (e.g. "gemini", "claude").
|
// Provider is the upstream provider key (e.g. "gemini", "claude").
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
// Prefix optionally namespaces models for routing (e.g., "teamA/gemini-3-pro-preview").
|
||||||
|
Prefix string `json:"prefix,omitempty"`
|
||||||
// FileName stores the relative or absolute path of the backing auth file.
|
// FileName stores the relative or absolute path of the backing auth file.
|
||||||
FileName string `json:"-"`
|
FileName string `json:"-"`
|
||||||
// Storage holds the token persistence implementation used during login flows.
|
// Storage holds the token persistence implementation used during login flows.
|
||||||
|
|||||||
@@ -787,7 +787,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
|||||||
if providerKey == "" {
|
if providerKey == "" {
|
||||||
providerKey = "openai-compatibility"
|
providerKey = "openai-compatibility"
|
||||||
}
|
}
|
||||||
GlobalModelRegistry().RegisterClient(a.ID, providerKey, ms)
|
GlobalModelRegistry().RegisterClient(a.ID, providerKey, applyModelPrefixes(ms, a.Prefix, s.cfg.ForceModelPrefix))
|
||||||
} else {
|
} else {
|
||||||
// Ensure stale registrations are cleared when model list becomes empty.
|
// Ensure stale registrations are cleared when model list becomes empty.
|
||||||
GlobalModelRegistry().UnregisterClient(a.ID)
|
GlobalModelRegistry().UnregisterClient(a.ID)
|
||||||
@@ -807,7 +807,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
key = strings.ToLower(strings.TrimSpace(a.Provider))
|
key = strings.ToLower(strings.TrimSpace(a.Provider))
|
||||||
}
|
}
|
||||||
GlobalModelRegistry().RegisterClient(a.ID, key, models)
|
GlobalModelRegistry().RegisterClient(a.ID, key, applyModelPrefixes(models, a.Prefix, s.cfg != nil && s.cfg.ForceModelPrefix))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -987,6 +987,48 @@ func applyExcludedModels(models []*ModelInfo, excluded []string) []*ModelInfo {
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyModelPrefixes(models []*ModelInfo, prefix string, forceModelPrefix bool) []*ModelInfo {
|
||||||
|
trimmedPrefix := strings.TrimSpace(prefix)
|
||||||
|
if trimmedPrefix == "" || len(models) == 0 {
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*ModelInfo, 0, len(models)*2)
|
||||||
|
seen := make(map[string]struct{}, len(models)*2)
|
||||||
|
|
||||||
|
addModel := func(model *ModelInfo) {
|
||||||
|
if model == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := strings.TrimSpace(model.ID)
|
||||||
|
if id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := seen[id]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
out = append(out, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
if model == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
baseID := strings.TrimSpace(model.ID)
|
||||||
|
if baseID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !forceModelPrefix || trimmedPrefix == baseID {
|
||||||
|
addModel(model)
|
||||||
|
}
|
||||||
|
clone := *model
|
||||||
|
clone.ID = trimmedPrefix + "/" + baseID
|
||||||
|
addModel(&clone)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// matchWildcard performs case-insensitive wildcard matching where '*' matches any substring.
|
// matchWildcard performs case-insensitive wildcard matching where '*' matches any substring.
|
||||||
func matchWildcard(pattern, value string) bool {
|
func matchWildcard(pattern, value string) bool {
|
||||||
if pattern == "" {
|
if pattern == "" {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ type SDKConfig struct {
|
|||||||
// ProxyURL is the URL of an optional proxy server to use for outbound requests.
|
// ProxyURL is the URL of an optional proxy server to use for outbound requests.
|
||||||
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
||||||
|
|
||||||
|
// ForceModelPrefix requires explicit model prefixes (e.g., "teamA/gemini-3-pro-preview")
|
||||||
|
// to target prefixed credentials. When false, unprefixed model requests may use prefixed
|
||||||
|
// credentials as well.
|
||||||
|
ForceModelPrefix bool `yaml:"force-model-prefix" json:"force-model-prefix"`
|
||||||
|
|
||||||
// RequestLog enables or disables detailed request logging functionality.
|
// RequestLog enables or disables detailed request logging functionality.
|
||||||
RequestLog bool `yaml:"request-log" json:"request-log"`
|
RequestLog bool `yaml:"request-log" json:"request-log"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user