sshinmen factory-droid[bot] commited on
Commit
d254e3e
·
1 Parent(s): e9f4ef2

Support multiple Claude API keys with round-robin

Browse files

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

internal/config/config.go CHANGED
@@ -270,12 +270,26 @@ type CloakConfig struct {
270
  SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"`
271
  }
272
 
 
 
 
 
 
 
 
 
 
273
  // ClaudeKey represents the configuration for a Claude API key,
274
  // including the API key itself and an optional base URL for the API endpoint.
275
  type ClaudeKey struct {
276
  // APIKey is the authentication key for accessing Claude API services.
 
277
  APIKey string `yaml:"api-key" json:"api-key"`
278
 
 
 
 
 
279
  // Priority controls selection preference when multiple credentials match.
280
  // Higher values are preferred; defaults to 0.
281
  Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
@@ -288,6 +302,7 @@ type ClaudeKey struct {
288
  BaseURL string `yaml:"base-url" json:"base-url"`
289
 
290
  // ProxyURL overrides the global proxy setting for this API key if provided.
 
291
  ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
292
 
293
  // Models defines upstream model names and aliases for request routing.
@@ -787,7 +802,7 @@ func (cfg *Config) SanitizeCodexKeys() {
787
  cfg.CodexKey = out
788
  }
789
 
790
- // SanitizeClaudeKeys normalizes headers for Claude credentials.
791
  func (cfg *Config) SanitizeClaudeKeys() {
792
  if cfg == nil || len(cfg.ClaudeKey) == 0 {
793
  return
@@ -797,6 +812,15 @@ func (cfg *Config) SanitizeClaudeKeys() {
797
  entry.Prefix = normalizeModelPrefix(entry.Prefix)
798
  entry.Headers = NormalizeHeaders(entry.Headers)
799
  entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
 
 
 
 
 
 
 
 
 
800
  }
801
  }
802
 
 
270
  SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"`
271
  }
272
 
273
+ // ClaudeAPIKeyEntry represents an API key configuration with optional proxy setting for Claude.
274
+ type ClaudeAPIKeyEntry struct {
275
+ // APIKey is the authentication key for accessing Claude API services.
276
+ APIKey string `yaml:"api-key" json:"api-key"`
277
+
278
+ // ProxyURL overrides the global proxy setting for this API key if provided.
279
+ ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"`
280
+ }
281
+
282
  // ClaudeKey represents the configuration for a Claude API key,
283
  // including the API key itself and an optional base URL for the API endpoint.
284
  type ClaudeKey struct {
285
  // APIKey is the authentication key for accessing Claude API services.
286
+ // Deprecated: Use APIKeyEntries for multiple API keys with round-robin support.
287
  APIKey string `yaml:"api-key" json:"api-key"`
288
 
289
+ // APIKeyEntries defines API keys with optional per-key proxy configuration.
290
+ // When provided, each entry creates a separate Auth for round-robin load balancing.
291
+ APIKeyEntries []ClaudeAPIKeyEntry `yaml:"api-key-entries,omitempty" json:"api-key-entries,omitempty"`
292
+
293
  // Priority controls selection preference when multiple credentials match.
294
  // Higher values are preferred; defaults to 0.
295
  Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
 
302
  BaseURL string `yaml:"base-url" json:"base-url"`
303
 
304
  // ProxyURL overrides the global proxy setting for this API key if provided.
305
+ // Used as fallback when APIKeyEntries is empty or when entries don't specify ProxyURL.
306
  ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
307
 
308
  // Models defines upstream model names and aliases for request routing.
 
802
  cfg.CodexKey = out
803
  }
804
 
805
+ // SanitizeClaudeKeys normalizes headers and API key entries for Claude credentials.
806
  func (cfg *Config) SanitizeClaudeKeys() {
807
  if cfg == nil || len(cfg.ClaudeKey) == 0 {
808
  return
 
812
  entry.Prefix = normalizeModelPrefix(entry.Prefix)
813
  entry.Headers = NormalizeHeaders(entry.Headers)
814
  entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
815
+ entry.BaseURL = strings.TrimSpace(entry.BaseURL)
816
+ entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
817
+ entry.APIKey = strings.TrimSpace(entry.APIKey)
818
+ // Sanitize APIKeyEntries
819
+ for j := range entry.APIKeyEntries {
820
+ apiKeyEntry := &entry.APIKeyEntries[j]
821
+ apiKeyEntry.APIKey = strings.TrimSpace(apiKeyEntry.APIKey)
822
+ apiKeyEntry.ProxyURL = strings.TrimSpace(apiKeyEntry.ProxyURL)
823
+ }
824
  }
825
  }
826
 
internal/watcher/diff/config_diff.go CHANGED
@@ -125,6 +125,11 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
125
  if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
126
  changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
127
  }
 
 
 
 
 
128
  if !equalStringMap(o.Headers, n.Headers) {
129
  changes = append(changes, fmt.Sprintf("claude[%d].headers: updated", i))
130
  }
@@ -367,3 +372,19 @@ func equalUpstreamAPIKeys(a, b []config.AmpUpstreamAPIKeyEntry) bool {
367
  }
368
  return true
369
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
126
  changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
127
  }
