feat: revise core DB schema for provider catalog, settings, and rate limits

This commit is contained in:
2026-06-28 23:14:44 +02:00
parent 2fa715d108
commit 1d3a419777
2 changed files with 113 additions and 55 deletions

View File

@@ -1,10 +1,12 @@
-- Drop in reverse dependency order.
DROP TABLE IF EXISTS settings;
DROP TABLE IF EXISTS request_logs;
DROP TABLE IF EXISTS group_models;
DROP TABLE IF EXISTS virtual_key_models;
DROP TABLE IF EXISTS group_model_rules;
DROP TABLE IF EXISTS virtual_key_model_rules;
DROP TABLE IF EXISTS virtual_keys;
DROP TABLE IF EXISTS model_targets;
DROP TABLE IF EXISTS models;
DROP TABLE IF EXISTS provider_models;
DROP TABLE IF EXISTS providers;
DROP TABLE IF EXISTS groups;
DROP TABLE IF EXISTS users;

View File

@@ -1,5 +1,5 @@
-- Core gateway schema: identities, providers/models/routing, virtual keys and
-- their allowlists, and the request log that billing reads from.
-- Core gateway schema: identities, providers, the discovered model catalog,
-- curated aliases, virtual keys + allowlists, and the request log.
-- Shared trigger to keep updated_at columns current on UPDATE.
CREATE OR REPLACE FUNCTION set_updated_at()
@@ -28,16 +28,16 @@ CREATE TABLE users (
CREATE INDEX idx_users_email ON users (email);
-- OIDC groups the gateway knows about. Membership is dynamic (read from the
-- JWT); this table exists to attach model allowlists / budgets to a group so
-- session keys minted via /auth/exchange can be scoped.
-- JWT); this table attaches allowlists/budgets to a group so session keys
-- minted via /auth/exchange can be scoped.
CREATE TABLE groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE, -- matches the OIDC group name
description text,
allow_all_models boolean NOT NULL DEFAULT false,
monthly_budget numeric(14,6), -- NULL = unlimited
rpm_limit integer, -- requests/min, NULL = unlimited
tpm_limit integer, -- tokens/min, NULL = unlimited
rpm_limit integer,
tpm_limit integer,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
@@ -45,18 +45,20 @@ CREATE TRIGGER trg_groups_updated_at BEFORE UPDATE ON groups
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- ===========================================================================
-- Providers, models, and routing targets
-- Providers and the discovered model catalog
-- ===========================================================================
-- An upstream backend. Credentials are AES-256-GCM encrypted by the app
-- (nonce || ciphertext over a JSON blob) using GATEWAY_SECURITY_ENCRYPTION_KEY;
-- the database never sees plaintext.
-- A configured upstream backend instance. Its `name` is the addressing prefix:
-- clients reference a model as "<provider name>/<slug>". Credentials are
-- AES-256-GCM encrypted by the app (nonce || ciphertext) using
-- GATEWAY_SECURITY_ENCRYPTION_KEY; the database never sees plaintext.
CREATE TABLE providers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE, -- admin-facing identifier
type text NOT NULL CHECK (type IN ('llamacpp','ollama','openai','anthropic','github_copilot')),
-- "any" is reserved as the cross-provider routing prefix (any/<slug>).
name text NOT NULL UNIQUE CHECK (lower(name) <> 'any'),
type text NOT NULL CHECK (type IN ('llamacpp','ollama','openai','openrouter','anthropic','github_copilot')),
base_url text,
credentials bytea, -- NULL for keyless local providers
credentials bytea,
enabled boolean NOT NULL DEFAULT true,
config jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
@@ -65,44 +67,82 @@ CREATE TABLE providers (
CREATE TRIGGER trg_providers_updated_at BEFORE UPDATE ON providers
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- A public, client-facing model alias (what callers put in the request "model"
-- field). Allowlists reference these, not the backends behind them.
-- The model library: one row per (provider, upstream model slug), built at
-- runtime by syncing each provider's model listing and refreshed on a timer.
-- `disc_*` columns are owned by the syncer; the admin override columns
-- (enabled/blocked/param_overrides/price_*/limits) are owned by the dashboard
-- and are never clobbered by a sync. Effective price = COALESCE(override,
-- discovered, 0). `source = 'manual'` rows are admin-created (providers that
-- don't list, or custom entries) and are not touched by discovery.
CREATE TABLE provider_models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id uuid NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
slug text NOT NULL, -- upstream model id
display_name text,
capabilities text[] NOT NULL DEFAULT ARRAY['chat'],
context_length integer,
source text NOT NULL DEFAULT 'discovered' CHECK (source IN ('discovered','manual')),
available boolean NOT NULL DEFAULT true, -- seen in the most recent sync
last_seen_at timestamptz,
raw jsonb NOT NULL DEFAULT '{}'::jsonb, -- raw provider listing entry
-- Discovered pricing (per 1M tokens); NULL when the provider does not report it.
disc_input_price_per_mtok numeric(12,6),
disc_output_price_per_mtok numeric(12,6),
-- Admin overrides.
enabled boolean NOT NULL DEFAULT true,
blocked boolean NOT NULL DEFAULT false,
param_overrides jsonb NOT NULL DEFAULT '{}'::jsonb,
price_input_per_mtok numeric(12,6),
price_output_per_mtok numeric(12,6),
price_cached_per_mtok numeric(12,6),
rpm_limit integer,
tpm_limit integer,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (provider_id, slug)
);
CREATE INDEX idx_provider_models_provider ON provider_models (provider_id);
CREATE INDEX idx_provider_models_servable ON provider_models (enabled, blocked, available);
CREATE TRIGGER trg_provider_models_updated_at BEFORE UPDATE ON provider_models
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- ===========================================================================
-- Curated aliases (optional, admin-defined)
-- ===========================================================================
-- A friendly client-facing alias (e.g. "gpt-fast") that the gateway exposes
-- alongside direct "<provider>/<slug>" addressing.
CREATE TABLE models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE, -- e.g. "qwen3-coder", "gpt-4o"
name text NOT NULL UNIQUE,
display_name text,
type text NOT NULL DEFAULT 'chat' CHECK (type IN ('chat','completion','embedding')),
enabled boolean NOT NULL DEFAULT true,
params jsonb NOT NULL DEFAULT '{}'::jsonb, -- default sampling params
params jsonb NOT NULL DEFAULT '{}'::jsonb, -- default/override sampling params
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TRIGGER trg_models_updated_at BEFORE UPDATE ON models
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- A concrete backend for a model. Multiple targets per model enable failover
-- The catalog entries an alias routes to. Multiple targets enable failover
-- ("local first, cloud backup"): lower priority wins, weight balances ties.
-- Pricing lives here because cost depends on which backend served the request.
CREATE TABLE model_targets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
model_id uuid NOT NULL REFERENCES models (id) ON DELETE CASCADE,
provider_id uuid NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
upstream_model text NOT NULL, -- model id sent to the provider
priority integer NOT NULL DEFAULT 100, -- lower = preferred
weight integer NOT NULL DEFAULT 1, -- tie-break load balancing
enabled boolean NOT NULL DEFAULT true,
context_length integer,
input_price_per_mtok numeric(12,6) NOT NULL DEFAULT 0, -- per 1M prompt tokens
output_price_per_mtok numeric(12,6) NOT NULL DEFAULT 0, -- per 1M completion tokens
cached_price_per_mtok numeric(12,6), -- per 1M cached tokens, NULL = input price
currency char(3) NOT NULL DEFAULT 'USD',
config jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (model_id, provider_id, upstream_model)
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
model_id uuid NOT NULL REFERENCES models (id) ON DELETE CASCADE,
provider_model_id uuid NOT NULL REFERENCES provider_models (id) ON DELETE CASCADE,
priority integer NOT NULL DEFAULT 100, -- lower = preferred
weight integer NOT NULL DEFAULT 1,
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (model_id, provider_model_id)
);
CREATE INDEX idx_model_targets_model ON model_targets (model_id, priority);
CREATE INDEX idx_model_targets_provider ON model_targets (provider_id);
CREATE INDEX idx_model_targets_provider_model ON model_targets (provider_model_id);
CREATE TRIGGER trg_model_targets_updated_at BEFORE UPDATE ON model_targets
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
@@ -117,7 +157,7 @@ CREATE TABLE virtual_keys (
key_prefix text NOT NULL, -- e.g. "sk-a1b2c3" for display
type text NOT NULL DEFAULT 'standard' CHECK (type IN ('standard','session')),
user_id uuid REFERENCES users (id) ON DELETE CASCADE,
group_id uuid REFERENCES groups (id) ON DELETE SET NULL, -- source group for session keys
group_id uuid REFERENCES groups (id) ON DELETE SET NULL,
allow_all_models boolean NOT NULL DEFAULT false,
monthly_budget numeric(14,6),
rpm_limit integer,
@@ -132,22 +172,21 @@ CREATE INDEX idx_virtual_keys_user ON virtual_keys (user_id);
CREATE INDEX idx_virtual_keys_group ON virtual_keys (group_id);
CREATE INDEX idx_virtual_keys_active ON virtual_keys (revoked_at, expires_at);
-- Per-key model allowlist (ignored when allow_all_models = true).
CREATE TABLE virtual_key_models (
-- Per-key allowlist rules (ignored when allow_all_models = true). A pattern is
-- an exact address ("openrouter/openai/gpt-4o") or a provider wildcard
-- ("openrouter/*").
CREATE TABLE virtual_key_model_rules (
virtual_key_id uuid NOT NULL REFERENCES virtual_keys (id) ON DELETE CASCADE,
model_id uuid NOT NULL REFERENCES models (id) ON DELETE CASCADE,
PRIMARY KEY (virtual_key_id, model_id)
pattern text NOT NULL,
PRIMARY KEY (virtual_key_id, pattern)
);
CREATE INDEX idx_virtual_key_models_model ON virtual_key_models (model_id);
-- Per-group model allowlist; the scope snapshot source for session keys
-- (ignored when groups.allow_all_models = true).
CREATE TABLE group_models (
-- Per-group allowlist rules; the scope source for session keys.
CREATE TABLE group_model_rules (
group_id uuid NOT NULL REFERENCES groups (id) ON DELETE CASCADE,
model_id uuid NOT NULL REFERENCES models (id) ON DELETE CASCADE,
PRIMARY KEY (group_id, model_id)
pattern text NOT NULL,
PRIMARY KEY (group_id, pattern)
);
CREATE INDEX idx_group_models_model ON group_models (model_id);
-- ===========================================================================
-- Request log (billing source of truth; written async after the response)
@@ -160,13 +199,13 @@ CREATE TABLE request_logs (
-- Soft references: keep logs even after the referent is deleted.
virtual_key_id uuid REFERENCES virtual_keys (id) ON DELETE SET NULL,
user_id uuid REFERENCES users (id) ON DELETE SET NULL,
model_id uuid REFERENCES models (id) ON DELETE SET NULL,
target_id uuid REFERENCES model_targets (id) ON DELETE SET NULL,
model_id uuid REFERENCES models (id) ON DELETE SET NULL, -- alias, if one was used
provider_model_id uuid REFERENCES provider_models (id) ON DELETE SET NULL, -- catalog entry served
provider_id uuid REFERENCES providers (id) ON DELETE SET NULL,
-- Snapshots so a row stays meaningful after deletes.
key_prefix text,
model_name text,
model_name text, -- the address the client requested
provider_name text,
-- Outcome.
@@ -185,7 +224,7 @@ CREATE TABLE request_logs (
-- Timing.
latency_ms integer,
ttft_ms integer, -- time to first token (streaming)
ttft_ms integer, -- time to first token (streaming)
-- Request metadata.
request_id text,
@@ -194,4 +233,21 @@ CREATE TABLE request_logs (
CREATE INDEX idx_request_logs_created ON request_logs (created_at DESC);
CREATE INDEX idx_request_logs_key ON request_logs (virtual_key_id, created_at DESC);
CREATE INDEX idx_request_logs_user ON request_logs (user_id, created_at DESC);
CREATE INDEX idx_request_logs_model ON request_logs (model_id, created_at DESC);
CREATE INDEX idx_request_logs_provider_model ON request_logs (provider_model_id, created_at DESC);
-- ===========================================================================
-- Global settings
-- ===========================================================================
-- Dashboard-editable key/value settings. Notably the routing strategy used to
-- pick among candidates for an "any/<slug>" address (or a multi-target alias):
-- round_robin | lowest_cost | priority.
CREATE TABLE settings (
key text PRIMARY KEY,
value jsonb NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TRIGGER trg_settings_updated_at BEFORE UPDATE ON settings
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
INSERT INTO settings (key, value) VALUES ('routing.strategy', '"round_robin"');