From 6af938448ec806d2bf4b158c744d4fcabfc77925 Mon Sep 17 00:00:00 2001 From: zenfun Date: Sat, 10 Jan 2026 00:58:02 +0800 Subject: [PATCH] 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 --- cmd/seeder/client.go | 24 ++++++++++- cmd/seeder/generator.go | 12 ++++-- cmd/seeder/seeder.go | 94 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/cmd/seeder/client.go b/cmd/seeder/client.go index 5e6d6fa..e7330df 100644 --- a/cmd/seeder/client.go +++ b/cmd/seeder/client.go @@ -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 { diff --git a/cmd/seeder/generator.go b/cmd/seeder/generator.go index fa6a1a7..1242107 100644 --- a/cmd/seeder/generator.go +++ b/cmd/seeder/generator.go @@ -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, diff --git a/cmd/seeder/seeder.go b/cmd/seeder/seeder.go index ee11f29..f7b4946 100644 --- a/cmd/seeder/seeder.go +++ b/cmd/seeder/seeder.go @@ -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)) } }