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:
Sakurasan
2026-08-15 21:05:02 +08:00
co-authored by Claude Sonnet 5
parent b0c7439c01
commit d0e31b198f
45 changed files with 6222 additions and 0 deletions
+196
View File
@@ -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{},
)
}