Initial project scaffold

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-09 14:36:35 +08:00
co-authored by Cursor
commit 6323e70e49
39 changed files with 8664 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Permission string
const (
PermissionPublic Permission = "public"
PermissionBasic Permission = "basic"
PermissionCreat Permission = "creat"
PermissionPower Permission = "power"
PermissionManager Permission = "manager"
)
type User struct {
ID string `json:"sub"`
Username string `json:"username"`
Roles []string `json:"role,omitempty"`
TenantID string `json:"tenantId,omitempty"`
SSOID string `json:"sso_id,omitempty"`
APIKeyID string `json:"apiKeyId,omitempty"`
APIKeySecret string `json:"apiKeySecret,omitempty"`
APIKeyName string `json:"apiKeyName,omitempty"`
}
type contextKey string
const userContextKey contextKey = "easyai-auth-user"
var ErrUnauthorized = errors.New("unauthorized")
type Authenticator struct {
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
HTTPClient *http.Client
}
func New(jwtSecret string, serverMainBaseURL string, internalToken string) *Authenticator {
return &Authenticator{
JWTSecret: jwtSecret,
ServerMainBaseURL: strings.TrimRight(serverMainBaseURL, "/"),
ServerMainInternalToken: internalToken,
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func UserFromContext(ctx context.Context) (*User, bool) {
user, ok := ctx.Value(userContextKey).(*User)
return user, ok
}
func (a *Authenticator) Require(permission Permission, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := a.Authenticate(r)
if err != nil {
if permission == PermissionPublic {
next.ServeHTTP(w, r)
return
}
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !hasPermission(user.Roles, permission) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userContextKey, user)))
})
}
func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
token := extractBearer(r.Header.Get("Authorization"))
if token == "" {
token = strings.TrimSpace(r.Header.Get("x-comfy-api-key"))
}
if token == "" {
return nil, ErrUnauthorized
}
if strings.HasPrefix(token, "sk-") {
return a.verifyAPIKey(r.Context(), token)
}
return a.verifyJWT(token)
}
func (a *Authenticator) verifyJWT(tokenString string) (*User, error) {
token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(a.JWTSecret), nil
})
if err != nil || !token.Valid {
return nil, ErrUnauthorized
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrUnauthorized
}
user := &User{
ID: stringClaim(claims, "sub"),
Username: stringClaim(claims, "username"),
Roles: stringSliceClaim(claims, "role"),
TenantID: stringClaim(claims, "tenantId"),
SSOID: stringClaim(claims, "sso_id"),
APIKeyID: stringClaim(claims, "apiKeyId"),
APIKeySecret: stringClaim(claims, "apiKeySecret"),
APIKeyName: stringClaim(claims, "apiKeyName"),
}
if user.ID == "" {
return nil, ErrUnauthorized
}
return user, nil
}
func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User, error) {
if a.ServerMainBaseURL == "" || a.ServerMainInternalToken == "" {
return nil, ErrUnauthorized
}
body, _ := json.Marshal(map[string]string{"apiKey": apiKey})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.ServerMainBaseURL+"/internal/platform/auth/verify-api-key", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+a.ServerMainInternalToken)
resp, err := a.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ErrUnauthorized
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
if user.ID == "" {
return nil, ErrUnauthorized
}
return &user, nil
}
func extractBearer(value string) string {
fields := strings.Fields(value)
if len(fields) == 2 && strings.EqualFold(fields[0], "bearer") {
return fields[1]
}
return ""
}
func hasPermission(roles []string, required Permission) bool {
if required == PermissionPublic {
return true
}
granted := map[Permission]bool{PermissionPublic: true}
for _, role := range roles {
for _, permission := range permissionsForRole(role) {
granted[permission] = true
}
}
return granted[required]
}
func permissionsForRole(role string) []Permission {
switch role {
case "admin", "manager":
return []Permission{PermissionPublic, PermissionBasic, PermissionCreat, PermissionPower, PermissionManager}
case "operator":
return []Permission{PermissionPublic, PermissionBasic, PermissionCreat, PermissionPower}
case "creator":
return []Permission{PermissionPublic, PermissionBasic, PermissionCreat}
case "user":
return []Permission{PermissionPublic, PermissionBasic}
default:
return []Permission{PermissionPublic}
}
}
func stringClaim(claims jwt.MapClaims, key string) string {
value, _ := claims[key].(string)
return value
}
func stringSliceClaim(claims jwt.MapClaims, key string) []string {
value := claims[key]
switch typed := value.(type) {
case []string:
return typed
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
if s, ok := item.(string); ok && s != "" {
out = append(out, s)
}
}
return out
case string:
if typed == "" {
return nil
}
return []string{typed}
default:
return nil
}
}
+99
View File
@@ -0,0 +1,99 @@
package config
import (
"log/slog"
"net/url"
"os"
"strings"
)
type Config struct {
AppEnv string
HTTPAddr string
DatabaseURL string
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
CORSAllowedOrigin string
LogLevel slog.Level
}
func Load() Config {
return Config{
AppEnv: env("APP_ENV", "development"),
HTTPAddr: env("HTTP_ADDR", ":8088"),
DatabaseURL: gatewayDatabaseURL(),
JWTSecret: env("CONFIG_JWT_SECRET", "this is a very secret secret"),
ServerMainBaseURL: strings.TrimRight(
env("SERVER_MAIN_BASE_URL", "http://localhost:3000"),
"/",
),
ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""),
CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178"),
LogLevel: logLevel(env("LOG_LEVEL", "info")),
}
}
func gatewayDatabaseURL() string {
if value := envValue("AI_GATEWAY_DATABASE_URL"); value != "" {
return normalizePostgresURL(value)
}
if value := envValue("DATABASE_URL"); value != "" {
return normalizePostgresURL(value)
}
if memoryURL := envValue("MEMORY_DATABASE_URL"); memoryURL != "" {
return normalizePostgresURL(withDatabase(memoryURL, env("AI_GATEWAY_DATABASE_NAME", "easyai_ai_gateway")))
}
return normalizePostgresURL("postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable")
}
func normalizePostgresURL(raw string) string {
parsed, err := url.Parse(raw)
if err != nil {
return raw
}
values := parsed.Query()
schema := values.Get("schema")
if schema == "" {
return raw
}
values.Del("schema")
if values.Get("search_path") == "" {
values.Set("search_path", schema)
}
parsed.RawQuery = values.Encode()
return parsed.String()
}
func withDatabase(raw string, databaseName string) string {
parsed, err := url.Parse(raw)
if err != nil || databaseName == "" {
return raw
}
parsed.Path = "/" + databaseName
return parsed.String()
}
func envValue(key string) string {
return strings.TrimSpace(os.Getenv(key))
}
func env(key string, fallback string) string {
if value := envValue(key); value != "" {
return value
}
return fallback
}
func logLevel(value string) slog.Level {
switch strings.ToLower(value) {
case "debug":
return slog.LevelDebug
case "warn", "warning":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
+218
View File
@@ -0,0 +1,218 @@
package httpapi
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"service": "easyai-ai-gateway",
"env": s.cfg.AppEnv,
})
}
func (s *Server) ready(w http.ResponseWriter, r *http.Request) {
if err := s.store.Ping(r.Context()); err != nil {
writeError(w, http.StatusServiceUnavailable, "postgres unavailable")
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (s *Server) me(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
writeJSON(w, http.StatusOK, user)
}
func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
platforms, err := s.store.ListPlatforms(r.Context())
if err != nil {
s.logger.Error("list platforms failed", "error", err)
writeError(w, http.StatusInternalServerError, "list platforms failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": platforms})
}
func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
var input store.CreatePlatformInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if input.Provider == "" || input.Name == "" {
writeError(w, http.StatusBadRequest, "provider and name are required")
return
}
if input.AuthType == "" {
input.AuthType = "bearer"
}
platform, err := s.store.CreatePlatform(r.Context(), input)
if err != nil {
s.logger.Error("create platform failed", "error", err)
writeError(w, http.StatusInternalServerError, "create platform failed")
return
}
writeJSON(w, http.StatusCreated, platform)
}
func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
models, err := s.store.ListModels(r.Context())
if err != nil {
s.logger.Error("list models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": models})
}
func (s *Server) listCatalogProviders(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListCatalogProviders(r.Context())
if err != nil {
s.logger.Error("list catalog providers failed", "error", err)
writeError(w, http.StatusInternalServerError, "list catalog providers failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listBaseModels(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListBaseModels(r.Context())
if err != nil {
s.logger.Error("list base models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list base models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListPricingRules(r.Context())
if err != nil {
s.logger.Error("list pricing rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "list pricing rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"items": []any{},
"resolver": "effective-pricing-placeholder",
"request": body,
})
}
func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListRateLimitWindows(r.Context())
if err != nil {
s.logger.Error("list rate limit windows failed", "error", err)
writeError(w, http.StatusInternalServerError, "list rate limit windows failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createTask(kind string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := auth.UserFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
model, _ := body["model"].(string)
if model == "" {
writeError(w, http.StatusBadRequest, "model is required")
return
}
task, err := s.store.CreateTask(r.Context(), store.CreateTaskInput{
Kind: kind,
Model: model,
Request: body,
}, user)
if err != nil {
s.logger.Error("create task failed", "kind", kind, "error", err)
writeError(w, http.StatusInternalServerError, "create task failed")
return
}
writeJSON(w, http.StatusAccepted, map[string]any{
"task": task,
"next": map[string]string{
"events": fmt.Sprintf("/api/v1/tasks/%s/events", task.ID),
"detail": fmt.Sprintf("/api/v1/tasks/%s", task.ID),
},
})
})
}
func (s *Server) getTask(w http.ResponseWriter, r *http.Request) {
task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
if err == nil {
writeJSON(w, http.StatusOK, task)
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "task not found")
return
}
s.logger.Error("get task failed", "error", err)
writeError(w, http.StatusInternalServerError, "get task failed")
}
func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) {
task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "task not found")
return
}
writeError(w, http.StatusInternalServerError, "get task failed")
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
sendSSE(w, "task.accepted", map[string]any{
"taskId": task.ID,
"status": task.Status,
})
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
timer := time.NewTimer(250 * time.Millisecond)
defer timer.Stop()
select {
case <-r.Context().Done():
return
case <-timer.C:
sendSSE(w, "task.placeholder", map[string]any{
"taskId": task.ID,
"message": "runtime worker is not wired yet",
})
}
}
+28
View File
@@ -0,0 +1,28 @@
package httpapi
import (
"encoding/json"
"fmt"
"net/http"
)
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]any{
"error": map[string]any{
"message": message,
"status": status,
},
})
}
func sendSSE(w http.ResponseWriter, event string, payload any) {
bytes, _ := json.Marshal(payload)
_, _ = fmt.Fprintf(w, "event: %s\n", event)
_, _ = fmt.Fprintf(w, "data: %s\n\n", bytes)
}
+78
View File
@@ -0,0 +1,78 @@
package httpapi
import (
"log/slog"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type Server struct {
cfg config.Config
store *store.Store
auth *auth.Authenticator
logger *slog.Logger
}
func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Handler {
server := &Server{
cfg: cfg,
store: db,
auth: auth.New(cfg.JWTSecret, cfg.ServerMainBaseURL, cfg.ServerMainInternalToken),
logger: logger,
}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", server.health)
mux.HandleFunc("GET /readyz", server.ready)
mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me)))
mux.Handle("GET /api/v1/catalog/providers", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("GET /api/v1/catalog/base-models", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
mux.Handle("GET /api/v1/pricing/rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
mux.Handle("POST /api/v1/pricing/estimate", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing)))
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
mux.Handle("POST /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.createPlatform)))
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listModels)))
mux.Handle("GET /api/v1/runtime/rate-limit-windows", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions")))
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations")))
mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations")))
mux.Handle("GET /api/v1/tasks/{taskID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
mux.Handle("GET /api/v1/tasks/{taskID}/events", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.taskEvents)))
return server.recover(server.cors(mux))
}
func (s *Server) cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" && (s.cfg.CORSAllowedOrigin == "*" || strings.EqualFold(origin, s.cfg.CORSAllowedOrigin)) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Comfy-Api-Key")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
s.logger.Error("panic recovered", "error", err, "path", r.URL.Path)
writeError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
+508
View File
@@ -0,0 +1,508 @@
package store
import (
"context"
"encoding/json"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Store struct {
pool *pgxpool.Pool
}
func Connect(ctx context.Context, databaseURL string) (*Store, error) {
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, err
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, err
}
return &Store{pool: pool}, nil
}
func (s *Store) Close() {
s.pool.Close()
}
func (s *Store) Ping(ctx context.Context) error {
return s.pool.Ping(ctx)
}
type Platform struct {
ID string `json:"id"`
Provider string `json:"provider"`
PlatformKey string `json:"platformKey"`
Name string `json:"name"`
BaseURL string `json:"baseUrl,omitempty"`
AuthType string `json:"authType"`
Status string `json:"status"`
Priority int `json:"priority"`
DefaultPricingMode string `json:"defaultPricingMode"`
DefaultDiscountFactor float64 `json:"defaultDiscountFactor"`
Config map[string]any `json:"config,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreatePlatformInput struct {
Provider string `json:"provider"`
PlatformKey string `json:"platformKey"`
Name string `json:"name"`
BaseURL string `json:"baseUrl"`
AuthType string `json:"authType"`
Credentials map[string]any `json:"credentials"`
Config map[string]any `json:"config"`
DefaultPricingMode string `json:"defaultPricingMode"`
DefaultDiscountFactor float64 `json:"defaultDiscountFactor"`
Priority int `json:"priority"`
}
type PlatformModel struct {
ID string `json:"id"`
PlatformID string `json:"platformId"`
BaseModelID string `json:"baseModelId,omitempty"`
Provider string `json:"provider,omitempty"`
PlatformName string `json:"platformName,omitempty"`
ModelName string `json:"modelName"`
ModelAlias string `json:"modelAlias,omitempty"`
ModelType string `json:"modelType"`
DisplayName string `json:"displayName"`
CapabilityOverride map[string]any `json:"capabilityOverride,omitempty"`
Capabilities map[string]any `json:"capabilities,omitempty"`
PricingMode string `json:"pricingMode"`
DiscountFactor float64 `json:"discountFactor,omitempty"`
BillingConfigOverride map[string]any `json:"billingConfigOverride,omitempty"`
BillingConfig map[string]any `json:"billingConfig,omitempty"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CatalogProvider struct {
ID string `json:"id"`
ProviderKey string `json:"providerKey"`
DisplayName string `json:"displayName"`
ProviderType string `json:"providerType"`
CapabilitySchema map[string]any `json:"capabilitySchema,omitempty"`
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy,omitempty"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type BaseModel struct {
ID string `json:"id"`
ProviderKey string `json:"providerKey"`
CanonicalModelKey string `json:"canonicalModelKey"`
ProviderModelName string `json:"providerModelName"`
ModelType string `json:"modelType"`
DisplayName string `json:"displayName"`
Capabilities map[string]any `json:"capabilities,omitempty"`
BaseBillingConfig map[string]any `json:"baseBillingConfig,omitempty"`
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy,omitempty"`
PricingVersion int `json:"pricingVersion"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PricingRule struct {
ID string `json:"id"`
ScopeType string `json:"scopeType"`
ScopeID string `json:"scopeId,omitempty"`
ResourceType string `json:"resourceType"`
Unit string `json:"unit"`
BasePrice float64 `json:"basePrice"`
Currency string `json:"currency"`
BaseWeight map[string]any `json:"baseWeight,omitempty"`
DynamicWeight map[string]any `json:"dynamicWeight,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type RateLimitWindow struct {
ScopeType string `json:"scopeType"`
ScopeKey string `json:"scopeKey"`
Metric string `json:"metric"`
WindowStart time.Time `json:"windowStart"`
LimitValue float64 `json:"limitValue"`
UsedValue float64 `json:"usedValue"`
ReservedValue float64 `json:"reservedValue"`
ResetAt time.Time `json:"resetAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateTaskInput struct {
Kind string `json:"kind"`
Model string `json:"model"`
Request map[string]any `json:"request"`
}
type GatewayTask struct {
ID string `json:"id"`
Kind string `json:"kind"`
UserID string `json:"userId"`
TenantID string `json:"tenantId,omitempty"`
Model string `json:"model"`
Request map[string]any `json:"request,omitempty"`
Status string `json:"status"`
Result map[string]any `json:"result,omitempty"`
Billings []any `json:"billings,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (s *Store) ListPlatforms(ctx context.Context) ([]Platform, error) {
rows, err := s.pool.Query(ctx, `
SELECT id::text, provider, platform_key, name, COALESCE(base_url, ''), auth_type, status, priority,
default_pricing_mode, default_discount_factor::float8, config, created_at, updated_at
FROM integration_platforms
ORDER BY priority ASC, created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
platforms := make([]Platform, 0)
for rows.Next() {
var platform Platform
var configBytes []byte
if err := rows.Scan(
&platform.ID,
&platform.Provider,
&platform.PlatformKey,
&platform.Name,
&platform.BaseURL,
&platform.AuthType,
&platform.Status,
&platform.Priority,
&platform.DefaultPricingMode,
&platform.DefaultDiscountFactor,
&configBytes,
&platform.CreatedAt,
&platform.UpdatedAt,
); err != nil {
return nil, err
}
platform.Config = decodeObject(configBytes)
platforms = append(platforms, platform)
}
return platforms, rows.Err()
}
func (s *Store) CreatePlatform(ctx context.Context, input CreatePlatformInput) (Platform, error) {
credentials, _ := json.Marshal(input.Credentials)
config, _ := json.Marshal(input.Config)
if input.DefaultPricingMode == "" {
input.DefaultPricingMode = "inherit_discount"
}
if input.DefaultDiscountFactor == 0 {
input.DefaultDiscountFactor = 1
}
if input.Priority == 0 {
input.Priority = 100
}
var platform Platform
var configBytes []byte
err := s.pool.QueryRow(ctx, `
INSERT INTO integration_platforms (provider, platform_key, name, base_url, auth_type, credentials, config, default_pricing_mode, default_discount_factor, priority)
VALUES ($1, COALESCE(NULLIF($2, ''), 'platform_' || replace(gen_random_uuid()::text, '-', '')), $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id::text, provider, platform_key, name, COALESCE(base_url, ''), auth_type, status, priority,
default_pricing_mode, default_discount_factor::float8, config, created_at, updated_at`,
input.Provider, input.PlatformKey, input.Name, input.BaseURL, input.AuthType, credentials, config, input.DefaultPricingMode, input.DefaultDiscountFactor, input.Priority,
).Scan(
&platform.ID,
&platform.Provider,
&platform.PlatformKey,
&platform.Name,
&platform.BaseURL,
&platform.AuthType,
&platform.Status,
&platform.Priority,
&platform.DefaultPricingMode,
&platform.DefaultDiscountFactor,
&configBytes,
&platform.CreatedAt,
&platform.UpdatedAt,
)
if err != nil {
return Platform{}, err
}
platform.Config = decodeObject(configBytes)
return platform, nil
}
func (s *Store) ListModels(ctx context.Context) ([]PlatformModel, error) {
rows, err := s.pool.Query(ctx, `
SELECT m.id::text, m.platform_id::text, COALESCE(m.base_model_id::text, ''), p.provider, p.name,
m.model_name, COALESCE(m.model_alias, ''), m.model_type, m.display_name,
m.capability_override, m.capabilities, m.pricing_mode, COALESCE(m.discount_factor, 0)::float8,
m.billing_config_override, m.billing_config, m.enabled, m.created_at, m.updated_at
FROM platform_models m
JOIN integration_platforms p ON p.id = m.platform_id
ORDER BY m.model_type ASC, m.model_name ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
models := make([]PlatformModel, 0)
for rows.Next() {
var model PlatformModel
var capabilityOverride []byte
var capabilities []byte
var billingConfigOverride []byte
var billingConfig []byte
if err := rows.Scan(
&model.ID,
&model.PlatformID,
&model.BaseModelID,
&model.Provider,
&model.PlatformName,
&model.ModelName,
&model.ModelAlias,
&model.ModelType,
&model.DisplayName,
&capabilityOverride,
&capabilities,
&model.PricingMode,
&model.DiscountFactor,
&billingConfigOverride,
&billingConfig,
&model.Enabled,
&model.CreatedAt,
&model.UpdatedAt,
); err != nil {
return nil, err
}
model.CapabilityOverride = decodeObject(capabilityOverride)
model.Capabilities = decodeObject(capabilities)
model.BillingConfigOverride = decodeObject(billingConfigOverride)
model.BillingConfig = decodeObject(billingConfig)
models = append(models, model)
}
return models, rows.Err()
}
func (s *Store) ListCatalogProviders(ctx context.Context) ([]CatalogProvider, error) {
rows, err := s.pool.Query(ctx, `
SELECT id::text, provider_key, display_name, provider_type, capability_schema,
default_rate_limit_policy, status, created_at, updated_at
FROM model_catalog_providers
ORDER BY provider_key ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]CatalogProvider, 0)
for rows.Next() {
var item CatalogProvider
var capabilitySchema []byte
var rateLimitPolicy []byte
if err := rows.Scan(
&item.ID,
&item.ProviderKey,
&item.DisplayName,
&item.ProviderType,
&capabilitySchema,
&rateLimitPolicy,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return nil, err
}
item.CapabilitySchema = decodeObject(capabilitySchema)
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) ListBaseModels(ctx context.Context) ([]BaseModel, error) {
rows, err := s.pool.Query(ctx, `
SELECT id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, pricing_version,
status, created_at, updated_at
FROM base_model_catalog
ORDER BY provider_key ASC, model_type ASC, canonical_model_key ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]BaseModel, 0)
for rows.Next() {
var item BaseModel
var capabilities []byte
var billingConfig []byte
var rateLimitPolicy []byte
if err := rows.Scan(
&item.ID,
&item.ProviderKey,
&item.CanonicalModelKey,
&item.ProviderModelName,
&item.ModelType,
&item.DisplayName,
&capabilities,
&billingConfig,
&rateLimitPolicy,
&item.PricingVersion,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return nil, err
}
item.Capabilities = decodeObject(capabilities)
item.BaseBillingConfig = decodeObject(billingConfig)
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) ListPricingRules(ctx context.Context) ([]PricingRule, error) {
rows, err := s.pool.Query(ctx, `
SELECT id::text, scope_type, COALESCE(scope_id::text, ''), resource_type, unit,
base_price::float8, currency, base_weight, dynamic_weight, created_at, updated_at
FROM model_pricing_rules
ORDER BY scope_type ASC, resource_type ASC, created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]PricingRule, 0)
for rows.Next() {
var item PricingRule
var baseWeight []byte
var dynamicWeight []byte
if err := rows.Scan(
&item.ID,
&item.ScopeType,
&item.ScopeID,
&item.ResourceType,
&item.Unit,
&item.BasePrice,
&item.Currency,
&baseWeight,
&dynamicWeight,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return nil, err
}
item.BaseWeight = decodeObject(baseWeight)
item.DynamicWeight = decodeObject(dynamicWeight)
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) ListRateLimitWindows(ctx context.Context) ([]RateLimitWindow, error) {
rows, err := s.pool.Query(ctx, `
SELECT scope_type, scope_key, metric, window_start, limit_value::float8, used_value::float8,
reserved_value::float8, reset_at, updated_at
FROM gateway_rate_limit_counters
WHERE reset_at >= now() - interval '5 minutes'
ORDER BY window_start DESC, scope_type ASC, scope_key ASC, metric ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]RateLimitWindow, 0)
for rows.Next() {
var item RateLimitWindow
if err := rows.Scan(
&item.ScopeType,
&item.ScopeKey,
&item.Metric,
&item.WindowStart,
&item.LimitValue,
&item.UsedValue,
&item.ReservedValue,
&item.ResetAt,
&item.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) CreateTask(ctx context.Context, input CreateTaskInput, user *auth.User) (GatewayTask, error) {
requestBody, _ := json.Marshal(input.Request)
var task GatewayTask
var requestBytes []byte
var resultBytes []byte
var billingsBytes []byte
err := s.pool.QueryRow(ctx, `
INSERT INTO gateway_tasks (kind, user_id, tenant_id, model, request, status)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, 'queued')
RETURNING id::text, kind, user_id, COALESCE(tenant_id, ''), model, request, status, result, billings, COALESCE(error, ''), created_at, updated_at`,
input.Kind, user.ID, user.TenantID, input.Model, requestBody,
).Scan(&task.ID, &task.Kind, &task.UserID, &task.TenantID, &task.Model, &requestBytes, &task.Status, &resultBytes, &billingsBytes, &task.Error, &task.CreatedAt, &task.UpdatedAt)
if err != nil {
return GatewayTask{}, err
}
task.Request = decodeObject(requestBytes)
task.Result = decodeObject(resultBytes)
task.Billings = decodeArray(billingsBytes)
return task, nil
}
func (s *Store) GetTask(ctx context.Context, taskID string) (GatewayTask, error) {
var task GatewayTask
var requestBytes []byte
var resultBytes []byte
var billingsBytes []byte
err := s.pool.QueryRow(ctx, `
SELECT id::text, kind, user_id, COALESCE(tenant_id, ''), model, request, status, result, billings, COALESCE(error, ''), created_at, updated_at
FROM gateway_tasks
WHERE id=$1`, taskID,
).Scan(&task.ID, &task.Kind, &task.UserID, &task.TenantID, &task.Model, &requestBytes, &task.Status, &resultBytes, &billingsBytes, &task.Error, &task.CreatedAt, &task.UpdatedAt)
if err != nil {
return GatewayTask{}, err
}
task.Request = decodeObject(requestBytes)
task.Result = decodeObject(resultBytes)
task.Billings = decodeArray(billingsBytes)
return task, nil
}
func IsNotFound(err error) bool {
return err == pgx.ErrNoRows
}
func decodeObject(bytes []byte) map[string]any {
if len(bytes) == 0 {
return nil
}
var out map[string]any
if err := json.Unmarshal(bytes, &out); err != nil {
return nil
}
return out
}
func decodeArray(bytes []byte) []any {
if len(bytes) == 0 {
return nil
}
var out []any
if err := json.Unmarshal(bytes, &out); err != nil {
return nil
}
return out
}