184 lines
8.4 KiB
SQL
184 lines
8.4 KiB
SQL
-- ==========================================
|
|
-- Chat Switchboard — 003 Providers & Routing
|
|
-- ==========================================
|
|
-- Provider configs, model catalog, pricing, health, capability
|
|
-- overrides, and routing policies.
|
|
-- Consolidated v0.28.0: adds capability_match policy type.
|
|
-- ==========================================
|
|
|
|
-- =========================================
|
|
-- PROVIDER CONFIGS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS provider_configs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
scope VARCHAR(10) NOT NULL
|
|
CHECK (scope IN ('global', 'team', 'personal')),
|
|
owner_id UUID,
|
|
name VARCHAR(100) NOT NULL,
|
|
provider VARCHAR(50) NOT NULL,
|
|
endpoint TEXT NOT NULL,
|
|
|
|
-- Encrypted API key (AES-256-GCM)
|
|
api_key_enc BYTEA,
|
|
key_nonce BYTEA,
|
|
key_scope TEXT NOT NULL DEFAULT 'global',
|
|
|
|
model_default VARCHAR(100),
|
|
config JSONB DEFAULT '{}'::jsonb,
|
|
headers JSONB DEFAULT '{}'::jsonb,
|
|
settings JSONB DEFAULT '{}'::jsonb,
|
|
is_active BOOLEAN DEFAULT true,
|
|
is_private BOOLEAN DEFAULT false,
|
|
proxy_mode TEXT NOT NULL DEFAULT 'system'
|
|
CHECK (proxy_mode IN ('system', 'direct', 'custom')),
|
|
proxy_url TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
CONSTRAINT chk_proxy_url CHECK (proxy_mode != 'custom' OR proxy_url IS NOT NULL)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
|
|
CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id) WHERE owner_id IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active) WHERE is_active = true;
|
|
DROP TRIGGER IF EXISTS provider_configs_updated_at ON provider_configs;
|
|
CREATE TRIGGER provider_configs_updated_at BEFORE UPDATE ON provider_configs
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
COMMENT ON COLUMN provider_configs.scope IS 'global=admin-managed, team=team-scoped, personal=user BYOK';
|
|
COMMENT ON COLUMN provider_configs.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal scope';
|
|
COMMENT ON COLUMN provider_configs.api_key_enc IS 'AES-256-GCM encrypted API key (BYTEA)';
|
|
COMMENT ON COLUMN provider_configs.key_nonce IS 'AES-GCM nonce for api_key_enc';
|
|
COMMENT ON COLUMN provider_configs.key_scope IS 'Encryption tier: global/team use env key, personal uses UEK';
|
|
COMMENT ON COLUMN provider_configs.headers IS 'Custom HTTP headers (e.g. OpenRouter HTTP-Referer)';
|
|
COMMENT ON COLUMN provider_configs.settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)';
|
|
COMMENT ON COLUMN provider_configs.is_private IS 'Data stays on-prem (local/self-hosted provider)';
|
|
COMMENT ON COLUMN provider_configs.proxy_mode IS 'system=env proxy, direct=no proxy, custom=explicit URL';
|
|
COMMENT ON COLUMN provider_configs.proxy_url IS 'Explicit proxy URL when proxy_mode=custom (http/https/socks5)';
|
|
|
|
|
|
-- =========================================
|
|
-- MODEL CATALOG
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS model_catalog (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
|
model_id TEXT NOT NULL,
|
|
display_name TEXT,
|
|
model_type VARCHAR(20) DEFAULT 'chat',
|
|
capabilities JSONB NOT NULL DEFAULT '{}',
|
|
pricing JSONB,
|
|
visibility VARCHAR(10) DEFAULT 'disabled'
|
|
CHECK (visibility IN ('enabled', 'disabled', 'team')),
|
|
last_synced_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(provider_config_id, model_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id);
|
|
CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility) WHERE visibility = 'enabled';
|
|
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
|
|
DROP TRIGGER IF EXISTS model_catalog_updated_at ON model_catalog;
|
|
CREATE TRIGGER model_catalog_updated_at BEFORE UPDATE ON model_catalog
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
COMMENT ON COLUMN model_catalog.visibility IS 'disabled by default — admin must enable for global models';
|
|
COMMENT ON COLUMN model_catalog.model_type IS 'chat, embedding, image — from provider API sync';
|
|
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';
|
|
|
|
|
|
-- =========================================
|
|
-- MODEL PRICING
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
|
model_id TEXT NOT NULL,
|
|
input_per_m NUMERIC(10,6),
|
|
output_per_m NUMERIC(10,6),
|
|
cache_create_per_m NUMERIC(10,6),
|
|
cache_read_per_m NUMERIC(10,6),
|
|
currency TEXT NOT NULL DEFAULT 'USD',
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_by UUID REFERENCES users(id),
|
|
UNIQUE(provider_config_id, model_id)
|
|
);
|
|
|
|
|
|
-- =========================================
|
|
-- PROVIDER HEALTH
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS provider_health (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
|
window_start TIMESTAMPTZ NOT NULL,
|
|
request_count INT NOT NULL DEFAULT 0,
|
|
error_count INT NOT NULL DEFAULT 0,
|
|
timeout_count INT NOT NULL DEFAULT 0,
|
|
total_latency_ms BIGINT NOT NULL DEFAULT 0,
|
|
max_latency_ms INT NOT NULL DEFAULT 0,
|
|
rate_limit_count INT NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
last_error_at TIMESTAMPTZ,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
UNIQUE (provider_config_id, window_start)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_provider_health_window
|
|
ON provider_health (provider_config_id, window_start DESC);
|
|
|
|
|
|
-- =========================================
|
|
-- CAPABILITY OVERRIDES
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS capability_overrides (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE CASCADE,
|
|
model_id TEXT NOT NULL,
|
|
field TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
set_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
UNIQUE (provider_config_id, model_id, field)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_capability_overrides_model
|
|
ON capability_overrides (model_id);
|
|
|
|
|
|
-- =========================================
|
|
-- ROUTING POLICIES
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS routing_policies (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(200) NOT NULL,
|
|
scope VARCHAR(10) NOT NULL DEFAULT 'global'
|
|
CHECK (scope IN ('global', 'team')),
|
|
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
priority INTEGER NOT NULL DEFAULT 100,
|
|
policy_type VARCHAR(30) NOT NULL
|
|
CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')),
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_routing_policies_active
|
|
ON routing_policies(is_active, priority) WHERE is_active = true;
|
|
CREATE INDEX IF NOT EXISTS idx_routing_policies_team
|
|
ON routing_policies(team_id) WHERE team_id IS NOT NULL;
|
|
|
|
DROP TRIGGER IF EXISTS routing_policies_updated_at ON routing_policies;
|
|
CREATE TRIGGER routing_policies_updated_at BEFORE UPDATE ON routing_policies
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|