Add docs and management API support for multiple Claude keys
Browse filesCo-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
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|