OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
// ProviderCategory 出口服务分类
|
||||
type ProviderCategory string
|
||||
|
||||
const (
|
||||
CategoryLLM ProviderCategory = "llm" // 语言模型
|
||||
CategoryEmbedding ProviderCategory = "embedding" // 向量嵌入
|
||||
CategoryRerank ProviderCategory = "rerank" // 重排序
|
||||
CategorySpeech ProviderCategory = "speech" // 语音合成(预留)
|
||||
CategoryImage ProviderCategory = "image" // 图像生成
|
||||
CategoryAudio ProviderCategory = "audio" // 音频转写(预留)
|
||||
)
|
||||
|
||||
func SupportedCategories() []ProviderCategory {
|
||||
return []ProviderCategory{CategoryLLM, CategoryEmbedding, CategoryRerank, CategoryImage}
|
||||
}
|
||||
|
||||
func IsCategorySupported(c ProviderCategory) bool {
|
||||
for _, s := range SupportedCategories() {
|
||||
if s == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProviderType string
|
||||
|
||||
const (
|
||||
ProviderOpenAI ProviderType = "openai"
|
||||
ProviderAnthropic ProviderType = "anthropic"
|
||||
ProviderCustom ProviderType = "custom"
|
||||
ProviderAzureOpenAI ProviderType = "azure_openai"
|
||||
ProviderOpenRouter ProviderType = "openrouter"
|
||||
ProviderOllama ProviderType = "ollama"
|
||||
ProviderComfyUI ProviderType = "comfyui"
|
||||
)
|
||||
|
||||
type WorkflowType string
|
||||
|
||||
const (
|
||||
WorkflowTxt2Img WorkflowType = "txt2img"
|
||||
WorkflowImg2Img WorkflowType = "img2img"
|
||||
)
|
||||
|
||||
type PredictionStatus string
|
||||
|
||||
const (
|
||||
PredictionStarting PredictionStatus = "starting"
|
||||
PredictionProcessing PredictionStatus = "processing"
|
||||
PredictionSucceeded PredictionStatus = "succeeded"
|
||||
PredictionFailed PredictionStatus = "failed"
|
||||
PredictionCanceled PredictionStatus = "canceled"
|
||||
)
|
||||
|
||||
type ProviderStatus string
|
||||
|
||||
const (
|
||||
ProviderHealthy ProviderStatus = "healthy"
|
||||
ProviderUnhealthy ProviderStatus = "unhealthy"
|
||||
ProviderUnknown ProviderStatus = "unknown"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;not null" json:"name"`
|
||||
Type ProviderType `gorm:"not null" json:"type"`
|
||||
Category ProviderCategory `gorm:"not null;default:llm" json:"category"`
|
||||
BaseURL string `json:"base_url"`
|
||||
AllowedEndpointsJSON string `gorm:"default:'[\"list_models\",\"chat_completion\"]'" json:"allowed_endpoints_json"`
|
||||
EndpointPathsJSON string `gorm:"default:'{}'" json:"endpoint_paths_json"`
|
||||
Status ProviderStatus `gorm:"default:unknown" json:"status"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Keys []ProviderKey `gorm:"foreignKey:ProviderID" json:"keys,omitempty"`
|
||||
Models []ModelEntry `gorm:"foreignKey:ProviderID" json:"models,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderKey struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ProviderID uint `gorm:"index;not null" json:"provider_id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
APIKeyEnc string `gorm:"not null" json:"-"`
|
||||
Weight float64 `gorm:"default:1" json:"weight"`
|
||||
ModelsJSON string `gorm:"default:'[\"*\"]'" json:"models_json"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
MaxRequestsPerDay int `gorm:"default:0" json:"max_requests_per_day"`
|
||||
MaxTokensPerDay int64 `gorm:"default:0" json:"max_tokens_per_day"`
|
||||
RequestsToday int `gorm:"default:0" json:"requests_today"`
|
||||
TokensToday int64 `gorm:"default:0" json:"tokens_today"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ModelEntry struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ProviderID uint `gorm:"index;not null" json:"provider_id"`
|
||||
ModelID string `gorm:"not null" json:"model_id"`
|
||||
Alias string `gorm:"index" json:"alias"`
|
||||
DisplayName string `json:"display_name"`
|
||||
WorkflowType WorkflowType `gorm:"default:txt2img" json:"workflow_type"`
|
||||
WorkflowJSON string `gorm:"type:text" json:"workflow_json"`
|
||||
InputBindingJSON string `gorm:"type:text" json:"input_binding_json"`
|
||||
VersionHash string `gorm:"index" json:"version_hash"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type IngressKey struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||
Prefix string `gorm:"not null" json:"prefix"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
BudgetLimit float64 `gorm:"default:0" json:"budget_limit"`
|
||||
BudgetUsed float64 `gorm:"default:0" json:"budget_used"`
|
||||
RateLimitRPM int `gorm:"default:0" json:"rate_limit_rpm"`
|
||||
RateLimitTPM int `gorm:"default:0" json:"rate_limit_tpm"`
|
||||
AllowedProvidersJSON string `gorm:"default:'[\"*\"]'" json:"allowed_providers_json"`
|
||||
AllowedModelsJSON string `gorm:"default:'[\"*\"]'" json:"allowed_models_json"`
|
||||
RequestCount int64 `gorm:"default:0" json:"request_count"`
|
||||
TokenCount int64 `gorm:"default:0" json:"token_count"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AdminUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;not null" json:"username"`
|
||||
PasswordHash string `gorm:"not null" json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AdminSession struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TokenHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type IPRuleType string
|
||||
|
||||
const (
|
||||
IPRuleAllow IPRuleType = "allow"
|
||||
IPRuleDeny IPRuleType = "deny"
|
||||
)
|
||||
|
||||
type IPRuleScope string
|
||||
|
||||
const (
|
||||
IPScopeAdmin IPRuleScope = "admin"
|
||||
IPScopeProxy IPRuleScope = "proxy"
|
||||
IPScopeAll IPRuleScope = "all"
|
||||
)
|
||||
|
||||
type IPRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CIDR string `gorm:"column:cidr;not null" json:"cidr"`
|
||||
Type IPRuleType `gorm:"not null" json:"type"`
|
||||
Scope IPRuleScope `gorm:"not null;default:proxy" json:"scope"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type UsageRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
IngressKeyID uint `gorm:"index" json:"ingress_key_id"`
|
||||
ProviderID uint `gorm:"index" json:"provider_id"`
|
||||
ProviderKeyID uint `gorm:"index" json:"provider_key_id"`
|
||||
ClientIP string `gorm:"index" json:"client_ip"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Model string `json:"model"`
|
||||
TokensIn int `json:"tokens_in"`
|
||||
TokensOut int `json:"tokens_out"`
|
||||
Cost float64 `json:"cost"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
Status string `json:"status"`
|
||||
Stream bool `json:"stream"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
RequestBody string `gorm:"type:text" json:"request_body,omitempty"`
|
||||
ResponseBody string `gorm:"type:text" json:"response_body,omitempty"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
const (
|
||||
MaxUsageLogRecords = 500
|
||||
EndpointAdminLogin = "admin_login"
|
||||
EndpointPrediction = "prediction"
|
||||
)
|
||||
|
||||
type Prediction struct {
|
||||
ID string `gorm:"primaryKey" json:"id"`
|
||||
IngressKeyID uint `gorm:"index;not null" json:"ingress_key_id"`
|
||||
ProviderID uint `gorm:"index;not null" json:"provider_id"`
|
||||
ProviderKeyID uint `gorm:"index" json:"provider_key_id"`
|
||||
ModelEntryID uint `gorm:"index" json:"model_entry_id"`
|
||||
Version string `json:"version"`
|
||||
Model string `json:"model"`
|
||||
InputJSON string `gorm:"type:text" json:"input_json"`
|
||||
Status PredictionStatus `gorm:"index;not null;default:starting" json:"status"`
|
||||
ComfyPromptID string `json:"comfy_prompt_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
OutputJSON string `gorm:"type:text" json:"output_json"`
|
||||
Error string `gorm:"type:text" json:"error"`
|
||||
Logs string `gorm:"type:text" json:"logs"`
|
||||
MetricsJSON string `gorm:"type:text" json:"metrics_json"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
type StoredImage struct {
|
||||
ID string `gorm:"primaryKey" json:"id"`
|
||||
PredictionID string `gorm:"index;not null" json:"prediction_id"`
|
||||
Filename string `json:"filename"`
|
||||
LocalPath string `json:"-"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// RoutingRule Virtual Key 路由:按模型匹配绑定 Provider / ProviderKey
|
||||
type RoutingRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
IngressKeyID uint `gorm:"index;not null" json:"ingress_key_id"`
|
||||
ModelPattern string `gorm:"not null" json:"model_pattern"`
|
||||
ProviderID uint `gorm:"not null" json:"provider_id"`
|
||||
ProviderKeyID uint `gorm:"default:0" json:"provider_key_id"`
|
||||
Priority int `gorm:"default:0" json:"priority"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// SystemSettings singleton (id=1) for runtime configuration managed via admin UI.
|
||||
type SystemSettings struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
EncryptionKey string `gorm:"not null" json:"-"`
|
||||
SessionTTLSeconds int64 `gorm:"not null;default:86400" json:"session_ttl_seconds"`
|
||||
LoginMaxAttempts int `gorm:"not null;default:5" json:"login_max_attempts"`
|
||||
LoginLockoutSeconds int64 `gorm:"not null;default:900" json:"login_lockout_seconds"`
|
||||
TrustedProxiesJSON string `gorm:"default:'[]'" json:"trusted_proxies_json"`
|
||||
UsageFlushSeconds int64 `gorm:"not null;default:5" json:"usage_flush_seconds"`
|
||||
HealthCheckSeconds int64 `gorm:"not null;default:30" json:"health_check_seconds"`
|
||||
GatewayMaxRetries int `gorm:"not null;default:3" json:"gateway_max_retries"`
|
||||
GatewayRetryBackoffMs int `gorm:"not null;default:200" json:"gateway_retry_backoff_ms"`
|
||||
ImageStoragePath string `gorm:"default:'images'" json:"image_storage_path"`
|
||||
PublicBaseURL string `json:"public_base_url"`
|
||||
SignedURLTTLSeconds int64 `gorm:"not null;default:3600" json:"signed_url_ttl_seconds"`
|
||||
PredictionWaitSeconds int64 `gorm:"not null;default:60" json:"prediction_wait_seconds"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type HealthCheckRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ProviderID uint `gorm:"index" json:"provider_id"`
|
||||
Status ProviderStatus `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
CheckedAt time.Time `gorm:"index" json:"checked_at"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
// IngressID returns the model name exposed to ingress API clients.
|
||||
func (m ModelEntry) IngressID() string {
|
||||
if m.Alias != "" {
|
||||
return m.Alias
|
||||
}
|
||||
return m.ModelID
|
||||
}
|
||||
|
||||
// MatchesIngressName reports whether the entry matches an ingress model identifier.
|
||||
func (m ModelEntry) MatchesIngressName(name string) bool {
|
||||
if name == "" || !m.Enabled {
|
||||
return false
|
||||
}
|
||||
if m.ModelID == name || m.VersionHash == name {
|
||||
return true
|
||||
}
|
||||
return m.Alias != "" && m.Alias == name
|
||||
}
|
||||
Reference in New Issue
Block a user