mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
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.
453 lines
13 KiB
Go
453 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)]
|
|
|
|
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 ""
|
|
}
|