From 5431e249237dee9eb7a7949d53305adb8c3341dd Mon Sep 17 00:00:00 2001 From: zenfun Date: Sat, 10 Jan 2026 00:46:03 +0800 Subject: [PATCH] fix(seeder): correct log generation fields - Parse provider group models from API response string and expose as slice - Send `model` field (not `model_name`) when creating logs - Use API key ID as `provider_id` instead of provider group ID - Restrict reset behavior to resources matching seeder tag/prefix - Refactor usage sample generation to accept a context struct --- cmd/seeder/client.go | 29 ++++++++++++------- cmd/seeder/generator.go | 63 +++++++++++++++++++++++------------------ cmd/seeder/seeder.go | 58 ++++++++++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/cmd/seeder/client.go b/cmd/seeder/client.go index 2e288c6..5e6d6fa 100644 --- a/cmd/seeder/client.go +++ b/cmd/seeder/client.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" ) @@ -182,19 +183,27 @@ type ProviderGroupRequest struct { GoogleLocation string `json:"google_location,omitempty"` StaticHeaders string `json:"static_headers,omitempty"` HeadersProfile string `json:"headers_profile,omitempty"` - Models []string `json:"models,omitempty"` + Models []string `json:"models,omitempty"` // Request uses []string Status string `json:"status,omitempty"` } type ProviderGroupResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - BaseURL string `json:"base_url"` - GoogleProject string `json:"google_project"` - GoogleLocation string `json:"google_location"` - Models []string `json:"models"` - Status string `json:"status"` + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + BaseURL string `json:"base_url"` + GoogleProject string `json:"google_project"` + GoogleLocation string `json:"google_location"` + Models string `json:"models"` // Response is comma-separated string + Status string `json:"status"` +} + +// GetModelsSlice returns the Models field as a slice +func (p *ProviderGroupResponse) GetModelsSlice() []string { + if p.Models == "" { + return nil + } + return strings.Split(p.Models, ",") } func (c *Client) ListProviderGroups() ([]ProviderGroupResponse, error) { @@ -469,7 +478,7 @@ type LogRequest struct { Group string `json:"group,omitempty"` MasterID uint `json:"master_id,omitempty"` KeyID uint `json:"key_id,omitempty"` - ModelName string `json:"model_name,omitempty"` + Model string `json:"model,omitempty"` // Field name is "model" not "model_name" ProviderID uint `json:"provider_id,omitempty"` ProviderType string `json:"provider_type,omitempty"` ProviderName string `json:"provider_name,omitempty"` diff --git a/cmd/seeder/generator.go b/cmd/seeder/generator.go index 7c13e94..fa6a1a7 100644 --- a/cmd/seeder/generator.go +++ b/cmd/seeder/generator.go @@ -228,14 +228,15 @@ func (g *Generator) GenerateBindings(namespaces []string, groups []ProviderGroup for i := 0; i < count; i++ { ns := namespaces[i%len(namespaces)] group := groups[i%len(groups)] + groupModels := group.GetModelsSlice() var modelName string if i < len(commonModels) { modelName = commonModels[i] } else { // Pick a model from the group's models - if len(group.Models) > 0 { - modelName = group.Models[g.rng.Intn(len(group.Models))] + if len(groupModels) > 0 { + modelName = groupModels[g.rng.Intn(len(groupModels))] } else { modelName = fmt.Sprintf("model-%d", i) } @@ -302,23 +303,27 @@ func (g *Generator) GenerateKeys(count int) []KeyRequest { // --- Usage Sample Generation --- -func (g *Generator) GenerateUsageSamples( - masters []MasterResponse, - keys map[uint][]KeyResponse, - groups []ProviderGroupResponse, - usageDays int, -) []LogRequest { +// UsageSampleContext contains all the data needed for generating usage samples +type UsageSampleContext struct { + Masters []MasterResponse + Keys map[uint][]KeyResponse // master_id -> keys + Groups []ProviderGroupResponse + Providers map[uint][]APIKeyResponse // group_id -> api keys (providers) + UsageDays int +} + +func (g *Generator) GenerateUsageSamples(ctx UsageSampleContext) []LogRequest { result := make([]LogRequest, 0) - if len(masters) == 0 || len(groups) == 0 { + if len(ctx.Masters) == 0 || len(ctx.Groups) == 0 { return result } now := time.Now() - startTime := now.AddDate(0, 0, -usageDays) + startTime := now.AddDate(0, 0, -ctx.UsageDays) // Generate samples for each day - for day := 0; day < usageDays; day++ { + for day := 0; day < ctx.UsageDays; day++ { dayTime := startTime.AddDate(0, 0, day) // More traffic on weekdays @@ -329,39 +334,38 @@ func (g *Generator) GenerateUsageSamples( } // Gradual growth trend - growthFactor := 1.0 + float64(day)/float64(usageDays)*0.5 + growthFactor := 1.0 + float64(day)/float64(ctx.UsageDays)*0.5 samplesPerDay := int(float64(baseCount) * growthFactor) for i := 0; i < samplesPerDay; i++ { // Pick random master - master := masters[g.rng.Intn(len(masters))] + master := ctx.Masters[g.rng.Intn(len(ctx.Masters))] // Pick random key for this master - masterKeys := keys[master.ID] + masterKeys := ctx.Keys[master.ID] var keyID uint if len(masterKeys) > 0 { keyID = masterKeys[g.rng.Intn(len(masterKeys))].ID } // Pick random group - group := groups[g.rng.Intn(len(groups))] + group := ctx.Groups[g.rng.Intn(len(ctx.Groups))] + groupModels := group.GetModelsSlice() // Pick random model from group var modelName string - if len(group.Models) > 0 { - modelName = group.Models[g.rng.Intn(len(group.Models))] + if len(groupModels) > 0 { + modelName = groupModels[g.rng.Intn(len(groupModels))] } else { modelName = "gpt-4o" } - // Random timestamp within the day - hour := g.rng.Intn(24) - minute := g.rng.Intn(60) - second := g.rng.Intn(60) - timestamp := time.Date( - dayTime.Year(), dayTime.Month(), dayTime.Day(), - hour, minute, second, 0, time.UTC, - ) + // Pick random provider (API key) from group for ProviderID + var providerID uint + groupProviders := ctx.Providers[group.ID] + if len(groupProviders) > 0 { + providerID = groupProviders[g.rng.Intn(len(groupProviders))].ID + } // Generate realistic metrics statusCode := 200 @@ -398,14 +402,17 @@ func (g *Generator) GenerateUsageSamples( // Random client IP clientIP := fmt.Sprintf("192.168.%d.%d", g.rng.Intn(256), g.rng.Intn(256)) - _ = timestamp // TODO: need to set created_at somehow + // NOTE: The /logs API uses gorm.Model which auto-sets created_at + // We cannot inject historical timestamps via API. + // For historical data, we would need direct DB access. + // Current implementation creates logs at "now" time. result = append(result, LogRequest{ Group: master.Group, MasterID: master.ID, KeyID: keyID, - ModelName: modelName, - ProviderID: group.ID, + Model: modelName, // Fixed: use Model not ModelName + ProviderID: providerID, // Fixed: use APIKey ID not group ID ProviderType: group.Type, ProviderName: group.Name, StatusCode: statusCode, diff --git a/cmd/seeder/seeder.go b/cmd/seeder/seeder.go index 3338f93..ee11f29 100644 --- a/cmd/seeder/seeder.go +++ b/cmd/seeder/seeder.go @@ -11,12 +11,25 @@ var seededNamespaces []string // seededProviderGroups stores created provider groups for later use var seededProviderGroups []ProviderGroupResponse +// seededProviders stores created providers (API keys) per group for usage samples +var seededProviders map[uint][]APIKeyResponse + // seededMasters stores created masters for later use var seededMasters []MasterResponse // seededKeys stores created keys per master for usage samples var seededKeys map[uint][]KeyResponse +// matchesSeederTag checks if a string contains the current seeder tag +func (s *Seeder) matchesSeederTag(text string) bool { + return strings.Contains(text, s.seederTag) +} + +// matchesSeederPrefix checks if a name matches seeder naming convention +func (s *Seeder) matchesSeederPrefix(name string) bool { + return strings.HasPrefix(name, "demo-") || strings.HasPrefix(name, "seeder-") || strings.HasSuffix(name, "-demo") +} + func (s *Seeder) seedNamespaces() error { gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile) namespaces := gen.GenerateNamespaces(s.profile.Namespaces) @@ -25,7 +38,6 @@ func (s *Seeder) seedNamespaces() error { existing, err := s.client.ListNamespaces() if err != nil { if s.cfg.DryRun { - // In dry-run mode, continue without checking existing existing = []NamespaceResponse{} fmt.Printf(" [DRY-RUN] Unable to list existing namespaces, proceeding anyway\n") } else { @@ -42,8 +54,8 @@ func (s *Seeder) seedNamespaces() error { for _, ns := range namespaces { if existingNs, exists := existingMap[ns.Name]; exists { - // Check if we should reset - if s.cfg.Reset && HasSeederTag(existingNs.Description, s.seederTag) { + // Check if we should reset - only reset if tag matches + if s.cfg.Reset && s.matchesSeederTag(existingNs.Description) { if err := s.client.DeleteNamespace(existingNs.ID); err != nil { return fmt.Errorf("delete namespace %s: %w", ns.Name, err) } @@ -63,7 +75,6 @@ func (s *Seeder) seedNamespaces() error { // Create namespace created, err := s.client.CreateNamespace(ns) if err != nil { - // Check if it's a conflict (already exists) if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() { if s.cfg.Verbose { fmt.Printf(" ○ %s (exists, skipped)\n", ns.Name) @@ -113,8 +124,9 @@ func (s *Seeder) seedProviderGroups() error { for _, group := range groups { if existingGroup, exists := existingMap[group.Name]; exists { - // Check if we should reset (check name prefix for seeder tag) - if s.cfg.Reset && strings.HasPrefix(group.Name, "demo-") { + // Check if we should reset - match by seeder naming convention + // Provider groups use name suffix like "openai-demo", "anthropic-demo" + if s.cfg.Reset && s.matchesSeederPrefix(group.Name) { if err := s.client.DeleteProviderGroup(existingGroup.ID); err != nil { return fmt.Errorf("delete provider group %s: %w", group.Name, err) } @@ -158,13 +170,15 @@ func (s *Seeder) seedProviderGroups() error { func (s *Seeder) seedProviders() error { gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile) + seededProviders = make(map[uint][]APIKeyResponse) for _, group := range seededProviderGroups { providers := gen.GenerateProviders(group.ID, group.Name, s.profile.ProvidersPerGroup) + groupProviders := make([]APIKeyResponse, 0, len(providers)) createdCount := 0 for _, provider := range providers { - _, err := s.client.CreateAPIKey(provider) + created, err := s.client.CreateAPIKey(provider) if err != nil { if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() { s.summary.Providers.Skipped++ @@ -174,8 +188,13 @@ func (s *Seeder) seedProviders() error { } s.summary.Providers.Created++ createdCount++ + if created != nil { + groupProviders = append(groupProviders, *created) + } } + seededProviders[group.ID] = groupProviders + if s.cfg.Verbose || s.cfg.DryRun { fmt.Printf(" ✓ %s: %d providers created\n", group.Name, createdCount) } @@ -206,7 +225,9 @@ func (s *Seeder) seedModels() error { for _, model := range models { if existingModel, exists := existingMap[model.Name]; exists { - if s.cfg.Reset { + // Models are shared resources - only reset if explicitly requested + // and the model name matches seeder pattern (unlikely for real models) + if s.cfg.Reset && s.matchesSeederPrefix(model.Name) { if err := s.client.DeleteModel(existingModel.ID); err != nil { return fmt.Errorf("delete model %s: %w", model.Name, err) } @@ -269,7 +290,8 @@ func (s *Seeder) seedBindings() error { for _, binding := range bindings { key := fmt.Sprintf("%s:%s:%d", binding.Namespace, binding.PublicModel, binding.GroupID) if existingBinding, exists := existingMap[key]; exists { - if s.cfg.Reset { + // Only reset bindings in seeder namespaces + if s.cfg.Reset && s.matchesSeederPrefix(binding.Namespace) { if err := s.client.DeleteBinding(existingBinding.ID); err != nil { return fmt.Errorf("delete binding: %w", err) } @@ -321,7 +343,8 @@ func (s *Seeder) seedMasters() error { for _, master := range masters { if existingMaster, exists := existingMap[master.Name]; exists { - if s.cfg.Reset && strings.HasSuffix(master.Name, "-demo") { + // Reset masters with seeder naming pattern + if s.cfg.Reset && s.matchesSeederPrefix(master.Name) { if err := s.client.DeleteMaster(existingMaster.ID); err != nil { return fmt.Errorf("delete master %s: %w", master.Name, err) } @@ -402,13 +425,26 @@ func (s *Seeder) seedKeys() error { func (s *Seeder) seedUsageSamples() error { gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile) - logs := gen.GenerateUsageSamples(seededMasters, seededKeys, seededProviderGroups, s.cfg.UsageDays) + ctx := UsageSampleContext{ + Masters: seededMasters, + Keys: seededKeys, + Groups: seededProviderGroups, + Providers: seededProviders, + UsageDays: s.cfg.UsageDays, + } + + logs := gen.GenerateUsageSamples(ctx) if len(logs) == 0 { fmt.Printf(" ○ No masters or groups to generate samples for\n") return nil } + // Note about timestamps + if s.cfg.Verbose { + fmt.Printf(" ⚠ Note: Usage samples will have current timestamp (API does not support historical timestamps)\n") + } + // Batch insert logs batchSize := 100 for i := 0; i < len(logs); i += batchSize {