mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
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
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ type ProviderGroupRequest struct {
|
|||||||
GoogleLocation string `json:"google_location,omitempty"`
|
GoogleLocation string `json:"google_location,omitempty"`
|
||||||
StaticHeaders string `json:"static_headers,omitempty"`
|
StaticHeaders string `json:"static_headers,omitempty"`
|
||||||
HeadersProfile string `json:"headers_profile,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"`
|
Status string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +194,18 @@ type ProviderGroupResponse struct {
|
|||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
GoogleProject string `json:"google_project"`
|
GoogleProject string `json:"google_project"`
|
||||||
GoogleLocation string `json:"google_location"`
|
GoogleLocation string `json:"google_location"`
|
||||||
Models []string `json:"models"`
|
Models string `json:"models"` // Response is comma-separated string
|
||||||
Status string `json:"status"`
|
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) {
|
func (c *Client) ListProviderGroups() ([]ProviderGroupResponse, error) {
|
||||||
data, err := c.get("/admin/provider-groups")
|
data, err := c.get("/admin/provider-groups")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -469,7 +478,7 @@ type LogRequest struct {
|
|||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
MasterID uint `json:"master_id,omitempty"`
|
MasterID uint `json:"master_id,omitempty"`
|
||||||
KeyID uint `json:"key_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"`
|
ProviderID uint `json:"provider_id,omitempty"`
|
||||||
ProviderType string `json:"provider_type,omitempty"`
|
ProviderType string `json:"provider_type,omitempty"`
|
||||||
ProviderName string `json:"provider_name,omitempty"`
|
ProviderName string `json:"provider_name,omitempty"`
|
||||||
|
|||||||
@@ -228,14 +228,15 @@ func (g *Generator) GenerateBindings(namespaces []string, groups []ProviderGroup
|
|||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
ns := namespaces[i%len(namespaces)]
|
ns := namespaces[i%len(namespaces)]
|
||||||
group := groups[i%len(groups)]
|
group := groups[i%len(groups)]
|
||||||
|
groupModels := group.GetModelsSlice()
|
||||||
|
|
||||||
var modelName string
|
var modelName string
|
||||||
if i < len(commonModels) {
|
if i < len(commonModels) {
|
||||||
modelName = commonModels[i]
|
modelName = commonModels[i]
|
||||||
} else {
|
} else {
|
||||||
// Pick a model from the group's models
|
// Pick a model from the group's models
|
||||||
if len(group.Models) > 0 {
|
if len(groupModels) > 0 {
|
||||||
modelName = group.Models[g.rng.Intn(len(group.Models))]
|
modelName = groupModels[g.rng.Intn(len(groupModels))]
|
||||||
} else {
|
} else {
|
||||||
modelName = fmt.Sprintf("model-%d", i)
|
modelName = fmt.Sprintf("model-%d", i)
|
||||||
}
|
}
|
||||||
@@ -302,23 +303,27 @@ func (g *Generator) GenerateKeys(count int) []KeyRequest {
|
|||||||
|
|
||||||
// --- Usage Sample Generation ---
|
// --- Usage Sample Generation ---
|
||||||
|
|
||||||
func (g *Generator) GenerateUsageSamples(
|
// UsageSampleContext contains all the data needed for generating usage samples
|
||||||
masters []MasterResponse,
|
type UsageSampleContext struct {
|
||||||
keys map[uint][]KeyResponse,
|
Masters []MasterResponse
|
||||||
groups []ProviderGroupResponse,
|
Keys map[uint][]KeyResponse // master_id -> keys
|
||||||
usageDays int,
|
Groups []ProviderGroupResponse
|
||||||
) []LogRequest {
|
Providers map[uint][]APIKeyResponse // group_id -> api keys (providers)
|
||||||
|
UsageDays int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) GenerateUsageSamples(ctx UsageSampleContext) []LogRequest {
|
||||||
result := make([]LogRequest, 0)
|
result := make([]LogRequest, 0)
|
||||||
|
|
||||||
if len(masters) == 0 || len(groups) == 0 {
|
if len(ctx.Masters) == 0 || len(ctx.Groups) == 0 {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
startTime := now.AddDate(0, 0, -usageDays)
|
startTime := now.AddDate(0, 0, -ctx.UsageDays)
|
||||||
|
|
||||||
// Generate samples for each day
|
// Generate samples for each day
|
||||||
for day := 0; day < usageDays; day++ {
|
for day := 0; day < ctx.UsageDays; day++ {
|
||||||
dayTime := startTime.AddDate(0, 0, day)
|
dayTime := startTime.AddDate(0, 0, day)
|
||||||
|
|
||||||
// More traffic on weekdays
|
// More traffic on weekdays
|
||||||
@@ -329,39 +334,38 @@ func (g *Generator) GenerateUsageSamples(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gradual growth trend
|
// 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)
|
samplesPerDay := int(float64(baseCount) * growthFactor)
|
||||||
|
|
||||||
for i := 0; i < samplesPerDay; i++ {
|
for i := 0; i < samplesPerDay; i++ {
|
||||||
// Pick random master
|
// 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
|
// Pick random key for this master
|
||||||
masterKeys := keys[master.ID]
|
masterKeys := ctx.Keys[master.ID]
|
||||||
var keyID uint
|
var keyID uint
|
||||||
if len(masterKeys) > 0 {
|
if len(masterKeys) > 0 {
|
||||||
keyID = masterKeys[g.rng.Intn(len(masterKeys))].ID
|
keyID = masterKeys[g.rng.Intn(len(masterKeys))].ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick random group
|
// 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
|
// Pick random model from group
|
||||||
var modelName string
|
var modelName string
|
||||||
if len(group.Models) > 0 {
|
if len(groupModels) > 0 {
|
||||||
modelName = group.Models[g.rng.Intn(len(group.Models))]
|
modelName = groupModels[g.rng.Intn(len(groupModels))]
|
||||||
} else {
|
} else {
|
||||||
modelName = "gpt-4o"
|
modelName = "gpt-4o"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random timestamp within the day
|
// Pick random provider (API key) from group for ProviderID
|
||||||
hour := g.rng.Intn(24)
|
var providerID uint
|
||||||
minute := g.rng.Intn(60)
|
groupProviders := ctx.Providers[group.ID]
|
||||||
second := g.rng.Intn(60)
|
if len(groupProviders) > 0 {
|
||||||
timestamp := time.Date(
|
providerID = groupProviders[g.rng.Intn(len(groupProviders))].ID
|
||||||
dayTime.Year(), dayTime.Month(), dayTime.Day(),
|
}
|
||||||
hour, minute, second, 0, time.UTC,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate realistic metrics
|
// Generate realistic metrics
|
||||||
statusCode := 200
|
statusCode := 200
|
||||||
@@ -398,14 +402,17 @@ func (g *Generator) GenerateUsageSamples(
|
|||||||
// Random client IP
|
// Random client IP
|
||||||
clientIP := fmt.Sprintf("192.168.%d.%d", g.rng.Intn(256), g.rng.Intn(256))
|
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{
|
result = append(result, LogRequest{
|
||||||
Group: master.Group,
|
Group: master.Group,
|
||||||
MasterID: master.ID,
|
MasterID: master.ID,
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
ModelName: modelName,
|
Model: modelName, // Fixed: use Model not ModelName
|
||||||
ProviderID: group.ID,
|
ProviderID: providerID, // Fixed: use APIKey ID not group ID
|
||||||
ProviderType: group.Type,
|
ProviderType: group.Type,
|
||||||
ProviderName: group.Name,
|
ProviderName: group.Name,
|
||||||
StatusCode: statusCode,
|
StatusCode: statusCode,
|
||||||
|
|||||||
@@ -11,12 +11,25 @@ var seededNamespaces []string
|
|||||||
// seededProviderGroups stores created provider groups for later use
|
// seededProviderGroups stores created provider groups for later use
|
||||||
var seededProviderGroups []ProviderGroupResponse
|
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
|
// seededMasters stores created masters for later use
|
||||||
var seededMasters []MasterResponse
|
var seededMasters []MasterResponse
|
||||||
|
|
||||||
// seededKeys stores created keys per master for usage samples
|
// seededKeys stores created keys per master for usage samples
|
||||||
var seededKeys map[uint][]KeyResponse
|
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 {
|
func (s *Seeder) seedNamespaces() error {
|
||||||
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
||||||
namespaces := gen.GenerateNamespaces(s.profile.Namespaces)
|
namespaces := gen.GenerateNamespaces(s.profile.Namespaces)
|
||||||
@@ -25,7 +38,6 @@ func (s *Seeder) seedNamespaces() error {
|
|||||||
existing, err := s.client.ListNamespaces()
|
existing, err := s.client.ListNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s.cfg.DryRun {
|
if s.cfg.DryRun {
|
||||||
// In dry-run mode, continue without checking existing
|
|
||||||
existing = []NamespaceResponse{}
|
existing = []NamespaceResponse{}
|
||||||
fmt.Printf(" [DRY-RUN] Unable to list existing namespaces, proceeding anyway\n")
|
fmt.Printf(" [DRY-RUN] Unable to list existing namespaces, proceeding anyway\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -42,8 +54,8 @@ func (s *Seeder) seedNamespaces() error {
|
|||||||
|
|
||||||
for _, ns := range namespaces {
|
for _, ns := range namespaces {
|
||||||
if existingNs, exists := existingMap[ns.Name]; exists {
|
if existingNs, exists := existingMap[ns.Name]; exists {
|
||||||
// Check if we should reset
|
// Check if we should reset - only reset if tag matches
|
||||||
if s.cfg.Reset && HasSeederTag(existingNs.Description, s.seederTag) {
|
if s.cfg.Reset && s.matchesSeederTag(existingNs.Description) {
|
||||||
if err := s.client.DeleteNamespace(existingNs.ID); err != nil {
|
if err := s.client.DeleteNamespace(existingNs.ID); err != nil {
|
||||||
return fmt.Errorf("delete namespace %s: %w", ns.Name, err)
|
return fmt.Errorf("delete namespace %s: %w", ns.Name, err)
|
||||||
}
|
}
|
||||||
@@ -63,7 +75,6 @@ func (s *Seeder) seedNamespaces() error {
|
|||||||
// Create namespace
|
// Create namespace
|
||||||
created, err := s.client.CreateNamespace(ns)
|
created, err := s.client.CreateNamespace(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if it's a conflict (already exists)
|
|
||||||
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
||||||
if s.cfg.Verbose {
|
if s.cfg.Verbose {
|
||||||
fmt.Printf(" ○ %s (exists, skipped)\n", ns.Name)
|
fmt.Printf(" ○ %s (exists, skipped)\n", ns.Name)
|
||||||
@@ -113,8 +124,9 @@ func (s *Seeder) seedProviderGroups() error {
|
|||||||
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if existingGroup, exists := existingMap[group.Name]; exists {
|
if existingGroup, exists := existingMap[group.Name]; exists {
|
||||||
// Check if we should reset (check name prefix for seeder tag)
|
// Check if we should reset - match by seeder naming convention
|
||||||
if s.cfg.Reset && strings.HasPrefix(group.Name, "demo-") {
|
// 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 {
|
if err := s.client.DeleteProviderGroup(existingGroup.ID); err != nil {
|
||||||
return fmt.Errorf("delete provider group %s: %w", group.Name, err)
|
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 {
|
func (s *Seeder) seedProviders() error {
|
||||||
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
||||||
|
seededProviders = make(map[uint][]APIKeyResponse)
|
||||||
|
|
||||||
for _, group := range seededProviderGroups {
|
for _, group := range seededProviderGroups {
|
||||||
providers := gen.GenerateProviders(group.ID, group.Name, s.profile.ProvidersPerGroup)
|
providers := gen.GenerateProviders(group.ID, group.Name, s.profile.ProvidersPerGroup)
|
||||||
|
groupProviders := make([]APIKeyResponse, 0, len(providers))
|
||||||
|
|
||||||
createdCount := 0
|
createdCount := 0
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
_, err := s.client.CreateAPIKey(provider)
|
created, err := s.client.CreateAPIKey(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
||||||
s.summary.Providers.Skipped++
|
s.summary.Providers.Skipped++
|
||||||
@@ -174,7 +188,12 @@ func (s *Seeder) seedProviders() error {
|
|||||||
}
|
}
|
||||||
s.summary.Providers.Created++
|
s.summary.Providers.Created++
|
||||||
createdCount++
|
createdCount++
|
||||||
|
if created != nil {
|
||||||
|
groupProviders = append(groupProviders, *created)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seededProviders[group.ID] = groupProviders
|
||||||
|
|
||||||
if s.cfg.Verbose || s.cfg.DryRun {
|
if s.cfg.Verbose || s.cfg.DryRun {
|
||||||
fmt.Printf(" ✓ %s: %d providers created\n", group.Name, createdCount)
|
fmt.Printf(" ✓ %s: %d providers created\n", group.Name, createdCount)
|
||||||
@@ -206,7 +225,9 @@ func (s *Seeder) seedModels() error {
|
|||||||
|
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if existingModel, exists := existingMap[model.Name]; exists {
|
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 {
|
if err := s.client.DeleteModel(existingModel.ID); err != nil {
|
||||||
return fmt.Errorf("delete model %s: %w", model.Name, err)
|
return fmt.Errorf("delete model %s: %w", model.Name, err)
|
||||||
}
|
}
|
||||||
@@ -269,7 +290,8 @@ func (s *Seeder) seedBindings() error {
|
|||||||
for _, binding := range bindings {
|
for _, binding := range bindings {
|
||||||
key := fmt.Sprintf("%s:%s:%d", binding.Namespace, binding.PublicModel, binding.GroupID)
|
key := fmt.Sprintf("%s:%s:%d", binding.Namespace, binding.PublicModel, binding.GroupID)
|
||||||
if existingBinding, exists := existingMap[key]; exists {
|
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 {
|
if err := s.client.DeleteBinding(existingBinding.ID); err != nil {
|
||||||
return fmt.Errorf("delete binding: %w", err)
|
return fmt.Errorf("delete binding: %w", err)
|
||||||
}
|
}
|
||||||
@@ -321,7 +343,8 @@ func (s *Seeder) seedMasters() error {
|
|||||||
|
|
||||||
for _, master := range masters {
|
for _, master := range masters {
|
||||||
if existingMaster, exists := existingMap[master.Name]; exists {
|
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 {
|
if err := s.client.DeleteMaster(existingMaster.ID); err != nil {
|
||||||
return fmt.Errorf("delete master %s: %w", master.Name, err)
|
return fmt.Errorf("delete master %s: %w", master.Name, err)
|
||||||
}
|
}
|
||||||
@@ -402,13 +425,26 @@ func (s *Seeder) seedKeys() error {
|
|||||||
func (s *Seeder) seedUsageSamples() error {
|
func (s *Seeder) seedUsageSamples() error {
|
||||||
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
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 {
|
if len(logs) == 0 {
|
||||||
fmt.Printf(" ○ No masters or groups to generate samples for\n")
|
fmt.Printf(" ○ No masters or groups to generate samples for\n")
|
||||||
return nil
|
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
|
// Batch insert logs
|
||||||
batchSize := 100
|
batchSize := 100
|
||||||
for i := 0; i < len(logs); i += batchSize {
|
for i := 0; i < len(logs); i += batchSize {
|
||||||
|
|||||||
Reference in New Issue
Block a user