-
-
Notifications
You must be signed in to change notification settings - Fork 658
Pr/key enabled switch upstream #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -503,6 +503,11 @@ type UpdateKeyNotesRequest struct { | |||||||||||||||||||||||||||||
| Notes string `json:"notes"` | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // UpdateKeyEnabledRequest defines the payload for toggling a key's manual enabled switch. | ||||||||||||||||||||||||||||||
| type UpdateKeyEnabledRequest struct { | ||||||||||||||||||||||||||||||
| Enabled bool `json:"enabled"` | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // UpdateKeyNotes handles updating the notes of a specific API key. | ||||||||||||||||||||||||||||||
| func (s *Server) UpdateKeyNotes(c *gin.Context) { | ||||||||||||||||||||||||||||||
| keyIDStr := c.Param("id") | ||||||||||||||||||||||||||||||
|
|
@@ -544,3 +549,35 @@ func (s *Server) UpdateKeyNotes(c *gin.Context) { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| response.Success(c, nil) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // UpdateKeyEnabled handles toggling whether a key can enter the active routing pool. | ||||||||||||||||||||||||||||||
| func (s *Server) UpdateKeyEnabled(c *gin.Context) { | ||||||||||||||||||||||||||||||
| keyIDStr := c.Param("id") | ||||||||||||||||||||||||||||||
| keyID, err := strconv.Atoi(keyIDStr) | ||||||||||||||||||||||||||||||
| if err != nil || keyID <= 0 { | ||||||||||||||||||||||||||||||
| response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "invalid key ID format")) | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var req UpdateKeyEnabledRequest | ||||||||||||||||||||||||||||||
| if err := c.ShouldBindJSON(&req); err != nil { | ||||||||||||||||||||||||||||||
| response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| if strings.Contains(err.Error(), "record not found") { | ||||||||||||||||||||||||||||||
| response.Error(c, app_errors.ErrResourceNotFound) | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| response.Error(c, app_errors.ParseDBError(err)) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+568
to
+574
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and print SetKeyEnabled implementation in internal/keypool/provider.go
file="internal/keypool/provider.go"
if [ -f "$file" ]; then
echo "== $file =="
# Print around function definition
line=$(rg -n "func \\(p \\*KeyProvider\\) SetKeyEnabled\\b" "$file" | head -n1 | cut -d: -f1)
if [ -n "${line:-}" ]; then
start=$((line-30))
end=$((line+120))
sed -n "${start},${end}p" "$file" | nl -ba | sed -n '1,200p'
else
echo "SetKeyEnabled not found in $file"
fi
else
echo "File not found: $file"
fi
# Also inspect internal/services/key_service.go just in case it's implemented there
file2="internal/services/key_service.go"
if [ -f "$file2" ]; then
echo "== $file2 =="
line=$(rg -n "func \\(s \\*KeyService\\) SetKeyEnabled\\b" "$file2" | head -n1 | cut -d: -f1)
if [ -n "${line:-}" ]; then
start=$((line-20))
end=$((line+80))
sed -n "${start},${end}p" "$file2" | nl -ba | sed -n '1,200p'
else
echo "SetKeyEnabled not found in $file2"
fi
else
echo "File not found: $file2"
fiRepository: tbphp/gpt-load Length of output: 155 🏁 Script executed: #!/bin/bash
set -euo pipefail
# helper to print with line numbers (no nl dependency)
print_with_numbers() {
local f="$1"; local start="$2"; local end="$3"
local i=1
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%5d\t%s\n", NR, $0 }' "$f"
}
file="internal/keypool/provider.go"
echo "== $file =="
if [ -f "$file" ]; then
line=$(rg -n "func \\(p \\*KeyProvider\\) SetKeyEnabled\\b" "$file" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-30))
end=$((line+160))
print_with_numbers "$file" "$start" "$end"
else
echo "SetKeyEnabled not found"
fi
else
echo "File not found: $file"
fi
file2="internal/services/key_service.go"
echo "== $file2 =="
if [ -f "$file2" ]; then
line=$(rg -n "func \\(s \\*KeyService\\) SetKeyEnabled\\b" "$file2" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-20))
end=$((line+120))
print_with_numbers "$file2" "$start" "$end"
else
echo "SetKeyEnabled not found"
fi
else
echo "File not found: $file2"
fiRepository: tbphp/gpt-load Length of output: 9725 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="internal/handler/key_handler.go"
echo "== $file (imports) =="
# Print import block (best-effort)
start=$(rg -n "^import \\(" "$file" | head -n1 | cut -d: -f1 || true)
if [ -n "${start:-}" ]; then
# find closing paren
end=$(awk -v s="$start" 'NR>=s{ if($0 ~ /^\\)/){ print NR; exit } }' "$file")
sed -n "${start},${end}p" "$file" | nl -ba | head -n 200
else
echo "No multi-line import block found"
fi
echo
echo "== $file (around SetKeyEnabled handler error mapping) =="
# Print around provided line numbers
sed -n '540,610p' "$file" | nl -ba | sed -n '1,200p'Repository: tbphp/gpt-load Length of output: 214 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="internal/handler/key_handler.go"
echo "== $file (import block) =="
# find import block start
start=$(rg -n "^import \\(" "$file" | head -n1 | cut -d: -f1 || true)
if [ -z "${start:-}" ]; then
# single-line import fallback
rg -n "^import " "$file" | head -n 50
else
end=$(awk -v s="$start" 'NR>=s{ if($0 ~ /^\\)/){ print NR; exit } }' "$file")
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%5d\t%s\n", NR, $0 }' "$file"
fi
echo
echo "== $file (around SetKeyEnabled mapping) =="
awk 'NR>=540 && NR<=610 { printf "%5d\t%s\n", NR, $0 }' "$file"Repository: tbphp/gpt-load Length of output: 1544 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="internal/handler/key_handler.go"
awk 'NR<=140 { printf "%5d\t%s\n", NR, $0 }' "$file" | sed -n '1,140p'Repository: tbphp/gpt-load Length of output: 5124 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="internal/handler/key_handler.go"
echo "== search for errors import/usages =="
rg -n "\"errors\"" "$file" || true
rg -n "\berrors\." "$file" || trueRepository: tbphp/gpt-load Length of output: 96 Use
Suggested fix import (
+ "errors"
"fmt"
app_errors "gpt-load/internal/errors"
@@
key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
if err != nil {
- if strings.Contains(err.Error(), "record not found") {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, app_errors.ErrResourceNotFound)
} else {
response.Error(c, app_errors.ParseDBError(err))
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| response.Success(c, gin.H{ | ||||||||||||||||||||||||||||||
| "id": key.ID, | ||||||||||||||||||||||||||||||
| "enabled": key.Enabled, | ||||||||||||||||||||||||||||||
| "status": key.Status, | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,7 @@ func (p *KeyProvider) SelectKey(groupID uint) (*models.APIKey, error) { | |
| ID: uint(keyID), | ||
| KeyValue: decryptedKeyValue, | ||
| Status: keyDetails["status"], | ||
| Enabled: parseEnabled(keyDetails), | ||
| FailureCount: failureCount, | ||
| GroupID: groupID, | ||
| CreatedAt: time.Unix(createdAt, 0), | ||
|
|
@@ -147,6 +148,7 @@ func (p *KeyProvider) handleSuccess(keyID uint, keyHashKey, activeKeysListKey st | |
|
|
||
| failureCount, _ := strconv.ParseInt(keyDetails["failure_count"], 10, 64) | ||
| isActive := keyDetails["status"] == models.KeyStatusActive | ||
| isEnabled := parseEnabled(keyDetails) | ||
|
|
||
| if failureCount == 0 && isActive { | ||
| return nil | ||
|
|
@@ -171,7 +173,7 @@ func (p *KeyProvider) handleSuccess(keyID uint, keyHashKey, activeKeysListKey st | |
| return fmt.Errorf("failed to update key details in store: %w", err) | ||
| } | ||
|
|
||
| if !isActive { | ||
| if !isActive && isEnabled { | ||
| logrus.WithField("keyID", keyID).Debug("Key has recovered and is being restored to active pool.") | ||
| if err := p.store.LRem(activeKeysListKey, 0, keyID); err != nil { | ||
| return fmt.Errorf("failed to LRem key before LPush on recovery: %w", err) | ||
|
|
@@ -265,7 +267,7 @@ func (p *KeyProvider) LoadKeysFromDB() error { | |
| } | ||
| } | ||
|
|
||
| if key.Status == models.KeyStatusActive { | ||
| if isKeyAvailableForPool(key) { | ||
| allActiveKeyIDs[key.GroupID] = append(allActiveKeyIDs[key.GroupID], key.ID) | ||
| } | ||
| } | ||
|
|
@@ -560,7 +562,7 @@ func (p *KeyProvider) addKeyToStore(key *models.APIKey) error { | |
| } | ||
|
|
||
| // 2. If active, add to the active LIST | ||
| if key.Status == models.KeyStatusActive { | ||
| if isKeyAvailableForPool(key) { | ||
| activeKeysListKey := fmt.Sprintf("group:%d:active_keys", key.GroupID) | ||
| if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil { | ||
| return fmt.Errorf("failed to LRem key %d before LPush for group %d: %w", key.ID, key.GroupID, err) | ||
|
|
@@ -601,19 +603,65 @@ func (p *KeyProvider) addKeysToCacheBatch(groupID uint, keys []models.APIKey) er | |
|
|
||
| // 2. 收集所有密钥 ID | ||
| activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID) | ||
| activeKeyIDs := make([]any, len(keys)) | ||
| activeKeyIDs := make([]any, 0, len(keys)) | ||
| for i := range keys { | ||
| activeKeyIDs[i] = keys[i].ID | ||
| if isKeyAvailableForPool(&keys[i]) { | ||
| activeKeyIDs = append(activeKeyIDs, keys[i].ID) | ||
| } | ||
| } | ||
|
|
||
| // 3. 批量 LPush 活跃密钥 | ||
| if err := p.store.LPush(activeKeysListKey, activeKeyIDs...); err != nil { | ||
| return fmt.Errorf("failed to batch LPush keys to group %d: %w", groupID, err) | ||
| if len(activeKeyIDs) > 0 { | ||
| if err := p.store.LPush(activeKeysListKey, activeKeyIDs...); err != nil { | ||
| return fmt.Errorf("failed to batch LPush keys to group %d: %w", groupID, err) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // SetKeyEnabled updates a key's manual enabled switch and keeps the active pool in sync. | ||
| func (p *KeyProvider) SetKeyEnabled(keyID uint, enabled bool) (*models.APIKey, error) { | ||
| var updatedKey models.APIKey | ||
|
|
||
| err := p.executeTransactionWithRetry(func(tx *gorm.DB) error { | ||
| var key models.APIKey | ||
| if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&key, keyID).Error; err != nil { | ||
| return fmt.Errorf("failed to lock key %d for enabled update: %w", keyID, err) | ||
| } | ||
|
|
||
| if err := tx.Model(&key).Update("enabled", enabled).Error; err != nil { | ||
| return fmt.Errorf("failed to update key enabled flag in DB: %w", err) | ||
| } | ||
| key.Enabled = enabled | ||
| updatedKey = key | ||
|
|
||
| keyHashKey := fmt.Sprintf("key:%d", key.ID) | ||
| activeKeysListKey := fmt.Sprintf("group:%d:active_keys", key.GroupID) | ||
|
|
||
| if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil { | ||
| return fmt.Errorf("failed to update key enabled flag in store: %w", err) | ||
| } | ||
|
|
||
| if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil { | ||
| return fmt.Errorf("failed to LRem key from active list: %w", err) | ||
| } | ||
|
|
||
| if isKeyAvailableForPool(&key) { | ||
| if err := p.store.LPush(activeKeysListKey, key.ID); err != nil { | ||
| return fmt.Errorf("failed to LPush key to active list: %w", err) | ||
| } | ||
|
Comment on lines
+642
to
+653
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail closed when syncing the active pool on disable. This updates the hash before removing the key from Suggested direction- if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil {
- return fmt.Errorf("failed to update key enabled flag in store: %w", err)
- }
-
- if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
- return fmt.Errorf("failed to LRem key from active list: %w", err)
- }
-
- if isKeyAvailableForPool(&key) {
+ if !enabled {
+ if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
+ return fmt.Errorf("failed to LRem key from active list: %w", err)
+ }
+ }
+
+ if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil {
+ return fmt.Errorf("failed to update key enabled flag in store: %w", err)
+ }
+
+ if enabled && isKeyAvailableForPool(&key) {
if err := p.store.LPush(activeKeysListKey, key.ID); err != nil {
return fmt.Errorf("failed to LPush key to active list: %w", err)
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return nil | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &updatedKey, nil | ||
| } | ||
|
|
||
| // removeKeyFromStore is a helper to remove a single key from the cache. | ||
| func (p *KeyProvider) removeKeyFromStore(keyID, groupID uint) error { | ||
| activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID) | ||
|
|
@@ -634,12 +682,22 @@ func (p *KeyProvider) apiKeyToMap(key *models.APIKey) map[string]any { | |
| "id": fmt.Sprint(key.ID), | ||
| "key_string": key.KeyValue, | ||
| "status": key.Status, | ||
| "enabled": key.Enabled, | ||
| "failure_count": key.FailureCount, | ||
| "group_id": key.GroupID, | ||
| "created_at": key.CreatedAt.Unix(), | ||
| } | ||
| } | ||
|
|
||
| func isKeyAvailableForPool(key *models.APIKey) bool { | ||
| return key.Enabled && key.Status == models.KeyStatusActive | ||
| } | ||
|
|
||
| func parseEnabled(details map[string]string) bool { | ||
| enabled, ok := details["enabled"] | ||
| return !ok || enabled == "" || enabled == "true" || enabled == "1" | ||
| } | ||
|
|
||
| // pluckIDs extracts IDs from a slice of APIKey. | ||
| func pluckIDs(keys []models.APIKey) []uint { | ||
| ids := make([]uint, len(keys)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: tbphp/gpt-load
Length of output: 164
🏁 Script executed:
Repository: tbphp/gpt-load
Length of output: 5380
🏁 Script executed:
Repository: tbphp/gpt-load
Length of output: 16179
🏁 Script executed:
Repository: tbphp/gpt-load
Length of output: 10432
Reject requests that omit
enabled(UpdateKeyEnabled).Enabled booltreats{}asenabled=false, causing an unintended disable; use*booland return 400 whenenabledis missing. Also replace the brittlestrings.Contains(err.Error(), "record not found")check witherrors.Is(err, gorm.ErrRecordNotFound)(or rely onapp_errors.ParseDBError), since the provider wraps underlying GORM errors with%w.Suggested fix
type UpdateKeyEnabledRequest struct { - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` } @@ var req UpdateKeyEnabledRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } + if req.Enabled == nil { + response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "enabled is required")) + return + } - key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled) + key, err := s.KeyService.SetKeyEnabled(uint(keyID), *req.Enabled)🤖 Prompt for AI Agents