GT AI OS Community Edition v2.0.33

Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
-- Add frontend_url column to tenants table
-- Migration: 006_add_tenant_frontend_url
-- Date: October 6, 2025
BEGIN;
-- Add frontend_url column if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'tenants'
AND column_name = 'frontend_url'
) THEN
ALTER TABLE tenants ADD COLUMN frontend_url VARCHAR(255);
RAISE NOTICE 'Added frontend_url column to tenants table';
ELSE
RAISE NOTICE 'Column frontend_url already exists in tenants table';
END IF;
END
$$;
-- Mark migration as applied in Alembic version table (if it exists)
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'alembic_version') THEN
INSERT INTO alembic_version (version_num)
VALUES ('006_frontend_url')
ON CONFLICT (version_num) DO NOTHING;
RAISE NOTICE 'Marked migration in alembic_version table';
ELSE
RAISE NOTICE 'No alembic_version table found (skipping)';
END IF;
END
$$;
COMMIT;
-- Verify column was added
\echo 'Migration 006_add_tenant_frontend_url completed successfully'

View File

@@ -0,0 +1,54 @@
-- Remove ip_address column from password_reset_rate_limits
-- Migration: 008_remove_ip_address_from_rate_limits
-- Date: October 7, 2025
-- Database: gt2_admin (Control Panel)
--
-- Description:
-- Removes ip_address column that was incorrectly added by Alembic auto-migration
-- Application only uses email-based rate limiting, not IP-based
--
-- Usage:
-- psql -U postgres -d gt2_admin -f 008_remove_ip_address_from_rate_limits.sql
--
-- OR via Docker:
-- docker exec -i gentwo-controlpanel-postgres psql -U postgres -d gt2_admin < 008_remove_ip_address_from_rate_limits.sql
BEGIN;
-- Remove ip_address column if it exists
DO $$
BEGIN
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'password_reset_rate_limits'
AND column_name = 'ip_address'
) THEN
ALTER TABLE password_reset_rate_limits DROP COLUMN ip_address CASCADE;
RAISE NOTICE 'Removed ip_address column from password_reset_rate_limits';
ELSE
RAISE NOTICE 'Column ip_address does not exist, skipping';
END IF;
END
$$;
-- Mark migration as applied in Alembic version table (if it exists)
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'alembic_version') THEN
INSERT INTO alembic_version (version_num)
VALUES ('008_remove_ip')
ON CONFLICT (version_num) DO NOTHING;
RAISE NOTICE 'Marked migration in alembic_version table';
ELSE
RAISE NOTICE 'No alembic_version table found (skipping)';
END IF;
END
$$;
COMMIT;
-- Verify table structure
\d password_reset_rate_limits
\echo 'Migration 008_remove_ip_address_from_rate_limits completed successfully'

View File

