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

Add docs and management API support for multiple Claude keys

Browse files

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

README.hf-spaces.md CHANGED
@@ -56,22 +56,8 @@ cp Dockerfile.hf-spaces Dockerfile
56
 
57
  ## Environment Variables
58
 
59
- The startup script will automatically create a `config.yaml` from environment variables:
60
-
61
- ```yaml
62
- host: "0.0.0.0"
63
- port: 7860
64
- auth-dir: "/app/auth"
65
- debug: false
66
-
67
- api-keys:
68
- - "your-api-key"
69
-
70
- kiro-api-key:
71
- - refresh-token: "your-kiro-refresh-token"
72
- region: "us-east-1"
73
- # profile-arn is auto-fetched from token refresh!
74
- ```
75
 
76
  ## Usage
77
 
@@ -80,7 +66,6 @@ Once deployed, use the Space URL as your API base URL:
80
  ```bash
81
  # OpenAI-compatible endpoint
82
  curl https://your-username-your-space.hf.space/v1/chat/completions \
83
- -H "Authorization: Bearer your-api-key" \
84
  -H "Content-Type: application/json" \
85
  -d '{
86
  "model": "claude-sonnet-4.5",
@@ -99,6 +84,40 @@ curl https://your-username-your-space.hf.space/v1/chat/completions \
99
  | `claude-opus-4.5` | Claude 4.5 Opus (may require paid tier) |
100
  | `claude-3.7-sonnet` | Claude 3.7 Sonnet (legacy, hidden but functional) |
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  ## Troubleshooting
103
 
104
  ### Token Refresh Errors
 
56
 
57
  ## Environment Variables
58
 
59
+ The startup script will automatically create a `config.yaml` from environment variables.
60
+ See `config.example.yaml` for full configuration options.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  ## Usage
63
 
 
66
  ```bash
67
  # OpenAI-compatible endpoint
68
  curl https://your-username-your-space.hf.space/v1/chat/completions \
 
69
  -H "Content-Type: application/json" \
70
  -d '{
71
  "model": "claude-sonnet-4.5",
 
84
  | `claude-opus-4.5` | Claude 4.5 Opus (may require paid tier) |
85
  | `claude-3.7-sonnet` | Claude 3.7 Sonnet (legacy, hidden but functional) |
86
 
87
+ ## Claude API Configuration with Multiple Keys
88
+
89
+ This version supports multiple Claude API keys with round-robin load balancing!
90
+
91
+ ### Via Management UI
92
+
93
+ 1. Access the management panel at `https://your-space.hf.space/management.html`
94
+ 2. Go to **AI Providers** → **Claude API Configuration**
95
+ 3. Click **Add Configuration**
96
+ 4. In the **API Keys** section, add multiple keys with optional per-key proxy URLs
97
+ 5. Save the configuration
98
+
99
+ ### Via Config File
100
+
101
+ You can also configure multiple Claude API keys directly in your `config.yaml`:
102
+
103
+ ```yaml
104
+ claude-api-key:
105
+ - api-key-entries:
106
+ # List your API keys and optional proxies here
107
+ # See documentation for full syntax
108
+ base-url: "https://api.anthropic.com"
109
+ priority: 10
110
+ models:
111
+ - name: claude-3-5-sonnet-20241022
112
+ alias: claude-sonnet
113
+ ```
114
+
115
+ **Features:**
116
+ - **Round-robin load balancing**: Requests automatically rotate between all API keys
117
+ - **Per-key proxy support**: Each API key can have its own proxy configuration
118
+ - **Backward compatible**: Single `api-key` field still works
119
+ - **Automatic failover**: If one key fails, the system tries the next one
120
+
121
  ## Troubleshooting
122
 
123
  ### Token Refresh Errors
internal/api/handlers/management/config_lists.go CHANGED
@@ -400,13 +400,14 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) {
400
  }
