mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Trim whitespace in provider model lists, format provider names as `group#keyID` to match DP logs, and skip existing API keys during seeding (deleting on reset) to keep runs idempotent and summaries accurate
466 lines
13 KiB
Go
466 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.
|
|
|
|
// Format provider_name as group#keyID to match DP log format
|
|
providerName := group.Name
|
|
if providerID > 0 {
|
|
providerName = fmt.Sprintf("%s#%d", group.Name, providerID)
|
|
}
|
|
|
|
result = append(result, LogRequest{
|
|
Group: master.Group,
|
|
MasterID: master.ID,
|
|
KeyID: keyID,
|
|
Model: modelName,
|
|
ProviderID: providerID,
|
|
ProviderType: group.Type,
|
|
ProviderName: providerName,
|
|
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 ""
|
|
}
|