@@ -0,0 +1,42 @@
-- Migration 009: Add Two-Factor Authentication Schema
-- Creates TFA fields in users table and supporting tables for rate limiting and token management
-- Add TFA fields to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS tfa_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE users ADD COLUMN IF NOT EXISTS tfa_secret TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS tfa_required BOOLEAN NOT NULL DEFAULT false;
-- Add indexes for query optimization
CREATE INDEX IF NOT EXISTS ix_users_tfa_enabled ON users(tfa_enabled);
CREATE INDEX IF NOT EXISTS ix_users_tfa_required ON users(tfa_required);
-- Create TFA verification rate limits table
CREATE TABLE IF NOT EXISTS tfa_verification_rate_limits (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
request_count INTEGER NOT NULL DEFAULT 1,
window_start TIMESTAMP WITH TIME ZONE NOT NULL,
window_end TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS ix_tfa_verification_rate_limits_user_id ON tfa_verification_rate_limits(user_id);
CREATE INDEX IF NOT EXISTS ix_tfa_verification_rate_limits_window_end ON tfa_verification_rate_limits(window_end);
-- Create used temp tokens table for replay prevention
CREATE TABLE IF NOT EXISTS used_temp_tokens (
id SERIAL PRIMARY KEY,
token_id VARCHAR(255) NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_email VARCHAR(255),
tfa_configured BOOLEAN,
qr_code_uri TEXT,
manual_entry_key VARCHAR(255),
temp_token TEXT,
used_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_used_temp_tokens_token_id ON used_temp_tokens(token_id);
CREATE INDEX IF NOT EXISTS ix_used_temp_tokens_expires_at ON used_temp_tokens(expires_at);

View File

@@ -0,0 +1,167 @@
-- Migration 010: Update Model Context Windows and Max Tokens
-- Ensures all models in model_configs have proper context_window and max_tokens set
-- Update models with missing context_window and max_tokens based on deployment configs
-- Reference: scripts/seed/groq-models.sql and actual Groq API specifications
DO $$
DECLARE
updated_count INTEGER := 0;
BEGIN
-- LLaMA 3.1 8B Instant
UPDATE model_configs
SET context_window = 131072,
max_tokens = 131072,
updated_at = NOW()
WHERE model_id = 'llama-3.1-8b-instant'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for llama-3.1-8b-instant', updated_count;
END IF;
-- LLaMA 3.3 70B Versatile
UPDATE model_configs
SET context_window = 131072,
max_tokens = 32768,
updated_at = NOW()
WHERE model_id = 'llama-3.3-70b-versatile'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for llama-3.3-70b-versatile', updated_count;
END IF;
-- Groq Compound
UPDATE model_configs
SET context_window = 131072,
max_tokens = 8192,
updated_at = NOW()
WHERE model_id = 'groq/compound'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for groq/compound', updated_count;
END IF;
-- Groq Compound Mini
UPDATE model_configs
SET context_window = 131072,
max_tokens = 8192,
updated_at = NOW()
WHERE model_id = 'groq/compound-mini'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for groq/compound-mini', updated_count;
END IF;
-- GPT OSS 120B
UPDATE model_configs
SET context_window = 131072,
max_tokens = 65536,
updated_at = NOW()
WHERE model_id = 'openai/gpt-oss-120b'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for openai/gpt-oss-120b', updated_count;
END IF;
-- GPT OSS 20B
UPDATE model_configs
SET context_window = 131072,
max_tokens = 65536,
updated_at = NOW()
WHERE model_id = 'openai/gpt-oss-20b'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for openai/gpt-oss-20b', updated_count;
END IF;
-- Meta LLaMA Guard 4 12B
UPDATE model_configs
SET context_window = 131072,
max_tokens = 1024,
updated_at = NOW()
WHERE model_id = 'meta-llama/llama-guard-4-12b'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for meta-llama/llama-guard-4-12b', updated_count;
END IF;
-- Meta LLaMA 4 Maverick 17B
UPDATE model_configs
SET context_window = 131072,
max_tokens = 8192,
updated_at = NOW()
WHERE model_id = 'meta-llama/llama-4-maverick-17b-128e-instruct'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for meta-llama/llama-4-maverick-17b-128e-instruct', updated_count;
END IF;
-- Moonshot AI Kimi K2 (checking for common variations)
UPDATE model_configs
SET context_window = 262144,
max_tokens = 16384,
updated_at = NOW()
WHERE model_id IN ('moonshotai/kimi-k2-instruct-0905', 'kimi-k2-instruct-0905', 'moonshotai/kimi-k2')
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for moonshotai/kimi-k2-instruct-0905', updated_count;
END IF;
-- Whisper Large v3
UPDATE model_configs
SET context_window = 0,
max_tokens = 0,
updated_at = NOW()
WHERE model_id = 'whisper-large-v3'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for whisper-large-v3', updated_count;
END IF;
-- Whisper Large v3 Turbo
UPDATE model_configs
SET context_window = 0,
max_tokens = 0,
updated_at = NOW()
WHERE model_id = 'whisper-large-v3-turbo'
AND (context_window IS NULL OR max_tokens IS NULL);
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
RAISE NOTICE 'Updated % records for whisper-large-v3-turbo', updated_count;
END IF;
RAISE NOTICE 'Migration 010 completed: Updated model context windows and max tokens';
END $$;
-- Display updated models
SELECT
model_id,
name,
provider,
model_type,
context_window,
max_tokens
FROM model_configs
WHERE provider = 'groq' OR model_id LIKE '%moonshot%' OR model_id LIKE '%kimi%'
ORDER BY model_id;

View File

@@ -0,0 +1,70 @@
-- Migration 011: Add system management tables for version tracking, updates, and backups
-- Idempotent: Uses CREATE TABLE IF NOT EXISTS and exception handling for enums
-- Create enum types (safe to recreate)
DO $$ BEGIN
CREATE TYPE updatestatus AS ENUM ('pending', 'in_progress', 'completed', 'failed', 'rolled_back');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE backuptype AS ENUM ('manual', 'pre_update', 'scheduled');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- System versions table - tracks installed system versions
CREATE TABLE IF NOT EXISTS system_versions (
id SERIAL PRIMARY KEY,
uuid VARCHAR(36) NOT NULL UNIQUE DEFAULT gen_random_uuid()::text,
version VARCHAR(50) NOT NULL,
installed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
installed_by VARCHAR(255),
is_current BOOLEAN NOT NULL DEFAULT true,
release_notes TEXT,
git_commit VARCHAR(40)
);
CREATE INDEX IF NOT EXISTS ix_system_versions_id ON system_versions(id);
CREATE INDEX IF NOT EXISTS ix_system_versions_version ON system_versions(version);
-- Update jobs table - tracks update execution
CREATE TABLE IF NOT EXISTS update_jobs (
id SERIAL PRIMARY KEY,
uuid VARCHAR(36) NOT NULL UNIQUE DEFAULT gen_random_uuid()::text,
target_version VARCHAR(50) NOT NULL,
status updatestatus NOT NULL DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
current_stage VARCHAR(100),
logs JSONB NOT NULL DEFAULT '[]'::jsonb,
error_message TEXT,
backup_id INTEGER,
started_by VARCHAR(255),
rollback_reason TEXT
);
CREATE INDEX IF NOT EXISTS ix_update_jobs_id ON update_jobs(id);
CREATE INDEX IF NOT EXISTS ix_update_jobs_uuid ON update_jobs(uuid);
CREATE INDEX IF NOT EXISTS ix_update_jobs_status ON update_jobs(status);
-- Backup records table - tracks system backups
CREATE TABLE IF NOT EXISTS backup_records (
id SERIAL PRIMARY KEY,
uuid VARCHAR(36) NOT NULL UNIQUE DEFAULT gen_random_uuid()::text,
backup_type backuptype NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
size_bytes BIGINT,
location VARCHAR(500) NOT NULL,
version VARCHAR(50),
components JSONB NOT NULL DEFAULT '{}'::jsonb,
checksum VARCHAR(64),
created_by VARCHAR(255),
description TEXT,
is_valid BOOLEAN NOT NULL DEFAULT true,
expires_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX IF NOT EXISTS ix_backup_records_id ON backup_records(id);
CREATE INDEX IF NOT EXISTS ix_backup_records_uuid ON backup_records(uuid);
-- Seed initial version (idempotent - only inserts if no current version exists)
INSERT INTO system_versions (uuid, version, installed_by, is_current)
SELECT 'initial-version-uuid', 'v2.0.31', 'system', true
WHERE NOT EXISTS (SELECT 1 FROM system_versions WHERE is_current = true);

View File

@@ -0,0 +1,36 @@
-- T008_optics_feature.sql
-- Add Optics cost tracking feature toggle for tenants
-- This enables the Optics tab in tenant observability for cost visibility
BEGIN;
-- Add optics_enabled column to tenants table in control panel database
-- This column controls whether the Optics cost tracking tab is visible for a tenant
ALTER TABLE public.tenants
ADD COLUMN IF NOT EXISTS optics_enabled BOOLEAN DEFAULT FALSE;
-- Add comment for documentation
COMMENT ON COLUMN public.tenants.optics_enabled IS
'Enable Optics cost tracking tab in tenant observability dashboard';
-- Update existing test tenant to have optics enabled for demo purposes
UPDATE public.tenants
SET optics_enabled = TRUE
WHERE domain = 'test-company';
COMMIT;
-- Log completion
DO $$
BEGIN
RAISE NOTICE '=== T008 OPTICS FEATURE MIGRATION ===';
RAISE NOTICE 'Added optics_enabled column to tenants table';
RAISE NOTICE 'Default: FALSE (disabled)';
RAISE NOTICE 'Test tenant (test-company): enabled';
RAISE NOTICE '=====================================';
END $$;
-- Rollback (if needed):
-- BEGIN;
-- ALTER TABLE public.tenants DROP COLUMN IF EXISTS optics_enabled;
-- COMMIT;

View File

@@ -0,0 +1,15 @@
-- Migration 013: Rename cost columns from per_1k to per_million
-- This is idempotent - only runs if old columns exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'model_configs'
AND column_name = 'cost_per_1k_input') THEN
ALTER TABLE model_configs RENAME COLUMN cost_per_1k_input TO cost_per_million_input;
ALTER TABLE model_configs RENAME COLUMN cost_per_1k_output TO cost_per_million_output;
RAISE NOTICE 'Renamed cost columns from per_1k to per_million';
ELSE
RAISE NOTICE 'Cost columns already renamed or do not exist';
END IF;
END $$;

View File

@@ -0,0 +1,108 @@
-- Migration 014: Backfill missing Groq model pricing
-- Updates models with NULL or 0 pricing to use standard Groq rates
-- Prices sourced from https://groq.com/pricing (verified Dec 2, 2025)
-- Idempotent - only updates rows that need fixing
-- Groq Compound (estimated: includes underlying model + tool costs)
UPDATE model_configs
SET cost_per_million_input = 2.50, cost_per_million_output = 6.00, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound'
AND model_id NOT LIKE '%mini%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Groq Compound Mini (estimated: includes underlying model + tool costs)
UPDATE model_configs
SET cost_per_million_input = 1.00, cost_per_million_output = 2.50, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound-mini%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- LLaMA 3.1 8B Instant
UPDATE model_configs
SET cost_per_million_input = 0.05, cost_per_million_output = 0.08, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-3.1-8b-instant%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- LLaMA 3.3 70B Versatile
UPDATE model_configs
SET cost_per_million_input = 0.59, cost_per_million_output = 0.79, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-3.3-70b-versatile%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Meta Llama 4 Maverick 17B (17Bx128E MoE)
UPDATE model_configs
SET cost_per_million_input = 0.20, cost_per_million_output = 0.60, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-4-maverick%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Meta Llama 4 Scout 17B (17Bx16E MoE)
UPDATE model_configs
SET cost_per_million_input = 0.11, cost_per_million_output = 0.34, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-4-scout%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- LLaMA Guard 4 12B
UPDATE model_configs
SET cost_per_million_input = 0.20, cost_per_million_output = 0.20, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-guard%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Moonshot AI Kimi K2 (1T params, 256k context)
UPDATE model_configs
SET cost_per_million_input = 1.00, cost_per_million_output = 3.00, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%kimi-k2%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- OpenAI GPT OSS 120B 128k
UPDATE model_configs
SET cost_per_million_input = 0.15, cost_per_million_output = 0.60, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-120b%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- OpenAI GPT OSS 20B 128k
UPDATE model_configs
SET cost_per_million_input = 0.075, cost_per_million_output = 0.30, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-20b%'
AND model_id NOT LIKE '%safeguard%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- OpenAI GPT OSS Safeguard 20B
UPDATE model_configs
SET cost_per_million_input = 0.075, cost_per_million_output = 0.30, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-safeguard%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Qwen3 32B 131k
UPDATE model_configs
SET cost_per_million_input = 0.29, cost_per_million_output = 0.59, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%qwen3-32b%'
AND (cost_per_million_input IS NULL OR cost_per_million_input = 0
OR cost_per_million_output IS NULL OR cost_per_million_output = 0);
-- Report results
SELECT model_id, name, cost_per_million_input, cost_per_million_output
FROM model_configs
WHERE provider = 'groq'
ORDER BY model_id;

View File

@@ -0,0 +1,84 @@
-- Migration 015: Update Groq model pricing to December 2025 rates
-- Source: https://groq.com/pricing (verified Dec 2, 2025)
-- This migration updates ALL pricing values (not just NULL/0)
-- GPT OSS 120B 128k: Was $1.20/$1.20, now $0.15/$0.60
UPDATE model_configs
SET cost_per_million_input = 0.15, cost_per_million_output = 0.60, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-120b%'
AND model_id NOT LIKE '%safeguard%';
-- GPT OSS 20B 128k: Was $0.30/$0.30, now $0.075/$0.30
UPDATE model_configs
SET cost_per_million_input = 0.075, cost_per_million_output = 0.30, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-20b%'
AND model_id NOT LIKE '%safeguard%';
-- GPT OSS Safeguard 20B: $0.075/$0.30
UPDATE model_configs
SET cost_per_million_input = 0.075, cost_per_million_output = 0.30, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%gpt-oss-safeguard%';
-- Llama 4 Maverick (17Bx128E): Was $0.15/$0.25, now $0.20/$0.60
UPDATE model_configs
SET cost_per_million_input = 0.20, cost_per_million_output = 0.60, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-4-maverick%';
-- Llama 4 Scout (17Bx16E): $0.11/$0.34 (new model)
UPDATE model_configs
SET cost_per_million_input = 0.11, cost_per_million_output = 0.34, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-4-scout%';
-- Kimi K2: Was $0.30/$0.50, now $1.00/$3.00
UPDATE model_configs
SET cost_per_million_input = 1.00, cost_per_million_output = 3.00, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%kimi-k2%';
-- Llama Guard 4 12B: $0.20/$0.20
UPDATE model_configs
SET cost_per_million_input = 0.20, cost_per_million_output = 0.20, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-guard%';
-- Groq Compound: Was $2.00/$2.00, now $2.50/$6.00 (estimated with tool costs)
UPDATE model_configs
SET cost_per_million_input = 2.50, cost_per_million_output = 6.00, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound%'
AND model_id NOT LIKE '%mini%';
-- Groq Compound Mini: Was $0.80/$0.80, now $1.00/$2.50 (estimated with tool costs)
UPDATE model_configs
SET cost_per_million_input = 1.00, cost_per_million_output = 2.50, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound-mini%';
-- Qwen3 32B 131k: $0.29/$0.59 (new model)
UPDATE model_configs
SET cost_per_million_input = 0.29, cost_per_million_output = 0.59, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%qwen3-32b%';
-- LLaMA 3.1 8B Instant: $0.05/$0.08 (unchanged, ensure consistency)
UPDATE model_configs
SET cost_per_million_input = 0.05, cost_per_million_output = 0.08, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-3.1-8b-instant%';
-- LLaMA 3.3 70B Versatile: $0.59/$0.79 (unchanged, ensure consistency)
UPDATE model_configs
SET cost_per_million_input = 0.59, cost_per_million_output = 0.79, updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%llama-3.3-70b-versatile%';
-- Report updated pricing
SELECT model_id, name, cost_per_million_input as input_per_1m, cost_per_million_output as output_per_1m
FROM model_configs
WHERE provider = 'groq'
ORDER BY cost_per_million_input DESC, model_id;

View File

@@ -0,0 +1,24 @@
-- Migration 016: Add is_compound column to model_configs
-- Required for Compound model pass-through pricing
-- Date: 2025-12-02
-- Add column if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'model_configs' AND column_name = 'is_compound'
) THEN
ALTER TABLE public.model_configs
ADD COLUMN is_compound BOOLEAN DEFAULT FALSE;
END IF;
END $$;
-- Mark compound models
UPDATE public.model_configs
SET is_compound = true
WHERE model_id LIKE '%compound%'
AND is_compound IS NOT TRUE;
-- Verify
SELECT model_id, is_compound FROM public.model_configs WHERE model_id LIKE '%compound%';

View File

@@ -0,0 +1,31 @@
-- Migration 017: Fix Compound model pricing with correct blended rates
-- Source: https://groq.com/pricing (Dec 2025) + actual API response analysis
--
-- Compound uses GPT-OSS-120B ($0.15/$0.60) + Llama 4 Scout ($0.11/$0.34)
-- Blended 50/50: ($0.15+$0.11)/2 = $0.13 input, ($0.60+$0.34)/2 = $0.47 output
--
-- Compound Mini uses GPT-OSS-120B ($0.15/$0.60) + Llama 3.3 70B ($0.59/$0.79)
-- Blended 50/50: ($0.15+$0.59)/2 = $0.37 input, ($0.60+$0.79)/2 = $0.695 output
-- Fix Compound pricing (was incorrectly set to $2.50/$6.00)
UPDATE model_configs
SET cost_per_million_input = 0.13,
cost_per_million_output = 0.47,
updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound%'
AND model_id NOT LIKE '%mini%';
-- Fix Compound Mini pricing (was incorrectly set to $1.00/$2.50)
UPDATE model_configs
SET cost_per_million_input = 0.37,
cost_per_million_output = 0.695,
updated_at = NOW()
WHERE provider = 'groq'
AND model_id LIKE '%compound-mini%';
-- Report updated pricing
SELECT model_id, name, cost_per_million_input as input_per_1m, cost_per_million_output as output_per_1m
FROM model_configs
WHERE provider = 'groq' AND model_id LIKE '%compound%'
ORDER BY model_id;

View File

@@ -0,0 +1,19 @@
-- Migration 018: Add budget and storage pricing fields to tenants
-- Supports #234 (Budget Limits), #218 (Storage Tier Pricing)
-- Updated: Removed warm tier, changed cold tier to allocation-based model
-- Budget fields
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS monthly_budget_cents INTEGER DEFAULT NULL;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS budget_warning_threshold INTEGER DEFAULT 80;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS budget_critical_threshold INTEGER DEFAULT 90;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS budget_enforcement_enabled BOOLEAN DEFAULT true;
-- Hot tier storage pricing overrides (NULL = use system defaults)
-- Default: $0.15/GiB/month (in cents per MiB: ~0.0146 cents/MiB)
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS storage_price_dataset_hot DECIMAL(10,4) DEFAULT NULL;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS storage_price_conversation_hot DECIMAL(10,4) DEFAULT NULL;
-- Cold tier: Allocation-based model
-- Monthly cost = allocated_tibs × price_per_tib
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS cold_storage_allocated_tibs DECIMAL(10,4) DEFAULT NULL;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS cold_storage_price_per_tib DECIMAL(10,2) DEFAULT 10.00;

View File

@@ -0,0 +1,17 @@
-- Migration 019: Add embedding usage tracking table
-- Supports #241 (Embedding Model Pricing)
CREATE TABLE IF NOT EXISTS public.embedding_usage_logs (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR(100) NOT NULL,
user_id VARCHAR(100) NOT NULL,
tokens_used INTEGER NOT NULL,
embedding_count INTEGER NOT NULL,
model VARCHAR(100) DEFAULT 'BAAI/bge-m3',
cost_cents DECIMAL(10,4) NOT NULL,
request_id VARCHAR(100),
timestamp TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_embedding_usage_tenant_timestamp
ON public.embedding_usage_logs(tenant_id, timestamp);

View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Migration 020: Import GROQ_API_KEY from environment to database
Migrates API keys from .env file to encrypted database storage for test-company tenant.
This is part of the move away from environment variables for API keys (#158, #219).
Idempotency: Checks if key already exists before importing
Target: test-company tenant only (as specified in requirements)
Usage:
python scripts/migrations/020_migrate_env_api_keys.py
Environment variables required:
- GROQ_API_KEY: The Groq API key to migrate (optional - skips if not set)
- API_KEY_ENCRYPTION_KEY: Fernet encryption key (auto-generated if not set)
- CONTROL_PANEL_DB_HOST: Database host (default: localhost)
- CONTROL_PANEL_DB_PORT: Database port (default: 5432)
- CONTROL_PANEL_DB_NAME: Database name (default: gt2_admin)
- CONTROL_PANEL_DB_USER: Database user (default: postgres)
- ADMIN_POSTGRES_PASSWORD: Database password
"""
import os
import sys
import json
import logging
from datetime import datetime
try:
from cryptography.fernet import Fernet
import psycopg2
except ImportError as e:
print(f"Missing required package: {e}")
print("Run: pip install cryptography psycopg2-binary")
sys.exit(1)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Migration constants
TARGET_TENANT_DOMAIN = "test-company"
PROVIDER = "groq"
MIGRATION_ID = "020"
def get_db_connection():
"""Get database connection using environment variables or defaults"""
try:
conn = psycopg2.connect(
host=os.getenv("CONTROL_PANEL_DB_HOST", "localhost"),
port=os.getenv("CONTROL_PANEL_DB_PORT", "5432"),
database=os.getenv("CONTROL_PANEL_DB_NAME", "gt2_admin"),
user=os.getenv("CONTROL_PANEL_DB_USER", "postgres"),
password=os.getenv("ADMIN_POSTGRES_PASSWORD", "dev_password_change_in_prod")
)
return conn
except psycopg2.Error as e:
logger.error(f"Database connection failed: {e}")
raise
def get_encryption_key() -> str:
"""Get or generate Fernet encryption key"""
key = os.getenv("API_KEY_ENCRYPTION_KEY")
if not key:
# Generate a new key - in production this should be persisted
key = Fernet.generate_key().decode()
logger.warning("Generated new API_KEY_ENCRYPTION_KEY - add to .env for persistence:")
logger.warning(f" API_KEY_ENCRYPTION_KEY={key}")
return key
def check_env_key_exists() -> str | None:
"""Check if GROQ_API_KEY environment variable exists and is valid"""
groq_key = os.getenv("GROQ_API_KEY")
# Skip placeholder values
placeholder_values = [
"gsk_your_actual_groq_api_key_here",
"gsk_placeholder",
"",
None
]
if groq_key in placeholder_values:
logger.info("GROQ_API_KEY not set or is placeholder - skipping migration")
return None
# Validate format
if not groq_key.startswith("gsk_"):
logger.warning(f"GROQ_API_KEY has invalid format (should start with 'gsk_')")
return None
return groq_key
def get_tenant_id(conn, domain: str) -> int | None:
"""Get tenant ID by domain"""
with conn.cursor() as cur:
cur.execute(
"SELECT id FROM tenants WHERE domain = %s AND deleted_at IS NULL",
(domain,)
)
row = cur.fetchone()
return row[0] if row else None
def check_db_key_exists(conn, tenant_id: int) -> bool:
"""Check if Groq key already exists in database for tenant"""
with conn.cursor() as cur:
cur.execute(
"SELECT api_keys FROM tenants WHERE id = %s",
(tenant_id,)
)
row = cur.fetchone()
if row and row[0]:
api_keys = row[0] if isinstance(row[0], dict) else json.loads(row[0])
if PROVIDER in api_keys and api_keys[PROVIDER].get("key"):
return True
return False
def migrate_api_key(conn, tenant_id: int, api_key: str, encryption_key: str) -> bool:
"""Encrypt and store API key in database"""
try:
cipher = Fernet(encryption_key.encode())
encrypted_key = cipher.encrypt(api_key.encode()).decode()
api_keys_data = {
PROVIDER: {
"key": encrypted_key,
"secret": None,
"enabled": True,
"metadata": {
"migrated_from": "environment",
"migration_id": MIGRATION_ID,
"migration_date": datetime.utcnow().isoformat()
},
"updated_at": datetime.utcnow().isoformat(),
"updated_by": f"migration-{MIGRATION_ID}"
}
}
with conn.cursor() as cur:
cur.execute(
"""
UPDATE tenants
SET api_keys = %s::jsonb,
api_key_encryption_version = 'v1',
updated_at = NOW()
WHERE id = %s
""",
(json.dumps(api_keys_data), tenant_id)
)
conn.commit()
return True
except Exception as e:
conn.rollback()
logger.error(f"Failed to migrate API key: {e}")
return False
def run_migration() -> bool:
"""Main migration logic"""
logger.info(f"=== Migration {MIGRATION_ID}: Import GROQ_API_KEY from environment ===")
# Step 1: Check if env var exists
groq_key = check_env_key_exists()
if not groq_key:
logger.info("Migration skipped: No valid GROQ_API_KEY in environment")
return True # Not an error - just nothing to migrate
logger.info(f"Found GROQ_API_KEY in environment (length: {len(groq_key)})")
# Step 2: Connect to database
try:
conn = get_db_connection()
logger.info("Connected to database")
except Exception as e:
logger.error(f"Failed to connect to database: {e}")
return False
try:
# Step 3: Get tenant ID
tenant_id = get_tenant_id(conn, TARGET_TENANT_DOMAIN)
if not tenant_id:
logger.warning(f"Tenant '{TARGET_TENANT_DOMAIN}' not found - skipping migration")
logger.info("This is expected for fresh installs before tenant creation")
return True
logger.info(f"Found tenant '{TARGET_TENANT_DOMAIN}' with ID: {tenant_id}")
# Step 4: Check if DB key already exists (idempotency)
if check_db_key_exists(conn, tenant_id):
logger.info("Migration already complete - Groq key exists in database")
return True
# Step 5: Get/generate encryption key
encryption_key = get_encryption_key()
# Step 6: Migrate the key
logger.info(f"Migrating GROQ_API_KEY to database for tenant {tenant_id}...")
if migrate_api_key(conn, tenant_id, groq_key, encryption_key):
logger.info(f"=== Migration {MIGRATION_ID} completed successfully ===")
logger.info("The GROQ_API_KEY env var can now be removed from docker-compose.yml")
return True
else:
logger.error(f"Migration {MIGRATION_ID} failed")
return False
except Exception as e:
logger.error(f"Migration failed with error: {e}")
return False
finally:
conn.close()
if __name__ == "__main__":
success = run_migration()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,432 @@
-- Migration: 021_add_nvidia_models.sql
-- Description: Add NVIDIA NIM models to model_configs table
-- Date: 2025-12-08
-- Issue: #266 - Add NVIDIA API endpoint support
-- Reference: https://build.nvidia.com/models
-- NVIDIA NIM Models (build.nvidia.com)
-- Pricing: Estimated based on third-party providers and model size (Dec 2025)
-- Models selected: SOTA reasoning, coding, and general-purpose LLMs
INSERT INTO model_configs (
model_id,
name,
version,
provider,
model_type,
endpoint,
context_window,
max_tokens,
cost_per_million_input,
cost_per_million_output,
capabilities,
is_active,
description,
created_at,
updated_at,
request_count,
error_count,
success_rate,
avg_latency_ms,
health_status
)
VALUES
-- ==========================================
-- NVIDIA Llama Nemotron Family (Flagship)
-- ==========================================
-- Llama 3.3 Nemotron Super 49B v1 - Latest flagship reasoning model
(
'nvidia/llama-3.3-nemotron-super-49b-v1',
'NVIDIA Llama 3.3 Nemotron Super 49B',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
131072,
8192,
0.5,
1.5,
'{"streaming": true, "function_calling": true, "reasoning": true}',
true,
'NVIDIA flagship reasoning model - best accuracy/throughput on single GPU',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- Llama 3.1 Nemotron Ultra 253B - Maximum accuracy
(
'nvidia/llama-3.1-nemotron-ultra-253b-v1',
'NVIDIA Llama 3.1 Nemotron Ultra 253B',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
131072,
8192,
0.6,
1.8,
'{"streaming": true, "function_calling": true, "reasoning": true}',
true,
'Maximum agentic accuracy for scientific reasoning, math, and coding',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- Nemotron Nano 8B - Edge/PC deployment
(
'nvidia/llama-3.1-nemotron-nano-8b-v1',
'NVIDIA Llama 3.1 Nemotron Nano 8B',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
131072,
8192,
0.02,
0.06,
'{"streaming": true, "function_calling": true}',
true,
'Cost-effective model optimized for edge devices and low latency',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- Meta Llama 3.3 (via NVIDIA NIM)
-- ==========================================
-- Llama 3.3 70B Instruct - Latest Llama
(
'nvidia/meta-llama-3.3-70b-instruct',
'NVIDIA Meta Llama 3.3 70B Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
4096,
0.13,
0.4,
'{"streaming": true, "function_calling": true}',
true,
'Latest Meta Llama 3.3 - excellent for instruction following',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- DeepSeek Models (via NVIDIA NIM)
-- ==========================================
-- DeepSeek V3 - Hybrid inference with Think/Non-Think modes
(
'nvidia/deepseek-ai-deepseek-v3',
'NVIDIA DeepSeek V3',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.5,
1.5,
'{"streaming": true, "function_calling": true, "reasoning": true}',
true,
'Hybrid LLM with Think/Non-Think modes, 128K context, strong tool use',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- DeepSeek R1 - Enhanced reasoning
(
'nvidia/deepseek-ai-deepseek-r1',
'NVIDIA DeepSeek R1',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.6,
2.4,
'{"streaming": true, "function_calling": true, "reasoning": true}',
true,
'Enhanced reasoning model - reduced hallucination, strong math/coding',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- Kimi K2 (Moonshot AI via NVIDIA NIM)
-- ==========================================
(
'nvidia/moonshot-ai-kimi-k2-instruct',
'NVIDIA Kimi K2 Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.4,
1.2,
'{"streaming": true, "function_calling": true, "reasoning": true}',
true,
'Long context window with enhanced reasoning capabilities',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- Mistral Models (via NVIDIA NIM)
-- ==========================================
-- Mistral Large 3 - State-of-the-art MoE
(
'nvidia/mistralai-mistral-large-3-instruct',
'NVIDIA Mistral Large 3 Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.8,
2.4,
'{"streaming": true, "function_calling": true}',
true,
'State-of-the-art general purpose MoE model',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- Qwen Models (via NVIDIA NIM)
-- ==========================================
-- Qwen 3 - Ultra-long context (131K with YaRN extension)
(
'nvidia/qwen-qwen3-235b-a22b-fp8-instruct',
'NVIDIA Qwen 3 235B Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
131072,
8192,
0.7,
2.1,
'{"streaming": true, "function_calling": true}',
true,
'Ultra-long context AI with strong multilingual support',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- Meta Llama 3.1 (via NVIDIA NIM)
-- ==========================================
-- Llama 3.1 405B - Largest open model
(
'nvidia/meta-llama-3.1-405b-instruct',
'NVIDIA Meta Llama 3.1 405B Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
4096,
1.0,
3.0,
'{"streaming": true, "function_calling": true}',
true,
'Largest open-source LLM - exceptional quality across all tasks',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- Llama 3.1 70B
(
'nvidia/meta-llama-3.1-70b-instruct',
'NVIDIA Meta Llama 3.1 70B Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
4096,
0.13,
0.4,
'{"streaming": true, "function_calling": true}',
true,
'Excellent balance of quality and speed',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- Llama 3.1 8B - Fast and efficient
(
'nvidia/meta-llama-3.1-8b-instruct',
'NVIDIA Meta Llama 3.1 8B Instruct',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
4096,
0.02,
0.06,
'{"streaming": true, "function_calling": true}',
true,
'Fast and cost-effective for simpler tasks',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- ==========================================
-- OpenAI GPT-OSS Models (via NVIDIA NIM)
-- Released August 2025 - Apache 2.0 License
-- ==========================================
-- GPT-OSS 120B via NVIDIA NIM - Production flagship, MoE architecture (117B params, 5.7B active)
(
'nvidia/openai-gpt-oss-120b',
'NVIDIA OpenAI GPT-OSS 120B',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.7,
2.1,
'{"streaming": true, "function_calling": true, "reasoning": true, "tool_use": true}',
true,
'OpenAI flagship open model via NVIDIA NIM - production-grade reasoning, fits single H100 GPU',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
),
-- GPT-OSS 20B via NVIDIA NIM - Lightweight MoE for edge/local (21B params, 4B active)
(
'nvidia/openai-gpt-oss-20b',
'NVIDIA OpenAI GPT-OSS 20B',
'1.0',
'nvidia',
'llm',
'https://integrate.api.nvidia.com/v1/chat/completions',
128000,
8192,
0.15,
0.45,
'{"streaming": true, "function_calling": true, "reasoning": true, "tool_use": true}',
true,
'OpenAI lightweight open model via NVIDIA NIM - low latency, runs in 16GB VRAM',
NOW(),
NOW(),
0,
0,
100.0,
0,
'unknown'
)
ON CONFLICT (model_id) DO UPDATE SET
name = EXCLUDED.name,
version = EXCLUDED.version,
provider = EXCLUDED.provider,
endpoint = EXCLUDED.endpoint,
context_window = EXCLUDED.context_window,
max_tokens = EXCLUDED.max_tokens,
cost_per_million_input = EXCLUDED.cost_per_million_input,
cost_per_million_output = EXCLUDED.cost_per_million_output,
capabilities = EXCLUDED.capabilities,
is_active = EXCLUDED.is_active,
description = EXCLUDED.description,
updated_at = NOW();
-- Assign NVIDIA models to all existing tenants with 1000 RPM rate limits
-- Note: model_config_id (UUID) is the foreign key, model_id kept for convenience
INSERT INTO tenant_model_configs (tenant_id, model_config_id, model_id, is_enabled, priority, rate_limits, created_at, updated_at)
SELECT
t.id,
m.id, -- UUID foreign key (auto-generated in model_configs)
m.model_id, -- String identifier (kept for easier queries)
true,
5,
'{"max_requests_per_hour": 1000, "max_tokens_per_request": 4000, "concurrent_requests": 5, "max_cost_per_hour": 10.0, "requests_per_minute": 1000, "tokens_per_minute": 100000, "max_concurrent": 10}'::json,
NOW(),
NOW()
FROM tenants t
CROSS JOIN model_configs m
WHERE m.provider = 'nvidia'
ON CONFLICT (tenant_id, model_config_id) DO UPDATE SET
rate_limits = EXCLUDED.rate_limits;
-- Log migration completion
DO $$
BEGIN
RAISE NOTICE 'Migration 021: Added NVIDIA NIM models (Nemotron, Llama 3.3, DeepSeek, Kimi K2, Mistral, Qwen, OpenAI GPT-OSS) to model_configs and assigned to tenants';
END $$;

View File

@@ -0,0 +1,238 @@
-- Migration: 022_add_session_management.sql
-- Description: Server-side session tracking for OWASP/NIST compliance
-- Date: 2025-12-08
-- Issue: #264 - Session timeout warning not appearing
--
-- Timeout Configuration:
-- Idle Timeout: 4 hours (240 minutes) - covers meetings, lunch, context-switching
-- Absolute Timeout: 8 hours (maximum session lifetime) - full work day
-- Warning Threshold: 5 minutes before idle expiry
-- Active sessions table for server-side session tracking
-- This is the authoritative source of truth for session validity,
-- not the JWT expiration time alone.
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
session_token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 of session token (never store plaintext)
-- Session timing (NIST SP 800-63B compliant)
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_activity_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
absolute_expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- 8 hours from creation
-- Session metadata for security auditing
ip_address VARCHAR(45), -- IPv6 compatible (max 45 chars)
user_agent TEXT,
tenant_id INTEGER REFERENCES tenants(id),
-- Session state
is_active BOOLEAN NOT NULL DEFAULT true,
revoked_at TIMESTAMP WITH TIME ZONE,
revoke_reason VARCHAR(50), -- 'logout', 'idle_timeout', 'absolute_timeout', 'admin_revoke', 'password_change', 'cleanup_stale'
ended_at TIMESTAMP WITH TIME ZONE, -- When session ended (any reason: logout, timeout, etc.)
app_type VARCHAR(20) NOT NULL DEFAULT 'control_panel' -- 'control_panel' or 'tenant_app'
);
-- Indexes for session lookup and cleanup
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(session_token_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity_at);
CREATE INDEX IF NOT EXISTS idx_sessions_absolute_expires ON sessions(absolute_expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_id ON sessions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_sessions_ended_at ON sessions(ended_at);
CREATE INDEX IF NOT EXISTS idx_sessions_app_type ON sessions(app_type);
-- Function to clean up expired sessions (run periodically via cron or scheduled task)
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS INTEGER AS $$
DECLARE
rows_affected INTEGER := 0;
idle_rows INTEGER := 0;
idle_timeout_minutes INTEGER := 240; -- 4 hours
absolute_cutoff TIMESTAMP WITH TIME ZONE;
idle_cutoff TIMESTAMP WITH TIME ZONE;
BEGIN
absolute_cutoff := CURRENT_TIMESTAMP;
idle_cutoff := CURRENT_TIMESTAMP - (idle_timeout_minutes * INTERVAL '1 minute');
-- Mark sessions as inactive if absolute timeout exceeded
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = 'absolute_timeout'
WHERE is_active = true
AND absolute_expires_at < absolute_cutoff;
GET DIAGNOSTICS rows_affected = ROW_COUNT;
-- Mark sessions as inactive if idle timeout exceeded
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = 'idle_timeout'
WHERE is_active = true
AND last_activity_at < idle_cutoff;
GET DIAGNOSTICS idle_rows = ROW_COUNT;
rows_affected := rows_affected + idle_rows;
RETURN rows_affected;
END;
$$ LANGUAGE plpgsql;
-- Function to get session status (for internal API validation)
CREATE OR REPLACE FUNCTION get_session_status(p_token_hash VARCHAR(64))
RETURNS TABLE (
is_valid BOOLEAN,
expiry_reason VARCHAR(50),
seconds_until_idle_timeout INTEGER,
seconds_until_absolute_timeout INTEGER,
user_id INTEGER,
tenant_id INTEGER
) AS $$
DECLARE
v_session RECORD;
v_idle_timeout_minutes INTEGER := 240; -- 4 hours
v_warning_threshold_minutes INTEGER := 5;
v_idle_expires_at TIMESTAMP WITH TIME ZONE;
v_seconds_until_idle INTEGER;
v_seconds_until_absolute INTEGER;
BEGIN
-- Find the session
SELECT s.* INTO v_session
FROM sessions s
WHERE s.session_token_hash = p_token_hash
AND s.is_active = true;
-- Session not found or inactive
IF NOT FOUND THEN
RETURN QUERY SELECT
false::BOOLEAN,
NULL::VARCHAR(50),
NULL::INTEGER,
NULL::INTEGER,
NULL::INTEGER,
NULL::INTEGER;
RETURN;
END IF;
-- Calculate expiration times
v_idle_expires_at := v_session.last_activity_at + (v_idle_timeout_minutes * INTERVAL '1 minute');
-- Check absolute timeout first
IF CURRENT_TIMESTAMP >= v_session.absolute_expires_at THEN
-- Mark session as expired
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = 'absolute_timeout'
WHERE session_token_hash = p_token_hash;
RETURN QUERY SELECT
false::BOOLEAN,
'absolute'::VARCHAR(50),
NULL::INTEGER,
NULL::INTEGER,
v_session.user_id,
v_session.tenant_id;
RETURN;
END IF;
-- Check idle timeout
IF CURRENT_TIMESTAMP >= v_idle_expires_at THEN
-- Mark session as expired
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = 'idle_timeout'
WHERE session_token_hash = p_token_hash;
RETURN QUERY SELECT
false::BOOLEAN,
'idle'::VARCHAR(50),
NULL::INTEGER,
NULL::INTEGER,
v_session.user_id,
v_session.tenant_id;
RETURN;
END IF;
-- Session is valid - calculate remaining times
v_seconds_until_idle := EXTRACT(EPOCH FROM (v_idle_expires_at - CURRENT_TIMESTAMP))::INTEGER;
v_seconds_until_absolute := EXTRACT(EPOCH FROM (v_session.absolute_expires_at - CURRENT_TIMESTAMP))::INTEGER;
RETURN QUERY SELECT
true::BOOLEAN,
NULL::VARCHAR(50),
v_seconds_until_idle,
v_seconds_until_absolute,
v_session.user_id,
v_session.tenant_id;
END;
$$ LANGUAGE plpgsql;
-- Function to update session activity (called on each authenticated request)
CREATE OR REPLACE FUNCTION update_session_activity(p_token_hash VARCHAR(64))
RETURNS BOOLEAN AS $$
DECLARE
v_updated INTEGER;
BEGIN
UPDATE sessions
SET last_activity_at = CURRENT_TIMESTAMP
WHERE session_token_hash = p_token_hash
AND is_active = true;
GET DIAGNOSTICS v_updated = ROW_COUNT;
RETURN v_updated > 0;
END;
$$ LANGUAGE plpgsql;
-- Function to revoke a session
CREATE OR REPLACE FUNCTION revoke_session(p_token_hash VARCHAR(64), p_reason VARCHAR(50) DEFAULT 'logout')
RETURNS BOOLEAN AS $$
DECLARE
v_updated INTEGER;
BEGIN
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = p_reason
WHERE session_token_hash = p_token_hash
AND is_active = true;
GET DIAGNOSTICS v_updated = ROW_COUNT;
RETURN v_updated > 0;
END;
$$ LANGUAGE plpgsql;
-- Function to revoke all sessions for a user (e.g., on password change)
CREATE OR REPLACE FUNCTION revoke_all_user_sessions(p_user_id INTEGER, p_reason VARCHAR(50) DEFAULT 'password_change')
RETURNS INTEGER AS $$
DECLARE
v_updated INTEGER;
BEGIN
UPDATE sessions
SET is_active = false,
revoked_at = CURRENT_TIMESTAMP,
ended_at = CURRENT_TIMESTAMP,
revoke_reason = p_reason
WHERE user_id = p_user_id
AND is_active = true;
GET DIAGNOSTICS v_updated = ROW_COUNT;
RETURN v_updated;
END;
$$ LANGUAGE plpgsql;
-- Log migration completion
DO $$
BEGIN
RAISE NOTICE 'Migration 022: Created sessions table and session management functions for OWASP/NIST compliance';
END $$;

View File

@@ -0,0 +1,278 @@
-- Migration: 023_add_uuid_primary_key_to_model_configs.sql
-- Description: Add UUID primary key to model_configs table instead of using model_id string
-- This fixes the database design issue where model_id (a human-readable string) was used as primary key
-- Author: Claude Code
-- Date: 2025-12-08
-- ============================================================================
-- STEP 1: Ensure uuid-ossp extension is available
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================================================
-- STEP 2: Add new UUID 'id' column to model_configs
-- ============================================================================
DO $$
BEGIN
-- Check if 'id' column already exists
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'model_configs' AND column_name = 'id' AND table_schema = 'public'
) THEN
-- Add the new UUID column
ALTER TABLE model_configs ADD COLUMN id UUID DEFAULT uuid_generate_v4();
-- Populate UUIDs for all existing rows
UPDATE model_configs SET id = uuid_generate_v4() WHERE id IS NULL;
-- Make id NOT NULL
ALTER TABLE model_configs ALTER COLUMN id SET NOT NULL;
RAISE NOTICE 'Added id column to model_configs';
ELSE
RAISE NOTICE 'id column already exists in model_configs';
END IF;
END $$;
-- ============================================================================
-- STEP 3: Add new UUID 'model_config_id' column to tenant_model_configs
-- ============================================================================
DO $$
BEGIN
-- Check if 'model_config_id' column already exists
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'tenant_model_configs' AND column_name = 'model_config_id' AND table_schema = 'public'
) THEN
-- Add the new UUID column
ALTER TABLE tenant_model_configs ADD COLUMN model_config_id UUID;
RAISE NOTICE 'Added model_config_id column to tenant_model_configs';
ELSE
RAISE NOTICE 'model_config_id column already exists in tenant_model_configs';
END IF;
END $$;
-- ============================================================================
-- STEP 4: Populate model_config_id based on model_id mapping
-- ============================================================================
UPDATE tenant_model_configs tmc
SET model_config_id = mc.id
FROM model_configs mc
WHERE tmc.model_id = mc.model_id
AND tmc.model_config_id IS NULL;
-- ============================================================================
-- STEP 5: Drop the old foreign key constraint
-- ============================================================================
DO $$
BEGIN
-- Drop foreign key if it exists
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'tenant_model_configs_model_id_fkey'
AND table_name = 'tenant_model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE tenant_model_configs DROP CONSTRAINT tenant_model_configs_model_id_fkey;
RAISE NOTICE 'Dropped old foreign key constraint tenant_model_configs_model_id_fkey';
ELSE
RAISE NOTICE 'Foreign key constraint tenant_model_configs_model_id_fkey does not exist';
END IF;
END $$;
-- ============================================================================
-- STEP 6: Drop old unique constraint on (tenant_id, model_id)
-- ============================================================================
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'unique_tenant_model'
AND table_name = 'tenant_model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE tenant_model_configs DROP CONSTRAINT unique_tenant_model;
RAISE NOTICE 'Dropped old unique constraint unique_tenant_model';
ELSE
RAISE NOTICE 'Unique constraint unique_tenant_model does not exist';
END IF;
END $$;
-- ============================================================================
-- STEP 7: Drop the old primary key on model_configs.model_id
-- ============================================================================
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'model_configs_pkey'
AND constraint_type = 'PRIMARY KEY'
AND table_name = 'model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE model_configs DROP CONSTRAINT model_configs_pkey;
RAISE NOTICE 'Dropped old primary key model_configs_pkey';
ELSE
RAISE NOTICE 'Primary key model_configs_pkey does not exist';
END IF;
END $$;
-- ============================================================================
-- STEP 8: Add new primary key on model_configs.id
-- ============================================================================
DO $$
BEGIN
-- Check if primary key already exists on id column
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = 'model_configs'
AND tc.constraint_type = 'PRIMARY KEY'
AND kcu.column_name = 'id'
AND tc.table_schema = 'public'
) THEN
ALTER TABLE model_configs ADD CONSTRAINT model_configs_pkey PRIMARY KEY (id);
RAISE NOTICE 'Added new primary key on model_configs.id';
ELSE
RAISE NOTICE 'Primary key on model_configs.id already exists';
END IF;
END $$;
-- ============================================================================
-- STEP 9: Add unique constraint on model_configs.model_id
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'model_configs_model_id_unique'
AND table_name = 'model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE model_configs ADD CONSTRAINT model_configs_model_id_unique UNIQUE (model_id);
RAISE NOTICE 'Added unique constraint on model_configs.model_id';
ELSE
RAISE NOTICE 'Unique constraint on model_configs.model_id already exists';
END IF;
END $$;
-- ============================================================================
-- STEP 10: Make model_config_id NOT NULL and add foreign key
-- ============================================================================
DO $$
BEGIN
-- Make model_config_id NOT NULL (only if all values are populated)
IF EXISTS (
SELECT 1 FROM tenant_model_configs WHERE model_config_id IS NULL
) THEN
RAISE EXCEPTION 'Cannot make model_config_id NOT NULL: some values are NULL. Run the UPDATE first.';
END IF;
-- Alter column to NOT NULL
ALTER TABLE tenant_model_configs ALTER COLUMN model_config_id SET NOT NULL;
RAISE NOTICE 'Set model_config_id to NOT NULL';
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Could not set model_config_id to NOT NULL: %', SQLERRM;
END $$;
-- ============================================================================
-- STEP 11: Add foreign key from tenant_model_configs.model_config_id to model_configs.id
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'tenant_model_configs_model_config_id_fkey'
AND table_name = 'tenant_model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE tenant_model_configs
ADD CONSTRAINT tenant_model_configs_model_config_id_fkey
FOREIGN KEY (model_config_id) REFERENCES model_configs(id) ON DELETE CASCADE;
RAISE NOTICE 'Added foreign key on tenant_model_configs.model_config_id';
ELSE
RAISE NOTICE 'Foreign key tenant_model_configs_model_config_id_fkey already exists';
END IF;
END $$;
-- ============================================================================
-- STEP 12: Add new unique constraint on (tenant_id, model_config_id)
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'unique_tenant_model_config'
AND table_name = 'tenant_model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE tenant_model_configs
ADD CONSTRAINT unique_tenant_model_config UNIQUE (tenant_id, model_config_id);
RAISE NOTICE 'Added unique constraint unique_tenant_model_config';
ELSE
RAISE NOTICE 'Unique constraint unique_tenant_model_config already exists';
END IF;
END $$;
-- ============================================================================
-- STEP 13: Add index on model_configs.model_id for fast lookups
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'model_configs'
AND indexname = 'ix_model_configs_model_id'
AND schemaname = 'public'
) THEN
CREATE INDEX ix_model_configs_model_id ON model_configs(model_id);
RAISE NOTICE 'Created index ix_model_configs_model_id';
ELSE
RAISE NOTICE 'Index ix_model_configs_model_id already exists';
END IF;
END $$;
-- ============================================================================
-- STEP 14: Add index on tenant_model_configs.model_config_id
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'tenant_model_configs'
AND indexname = 'ix_tenant_model_configs_model_config_id'
AND schemaname = 'public'
) THEN
CREATE INDEX ix_tenant_model_configs_model_config_id ON tenant_model_configs(model_config_id);
RAISE NOTICE 'Created index ix_tenant_model_configs_model_config_id';
ELSE
RAISE NOTICE 'Index ix_tenant_model_configs_model_config_id already exists';
END IF;
END $$;
-- ============================================================================
-- VERIFICATION: Show final schema
-- ============================================================================
SELECT 'model_configs schema:' AS info;
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'model_configs' AND table_schema = 'public'
ORDER BY ordinal_position;
SELECT 'tenant_model_configs schema:' AS info;
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'tenant_model_configs' AND table_schema = 'public'
ORDER BY ordinal_position;
SELECT 'model_configs constraints:' AS info;
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'model_configs' AND table_schema = 'public';
SELECT 'tenant_model_configs constraints:' AS info;
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'tenant_model_configs' AND table_schema = 'public';

View File

@@ -0,0 +1,51 @@
-- Migration: 024_allow_same_model_id_different_providers.sql
-- Description: Allow same model_id with different providers
-- The unique constraint should be on (model_id, provider) not just model_id
-- This allows the same model to be registered from multiple providers (e.g., Groq and NVIDIA)
-- Author: Claude Code
-- Date: 2025-12-08
-- ============================================================================
-- STEP 1: Drop the unique constraint on model_id alone
-- ============================================================================
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'model_configs_model_id_unique'
AND table_name = 'model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE model_configs DROP CONSTRAINT model_configs_model_id_unique;
RAISE NOTICE 'Dropped unique constraint model_configs_model_id_unique';
ELSE
RAISE NOTICE 'Constraint model_configs_model_id_unique does not exist';
END IF;
END $$;
-- ============================================================================
-- STEP 2: Add new unique constraint on (model_id, provider)
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'model_configs_model_id_provider_unique'
AND table_name = 'model_configs'
AND table_schema = 'public'
) THEN
ALTER TABLE model_configs ADD CONSTRAINT model_configs_model_id_provider_unique UNIQUE (model_id, provider);
RAISE NOTICE 'Added unique constraint on (model_id, provider)';
ELSE
RAISE NOTICE 'Constraint model_configs_model_id_provider_unique already exists';
END IF;
END $$;
-- ============================================================================
-- VERIFICATION
-- ============================================================================
SELECT 'model_configs constraints after migration:' AS info;
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'model_configs' AND table_schema = 'public'
ORDER BY constraint_type, constraint_name;

View File

@@ -0,0 +1,117 @@
-- Migration 025: Fix NVIDIA model names to match API format
--
-- Problem: Model names stored with incorrect format (e.g., nvidia/meta-llama-3.1-8b-instruct)
-- Solution: Update to match NVIDIA NIM API expected format (e.g., meta/llama-3.1-8b-instruct)
--
-- NVIDIA NIM API model naming:
-- - Models from Meta: meta/llama-3.1-8b-instruct (NOT nvidia/meta-llama-*)
-- - Models from NVIDIA: nvidia/llama-3.1-nemotron-70b-instruct
-- - Models from Mistral: mistralai/mistral-large-3-instruct
-- - Models from DeepSeek: deepseek-ai/deepseek-v3
-- - Models from OpenAI-compatible: openai/gpt-oss-120b (already correct in groq provider)
-- Idempotency: Only update if old format exists
DO $$
BEGIN
-- Fix Meta Llama models (remove nvidia/ prefix for meta models)
UPDATE model_configs
SET model_id = 'meta/llama-3.1-8b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-8b-instruct' AND provider = 'nvidia';
UPDATE model_configs
SET model_id = 'meta/llama-3.1-70b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-70b-instruct' AND provider = 'nvidia';
UPDATE model_configs
SET model_id = 'meta/llama-3.1-405b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-405b-instruct' AND provider = 'nvidia';
UPDATE model_configs
SET model_id = 'meta/llama-3.3-70b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.3-70b-instruct' AND provider = 'nvidia';
-- Fix DeepSeek models
UPDATE model_configs
SET model_id = 'deepseek-ai/deepseek-v3'
WHERE model_id = 'nvidia/deepseek-ai-deepseek-v3' AND provider = 'nvidia';
UPDATE model_configs
SET model_id = 'deepseek-ai/deepseek-r1'
WHERE model_id = 'nvidia/deepseek-ai-deepseek-r1' AND provider = 'nvidia';
-- Fix Mistral models
UPDATE model_configs
SET model_id = 'mistralai/mistral-large-3-instruct'
WHERE model_id = 'nvidia/mistralai-mistral-large-3-instruct' AND provider = 'nvidia';
-- Fix Moonshot/Kimi models
UPDATE model_configs
SET model_id = 'moonshot-ai/kimi-k2-instruct'
WHERE model_id = 'nvidia/moonshot-ai-kimi-k2-instruct' AND provider = 'nvidia';
-- Fix Qwen models
UPDATE model_configs
SET model_id = 'qwen/qwen3-235b-a22b-fp8-instruct'
WHERE model_id = 'nvidia/qwen-qwen3-235b-a22b-fp8-instruct' AND provider = 'nvidia';
-- Fix OpenAI-compatible models (for NVIDIA provider)
UPDATE model_configs
SET model_id = 'openai/gpt-oss-120b'
WHERE model_id = 'nvidia/openai-gpt-oss-120b' AND provider = 'nvidia';
UPDATE model_configs
SET model_id = 'openai/gpt-oss-20b'
WHERE model_id = 'nvidia/openai-gpt-oss-20b' AND provider = 'nvidia';
-- Also update tenant_model_configs to match (if they reference old model_ids)
UPDATE tenant_model_configs
SET model_id = 'meta/llama-3.1-8b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-8b-instruct';
UPDATE tenant_model_configs
SET model_id = 'meta/llama-3.1-70b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-70b-instruct';
UPDATE tenant_model_configs
SET model_id = 'meta/llama-3.1-405b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.1-405b-instruct';
UPDATE tenant_model_configs
SET model_id = 'meta/llama-3.3-70b-instruct'
WHERE model_id = 'nvidia/meta-llama-3.3-70b-instruct';
UPDATE tenant_model_configs
SET model_id = 'deepseek-ai/deepseek-v3'
WHERE model_id = 'nvidia/deepseek-ai-deepseek-v3';
UPDATE tenant_model_configs
SET model_id = 'deepseek-ai/deepseek-r1'
WHERE model_id = 'nvidia/deepseek-ai-deepseek-r1';
UPDATE tenant_model_configs
SET model_id = 'mistralai/mistral-large-3-instruct'
WHERE model_id = 'nvidia/mistralai-mistral-large-3-instruct';
UPDATE tenant_model_configs
SET model_id = 'moonshot-ai/kimi-k2-instruct'
WHERE model_id = 'nvidia/moonshot-ai-kimi-k2-instruct';
UPDATE tenant_model_configs
SET model_id = 'qwen/qwen3-235b-a22b-fp8-instruct'
WHERE model_id = 'nvidia/qwen-qwen3-235b-a22b-fp8-instruct';
UPDATE tenant_model_configs
SET model_id = 'openai/gpt-oss-120b'
WHERE model_id = 'nvidia/openai-gpt-oss-120b';
UPDATE tenant_model_configs
SET model_id = 'openai/gpt-oss-20b'
WHERE model_id = 'nvidia/openai-gpt-oss-20b';
RAISE NOTICE 'Migration 025: Fixed NVIDIA model names to match API format';
END $$;
-- Log migration completion
INSERT INTO system_versions (version, component, description, applied_at)
VALUES ('025', 'model_configs', 'Fixed NVIDIA model names to match API format', NOW())
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,59 @@
-- Migration 026: Fix NVIDIA model_ids to exact NVIDIA NIM API format
--
-- Verified against docs.api.nvidia.com and build.nvidia.com (December 2025)
--
-- Issues found:
-- 1. moonshot-ai/kimi-k2-instruct -> should be moonshotai/kimi-k2-instruct (no hyphen)
-- 2. mistralai/mistral-large-3-instruct -> model doesn't exist, should be mistralai/mistral-large
-- 3. deepseek-ai/deepseek-v3 -> model doesn't exist on NVIDIA, should be deepseek-ai/deepseek-v3.1
-- 4. qwen/qwen3-235b-a22b-fp8-instruct -> should be qwen/qwen3-235b-a22b (no fp8-instruct suffix)
--
-- Note: These are the model_id strings passed to NVIDIA's API, not the names shown to users
DO $$
BEGIN
-- Fix Kimi K2: moonshot-ai -> moonshotai (NVIDIA uses no hyphen)
UPDATE model_configs
SET model_id = 'moonshotai/kimi-k2-instruct'
WHERE model_id = 'moonshot-ai/kimi-k2-instruct' AND provider = 'nvidia';
-- Fix Mistral Large 3: Use the correct model name from NVIDIA
-- The full name is mistralai/mistral-large or mistralai/mistral-large-3-675b-instruct-2512
UPDATE model_configs
SET model_id = 'mistralai/mistral-large'
WHERE model_id = 'mistralai/mistral-large-3-instruct' AND provider = 'nvidia';
-- Fix DeepSeek V3: NVIDIA only has v3.1, not plain v3
UPDATE model_configs
SET model_id = 'deepseek-ai/deepseek-v3.1'
WHERE model_id = 'deepseek-ai/deepseek-v3' AND provider = 'nvidia';
-- Fix Qwen 3 235B: Remove fp8-instruct suffix
UPDATE model_configs
SET model_id = 'qwen/qwen3-235b-a22b'
WHERE model_id = 'qwen/qwen3-235b-a22b-fp8-instruct' AND provider = 'nvidia';
-- Also update tenant_model_configs to match
UPDATE tenant_model_configs
SET model_id = 'moonshotai/kimi-k2-instruct'
WHERE model_id = 'moonshot-ai/kimi-k2-instruct';
UPDATE tenant_model_configs
SET model_id = 'mistralai/mistral-large'
WHERE model_id = 'mistralai/mistral-large-3-instruct';
UPDATE tenant_model_configs
SET model_id = 'deepseek-ai/deepseek-v3.1'
WHERE model_id = 'deepseek-ai/deepseek-v3';
UPDATE tenant_model_configs
SET model_id = 'qwen/qwen3-235b-a22b'
WHERE model_id = 'qwen/qwen3-235b-a22b-fp8-instruct';
RAISE NOTICE 'Migration 026: Fixed NVIDIA model_ids to match exact API format';
END $$;
-- Log migration completion
INSERT INTO system_versions (version, component, description, applied_at)
VALUES ('026', 'model_configs', 'Fixed NVIDIA model_ids to exact API format', NOW())
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,35 @@
-- Migration: 027_assign_nvidia_models_to_tenants.sql
-- Description: Ensure NVIDIA models are assigned to all tenants (fix for partial 021 migration)
-- Date: 2025-12-08
-- Issue: Deploy.sh updates add models but don't assign to existing tenants
-- Assign NVIDIA models to all existing tenants with 1000 RPM rate limits
-- This is idempotent - ON CONFLICT DO NOTHING means it won't duplicate
INSERT INTO tenant_model_configs (tenant_id, model_config_id, model_id, is_enabled, priority, rate_limits, created_at, updated_at)
SELECT
t.id,
m.id, -- UUID foreign key (auto-generated in model_configs)
m.model_id, -- String identifier (kept for easier queries)
true,
5,
'{"max_requests_per_hour": 1000, "max_tokens_per_request": 4000, "concurrent_requests": 5, "max_cost_per_hour": 10.0, "requests_per_minute": 1000, "tokens_per_minute": 100000, "max_concurrent": 10}'::json,
NOW(),
NOW()
FROM tenants t
CROSS JOIN model_configs m
WHERE m.provider = 'nvidia'
AND m.is_active = true
ON CONFLICT (tenant_id, model_config_id) DO NOTHING;
-- Log migration completion
DO $$
DECLARE
assigned_count INTEGER;
BEGIN
SELECT COUNT(*) INTO assigned_count
FROM tenant_model_configs tmc
JOIN model_configs mc ON mc.id = tmc.model_config_id
WHERE mc.provider = 'nvidia';
RAISE NOTICE 'Migration 027: Ensured NVIDIA models are assigned to all tenants (% total assignments)', assigned_count;
END $$;