feat: revise core DB schema for provider catalog, settings, and rate limits
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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"');
|
||||
|
||||
Reference in New Issue
Block a user