-- ============ LLM Endpoints ============ CREATE TABLE llm_endpoints ( id UUID PRIMARY KEY, provider TEXT NOT NULL CHECK (provider IN ('anthropic','openai','gemini')), display_name TEXT NOT NULL, base_url TEXT NOT NULL, api_key_encrypted BYTEA NOT NULL, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX llm_endpoints_provider_idx ON llm_endpoints(provider); -- ============ LLM Models ============ CREATE TABLE llm_models ( id UUID PRIMARY KEY, endpoint_id UUID NOT NULL REFERENCES llm_endpoints(id) ON DELETE RESTRICT, model_id TEXT NOT NULL, display_name TEXT NOT NULL, capabilities JSONB NOT NULL DEFAULT '{}'::jsonb, context_window INT NOT NULL, max_output_tokens INT NOT NULL, prompt_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0, completion_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0, thinking_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0, is_title_generator BOOLEAN NOT NULL DEFAULT false, enabled BOOLEAN NOT NULL DEFAULT true, sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (endpoint_id, model_id) ); CREATE UNIQUE INDEX llm_models_one_title_generator ON llm_models ((1)) WHERE is_title_generator; CREATE INDEX llm_models_enabled_sort_idx ON llm_models(enabled, sort_order); -- ============ Prompt Templates ============ CREATE TABLE prompt_templates ( id UUID PRIMARY KEY, name TEXT NOT NULL, content TEXT NOT NULL, scope TEXT NOT NULL CHECK (scope IN ('system','user')), owner_id UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CHECK ((scope='user' AND owner_id IS NOT NULL) OR (scope='system' AND owner_id IS NULL)) ); CREATE INDEX prompt_templates_scope_owner_idx ON prompt_templates(scope, owner_id); -- ============ Conversations ============ CREATE TABLE conversations ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id), project_id UUID REFERENCES projects(id), workspace_id UUID REFERENCES workspaces(id), issue_id UUID REFERENCES issues(id), model_id UUID NOT NULL REFERENCES llm_models(id) ON DELETE RESTRICT, system_prompt TEXT NOT NULL DEFAULT '', title TEXT, title_generated_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX conversations_user_list_idx ON conversations(user_id, deleted_at, updated_at DESC); CREATE INDEX conversations_project_idx ON conversations(project_id) WHERE project_id IS NOT NULL; CREATE INDEX conversations_workspace_idx ON conversations(workspace_id) WHERE workspace_id IS NOT NULL; CREATE INDEX conversations_issue_idx ON conversations(issue_id) WHERE issue_id IS NOT NULL; -- ============ Messages ============ CREATE TABLE messages ( id BIGSERIAL PRIMARY KEY, conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK (role IN ('user','assistant')), content TEXT NOT NULL DEFAULT '', thinking TEXT, tool_calls JSONB, status TEXT NOT NULL CHECK (status IN ('pending','ok','error','cancelled')), error_message TEXT, prompt_tokens INT, completion_tokens INT, thinking_tokens INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX messages_conv_id_idx ON messages(conversation_id, id); CREATE UNIQUE INDEX messages_one_pending_per_conv ON messages(conversation_id) WHERE status='pending'; -- ============ Message Attachments ============ CREATE TABLE message_attachments ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id), message_id BIGINT REFERENCES messages(id) ON DELETE CASCADE, filename TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes BIGINT NOT NULL, sha256 TEXT NOT NULL, storage_path TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX message_attachments_orphan_idx ON message_attachments(user_id, created_at) WHERE message_id IS NULL; CREATE INDEX message_attachments_msg_idx ON message_attachments(message_id) WHERE message_id IS NOT NULL; -- ============ LLM Usage ============ CREATE TABLE llm_usage ( id BIGSERIAL PRIMARY KEY, conversation_id UUID NOT NULL, message_id BIGINT NOT NULL, user_id UUID NOT NULL, endpoint_id UUID NOT NULL, model_id UUID NOT NULL, prompt_tokens INT NOT NULL, completion_tokens INT NOT NULL, thinking_tokens INT NOT NULL DEFAULT 0, cost_usd NUMERIC(10,6) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX llm_usage_user_time_idx ON llm_usage(user_id, created_at); CREATE INDEX llm_usage_model_time_idx ON llm_usage(model_id, created_at); CREATE INDEX llm_usage_time_idx ON llm_usage(created_at);