feat(server): API relay gateway backend M0-M4
Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with quotas, proxy gateway with weighted channel failover and health checks, usage/billing ledger, cross-protocol conversion (Anthropic Messages / OpenAI Chat Completions / OpenAI Responses), and channel/model admin API. Channels declare native API formats and auto-convert the rest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b0c7439c01
commit
d0e31b198f
@@ -0,0 +1,196 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User is a registered account.
|
||||
type User struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex;size:64"`
|
||||
Email string `gorm:"uniqueIndex;size:255"`
|
||||
PasswordHash string `gorm:"size:255"`
|
||||
Role string `gorm:"size:16;default:user"` // admin | user
|
||||
Balance decimal.Decimal `gorm:"type:numeric(20,8);default:0"`
|
||||
Status string `gorm:"size:16;default:active"` // active | disabled
|
||||
InviteCode string `gorm:"size:64"`
|
||||
LastLoginAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// ApiKey is an API key issued to a user.
|
||||
type ApiKey struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
UserID int64 `gorm:"index"`
|
||||
Name string `gorm:"size:128"`
|
||||
KeyHash string `gorm:"uniqueIndex;size:128"`
|
||||
KeyPrefix string `gorm:"size:16"`
|
||||
QuotaTokensPerDay *int64
|
||||
QuotaRequestsPerDay *int
|
||||
AllowedModels []string `gorm:"serializer:json"`
|
||||
ExpiresAt *time.Time
|
||||
Status string `gorm:"size:16;default:active"` // active | revoked
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Channel is an upstream provider.
|
||||
type Channel struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:128"`
|
||||
Provider string `gorm:"size:32"` // openai | anthropic | compatible
|
||||
BaseURL string `gorm:"size:512"`
|
||||
APIKeyEnc string `gorm:"size:2048"` // AES-GCM ciphertext
|
||||
Weight int `gorm:"default:1"`
|
||||
Priority int `gorm:"default:0"` // lower = preferred
|
||||
TimeoutMs int `gorm:"default:300000"`
|
||||
MaxConcurrency int `gorm:"default:100"`
|
||||
HealthStatus string `gorm:"size:16;default:healthy"` // healthy | degraded | cooldown
|
||||
HealthFailures int
|
||||
Enabled bool `gorm:"default:true"`
|
||||
Formats []string `gorm:"serializer:json"` // client API formats served natively; empty = derive from Provider
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Client API format keys a channel can serve natively. They match the proxy
|
||||
// route protocol values so SupportsFormat can compare them directly.
|
||||
const (
|
||||
FormatOpenAIChat = "openai-chat"
|
||||
FormatOpenAIResponses = "openai-responses"
|
||||
FormatAnthropic = "anthropic"
|
||||
)
|
||||
|
||||
// FormatsResolved returns the API formats the channel serves directly. When
|
||||
// Formats is empty it falls back to the provider's native formats so legacy
|
||||
// rows behave exactly as before.
|
||||
func (c *Channel) FormatsResolved() []string {
|
||||
if len(c.Formats) > 0 {
|
||||
out := make([]string, len(c.Formats))
|
||||
copy(out, c.Formats)
|
||||
return out
|
||||
}
|
||||
if c.Provider == "anthropic" {
|
||||
return []string{FormatAnthropic}
|
||||
}
|
||||
return []string{FormatOpenAIChat, FormatOpenAIResponses}
|
||||
}
|
||||
|
||||
// SupportsFormat reports whether the channel serves a client protocol
|
||||
// natively (passthrough) without conversion.
|
||||
func (c *Channel) SupportsFormat(f string) bool {
|
||||
for _, x := range c.FormatsResolved() {
|
||||
if x == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Model is the global model registry with pricing.
|
||||
type Model struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Name string `gorm:"uniqueIndex;size:128"`
|
||||
DisplayName string `gorm:"size:255"`
|
||||
InputPrice decimal.Decimal `gorm:"type:numeric(20,8);default:0"` // per 1M tokens
|
||||
OutputPrice decimal.Decimal `gorm:"type:numeric(20,8);default:0"`
|
||||
CacheReadPrice decimal.Decimal `gorm:"type:numeric(20,8);default:0"`
|
||||
Enabled bool `gorm:"default:true"`
|
||||
Sort int `gorm:"default:0"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// ChannelModelBinding binds a global model to a channel with an upstream name.
|
||||
type ChannelModelBinding struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
ChannelID int64 `gorm:"index;uniqueIndex:idx_channel_model"`
|
||||
ModelID int64 `gorm:"index;uniqueIndex:idx_channel_model"`
|
||||
UpstreamModel string `gorm:"size:255"`
|
||||
Weight int `gorm:"default:1"`
|
||||
Channel Channel `gorm:"foreignKey:ChannelID"`
|
||||
Model Model `gorm:"foreignKey:ModelID"`
|
||||
}
|
||||
|
||||
// UsageLog is one proxied request's billing record.
|
||||
type UsageLog struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
RequestID string `gorm:"size:128"`
|
||||
UserID int64 `gorm:"index:idx_user_created,priority:1"`
|
||||
KeyID int64
|
||||
ChannelID int64
|
||||
ModelID int64
|
||||
ModelName string `gorm:"size:128"`
|
||||
InputTokens int64
|
||||
OutputTokens int64
|
||||
CacheReadTokens int64
|
||||
CacheCreationTokens int64
|
||||
InputPrice decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
OutputPrice decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
CacheReadPrice decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
Cost decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
LatencyMs int
|
||||
Status string `gorm:"size:16"` // success | error | canceled
|
||||
ErrorCode string `gorm:"size:64"`
|
||||
CreatedAt time.Time `gorm:"index:idx_user_created,priority:2"`
|
||||
}
|
||||
|
||||
// UsageDaily is the pre-aggregated per-user per-model daily rollup.
|
||||
type UsageDaily struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
UserID int64 `gorm:"index:idx_user_date,priority:1"`
|
||||
ModelID int64
|
||||
Date string `gorm:"size:10;index:idx_user_date,priority:2"` // YYYY-MM-DD
|
||||
Requests int
|
||||
InputTokens int64
|
||||
OutputTokens int64
|
||||
CacheReadTokens int64
|
||||
CacheCreationTokens int64
|
||||
Cost decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
}
|
||||
|
||||
// RechargeOrder is reserved for the (paused) recharge feature.
|
||||
type RechargeOrder struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
UserID int64 `gorm:"index"`
|
||||
Amount decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
Status string `gorm:"size:16;default:pending"` // pending | credited | rejected
|
||||
Method string `gorm:"size:16;default:manual"` // manual | online
|
||||
TransactionID string `gorm:"size:128"`
|
||||
ReviewedBy *int64
|
||||
ReviewedAt *time.Time
|
||||
Remark string `gorm:"size:512"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
User User `gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BalanceLog is a user balance ledger entry.
|
||||
type BalanceLog struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
UserID int64 `gorm:"index:idx_balance_user_created,priority:1"`
|
||||
Change decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
BalanceAfter decimal.Decimal `gorm:"type:numeric(20,8)"`
|
||||
Type string `gorm:"size:32"` // recharge | usage | refund | admin_adjust
|
||||
RefID string `gorm:"size:128"`
|
||||
CreatedAt time.Time `gorm:"index:idx_balance_user_created,priority:2"`
|
||||
}
|
||||
|
||||
// SystemConfig is a key/value store for runtime settings.
|
||||
type SystemConfig struct {
|
||||
Key string `gorm:"primaryKey;size:128"`
|
||||
Value []byte `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// Migrate creates/upgrades the schema.
|
||||
func Migrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&User{}, &ApiKey{}, &Channel{}, &Model{}, &ChannelModelBinding{},
|
||||
&UsageLog{}, &UsageDaily{}, &RechargeOrder{}, &BalanceLog{}, &SystemConfig{},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user