128
+ if len(o.APIKeyEntries) != len(n.APIKeyEntries) {
129
+ changes = append(changes, fmt.Sprintf("claude[%d].api-key-entries count: %d -> %d", i, len(o.APIKeyEntries), len(n.APIKeyEntries)))
130
+ } else if !equalClaudeAPIKeyEntries(o.APIKeyEntries, n.APIKeyEntries) {
131
+ changes = append(changes, fmt.Sprintf("claude[%d].api-key-entries: updated", i))
132
+ }
133
  if !equalStringMap(o.Headers, n.Headers) {
134
  changes = append(changes, fmt.Sprintf("claude[%d].headers: updated", i))
135
  }
 
372
  }
373
  return true
374
  }
375
+
376
+ // equalClaudeAPIKeyEntries compares two slices of ClaudeAPIKeyEntry for equality.
377
+ func equalClaudeAPIKeyEntries(a, b []config.ClaudeAPIKeyEntry) bool {
378
+ if len(a) != len(b) {
379
+ return false
380
+ }
381
+ for i := range a {
382
+ if strings.TrimSpace(a[i].APIKey) != strings.TrimSpace(b[i].APIKey) {
383
+ return false
384
+ }
385
+ if strings.TrimSpace(a[i].ProxyURL) != strings.TrimSpace(b[i].ProxyURL) {
386
+ return false
387
+ }
388
+ }
389
+ return true
390
+ }
internal/watcher/synthesizer/config.go CHANGED
@@ -77,7 +77,7 @@ func (s *ConfigSynthesizer) synthesizeKiroKeys(ctx *SynthesisContext) []*coreaut
77
  }
78
 
79
  attrs := map[string]string{
80
- "source": fmt.Sprintf("config:kiro[%s]", suffix),
81
  }
82
  if entry.Priority != 0 {
83
  attrs["priority"] = strconv.Itoa(entry.Priority)
@@ -156,44 +156,93 @@ func (s *ConfigSynthesizer) synthesizeClaudeKeys(ctx *SynthesisContext) []*corea
156
  now := ctx.Now
157
  idGen := ctx.IDGenerator
158
 
159
- out := make([]*coreauth.Auth, 0, len(cfg.ClaudeKey))
160
  for i := range cfg.ClaudeKey {
161
  ck := cfg.ClaudeKey[i]
162
- key := strings.TrimSpace(ck.APIKey)
163
- if key == "" {
164
- continue
165
- }
166
  prefix := strings.TrimSpace(ck.Prefix)
167
  base := strings.TrimSpace(ck.BaseURL)
168
- id, token := idGen.Next("claude:apikey", key, base)
169
- attrs := map[string]string{
170
- "source": fmt.Sprintf("config:claude[%s]", token),
171
- "api_key": key,
172
- }
173
- if ck.Priority != 0 {
174
- attrs["priority"] = strconv.Itoa(ck.Priority)
175
- }
176
- if base != "" {
177
- attrs["base_url"] = base
178
- }
179
- if hash := diff.ComputeClaudeModelsHash(ck.Models); hash != "" {
180
- attrs["models_hash"] = hash
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  }
182
- addConfigHeadersToAttrs(ck.Headers, attrs)
183
- proxyURL := strings.TrimSpace(ck.ProxyURL)
184
- a := &coreauth.Auth{
185
- ID: id,
186
- Provider: "claude",
187
- Label: "claude-apikey",
188
- Prefix: prefix,
189
- Status: coreauth.StatusActive,
190
- ProxyURL: proxyURL,
191
- Attributes: attrs,
192
- CreatedAt: now,
193
- UpdatedAt: now,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
195
- ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey")
196
- out = append(out, a)
197
  }
198
  return out
199
  }
 
77
  }
