fix(seeder): improve key idempotency and log names

Trim whitespace in provider model lists, format provider names as `group#keyID`
to match DP logs, and skip existing API keys during seeding (deleting on reset)
to keep runs idempotent and summaries accurate
This commit is contained in:
zenfun
2026-01-10 00:58:02 +08:00
parent 5431e24923
commit 6af938448e
3 changed files with 121 additions and 9 deletions

View File

@@ -198,12 +198,16 @@ type ProviderGroupResponse struct {
Status string `json:"status"`
}
// GetModelsSlice returns the Models field as a slice
// GetModelsSlice returns the Models field as a slice with trimmed whitespace
func (p *ProviderGroupResponse) GetModelsSlice() []string {
if p.Models == "" {
return nil
}
return strings.Split(p.Models, ",")
parts := strings.Split(p.Models, ",")
for i, part := range parts {
parts[i] = strings.TrimSpace(part)
}
return parts
}
func (c *Client) ListProviderGroups() ([]ProviderGroupResponse, error) {
@@ -472,6 +476,22 @@ func (c *Client) CreateKey(masterID uint, req KeyRequest) (*KeyResponse, error)
return &result, nil
}
func (c *Client) ListKeys(masterID uint) ([]KeyResponse, error) {
data, err := c.get(fmt.Sprintf("/admin/masters/%d/keys", masterID))
if err != nil {
return nil, err
}
var result []KeyResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("unmarshal keys: %w", err)
}
return result, nil
}
func (c *Client) DeleteKey(masterID, keyID uint) error {
return c.delete(fmt.Sprintf("/admin/masters/%d/keys/%d", masterID, keyID))
}
// --- Log API ---
type LogRequest struct {

View File

@@ -407,14 +407,20 @@ func (g *Generator) GenerateUsageSamples(ctx UsageSampleContext) []LogRequest {
// For historical data, we would need direct DB access.
// Current implementation creates logs at "now" time.
// Format provider_name as group#keyID to match DP log format
providerName := group.Name
if providerID > 0 {
providerName = fmt.Sprintf("%s#%d", group.Name, providerID)
}
result = append(result, LogRequest{
Group: master.Group,
MasterID: master.ID,
KeyID: keyID,
Model: modelName, // Fixed: use Model not ModelName
ProviderID: providerID, // Fixed: use APIKey ID not group ID
Model: modelName,
ProviderID: providerID,
ProviderType: group.Type,
ProviderName: group.Name,
ProviderName: providerName,
StatusCode: statusCode,
LatencyMs: latencyMs,
TokensIn: tokensIn,

View File

@@ -172,16 +172,62 @@ func (s *Seeder) seedProviders() error {
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
seededProviders = make(map[uint][]APIKeyResponse)
// Get existing API keys for idempotency check
existingKeys, err := s.client.ListAPIKeys()
if err != nil {
if s.cfg.DryRun {
existingKeys = []APIKeyResponse{}
fmt.Printf(" [DRY-RUN] Unable to list existing API keys, proceeding anyway\n")
} else {
return fmt.Errorf("list api keys: %w", err)
}
}
// Build map of existing keys by group_id -> api_key prefix (for matching seeder-generated keys)
existingByGroup := make(map[uint]map[string]APIKeyResponse)
for _, key := range existingKeys {
if existingByGroup[key.GroupID] == nil {
existingByGroup[key.GroupID] = make(map[string]APIKeyResponse)
}
// Store by full key for exact match
existingByGroup[key.GroupID][key.APIKey] = key
}
for _, group := range seededProviderGroups {
providers := gen.GenerateProviders(group.ID, group.Name, s.profile.ProvidersPerGroup)
groupProviders := make([]APIKeyResponse, 0, len(providers))
createdCount := 0
skippedCount := 0
for _, provider := range providers {
// Check if this exact key already exists
if groupKeys, ok := existingByGroup[group.ID]; ok {
if existingKey, exists := groupKeys[provider.APIKey]; exists {
// Key already exists
if s.cfg.Reset && s.matchesSeederPrefix(group.Name) {
// Delete for reset
if err := s.client.DeleteAPIKey(existingKey.ID); err != nil {
return fmt.Errorf("delete api key for group %s: %w", group.Name, err)
}
if s.cfg.Verbose {
fmt.Printf(" ✗ api key %d (deleted for reset)\n", existingKey.ID)
}
} else {
// Skip existing
s.summary.Providers.Skipped++
skippedCount++
groupProviders = append(groupProviders, existingKey)
continue
}
}
}
created, err := s.client.CreateAPIKey(provider)
if err != nil {
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
s.summary.Providers.Skipped++
skippedCount++
continue
}
return fmt.Errorf("create provider for group %s: %w", group.Name, err)
@@ -196,7 +242,7 @@ func (s *Seeder) seedProviders() error {
seededProviders[group.ID] = groupProviders
if s.cfg.Verbose || s.cfg.DryRun {
fmt.Printf(" ✓ %s: %d providers created\n", group.Name, createdCount)
fmt.Printf(" ✓ %s: %d providers created, %d skipped\n", group.Name, createdCount, skippedCount)
}
}
@@ -391,9 +437,49 @@ func (s *Seeder) seedKeys() error {
seededKeys = make(map[uint][]KeyResponse)
for _, master := range seededMasters {
keys := gen.GenerateKeys(s.profile.KeysPerMaster)
// Get existing keys for this master for idempotency
existingKeys, err := s.client.ListKeys(master.ID)
if err != nil {
if s.cfg.DryRun {
existingKeys = []KeyResponse{}
if s.cfg.Verbose {
fmt.Printf(" [DRY-RUN] Unable to list existing keys for master %s\n", master.Name)
}
} else {
return fmt.Errorf("list keys for master %s: %w", master.Name, err)
}
}
masterKeys := make([]KeyResponse, 0, len(keys))
// For reset mode, delete existing keys that belong to seeder masters
if s.cfg.Reset && s.matchesSeederPrefix(master.Name) && len(existingKeys) > 0 {
for _, key := range existingKeys {
if err := s.client.DeleteKey(master.ID, key.ID); err != nil {
return fmt.Errorf("delete key %d for master %s: %w", key.ID, master.Name, err)
}
if s.cfg.Verbose {
fmt.Printf(" ✗ key %d (deleted for reset)\n", key.ID)
}
}
existingKeys = []KeyResponse{}
}
// Check if we already have enough keys (idempotency)
targetCount := s.profile.KeysPerMaster
if len(existingKeys) >= targetCount {
if s.cfg.Verbose {
fmt.Printf(" ○ %s: %d keys (exists, skipped)\n", master.Name, len(existingKeys))
}
s.summary.Keys.Skipped += targetCount
seededKeys[master.ID] = existingKeys[:targetCount]
continue
}
// Create only the missing keys
keysToCreate := targetCount - len(existingKeys)
keys := gen.GenerateKeys(keysToCreate)
masterKeys := make([]KeyResponse, 0, targetCount)
masterKeys = append(masterKeys, existingKeys...)
createdCount := 0
for _, key := range keys {
@@ -415,7 +501,7 @@ func (s *Seeder) seedKeys() error {
seededKeys[master.ID] = masterKeys
if s.cfg.Verbose || s.cfg.DryRun {
fmt.Printf(" ✓ %s: %d keys created\n", master.Name, createdCount)
fmt.Printf(" ✓ %s: %d keys created, %d existing\n", master.Name, createdCount, len(existingKeys))
}
}