401
  func (h *Handler) PatchClaudeKey(c *gin.Context) {
402
  type claudeKeyPatch struct {
403
- APIKey *string `json:"api-key"`
404
- Prefix *string `json:"prefix"`
405
- BaseURL *string `json:"base-url"`
406
- ProxyURL *string `json:"proxy-url"`
407
- Models *[]config.ClaudeModel `json:"models"`
408
- Headers *map[string]string `json:"headers"`
409
- ExcludedModels *[]string `json:"excluded-models"`
 
410
  }
411
  var body struct {
412
  Index *int `json:"index"`
@@ -424,10 +425,20 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
424
  if targetIndex == -1 && body.Match != nil {
425
  match := strings.TrimSpace(*body.Match)
426
  for i := range h.cfg.ClaudeKey {
 
427
  if h.cfg.ClaudeKey[i].APIKey == match {
428
  targetIndex = i
429
  break
430
  }
 
 
 
 
 
 
 
 
 
431
  }
432
  }
433
  if targetIndex == -1 {
@@ -439,6 +450,9 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
439
  if body.Value.APIKey != nil {
440
  entry.APIKey = strings.TrimSpace(*body.Value.APIKey)
441
  }
 
 
 
442
  if body.Value.Prefix != nil {
443
  entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
444
  }
@@ -467,7 +481,19 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
467
  if val := c.Query("api-key"); val != "" {
468
  out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey))
469
  for _, v := range h.cfg.ClaudeKey {
470
- if v.APIKey != val {
 
 
 
 
 
 
 
 
 
 
 
 
471
  out = append(out, v)
472
  }
473
  }
@@ -1132,6 +1158,22 @@ func normalizeClaudeKey(entry *config.ClaudeKey) {
1132
  entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
1133
  entry.Headers = config.NormalizeHeaders(entry.Headers)
1134
  entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
  if len(entry.Models) == 0 {
1136
  return
1137
  }
 
400
  }
401
  func (h *Handler) PatchClaudeKey(c *gin.Context) {
402
  type claudeKeyPatch struct {
403
+ APIKey *string `json:"api-key"`
404
+ APIKeyEntries *[]config.ClaudeAPIKeyEntry `json:"api-key-entries"`
405
+ Prefix *string `json:"prefix"`
406
+ BaseURL *string `json:"base-url"`
407
+ ProxyURL *string `json:"proxy-url"`
408
+ Models *[]config.ClaudeModel `json:"models"`
409
+ Headers *map[string]string `json:"headers"`
410
+ ExcludedModels *[]string `json:"excluded-models"`
411
  }
412
  var body struct {
413
  Index *int `json:"index"`
 
425
  if targetIndex == -1 && body.Match != nil {
426
  match := strings.TrimSpace(*body.Match)
427
  for i := range h.cfg.ClaudeKey {
428
+ // Match by single API key or check if match exists in APIKeyEntries
429
  if h.cfg.ClaudeKey[i].APIKey == match {
430
  targetIndex = i
431
  break
432
  }
433
+ for _, entry := range h.cfg.ClaudeKey[i].APIKeyEntries {
434
+ if entry.APIKey == match {
435
+ targetIndex = i
436
+ break
437
+ }
438
+ }
439
+ if targetIndex != -1 {
440
+ break
441
+ }
442
  }
443
  }
444
  if targetIndex == -1 {
 
450
  if body.Value.APIKey != nil {
451
  entry.APIKey = strings.TrimSpace(*body.Value.APIKey)
452
  }
453
+ if body.Value.APIKeyEntries != nil {
454
+ entry.APIKeyEntries = append([]config.ClaudeAPIKeyEntry(nil), (*body.Value.APIKeyEntries)...)
455
+ }
456
  if body.Value.Prefix != nil {
457
  entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
458
  }
 
481
  if val := c.Query("api-key"); val != "" {
482
  out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey))
483
  for _, v := range h.cfg.ClaudeKey {
484
+ // Check if single API key matches
485
+ if v.APIKey == val {
486
+ continue
487
+ }
488
+ // Check if any API key in APIKeyEntries matches
489
+ foundInEntries := false
490
+ for _, entry := range v.APIKeyEntries {
491
+ if entry.APIKey == val {
492
+ foundInEntries = true
493
+ break
494
+ }
495
+ }
496
+ if !foundInEntries {
497
  out = append(out, v)
498
  }
499
  }
 
1158
  entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
1159
  entry.Headers = config.NormalizeHeaders(entry.Headers)
1160
  entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels)
1161
+
1162
+ // Normalize APIKeyEntries
1163
+ if len(entry.APIKeyEntries) > 0 {
1164
+ normalizedEntries := make([]config.ClaudeAPIKeyEntry, 0, len(entry.APIKeyEntries))
1165
+ for i := range entry.APIKeyEntries {
1166
+ apiKeyEntry := entry.APIKeyEntries[i]
1167
+ apiKeyEntry.APIKey = strings.TrimSpace(apiKeyEntry.APIKey)
1168
+ apiKeyEntry.ProxyURL = strings.TrimSpace(apiKeyEntry.ProxyURL)
1169
+ // Skip empty API keys
1170
+ if apiKeyEntry.APIKey != "" {
1171
+ normalizedEntries = append(normalizedEntries, apiKeyEntry)
1172
+ }
1173
+ }
1174
+ entry.APIKeyEntries = normalizedEntries
1175
+ }
1176
+
1177
  if len(entry.Models) == 0 {
1178
  return
1179
  }