78
 
79
  attrs := map[string]string{
80
+ "source": fmt.Sprintf("config:kiro[%s]", suffix),
81
  }
82
  if entry.Priority != 0 {
83
  attrs["priority"] = strconv.Itoa(entry.Priority)
 
156
  now := ctx.Now
157
  idGen := ctx.IDGenerator
158
 
159
+ out := make([]*coreauth.Auth, 0)
160
  for i := range cfg.ClaudeKey {
161
  ck := cfg.ClaudeKey[i]
 
 
 
 
162
  prefix := strings.TrimSpace(ck.Prefix)
163
  base := strings.TrimSpace(ck.BaseURL)
164
+
165
+ // Handle new APIKeyEntries format (preferred) for round-robin support
166
+ createdEntries := 0
167
+ for j := range ck.APIKeyEntries {
168
+ entry := &ck.APIKeyEntries[j]
169
+ key := strings.TrimSpace(entry.APIKey)
170
+ if key == "" {
171
+ continue
172
+ }
173
+ // Use entry proxy if set, otherwise fall back to key-level proxy
174
+ proxyURL := strings.TrimSpace(entry.ProxyURL)
175
+ if proxyURL == "" {
176
+ proxyURL = strings.TrimSpace(ck.ProxyURL)
177
+ }
178
+
179
+ id, token := idGen.Next("claude:apikey", key, base, proxyURL)
180
+ attrs := map[string]string{
181
+ "source": fmt.Sprintf("config:claude[%s]", token),
182
+ "api_key": key,
183
+ }
184
+ if ck.Priority != 0 {
185
+ attrs["priority"] = strconv.Itoa(ck.Priority)
186
+ }
187
+ if base != "" {
188
+ attrs["base_url"] = base
189
+ }
190
+ if hash := diff.ComputeClaudeModelsHash(ck.Models); hash != "" {
191
+ attrs["models_hash"] = hash
192
+ }
193
+ addConfigHeadersToAttrs(ck.Headers, attrs)
194
+ a := &coreauth.Auth{
195
+ ID: id,
196
+ Provider: "claude",
197
+ Label: "claude-apikey",
198
+ Prefix: prefix,
199
+ Status: coreauth.StatusActive,
200
+ ProxyURL: proxyURL,
201
+ Attributes: attrs,
202
+ CreatedAt: now,
203
+ UpdatedAt: now,
204
+ }
205
+ ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey")
206
+ out = append(out, a)
207
+ createdEntries++
208
  }
209
+
210
+ // Fallback: use single APIKey if no APIKeyEntries (backward compatibility)
211
+ if createdEntries == 0 {
212
+ key := strings.TrimSpace(ck.APIKey)
213
+ if key == "" {
214
+ continue
215
+ }
216
+ proxyURL := strings.TrimSpace(ck.ProxyURL)
217
+ id, token := idGen.Next("claude:apikey", key, base, proxyURL)
218
+ attrs := map[string]string{
219
+ "source": fmt.Sprintf("config:claude[%s]", token),
220
+ "api_key": key,
221
+ }
222
+ if ck.Priority != 0 {
223
+ attrs["priority"] = strconv.Itoa(ck.Priority)
224
+ }
225
+ if base != "" {
226
+ attrs["base_url"] = base
227
+ }
228
+ if hash := diff.ComputeClaudeModelsHash(ck.Models); hash != "" {
229
+ attrs["models_hash"] = hash
230
+ }
231
+ addConfigHeadersToAttrs(ck.Headers, attrs)
232
+ a := &coreauth.Auth{
233
+ ID: id,
234
+ Provider: "claude",
235
+ Label: "claude-apikey",
236
+ Prefix: prefix,
237
+ Status: coreauth.StatusActive,
238
+ ProxyURL: proxyURL,
239
+ Attributes: attrs,
240
+ CreatedAt: now,
241
+ UpdatedAt: now,
242
+ }
243
+ ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey")
244
+ out = append(out, a)
245
  }
 
 
246
  }
247
  return out
248
  }