feat(auth): add per-key proxy support and enhance API key configuration handling

- Introduced `ProxyURL` field to Claude and Codex API key configurations.
- Added support for `api-key-entries` in OpenAI compatibility section with per-key proxy configuration.
- Maintained backward compatibility for legacy `api-keys` format.
- Updated logic to prioritize `api-key-entries` where applicable.
- Improved documentation and examples to reflect new proxy support.
This commit is contained in:
Luis Pater
2025-09-30 09:24:40 +08:00
parent de796ac1c2
commit f6de2a709f
5 changed files with 218 additions and 72 deletions

View File

@@ -288,13 +288,18 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
| `codex-api-key` | object | {} | List of Codex API keys. | | `codex-api-key` | object | {} | List of Codex API keys. |
| `codex-api-key.api-key` | string | "" | Codex API key. | | `codex-api-key.api-key` | string | "" | Codex API key. |
| `codex-api-key.base-url` | string | "" | Custom Codex API endpoint, if you use a third-party API endpoint. | | `codex-api-key.base-url` | string | "" | Custom Codex API endpoint, if you use a third-party API endpoint. |
| `codex-api-key.proxy-url` | string | "" | Proxy URL for this specific API key. Overrides the global proxy-url setting. Supports socks5/http/https protocols. |
| `claude-api-key` | object | {} | List of Claude API keys. | | `claude-api-key` | object | {} | List of Claude API keys. |
| `claude-api-key.api-key` | string | "" | Claude API key. | | `claude-api-key.api-key` | string | "" | Claude API key. |
| `claude-api-key.base-url` | string | "" | Custom Claude API endpoint, if you use a third-party API endpoint. | | `claude-api-key.base-url` | string | "" | Custom Claude API endpoint, if you use a third-party API endpoint. |
| `claude-api-key.proxy-url` | string | "" | Proxy URL for this specific API key. Overrides the global proxy-url setting. Supports socks5/http/https protocols. |
| `openai-compatibility` | object[] | [] | Upstream OpenAI-compatible providers configuration (name, base-url, api-keys, models). | | `openai-compatibility` | object[] | [] | Upstream OpenAI-compatible providers configuration (name, base-url, api-keys, models). |
| `openai-compatibility.*.name` | string | "" | The name of the provider. It will be used in the user agent and other places. | | `openai-compatibility.*.name` | string | "" | The name of the provider. It will be used in the user agent and other places. |
| `openai-compatibility.*.base-url` | string | "" | The base URL of the provider. | | `openai-compatibility.*.base-url` | string | "" | The base URL of the provider. |
| `openai-compatibility.*.api-keys` | string[] | [] | The API keys for the provider. Add multiple keys if needed. Omit if unauthenticated access is allowed. | | `openai-compatibility.*.api-keys` | string[] | [] | (Deprecated) The API keys for the provider. Use api-key-entries instead for per-key proxy support. |
| `openai-compatibility.*.api-key-entries` | object[] | [] | API key entries with optional per-key proxy configuration. Preferred over api-keys. |
| `openai-compatibility.*.api-key-entries.*.api-key` | string | "" | The API key for this entry. |
| `openai-compatibility.*.api-key-entries.*.proxy-url` | string | "" | Proxy URL for this specific API key. Overrides the global proxy-url setting. Supports socks5/http/https protocols. |
| `openai-compatibility.*.models` | object[] | [] | The actual model name. | | `openai-compatibility.*.models` | object[] | [] | The actual model name. |
| `openai-compatibility.*.models.*.name` | string | "" | The models supported by the provider. | | `openai-compatibility.*.models.*.name` | string | "" | The models supported by the provider. |
| `openai-compatibility.*.models.*.alias` | string | "" | The alias used in the API. | | `openai-compatibility.*.models.*.alias` | string | "" | The alias used in the API. |
@@ -361,20 +366,28 @@ generative-language-api-key:
codex-api-key: codex-api-key:
- api-key: "sk-atSM..." - api-key: "sk-atSM..."
base-url: "https://www.example.com" # use the custom codex API endpoint base-url: "https://www.example.com" # use the custom codex API endpoint
proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
# Claude API keys # Claude API keys
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..."
base-url: "https://www.example.com" # use the custom claude API endpoint base-url: "https://www.example.com" # use the custom claude API endpoint
proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
# 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.
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.
api-keys: # The API keys for the provider. Add multiple keys if needed. Omit if unauthenticated access is allowed. # New format with per-key proxy support (recommended):
- "sk-or-v1-...b780" api-key-entries:
- "sk-or-v1-...b781" - api-key: "sk-or-v1-...b780"
proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
- api-key: "sk-or-v1-...b781" # without proxy-url
# Legacy format (still supported, but cannot specify proxy per key):
# api-keys:
# - "sk-or-v1-...b780"
# - "sk-or-v1-...b781"
models: # The models supported by the provider. models: # The models supported by the provider.
- name: "moonshotai/kimi-k2:free" # The actual model name. - name: "moonshotai/kimi-k2:free" # The actual model name.
alias: "kimi-k2" # The alias used in the API. alias: "kimi-k2" # The alias used in the API.
@@ -386,10 +399,26 @@ Configure upstream OpenAI-compatible providers (e.g., OpenRouter) via `openai-co
- name: provider identifier used internally - name: provider identifier used internally
- base-url: provider base URL - base-url: provider base URL
- api-keys: optional list of API keys (omit if provider allows unauthenticated requests) - api-key-entries: list of API key entries with optional per-key proxy configuration (recommended)
- api-keys: (deprecated) simple list of API keys without proxy support
- models: list of mappings from upstream model `name` to local `alias` - models: list of mappings from upstream model `name` to local `alias`
Example: Example with per-key proxy support:
```yaml
openai-compatibility:
- name: "openrouter"
base-url: "https://openrouter.ai/api/v1"
api-key-entries:
- api-key: "sk-or-v1-...b780"
proxy-url: "socks5://proxy.example.com:1080"
- api-key: "sk-or-v1-...b781"
models:
- name: "moonshotai/kimi-k2:free"
alias: "kimi-k2"
```
Legacy format (still supported):
```yaml ```yaml
openai-compatibility: openai-compatibility:

View File

@@ -300,13 +300,18 @@ console.log(await claudeResponse.json());
| `codex-api-key` | object | {} | Codex API密钥列表。 | | `codex-api-key` | object | {} | Codex API密钥列表。 |
| `codex-api-key.api-key` | string | "" | Codex API密钥。 | | `codex-api-key.api-key` | string | "" | Codex API密钥。 |
| `codex-api-key.base-url` | string | "" | 自定义的Codex API端点 | | `codex-api-key.base-url` | string | "" | 自定义的Codex API端点 |
| `codex-api-key.proxy-url` | string | "" | 针对该API密钥的代理URL。会覆盖全局proxy-url设置。支持socks5/http/https协议。 |
| `claude-api-key` | object | {} | Claude API密钥列表。 | | `claude-api-key` | object | {} | Claude API密钥列表。 |
| `claude-api-key.api-key` | string | "" | Claude API密钥。 | | `claude-api-key.api-key` | string | "" | Claude API密钥。 |
| `claude-api-key.base-url` | string | "" | 自定义的Claude API端点如果您使用第三方的API端点。 | | `claude-api-key.base-url` | string | "" | 自定义的Claude API端点如果您使用第三方的API端点。 |
| `claude-api-key.proxy-url` | string | "" | 针对该API密钥的代理URL。会覆盖全局proxy-url设置。支持socks5/http/https协议。 |
| `openai-compatibility` | object[] | [] | 上游OpenAI兼容提供商的配置名称、基础URL、API密钥、模型。 | | `openai-compatibility` | object[] | [] | 上游OpenAI兼容提供商的配置名称、基础URL、API密钥、模型。 |
| `openai-compatibility.*.name` | string | "" | 提供商的名称。它将被用于用户代理User Agent和其他地方。 | | `openai-compatibility.*.name` | string | "" | 提供商的名称。它将被用于用户代理User Agent和其他地方。 |
| `openai-compatibility.*.base-url` | string | "" | 提供商的基础URL。 | | `openai-compatibility.*.base-url` | string | "" | 提供商的基础URL。 |
| `openai-compatibility.*.api-keys` | string[] | [] | 提供商的API密钥。如果需要,可以添加多个密钥。如果允许未经身份验证的访问,则可以省略。 | | `openai-compatibility.*.api-keys` | string[] | [] | (已弃用) 提供商的API密钥。建议改用api-key-entries以获得每密钥代理支持。 |
| `openai-compatibility.*.api-key-entries` | object[] | [] | API密钥条目支持可选的每密钥代理配置。优先于api-keys。 |
| `openai-compatibility.*.api-key-entries.*.api-key` | string | "" | 该条目的API密钥。 |
| `openai-compatibility.*.api-key-entries.*.proxy-url` | string | "" | 针对该API密钥的代理URL。会覆盖全局proxy-url设置。支持socks5/http/https协议。 |
| `openai-compatibility.*.models` | object[] | [] | 实际的模型名称。 | | `openai-compatibility.*.models` | object[] | [] | 实际的模型名称。 |
| `openai-compatibility.*.models.*.name` | string | "" | 提供商支持的模型。 | | `openai-compatibility.*.models.*.name` | string | "" | 提供商支持的模型。 |
| `openai-compatibility.*.models.*.alias` | string | "" | 在API中使用的别名。 | | `openai-compatibility.*.models.*.alias` | string | "" | 在API中使用的别名。 |
@@ -373,20 +378,28 @@ generative-language-api-key:
codex-api-key: codex-api-key:
- api-key: "sk-atSM..." - api-key: "sk-atSM..."
base-url: "https://www.example.com" # 第三方 Codex API 中转服务端点 base-url: "https://www.example.com" # 第三方 Codex API 中转服务端点
proxy-url: "socks5://proxy.example.com:1080" # 可选:针对该密钥的代理设置
# Claude API 密钥 # Claude API 密钥
claude-api-key: claude-api-key:
- api-key: "sk-atSM..." # 如果使用官方 Claude API无需设置 base-url - api-key: "sk-atSM..." # 如果使用官方 Claude API,无需设置 base-url
- api-key: "sk-atSM..." - api-key: "sk-atSM..."
base-url: "https://www.example.com" # 第三方 Claude API 中转服务端点 base-url: "https://www.example.com" # 第三方 Claude API 中转服务端点
proxy-url: "socks5://proxy.example.com:1080" # 可选:针对该密钥的代理设置
# OpenAI 兼容提供商 # OpenAI 兼容提供商
openai-compatibility: openai-compatibility:
- name: "openrouter" # 提供商的名称;它将被用于用户代理和其它地方。 - name: "openrouter" # 提供商的名称;它将被用于用户代理和其它地方。
base-url: "https://openrouter.ai/api/v1" # 提供商的基础URL。 base-url: "https://openrouter.ai/api/v1" # 提供商的基础URL。
api-keys: # 提供商的API密钥。如果需要可以添加多个密钥。如果允许未经身份验证的访问则可以省略。 # 新格式:支持每密钥代理配置(推荐):
- "sk-or-v1-...b780" api-key-entries:
- "sk-or-v1-...b781" - api-key: "sk-or-v1-...b780"
proxy-url: "socks5://proxy.example.com:1080" # 可选:针对该密钥的代理设置
- api-key: "sk-or-v1-...b781" # 不进行额外代理设置
# 旧格式(仍支持,但无法为每个密钥指定代理):
# api-keys:
# - "sk-or-v1-...b780"
# - "sk-or-v1-...b781"
models: # 提供商支持的模型。 models: # 提供商支持的模型。
- name: "moonshotai/kimi-k2:free" # 实际的模型名称。 - name: "moonshotai/kimi-k2:free" # 实际的模型名称。
alias: "kimi-k2" # 在API中使用的别名。 alias: "kimi-k2" # 在API中使用的别名。
@@ -398,10 +411,26 @@ openai-compatibility:
- name内部识别名 - name内部识别名
- base-url提供商基础地址 - base-url提供商基础地址
- api-keys可选多密钥轮询若提供商支持无鉴权可省略 - api-key-entriesAPI密钥条目列表支持可选的每密钥代理配置推荐
- api-keys(已弃用) 简单的API密钥列表不支持代理配置
- models将上游模型 `name` 映射为本地可用 `alias` - models将上游模型 `name` 映射为本地可用 `alias`
示例: 支持每密钥代理配置的示例:
```yaml
openai-compatibility:
- name: "openrouter"
base-url: "https://openrouter.ai/api/v1"
api-key-entries:
- api-key: "sk-or-v1-...b780"
proxy-url: "socks5://proxy.example.com:1080"
- api-key: "sk-or-v1-...b781"
models:
- name: "moonshotai/kimi-k2:free"
alias: "kimi-k2"
```
旧格式(仍支持):
```yaml ```yaml
openai-compatibility: openai-compatibility:

View File

@@ -51,20 +51,28 @@ quota-exceeded:
#codex-api-key: #codex-api-key:
# - api-key: "sk-atSM..." # - api-key: "sk-atSM..."
# base-url: "https://www.example.com" # use the custom codex API endpoint # base-url: "https://www.example.com" # use the custom codex API endpoint
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
# Claude API keys # Claude API keys
#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..."
# base-url: "https://www.example.com" # use the custom claude API endpoint # base-url: "https://www.example.com" # use the custom claude API endpoint
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
# 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.
# 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.
# api-keys: # The API keys for the provider. Add multiple keys if needed. Omit if unauthenticated access is allowed. # # New format with per-key proxy support (recommended):
# - "sk-or-v1-...b780" # api-key-entries:
# - "sk-or-v1-...b781" # - api-key: "sk-or-v1-...b780"
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
# - api-key: "sk-or-v1-...b781" # without proxy-url
# # Legacy format (still supported, but cannot specify proxy per key):
# # api-keys:
# # - "sk-or-v1-...b780"
# # - "sk-or-v1-...b781"
# models: # The models supported by the provider. # models: # The models supported by the provider.
# - name: "moonshotai/kimi-k2:free" # The actual model name. # - name: "moonshotai/kimi-k2:free" # The actual model name.
# alias: "kimi-k2" # The alias used in the API. # alias: "kimi-k2" # The alias used in the API.

View File

@@ -107,6 +107,9 @@ type ClaudeKey struct {
// 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"`
// ProxyURL overrides the global proxy setting for this API key if provided.
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
} }
// CodexKey represents the configuration for a Codex API key, // CodexKey represents the configuration for a Codex API key,
@@ -118,6 +121,9 @@ type CodexKey struct {
// 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"`
// ProxyURL overrides the global proxy setting for this API key if provided.
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
} }
// OpenAICompatibility represents the configuration for OpenAI API compatibility // OpenAICompatibility represents the configuration for OpenAI API compatibility
@@ -130,12 +136,25 @@ type OpenAICompatibility struct {
BaseURL string `yaml:"base-url" json:"base-url"` BaseURL string `yaml:"base-url" json:"base-url"`
// APIKeys are the authentication keys for accessing the external API services. // APIKeys are the authentication keys for accessing the external API services.
APIKeys []string `yaml:"api-keys" json:"api-keys"` // Deprecated: Use APIKeyEntries instead to support per-key proxy configuration.
APIKeys []string `yaml:"api-keys,omitempty" json:"api-keys,omitempty"`
// APIKeyEntries defines API keys with optional per-key proxy configuration.
APIKeyEntries []OpenAICompatibilityAPIKey `yaml:"api-key-entries,omitempty" json:"api-key-entries,omitempty"`
// Models defines the model configurations including aliases for routing. // Models defines the model configurations including aliases for routing.
Models []OpenAICompatibilityModel `yaml:"models" json:"models"` Models []OpenAICompatibilityModel `yaml:"models" json:"models"`
} }
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
type OpenAICompatibilityAPIKey struct {
// APIKey is the authentication key for accessing the external API services.
APIKey string `yaml:"api-key" json:"api-key"`
// ProxyURL overrides the global proxy setting for this API key if provided.
ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"`
}
// OpenAICompatibilityModel represents a model configuration for OpenAI compatibility, // OpenAICompatibilityModel represents a model configuration for OpenAI compatibility,
// including the actual model name and its alias for API routing. // including the actual model name and its alias for API routing.
type OpenAICompatibilityModel struct { type OpenAICompatibilityModel struct {

View File

@@ -746,11 +746,13 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
if ck.BaseURL != "" { if ck.BaseURL != "" {
attrs["base_url"] = ck.BaseURL attrs["base_url"] = ck.BaseURL
} }
proxyURL := strings.TrimSpace(ck.ProxyURL)
a := &coreauth.Auth{ a := &coreauth.Auth{
ID: id, ID: id,
Provider: "claude", Provider: "claude",
Label: "claude-apikey", Label: "claude-apikey",
Status: coreauth.StatusActive, Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs, Attributes: attrs,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@@ -772,11 +774,13 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
if ck.BaseURL != "" { if ck.BaseURL != "" {
attrs["base_url"] = ck.BaseURL attrs["base_url"] = ck.BaseURL
} }
proxyURL := strings.TrimSpace(ck.ProxyURL)
a := &coreauth.Auth{ a := &coreauth.Auth{
ID: id, ID: id,
Provider: "codex", Provider: "codex",
Label: "codex-apikey", Label: "codex-apikey",
Status: coreauth.StatusActive, Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs, Attributes: attrs,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@@ -790,6 +794,42 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
providerName = "openai-compatibility" providerName = "openai-compatibility"
} }
base := strings.TrimSpace(compat.BaseURL) base := strings.TrimSpace(compat.BaseURL)
// Handle new APIKeyEntries format (preferred)
if len(compat.APIKeyEntries) > 0 {
for j := range compat.APIKeyEntries {
entry := &compat.APIKeyEntries[j]
key := strings.TrimSpace(entry.APIKey)
if key == "" {
continue
}
proxyURL := strings.TrimSpace(entry.ProxyURL)
idKind := fmt.Sprintf("openai-compatibility:%s", providerName)
id, token := idGen.next(idKind, key, base, proxyURL)
attrs := map[string]string{
"source": fmt.Sprintf("config:%s[%s]", providerName, token),
"base_url": base,
"api_key": key,
"compat_name": compat.Name,
"provider_key": providerName,
}
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
attrs["models_hash"] = hash
}
a := &coreauth.Auth{
ID: id,
Provider: providerName,
Label: compat.Name,
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
CreatedAt: now,
UpdatedAt: now,
}
out = append(out, a)
}
} else {
// Handle legacy APIKeys format for backward compatibility
for j := range compat.APIKeys { for j := range compat.APIKeys {
key := strings.TrimSpace(compat.APIKeys[j]) key := strings.TrimSpace(compat.APIKeys[j])
if key == "" { if key == "" {
@@ -820,6 +860,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
} }
} }
} }
}
// Also synthesize auth entries directly from auth files (for OAuth/file-backed providers) // Also synthesize auth entries directly from auth files (for OAuth/file-backed providers)
entries, _ := os.ReadDir(w.authDir) entries, _ := os.ReadDir(w.authDir)
for _, e := range entries { for _, e := range entries {
@@ -937,9 +978,14 @@ func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
if len(cfg.OpenAICompatibility) > 0 { if len(cfg.OpenAICompatibility) > 0 {
// Do not construct legacy clients for OpenAI-compat providers; these are handled by the stateless executor. // Do not construct legacy clients for OpenAI-compat providers; these are handled by the stateless executor.
for _, compatConfig := range cfg.OpenAICompatibility { for _, compatConfig := range cfg.OpenAICompatibility {
// Count from new APIKeyEntries format if present, otherwise fall back to legacy APIKeys
if len(compatConfig.APIKeyEntries) > 0 {
openAICompatCount += len(compatConfig.APIKeyEntries)
} else {
openAICompatCount += len(compatConfig.APIKeys) openAICompatCount += len(compatConfig.APIKeys)
} }
} }
}
return glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount return glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
} }
@@ -980,9 +1026,9 @@ func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []st
} }
switch { switch {
case !oldOk: case !oldOk:
changes = append(changes, fmt.Sprintf("provider added: %s (api-keys=%d, models=%d)", label, countNonEmptyStrings(newEntry.APIKeys), countOpenAIModels(newEntry.Models))) changes = append(changes, fmt.Sprintf("provider added: %s (api-keys=%d, models=%d)", label, countAPIKeys(newEntry), countOpenAIModels(newEntry.Models)))
case !newOk: case !newOk:
changes = append(changes, fmt.Sprintf("provider removed: %s (api-keys=%d, models=%d)", label, countNonEmptyStrings(oldEntry.APIKeys), countOpenAIModels(oldEntry.Models))) changes = append(changes, fmt.Sprintf("provider removed: %s (api-keys=%d, models=%d)", label, countAPIKeys(oldEntry), countOpenAIModels(oldEntry.Models)))
default: default:
if detail := describeOpenAICompatibilityUpdate(oldEntry, newEntry); detail != "" { if detail := describeOpenAICompatibilityUpdate(oldEntry, newEntry); detail != "" {
changes = append(changes, fmt.Sprintf("provider updated: %s %s", label, detail)) changes = append(changes, fmt.Sprintf("provider updated: %s %s", label, detail))
@@ -993,8 +1039,8 @@ func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []st
} }
func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibility) string { func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibility) string {
oldKeyCount := countNonEmptyStrings(oldEntry.APIKeys) oldKeyCount := countAPIKeys(oldEntry)
newKeyCount := countNonEmptyStrings(newEntry.APIKeys) newKeyCount := countAPIKeys(newEntry)
oldModelCount := countOpenAIModels(oldEntry.Models) oldModelCount := countOpenAIModels(oldEntry.Models)
newModelCount := countOpenAIModels(newEntry.Models) newModelCount := countOpenAIModels(newEntry.Models)
details := make([]string, 0, 2) details := make([]string, 0, 2)
@@ -1010,6 +1056,21 @@ func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibi
return "(" + strings.Join(details, ", ") + ")" return "(" + strings.Join(details, ", ") + ")"
} }
func countAPIKeys(entry config.OpenAICompatibility) int {
// Prefer new APIKeyEntries format
if len(entry.APIKeyEntries) > 0 {
count := 0
for _, keyEntry := range entry.APIKeyEntries {
if strings.TrimSpace(keyEntry.APIKey) != "" {
count++
}
}
return count
}
// Fall back to legacy APIKeys format
return countNonEmptyStrings(entry.APIKeys)
}
func countNonEmptyStrings(values []string) int { func countNonEmptyStrings(values []string) int {
count := 0 count := 0
for _, value := range values { for _, value := range values {