mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(seeder): add control plane data seeder
Introduce a `cmd/seeder` CLI to generate deterministic demo datasets and seed them into the Control Plane via admin endpoints, supporting reset, dry-run, profiles, and usage sample generation. Add Cobra/Viper dependencies to support the new command.
This commit is contained in:
452
cmd/seeder/generator.go
Normal file
452
cmd/seeder/generator.go
Normal file
@@ -0,0 +1,452 @@
|
||||
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)]
|
||||
|
||||
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))]
|
||||
} 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 ---
|
||||
|
||||
func (g *Generator) GenerateUsageSamples(
|
||||
masters []MasterResponse,
|
||||
keys map[uint][]KeyResponse,
|
||||
groups []ProviderGroupResponse,
|
||||
usageDays int,
|
||||
) []LogRequest {
|
||||
result := make([]LogRequest, 0)
|
||||
|
||||
if len(masters) == 0 || len(groups) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
startTime := now.AddDate(0, 0, -usageDays)
|
||||
|
||||
// Generate samples for each day
|
||||
for day := 0; day < 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(usageDays)*0.5
|
||||
samplesPerDay := int(float64(baseCount) * growthFactor)
|
||||
|
||||
for i := 0; i < samplesPerDay; i++ {
|
||||
// Pick random master
|
||||
master := masters[g.rng.Intn(len(masters))]
|
||||
|
||||
// Pick random key for this master
|
||||
masterKeys := 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))]
|
||||
|
||||
// Pick random model from group
|
||||
var modelName string
|
||||
if len(group.Models) > 0 {
|
||||
modelName = group.Models[g.rng.Intn(len(group.Models))]
|
||||
} 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,
|
||||
)
|
||||
|
||||
// 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))
|
||||
|
||||
_ = timestamp // TODO: need to set created_at somehow
|
||||
|
||||
result = append(result, LogRequest{
|
||||
Group: master.Group,
|
||||
MasterID: master.ID,
|
||||
KeyID: keyID,
|
||||
ModelName: modelName,
|
||||
ProviderID: 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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user