Files
ez-api/cmd/seeder/generator.go
zenfun 5431e24923 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
2026-01-10 00:46:03 +08:00

460 lines
13 KiB
Go

package main
import (
"fmt"
"math/rand"
"strings"
"time"
)
// Generator creates deterministic demo data
type Generator struct {
rng *rand.Rand
seederTag string
profile string
}
// NewGenerator creates a new data generator
func NewGenerator(rng *rand.Rand, seederTag, profile string) *Generator {
return &Generator{
rng: rng,
seederTag: seederTag,
profile: profile,
}
}
// --- Namespace Generation ---
var namespaceTemplates = []struct {
name string
desc string
}{
{"default", "Default namespace for general use"},
{"premium", "Premium tier with higher limits"},
{"staging", "Staging environment namespace"},
{"internal", "Internal services namespace"},
{"partner", "Partner integrations namespace"},
}
func (g *Generator) GenerateNamespaces(count int) []NamespaceRequest {
result := make([]NamespaceRequest, 0, count)
for i := 0; i < count && i < len(namespaceTemplates); i++ {
t := namespaceTemplates[i]
result = append(result, NamespaceRequest{
Name: fmt.Sprintf("demo-%s", t.name),
Status: "active",
Description: fmt.Sprintf("%s [%s]", t.desc, g.seederTag),
})
}
return result
}
// --- Provider Group Generation ---
var providerGroupTemplates = []struct {
name string
ptype string
baseURL string
models []string
}{
{
name: "openai-demo",
ptype: "openai",
baseURL: "https://api.openai.com/v1",
models: []string{"gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"},
},
{
name: "anthropic-demo",
ptype: "anthropic",
baseURL: "https://api.anthropic.com",
models: []string{"claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"},
},
{
name: "gemini-demo",
ptype: "gemini",
baseURL: "https://generativelanguage.googleapis.com/v1beta",
models: []string{"gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-flash-exp"},
},
{
name: "deepseek-demo",
ptype: "openai",
baseURL: "https://api.deepseek.com/v1",
models: []string{"deepseek-chat", "deepseek-coder"},
},
{
name: "groq-demo",
ptype: "openai",
baseURL: "https://api.groq.com/openai/v1",
models: []string{"llama-3.1-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"},
},
{
name: "mistral-demo",
ptype: "openai",
baseURL: "https://api.mistral.ai/v1",
models: []string{"mistral-large-latest", "mistral-medium-latest", "mistral-small-latest"},
},
{
name: "cohere-demo",
ptype: "openai",
baseURL: "https://api.cohere.ai/v1",
models: []string{"command-r-plus", "command-r", "command"},
},
{
name: "together-demo",
ptype: "openai",
baseURL: "https://api.together.xyz/v1",
models: []string{"meta-llama/Llama-3-70b-chat-hf", "mistralai/Mixtral-8x22B-Instruct-v0.1"},
},
{
name: "openrouter-demo",
ptype: "openai",
baseURL: "https://openrouter.ai/api/v1",
models: []string{"openai/gpt-4o", "anthropic/claude-3.5-sonnet"},
},
{
name: "azure-demo",
ptype: "azure",
baseURL: "https://demo.openai.azure.com/openai/deployments",
models: []string{"gpt-4o-deploy", "gpt-35-turbo-deploy"},
},
}
func (g *Generator) GenerateProviderGroups(count int) []ProviderGroupRequest {
result := make([]ProviderGroupRequest, 0, count)
for i := 0; i < count && i < len(providerGroupTemplates); i++ {
t := providerGroupTemplates[i]
result = append(result, ProviderGroupRequest{
Name: t.name,
Type: t.ptype,
BaseURL: t.baseURL,
Models: t.models,
Status: "active",
})
}
return result
}
// --- Provider (APIKey) Generation ---
func (g *Generator) GenerateProviders(groupID uint, groupName string, count int) []APIKeyRequest {
result := make([]APIKeyRequest, 0, count)
for i := 0; i < count; i++ {
// Generate a fake API key based on group name and index
fakeKey := fmt.Sprintf("sk-%s-%s-%04d", groupName, g.randomString(24), i+1)
result = append(result, APIKeyRequest{
GroupID: groupID,
APIKey: fakeKey,
Weight: 100,
Status: "active",
})
}
return result
}
// --- Model Generation ---
var modelTemplates = []struct {
name string
kind string
contextWindow int
maxOutput int
vision bool
functions bool
toolChoice bool
costPerToken float64
}{
{"gpt-4o", "chat", 128000, 16384, true, true, true, 0.000005},
{"gpt-4o-mini", "chat", 128000, 16384, true, true, true, 0.00000015},
{"gpt-4-turbo", "chat", 128000, 4096, true, true, true, 0.00001},
{"gpt-3.5-turbo", "chat", 16385, 4096, false, true, true, 0.0000005},
{"claude-3-5-sonnet-20241022", "chat", 200000, 8192, true, true, true, 0.000003},
{"claude-3-5-haiku-20241022", "chat", 200000, 8192, true, true, true, 0.00000025},
{"claude-3-opus-20240229", "chat", 200000, 4096, true, true, true, 0.000015},
{"gemini-1.5-pro", "chat", 2097152, 8192, true, true, true, 0.00000125},
{"gemini-1.5-flash", "chat", 1048576, 8192, true, true, true, 0.000000075},
{"gemini-2.0-flash-exp", "chat", 1048576, 8192, true, true, true, 0.0000001},
{"deepseek-chat", "chat", 64000, 4096, false, true, true, 0.00000014},
{"deepseek-coder", "chat", 64000, 4096, false, true, true, 0.00000014},
{"llama-3.1-70b-versatile", "chat", 131072, 8192, false, true, true, 0.00000059},
{"llama-3.1-8b-instant", "chat", 131072, 8192, false, true, true, 0.00000005},
{"mixtral-8x7b-32768", "chat", 32768, 4096, false, true, false, 0.00000027},
{"mistral-large-latest", "chat", 128000, 8192, false, true, true, 0.000002},
{"command-r-plus", "chat", 128000, 4096, false, true, true, 0.000003},
{"command-r", "chat", 128000, 4096, false, true, true, 0.0000005},
{"text-embedding-3-small", "embedding", 8191, 0, false, false, false, 0.00000002},
{"text-embedding-3-large", "embedding", 8191, 0, false, false, false, 0.00000013},
{"voyage-large-2", "embedding", 16000, 0, false, false, false, 0.00000012},
{"rerank-english-v3.0", "rerank", 4096, 0, false, false, false, 0.000001},
{"rerank-multilingual-v3.0", "rerank", 4096, 0, false, false, false, 0.000001},
{"o1-preview", "chat", 128000, 32768, true, false, false, 0.000015},
{"o1-mini", "chat", 128000, 65536, true, false, false, 0.000003},
{"dall-e-3", "other", 0, 0, false, false, false, 0.04},
{"whisper-1", "other", 0, 0, false, false, false, 0.006},
{"tts-1", "other", 0, 0, false, false, false, 0.000015},
{"tts-1-hd", "other", 0, 0, false, false, false, 0.00003},
{"codestral-latest", "chat", 32000, 8192, false, true, true, 0.000001},
}
func (g *Generator) GenerateModels(count int) []ModelRequest {
result := make([]ModelRequest, 0, count)
for i := 0; i < count && i < len(modelTemplates); i++ {
t := modelTemplates[i]
result = append(result, ModelRequest{
Name: t.name,
Kind: t.kind,
ContextWindow: t.contextWindow,
MaxOutputTokens: t.maxOutput,
SupportsVision: t.vision,
SupportsFunctions: t.functions,
SupportsToolChoice: t.toolChoice,
CostPerToken: t.costPerToken,
})
}
return result
}
// --- Binding Generation ---
func (g *Generator) GenerateBindings(namespaces []string, groups []ProviderGroupResponse, count int) []BindingRequest {
result := make([]BindingRequest, 0, count)
if len(namespaces) == 0 || len(groups) == 0 {
return result
}
// Create bindings for common models
commonModels := []string{"gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-20241022", "gemini-1.5-pro"}
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(groupModels) > 0 {
modelName = groupModels[g.rng.Intn(len(groupModels))]
} else {
modelName = fmt.Sprintf("model-%d", i)
}
}
result = append(result, BindingRequest{
Namespace: ns,
PublicModel: modelName,
GroupID: group.ID,
Weight: 100,
SelectorType: "exact",
SelectorValue: modelName,
Status: "active",
})
}
return result
}
// --- Master Generation ---
var masterTemplates = []struct {
name string
group string
maxKeys int
qps int
}{
{"admin-demo", "default", 10, 100},
{"user-demo", "default", 5, 50},
{"developer-demo", "default", 20, 200},
{"partner-demo", "partner", 15, 150},
{"staging-demo", "staging", 10, 100},
{"internal-demo", "internal", 50, 500},
{"test-demo", "default", 5, 25},
{"premium-demo", "premium", 100, 1000},
{"enterprise-demo", "enterprise", 200, 2000},
{"trial-demo", "trial", 3, 10},
}
func (g *Generator) GenerateMasters(count int) []MasterRequest {
result := make([]MasterRequest, 0, count)
for i := 0; i < count && i < len(masterTemplates); i++ {
t := masterTemplates[i]
result = append(result, MasterRequest{
Name: t.name,
Group: t.group,
MaxChildKeys: t.maxKeys,
GlobalQPS: t.qps,
})
}
return result
}
// --- Key Generation ---
func (g *Generator) GenerateKeys(count int) []KeyRequest {
result := make([]KeyRequest, 0, count)
for i := 0; i < count; i++ {
result = append(result, KeyRequest{
Scopes: "chat,embedding",
})
}
return result
}
// --- Usage Sample Generation ---
// 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(ctx.Masters) == 0 || len(ctx.Groups) == 0 {
return result
}
now := time.Now()
startTime := now.AddDate(0, 0, -ctx.UsageDays)
// Generate samples for each day
for day := 0; day < ctx.UsageDays; day++ {
dayTime := startTime.AddDate(0, 0, day)
// More traffic on weekdays
isWeekday := dayTime.Weekday() >= time.Monday && dayTime.Weekday() <= time.Friday
baseCount := 50
if isWeekday {
baseCount = 100
}
// Gradual growth trend
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 := ctx.Masters[g.rng.Intn(len(ctx.Masters))]
// Pick random key for this master
masterKeys := ctx.Keys[master.ID]
var keyID uint
if len(masterKeys) > 0 {
keyID = masterKeys[g.rng.Intn(len(masterKeys))].ID
}
// Pick random group
group := ctx.Groups[g.rng.Intn(len(ctx.Groups))]
groupModels := group.GetModelsSlice()
// Pick random model from group
var modelName string
if len(groupModels) > 0 {
modelName = groupModels[g.rng.Intn(len(groupModels))]
} else {
modelName = "gpt-4o"
}
// 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
errorMsg := ""
// 2-5% error rate
if g.rng.Float64() < 0.03 {
errorCodes := []int{400, 401, 429, 500, 503}
statusCode = errorCodes[g.rng.Intn(len(errorCodes))]
errorMsgs := []string{
"rate limit exceeded",
"invalid request",
"authentication failed",
"internal server error",
"service unavailable",
}
errorMsg = errorMsgs[g.rng.Intn(len(errorMsgs))]
}
// Realistic latency (50ms - 5000ms, with most around 200-500ms)
latencyMs := int64(50 + g.rng.ExpFloat64()*300)
if latencyMs > 5000 {
latencyMs = 5000
}
// Realistic token counts
tokensIn := int64(100 + g.rng.Intn(2000))
tokensOut := int64(50 + g.rng.Intn(1000))
// Request/response sizes
requestSize := tokensIn * 4 // ~4 bytes per token
responseSize := tokensOut * 4
// Random client IP
clientIP := fmt.Sprintf("192.168.%d.%d", g.rng.Intn(256), g.rng.Intn(256))
// 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,
Model: modelName, // Fixed: use Model not ModelName
ProviderID: providerID, // Fixed: use APIKey ID not group ID
ProviderType: group.Type,
ProviderName: group.Name,
StatusCode: statusCode,
LatencyMs: latencyMs,
TokensIn: tokensIn,
TokensOut: tokensOut,
ErrorMessage: errorMsg,
ClientIP: clientIP,
RequestSize: requestSize,
ResponseSize: responseSize,
})
}
}
return result
}
// --- Helper functions ---
func (g *Generator) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[g.rng.Intn(len(charset))]
}
return string(b)
}
// HasSeederTag checks if a string contains the seeder tag
func HasSeederTag(s, tag string) bool {
return strings.Contains(s, tag)
}
// ExtractSeederTag extracts the seeder tag from a string
func ExtractSeederTag(s string) string {
if idx := strings.Index(s, "seeder:"); idx != -1 {
end := strings.Index(s[idx:], "]")
if end == -1 {
end = len(s) - idx
}
return s[idx : idx+end]
}
return ""
}