From dfe73b3647596c07b4b43ee0f891f5786e084489 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 7 Jul 2026 14:07:47 -0500 Subject: [PATCH] =?UTF-8?q?feat(automation):=20complete=20model=20provider?= =?UTF-8?q?=20registry=20epic=20(tasks=203=E2=80=936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ai.models tRPC router with provider CRUD, role routes, testProvider, and resolveForRole; Vercel AI SDK client factory in packages/ai; Settings pages for providers and role routing; agent_runs model metadata columns with MCP complete_task fields and runs UI column. Co-authored-by: Cursor --- apps/mcp-server/src/tools/complete-task.ts | 22 + .../settings/ai/providers/page.tsx | 419 ++ .../settings/ai/role-routing/page.tsx | 260 ++ .../[workspaceSlug]/settings/runs/page.tsx | 20 +- apps/web/components/layout/top-header.tsx | 19 + .../components/settings/settings-ai-nav.tsx | 39 + apps/web/server/routers/ai-models.ts | 533 +++ apps/web/server/routers/ai.ts | 3 + apps/web/server/routers/runs.ts | 8 + packages/ai/package.json | 1 + packages/ai/src/index.ts | 4 + packages/ai/src/providers/resolve-client.ts | 48 + .../migrations/0011_cool_captain_america.sql | 4 + .../migrations/meta/0011_snapshot.json | 3375 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + packages/database/src/schema/agent_runs.ts | 4 + .../Epic-model-provider-registry.md | 2 +- .../Task-agent-runs-model-logging.md | 2 +- .../Task-settings-ai-providers-ui.md | 2 +- .../Task-settings-ai-role-routing-ui.md | 2 +- .../Task-trpc-models-router.md | 2 +- pnpm-lock.yaml | 15 + 22 files changed, 4785 insertions(+), 6 deletions(-) create mode 100644 apps/web/app/(app)/[workspaceSlug]/settings/ai/providers/page.tsx create mode 100644 apps/web/app/(app)/[workspaceSlug]/settings/ai/role-routing/page.tsx create mode 100644 apps/web/components/settings/settings-ai-nav.tsx create mode 100644 apps/web/server/routers/ai-models.ts create mode 100644 packages/ai/src/providers/resolve-client.ts create mode 100644 packages/database/migrations/0011_cool_captain_america.sql create mode 100644 packages/database/migrations/meta/0011_snapshot.json diff --git a/apps/mcp-server/src/tools/complete-task.ts b/apps/mcp-server/src/tools/complete-task.ts index 1b49672..1ed44a3 100644 --- a/apps/mcp-server/src/tools/complete-task.ts +++ b/apps/mcp-server/src/tools/complete-task.ts @@ -1,5 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; +import { modelTierSchema, providerTypeSchema } from "@tasks/shared/model-routing"; import { db } from "../db.js"; import { eq } from "../drizzle.js"; import { agentRuns, auditLog, markdownBacklogItems } from "../schema.js"; @@ -44,6 +45,15 @@ const completeTaskInputSchema = z.object({ .describe( "Late-bound model handle. Same merge semantics as `client` — pass this when the model that closed the session differs from the one that opened it.", ), + modelAlias: z + .string() + .min(1) + .max(120) + .optional() + .describe("Registry alias used for this session (e.g. qwen-coder)."), + modelTier: modelTierSchema.optional(), + providerType: providerTypeSchema.optional(), + modelId: z.string().min(1).max(200).optional(), }); type CompleteTaskInput = z.infer; @@ -115,6 +125,10 @@ export function registerCompleteTaskTool(mcp: McpServer): void { const incomingMeta: Record = {}; if (input.client) incomingMeta.client = input.client; if (input.model) incomingMeta.model = input.model; + if (input.modelAlias) incomingMeta.modelAlias = input.modelAlias; + if (input.modelTier) incomingMeta.modelTier = input.modelTier; + if (input.providerType) incomingMeta.providerType = input.providerType; + if (input.modelId) incomingMeta.modelId = input.modelId; const hasIncomingMeta = Object.keys(incomingMeta).length > 0; const mergedMeta = hasIncomingMeta ? { ...(run.metadata ?? {}), ...incomingMeta } @@ -131,6 +145,14 @@ export function registerCompleteTaskTool(mcp: McpServer): void { tokensTotal, notes: input.notes ?? null, error: input.error ?? null, + ...(input.modelAlias !== undefined + ? { modelAlias: input.modelAlias } + : {}), + ...(input.modelTier !== undefined ? { modelTier: input.modelTier } : {}), + ...(input.providerType !== undefined + ? { providerType: input.providerType } + : {}), + ...(input.modelId !== undefined ? { modelId: input.modelId } : {}), ...(hasIncomingMeta ? { metadata: mergedMeta } : {}), }) .where(eq(agentRuns.id, input.runId)); diff --git a/apps/web/app/(app)/[workspaceSlug]/settings/ai/providers/page.tsx b/apps/web/app/(app)/[workspaceSlug]/settings/ai/providers/page.tsx new file mode 100644 index 0000000..f83f8b4 --- /dev/null +++ b/apps/web/app/(app)/[workspaceSlug]/settings/ai/providers/page.tsx @@ -0,0 +1,419 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Bot, Loader2, Plus, Sparkles, X } from "lucide-react"; +import { useParams } from "next/navigation"; + +import { SettingsAiNav } from "@/components/settings/settings-ai-nav"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { api } from "@/lib/trpc"; +import type { ProviderType } from "@tasks/shared/model-routing"; + +const PROVIDER_TYPES: ProviderType[] = [ + "anthropic", + "openai", + "google", + "openai_compatible", + "azure_openai", +]; + +type ProviderForm = { + alias: string; + displayName: string; + providerType: ProviderType; + modelId: string; + baseUrl: string; + apiKeyEnvVar: string; + enabled: boolean; +}; + +const EMPTY_FORM: ProviderForm = { + alias: "", + displayName: "", + providerType: "openai_compatible", + modelId: "", + baseUrl: "", + apiKeyEnvVar: "", + enabled: true, +}; + +const PRESETS: Array<{ label: string; form: Partial }> = [ + { + label: "Ollama local", + form: { + alias: "qwen-coder", + displayName: "Qwen Coder (Ollama)", + providerType: "openai_compatible", + modelId: "qwen2.5-coder:32b", + baseUrl: "http://127.0.0.1:11434/v1", + apiKeyEnvVar: "OLLAMA_API_KEY", + }, + }, + { + label: "MLX local", + form: { + alias: "qwen-coder-mlx", + displayName: "Qwen Coder (MLX)", + providerType: "openai_compatible", + modelId: "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit", + baseUrl: "http://127.0.0.1:8000/v1", + apiKeyEnvVar: "MLX_API_KEY", + }, + }, + { + label: "Anthropic", + form: { + alias: "claude-planning", + displayName: "Claude Planning", + providerType: "anthropic", + modelId: "claude-sonnet-4-20250514", + apiKeyEnvVar: "ANTHROPIC_API_KEY", + }, + }, + { + label: "Gemini", + form: { + alias: "gemini-flash", + displayName: "Gemini Flash", + providerType: "google", + modelId: "gemini-2.0-flash", + apiKeyEnvVar: "GOOGLE_GENERATIVE_AI_API_KEY", + }, + }, + { + label: "NVIDIA NIM", + form: { + alias: "nim-llama-70b", + displayName: "NIM Llama 70B", + providerType: "openai_compatible", + modelId: "meta/llama-3.1-70b-instruct", + baseUrl: "https://integrate.api.nvidia.com/v1", + apiKeyEnvVar: "NVIDIA_API_KEY", + }, + }, +]; + +export default function AiProvidersSettingsPage() { + const params = useParams(); + const workspaceSlug = params?.workspaceSlug as string | undefined; + const utils = api.useUtils(); + + const listQuery = api.ai.models.listProviders.useQuery( + { workspace: workspaceSlug ?? "" }, + { enabled: Boolean(workspaceSlug) }, + ); + + const [dialogOpen, setDialogOpen] = React.useState(false); + const [editingId, setEditingId] = React.useState(null); + const [form, setForm] = React.useState(EMPTY_FORM); + const [testResult, setTestResult] = React.useState>({}); + + const createMut = api.ai.models.createProvider.useMutation({ + onSuccess: () => { + void utils.ai.models.listProviders.invalidate(); + setDialogOpen(false); + }, + }); + const updateMut = api.ai.models.updateProvider.useMutation({ + onSuccess: () => { + void utils.ai.models.listProviders.invalidate(); + setDialogOpen(false); + }, + }); + const deleteMut = api.ai.models.deleteProvider.useMutation({ + onSuccess: () => void utils.ai.models.listProviders.invalidate(), + }); + const testMut = api.ai.models.testProvider.useMutation({ + onSuccess: (result, vars) => { + setTestResult((prev) => ({ + ...prev, + [vars.providerId]: result.ok + ? `OK in ${result.latencyMs}ms` + : `Failed: ${result.error ?? "unknown"}`, + })); + }, + }); + + const canManage = listQuery.data?.canManage ?? false; + const providers = listQuery.data?.providers ?? []; + + const openCreate = () => { + setEditingId(null); + setForm(EMPTY_FORM); + setDialogOpen(true); + }; + + const openEdit = (p: (typeof providers)[number]) => { + setEditingId(p.id); + setForm({ + alias: p.alias, + displayName: p.displayName, + providerType: p.providerType, + modelId: p.modelId, + baseUrl: p.baseUrl ?? "", + apiKeyEnvVar: p.apiKeyEnvVar ?? "", + enabled: p.enabled, + }); + setDialogOpen(true); + }; + + const submit = () => { + if (!workspaceSlug) return; + const payload = { + workspace: workspaceSlug, + alias: form.alias, + displayName: form.displayName, + providerType: form.providerType, + modelId: form.modelId, + baseUrl: form.baseUrl.trim() || null, + apiKeyEnvVar: form.apiKeyEnvVar.trim() || null, + enabled: form.enabled, + }; + if (editingId) { + updateMut.mutate({ ...payload, id: editingId }); + } else { + createMut.mutate(payload); + } + }; + + if (!workspaceSlug) { + return

No workspace selected.

; + } + + return ( +
+
+
+
+ +
+
+

AI model providers

+

+ Registry aliases for orchestrator and pipeline roles. API keys live in server env + vars only. +

+
+
+ {canManage ? ( + + ) : null} +
+ + + + {listQuery.isLoading ? ( +
+ +
+ ) : listQuery.error ? ( +

{listQuery.error.message}

+ ) : providers.length === 0 ? ( +
+ + No providers yet.{" "} + {canManage ? "Add Ollama, MLX, or cloud aliases to get started." : null} +
+ ) : ( +
+ + + + + + + + + {canManage ? ( + + ) : null} + + + + {providers.map((p) => ( + + + + + + + {canManage ? ( + + ) : null} + + ))} + +
AliasTypeModelBase URLOnActions
+
{p.alias}
+
{p.displayName}
+
{p.providerType} + {p.modelId} + + {p.baseUrl ?? "—"} + {p.enabled ? "yes" : "no"} + + + + {testResult[p.id] ? ( +
+ {testResult[p.id]} +
+ ) : null} +
+
+ )} + + + + + +
+ + {editingId ? "Edit provider" : "Add provider"} + + + + +
+ +
+ {PRESETS.map((preset) => ( + + ))} +
+ +
+ + setForm((f) => ({ ...f, alias: e.target.value }))} + /> + + + setForm((f) => ({ ...f, displayName: e.target.value }))} + /> + + + + + + setForm((f) => ({ ...f, modelId: e.target.value }))} + /> + + {form.providerType === "openai_compatible" || + form.providerType === "azure_openai" ? ( + + setForm((f) => ({ ...f, baseUrl: e.target.value }))} + /> + + ) : null} + + setForm((f) => ({ ...f, apiKeyEnvVar: e.target.value }))} + /> +

+ Secret value is read from the server environment at inference time — never stored + in the database. +

+
+ +
+ +
+ + +
+
+
+
+
+ ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} diff --git a/apps/web/app/(app)/[workspaceSlug]/settings/ai/role-routing/page.tsx b/apps/web/app/(app)/[workspaceSlug]/settings/ai/role-routing/page.tsx new file mode 100644 index 0000000..d4c2968 --- /dev/null +++ b/apps/web/app/(app)/[workspaceSlug]/settings/ai/role-routing/page.tsx @@ -0,0 +1,260 @@ +"use client"; + +import * as React from "react"; +import { GitBranch, Loader2, Plus, Route } from "lucide-react"; +import { useParams } from "next/navigation"; + +import { SettingsAiNav } from "@/components/settings/settings-ai-nav"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { api } from "@/lib/trpc"; + +const ROLE_LABELS: Record = { + "role-conductor": "Conductor", + "role-architect": "Architect", + "role-ia-architect": "IA architect", + "role-ux-reviewer": "UX reviewer", + "role-implementer": "Implementer", + "role-reviewer": "Reviewer", + "role-design-system-auditor": "Design system auditor", + "role-a11y-auditor": "A11y auditor", + "role-doc-writer": "Doc writer", + "gate-copilot": "Gate co-pilot", + "chat-default": "Chat default", +}; + +function roleLabel(roleId: string): string { + return ROLE_LABELS[roleId] ?? roleId; +} + +function readTier(metadata: unknown): string | null { + if (!metadata || typeof metadata !== "object") return null; + const tier = (metadata as { tier?: unknown }).tier; + return typeof tier === "string" ? tier : null; +} + +export default function AiRoleRoutingSettingsPage() { + const params = useParams(); + const workspaceSlug = params?.workspaceSlug as string | undefined; + const utils = api.useUtils(); + + const routesQuery = api.ai.models.listRoleRoutes.useQuery( + { workspace: workspaceSlug ?? "" }, + { enabled: Boolean(workspaceSlug) }, + ); + const providersQuery = api.ai.models.listProviders.useQuery( + { workspace: workspaceSlug ?? "" }, + { enabled: Boolean(workspaceSlug) }, + ); + + const [newRoleId, setNewRoleId] = React.useState(""); + const [newProviderId, setNewProviderId] = React.useState(""); + + const upsertMut = api.ai.models.upsertRoleRoute.useMutation({ + onSuccess: () => { + void utils.ai.models.listRoleRoutes.invalidate(); + setNewRoleId(""); + setNewProviderId(""); + }, + }); + const deleteMut = api.ai.models.deleteRoleRoute.useMutation({ + onSuccess: () => void utils.ai.models.listRoleRoutes.invalidate(), + }); + const resetMut = api.ai.models.resetPipelineDefaults.useMutation({ + onSuccess: () => void utils.ai.models.listRoleRoutes.invalidate(), + }); + + const canManage = routesQuery.data?.canManage ?? false; + const routes = routesQuery.data?.routes ?? []; + const enabledProviders = + providersQuery.data?.providers.filter((p) => p.enabled) ?? []; + + if (!workspaceSlug) { + return

No workspace selected.

; + } + + return ( +
+
+
+
+ +
+
+

Role model routing

+

+ Map pipeline role_id strings to provider aliases. + Convoy model_policy overrides these at dispatch + time — see docs/adr-model-routing.md. +

+
+
+ {canManage ? ( + + ) : null} +
+ + + + {routesQuery.isLoading ? ( +
+ +
+ ) : ( +
+ + + + + + + + {canManage ? ( + + ) : null} + + + + {routes.length === 0 ? ( + + + + ) : ( + routes.map((route) => { + const tier = readTier(route.providerMetadata); + return ( + + + + + + {canManage ? ( + + ) : null} + + ); + }) + )} + +
RoleProvider aliasTierOnActions
+ No routes configured. Add providers first, then map roles. +
+
{roleLabel(route.roleId)}
+
+ {route.roleId} +
+
{route.providerAlias} + {tier ? ( + + {tier} + + ) : ( + "—" + )} + + {route.enabled && route.providerEnabled ? "yes" : "no"} + + + +
+
+ )} + + {canManage ? ( +
+
+ + Add custom role +
+
+
+ + setNewRoleId(e.target.value)} + /> +
+
+ + +
+ +
+
+ ) : null} +
+ ); +} diff --git a/apps/web/app/(app)/[workspaceSlug]/settings/runs/page.tsx b/apps/web/app/(app)/[workspaceSlug]/settings/runs/page.tsx index f23b2f2..9d45c60 100644 --- a/apps/web/app/(app)/[workspaceSlug]/settings/runs/page.tsx +++ b/apps/web/app/(app)/[workspaceSlug]/settings/runs/page.tsx @@ -105,6 +105,19 @@ function describeOutcome( * null and ignore values that aren't strings (defensive against * hand-inserted rows from psql). */ +function describeModel(row: { + modelAlias: string | null; + modelTier: string | null; + modelId: string | null; + metadata: unknown; +}): string | null { + if (row.modelAlias) { + const tier = row.modelTier ? ` (${row.modelTier})` : ""; + return `${row.modelAlias}${tier}`; + } + return describeAgent(row.metadata).label; +} + function describeAgent(metadata: unknown): { client: string | null; model: string | null; @@ -221,6 +234,7 @@ export default function RunsPage() { Started Duration Actor + Model Outcome Tokens @@ -229,6 +243,7 @@ export default function RunsPage() { {runsQuery.data.rows.map((row) => { const { label, key } = describeOutcome(row.outcome, row.finishedAt); const agent = describeAgent(row.metadata); + const modelLabel = describeModel(row); const isOpen = expanded === row.id; const hasDetail = Boolean(row.notes || row.error); return ( @@ -289,6 +304,9 @@ export default function RunsPage() { ) : null} + + {modelLabel ?? "—"} + - + {row.notes ? (
diff --git a/apps/web/components/layout/top-header.tsx b/apps/web/components/layout/top-header.tsx index 3a71207..6f67233 100644 --- a/apps/web/components/layout/top-header.tsx +++ b/apps/web/components/layout/top-header.tsx @@ -19,6 +19,7 @@ import { ScrollText, Search, Settings, + Sparkles, User, Users, Zap, @@ -144,6 +145,24 @@ export function TopHeader({ onOpenSearch, onQuickAction }: TopHeaderProps) { Audit log + + workspaceId && router.push(`/${workspaceId}/settings/ai/providers`) + } + > + + AI providers + + + workspaceId && router.push(`/${workspaceId}/settings/ai/role-routing`) + } + > + + Role routing + diff --git a/apps/web/components/settings/settings-ai-nav.tsx b/apps/web/components/settings/settings-ai-nav.tsx new file mode 100644 index 0000000..4156c08 --- /dev/null +++ b/apps/web/components/settings/settings-ai-nav.tsx @@ -0,0 +1,39 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { cn } from "@/lib/utils"; + +const LINKS = [ + { segment: "providers", label: "Providers" }, + { segment: "role-routing", label: "Role routing" }, +] as const; + +export function SettingsAiNav({ workspaceSlug }: { workspaceSlug: string }) { + const pathname = usePathname(); + const base = `/${workspaceSlug}/settings/ai`; + + return ( + + ); +} diff --git a/apps/web/server/routers/ai-models.ts b/apps/web/server/routers/ai-models.ts new file mode 100644 index 0000000..7e5def3 --- /dev/null +++ b/apps/web/server/routers/ai-models.ts @@ -0,0 +1,533 @@ +import { createLanguageModelFromResolved } from "@tasks/ai"; +import { seedDefaultRoleRoutes } from "@tasks/database/model-routing"; +import { + modelProviders, + roleModelRoutes, +} from "@tasks/database/schema"; +import { + convoyModelPolicySchema, + providerTypeSchema, + resolveModel, + type ModelProvider, + type RoleModelRoute, +} from "@tasks/shared/model-routing"; +import { TRPCError } from "@trpc/server"; +import { generateText } from "ai"; +import { and, asc, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { rateLimit } from "@/server/lib/rate-limit"; +import { router, workspaceProcedure } from "@/server/trpc"; +import type { WorkspaceProcedureContext } from "@/server/trpc"; + +type ProviderRow = typeof modelProviders.$inferSelect; +type RouteRow = typeof roleModelRoutes.$inferSelect; + +function assertWorkspaceAdmin(ctx: WorkspaceProcedureContext): void { + if (ctx.workspace.role !== "owner" && ctx.workspace.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only workspace owners and admins can manage model providers.", + }); + } +} + +function canManageWorkspace(ctx: WorkspaceProcedureContext): boolean { + return ctx.workspace.role === "owner" || ctx.workspace.role === "admin"; +} + +function mapProviderRow(row: ProviderRow): ModelProvider { + return { + id: row.id, + workspaceId: row.workspaceId, + alias: row.alias, + displayName: row.displayName, + providerType: providerTypeSchema.parse(row.providerType), + modelId: row.modelId, + baseUrl: row.baseUrl, + apiKeyEnvVar: row.apiKeyEnvVar, + extraHeaders: row.extraHeaders, + defaultParams: row.defaultParams, + metadata: row.metadata, + enabled: row.enabled, + }; +} + +function mapRouteRow(row: RouteRow): RoleModelRoute { + return { + id: row.id, + workspaceId: row.workspaceId, + roleId: row.roleId, + providerAliasId: row.providerAliasId, + enabled: row.enabled, + sortOrder: row.sortOrder, + }; +} + +function readApiKey(envVarName: string): string | undefined { + return process.env[envVarName]?.trim() || undefined; +} + +function validateProviderFields(input: { + providerType: z.infer; + baseUrl?: string | null; +}): void { + if (input.providerType === "openai_compatible" && !input.baseUrl?.trim()) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "baseUrl is required for openai_compatible providers.", + }); + } +} + +function providerToResolved(row: ProviderRow): ReturnType { + const provider = mapProviderRow(row); + const result = resolveModel({ + roleId: "test", + providers: [provider], + routes: [], + briefRecommendedModel: provider.alias, + getApiKey: readApiKey, + }); + return result; +} + +const providerFieldsSchema = z.object({ + alias: z.string().min(1).max(120), + displayName: z.string().min(1).max(200), + providerType: providerTypeSchema, + modelId: z.string().min(1).max(200), + baseUrl: z.string().url().nullable().optional(), + apiKeyEnvVar: z.string().min(1).max(120).nullable().optional(), + extraHeaders: z.record(z.string()).nullable().optional(), + defaultParams: z + .object({ + temperature: z.number().min(0).max(2).optional(), + maxTokens: z.number().int().positive().optional(), + }) + .nullable() + .optional(), + metadata: z.record(z.unknown()).nullable().optional(), + enabled: z.boolean().optional(), +}); + +export const aiModelsRouter = router({ + listProviders: workspaceProcedure.query(async ({ ctx }) => { + const rows = await ctx.db + .select() + .from(modelProviders) + .where(eq(modelProviders.workspaceId, ctx.workspace.id)) + .orderBy(asc(modelProviders.alias)); + + return { + providers: rows.map(mapProviderRow), + canManage: canManageWorkspace(ctx), + }; + }), + + createProvider: workspaceProcedure + .input(providerFieldsSchema) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + validateProviderFields(input); + + try { + const [created] = await ctx.db + .insert(modelProviders) + .values({ + workspaceId: ctx.workspace.id, + alias: input.alias.trim(), + displayName: input.displayName.trim(), + providerType: input.providerType, + modelId: input.modelId.trim(), + baseUrl: input.baseUrl?.trim() || null, + apiKeyEnvVar: input.apiKeyEnvVar?.trim() || null, + extraHeaders: input.extraHeaders ?? null, + defaultParams: input.defaultParams ?? null, + metadata: input.metadata ?? null, + enabled: input.enabled ?? true, + updatedAt: new Date(), + }) + .returning(); + + if (!created) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create provider.", + }); + } + + return mapProviderRow(created); + } catch (e) { + if (isUniqueViolation(e)) { + throw new TRPCError({ + code: "CONFLICT", + message: `Alias "${input.alias}" already exists in this workspace.`, + }); + } + throw e; + } + }), + + updateProvider: workspaceProcedure + .input( + providerFieldsSchema.partial().extend({ + id: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const [existing] = await ctx.db + .select() + .from(modelProviders) + .where( + and( + eq(modelProviders.id, input.id), + eq(modelProviders.workspaceId, ctx.workspace.id), + ), + ) + .limit(1); + + if (!existing) { + throw new TRPCError({ code: "NOT_FOUND", message: "Provider not found." }); + } + + const nextType = input.providerType ?? existing.providerType; + const nextBaseUrl = + input.baseUrl !== undefined ? input.baseUrl : existing.baseUrl; + validateProviderFields({ + providerType: providerTypeSchema.parse(nextType), + baseUrl: nextBaseUrl, + }); + + try { + const [updated] = await ctx.db + .update(modelProviders) + .set({ + ...(input.alias !== undefined ? { alias: input.alias.trim() } : {}), + ...(input.displayName !== undefined + ? { displayName: input.displayName.trim() } + : {}), + ...(input.providerType !== undefined + ? { providerType: input.providerType } + : {}), + ...(input.modelId !== undefined ? { modelId: input.modelId.trim() } : {}), + ...(input.baseUrl !== undefined + ? { baseUrl: input.baseUrl?.trim() || null } + : {}), + ...(input.apiKeyEnvVar !== undefined + ? { apiKeyEnvVar: input.apiKeyEnvVar?.trim() || null } + : {}), + ...(input.extraHeaders !== undefined + ? { extraHeaders: input.extraHeaders } + : {}), + ...(input.defaultParams !== undefined + ? { defaultParams: input.defaultParams } + : {}), + ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), + ...(input.enabled !== undefined ? { enabled: input.enabled } : {}), + updatedAt: new Date(), + }) + .where( + and( + eq(modelProviders.id, input.id), + eq(modelProviders.workspaceId, ctx.workspace.id), + ), + ) + .returning(); + + if (!updated) { + throw new TRPCError({ code: "NOT_FOUND", message: "Provider not found." }); + } + + return mapProviderRow(updated); + } catch (e) { + if (isUniqueViolation(e)) { + throw new TRPCError({ + code: "CONFLICT", + message: "Another provider already uses that alias.", + }); + } + throw e; + } + }), + + deleteProvider: workspaceProcedure + .input(z.object({ id: z.string().uuid() })) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + const [deleted] = await ctx.db + .delete(modelProviders) + .where( + and( + eq(modelProviders.id, input.id), + eq(modelProviders.workspaceId, ctx.workspace.id), + ), + ) + .returning({ id: modelProviders.id }); + + if (!deleted) { + throw new TRPCError({ code: "NOT_FOUND", message: "Provider not found." }); + } + + return { id: deleted.id }; + }), + + testProvider: workspaceProcedure + .input(z.object({ providerId: z.string().uuid() })) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const limit = rateLimit({ + key: `ai:testProvider:${ctx.workspace.id}`, + limit: 10, + windowMs: 60_000, + }); + if (!limit.allowed) { + throw new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: "Test rate limit exceeded. Try again shortly.", + }); + } + + const [row] = await ctx.db + .select() + .from(modelProviders) + .where( + and( + eq(modelProviders.id, input.providerId), + eq(modelProviders.workspaceId, ctx.workspace.id), + ), + ) + .limit(1); + + if (!row) { + throw new TRPCError({ code: "NOT_FOUND", message: "Provider not found." }); + } + + const resolved = providerToResolved(row); + if (!resolved.ok) { + return { + ok: false as const, + latencyMs: 0, + error: resolved.error.message, + }; + } + + const client = createLanguageModelFromResolved(resolved.model); + if (!client.ok) { + return { ok: false as const, latencyMs: 0, error: client.reason }; + } + + const started = Date.now(); + try { + await generateText({ + model: client.model, + prompt: "Reply with exactly: OK", + maxTokens: 5, + temperature: 0, + }); + return { + ok: true as const, + latencyMs: Date.now() - started, + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { + ok: false as const, + latencyMs: Date.now() - started, + error: message, + }; + } + }), + + listRoleRoutes: workspaceProcedure.query(async ({ ctx }) => { + const routes = await ctx.db + .select({ + route: roleModelRoutes, + providerAlias: modelProviders.alias, + providerEnabled: modelProviders.enabled, + providerMetadata: modelProviders.metadata, + }) + .from(roleModelRoutes) + .innerJoin( + modelProviders, + eq(roleModelRoutes.providerAliasId, modelProviders.id), + ) + .where(eq(roleModelRoutes.workspaceId, ctx.workspace.id)) + .orderBy(asc(roleModelRoutes.sortOrder), asc(roleModelRoutes.roleId)); + + return { + routes: routes.map((row) => ({ + ...mapRouteRow(row.route), + providerAlias: row.providerAlias, + providerEnabled: row.providerEnabled, + providerMetadata: row.providerMetadata, + })), + canManage: canManageWorkspace(ctx), + }; + }), + + upsertRoleRoute: workspaceProcedure + .input( + z.object({ + roleId: z.string().min(1).max(120), + providerAliasId: z.string().uuid(), + enabled: z.boolean().optional(), + sortOrder: z.number().int().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const [provider] = await ctx.db + .select({ id: modelProviders.id }) + .from(modelProviders) + .where( + and( + eq(modelProviders.id, input.providerAliasId), + eq(modelProviders.workspaceId, ctx.workspace.id), + ), + ) + .limit(1); + + if (!provider) { + throw new TRPCError({ code: "NOT_FOUND", message: "Provider not found." }); + } + + const [existing] = await ctx.db + .select({ id: roleModelRoutes.id }) + .from(roleModelRoutes) + .where( + and( + eq(roleModelRoutes.workspaceId, ctx.workspace.id), + eq(roleModelRoutes.roleId, input.roleId), + ), + ) + .limit(1); + + if (existing) { + const [updated] = await ctx.db + .update(roleModelRoutes) + .set({ + providerAliasId: input.providerAliasId, + enabled: input.enabled ?? true, + ...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}), + updatedAt: new Date(), + }) + .where( + and( + eq(roleModelRoutes.id, existing.id), + eq(roleModelRoutes.workspaceId, ctx.workspace.id), + ), + ) + .returning(); + + if (!updated) { + throw new TRPCError({ code: "NOT_FOUND", message: "Route not found." }); + } + + return mapRouteRow(updated); + } + + const [created] = await ctx.db + .insert(roleModelRoutes) + .values({ + workspaceId: ctx.workspace.id, + roleId: input.roleId, + providerAliasId: input.providerAliasId, + enabled: input.enabled ?? true, + sortOrder: input.sortOrder ?? 0, + updatedAt: new Date(), + }) + .returning(); + + if (!created) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create role route.", + }); + } + + return mapRouteRow(created); + }), + + deleteRoleRoute: workspaceProcedure + .input(z.object({ id: z.string().uuid() })) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + const [deleted] = await ctx.db + .delete(roleModelRoutes) + .where( + and( + eq(roleModelRoutes.id, input.id), + eq(roleModelRoutes.workspaceId, ctx.workspace.id), + ), + ) + .returning({ id: roleModelRoutes.id }); + + if (!deleted) { + throw new TRPCError({ code: "NOT_FOUND", message: "Route not found." }); + } + + return { id: deleted.id }; + }), + + resetPipelineDefaults: workspaceProcedure.mutation(async ({ ctx }) => { + assertWorkspaceAdmin(ctx); + const result = await seedDefaultRoleRoutes(ctx.db, ctx.workspace.id); + return result; + }), + + resolveForRole: workspaceProcedure + .input( + z.object({ + roleId: z.string().min(1).max(120), + briefRecommendedModel: z.string().optional(), + convoyPolicy: convoyModelPolicySchema.optional(), + }), + ) + .query(async ({ ctx, input }) => { + const providerRows = await ctx.db + .select() + .from(modelProviders) + .where(eq(modelProviders.workspaceId, ctx.workspace.id)); + + const routeRows = await ctx.db + .select() + .from(roleModelRoutes) + .where(eq(roleModelRoutes.workspaceId, ctx.workspace.id)); + + const result = resolveModel({ + roleId: input.roleId, + providers: providerRows.map(mapProviderRow), + routes: routeRows.map(mapRouteRow), + briefRecommendedModel: input.briefRecommendedModel, + convoyPolicy: input.convoyPolicy, + getApiKey: readApiKey, + }); + + if (!result.ok) { + return result; + } + + const { apiKey: _removed, ...publicModel } = result.model; + void _removed; + return { + ok: true as const, + model: publicModel, + source: result.source, + resolvedAlias: result.resolvedAlias, + }; + }), +}); + +function isUniqueViolation(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error as { code?: string }).code === "23505" + ); +} diff --git a/apps/web/server/routers/ai.ts b/apps/web/server/routers/ai.ts index 74c2b34..c20935d 100644 --- a/apps/web/server/routers/ai.ts +++ b/apps/web/server/routers/ai.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { db as dbInstance } from "@tasks/database"; import { objects } from "@tasks/database/schema"; import { router, workspaceProcedure } from "@/server/trpc"; +import { aiModelsRouter } from "@/server/routers/ai-models"; type Db = typeof dbInstance; @@ -185,4 +186,6 @@ export const aiRouter = router({ } return { actions: suggestionsForContext({ ...input, objectType }) }; }), + + models: aiModelsRouter, }); diff --git a/apps/web/server/routers/runs.ts b/apps/web/server/routers/runs.ts index 9244e94..92b39d2 100644 --- a/apps/web/server/routers/runs.ts +++ b/apps/web/server/routers/runs.ts @@ -73,6 +73,10 @@ export const runsRouter = router({ tokensTotal: agentRuns.tokensTotal, notes: agentRuns.notes, metadata: agentRuns.metadata, + modelAlias: agentRuns.modelAlias, + providerType: agentRuns.providerType, + modelId: agentRuns.modelId, + modelTier: agentRuns.modelTier, }) .from(agentRuns) .leftJoin( @@ -147,6 +151,10 @@ export const runsRouter = router({ tokensTotal: agentRuns.tokensTotal, notes: agentRuns.notes, metadata: agentRuns.metadata, + modelAlias: agentRuns.modelAlias, + providerType: agentRuns.providerType, + modelId: agentRuns.modelId, + modelTier: agentRuns.modelTier, }) .from(agentRuns) .leftJoin( diff --git a/packages/ai/package.json b/packages/ai/package.json index 8735425..83d22ef 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -17,6 +17,7 @@ "ai": "^4.0.0", "@ai-sdk/openai": "^1.0.0", "@ai-sdk/anthropic": "^1.0.0", + "@ai-sdk/google": "^1.0.0", "@tasks/shared": "workspace:*", "zod": "^3.24.0" }, diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index ef3fdc6..9e26371 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -5,6 +5,10 @@ export { type OpenAIClientResult, type OpenAIModelId, } from "./providers/openai"; +export { + createLanguageModelFromResolved, + type CreateLanguageModelResult, +} from "./providers/resolve-client"; export { EDITOR_SYSTEM_PROMPT, GENERAL_SYSTEM_PROMPT, diff --git a/packages/ai/src/providers/resolve-client.ts b/packages/ai/src/providers/resolve-client.ts new file mode 100644 index 0000000..47fb70a --- /dev/null +++ b/packages/ai/src/providers/resolve-client.ts @@ -0,0 +1,48 @@ +import { createAnthropic } from "@ai-sdk/anthropic"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { createOpenAI } from "@ai-sdk/openai"; +import type { LanguageModel } from "ai"; + +import type { ResolvedModel } from "@tasks/shared/model-routing"; + +export type CreateLanguageModelResult = + | { ok: true; model: LanguageModel } + | { ok: false; reason: string }; + +/** + * Build a Vercel AI SDK `LanguageModel` from a resolved registry config. + * Callers must pass a fully resolved `ResolvedModel` (including apiKey). + */ +export function createLanguageModelFromResolved( + resolved: ResolvedModel, +): CreateLanguageModelResult { + try { + switch (resolved.providerType) { + case "anthropic": { + const anthropic = createAnthropic({ apiKey: resolved.apiKey }); + return { ok: true, model: anthropic(resolved.modelId) }; + } + case "openai": + case "openai_compatible": + case "azure_openai": { + const openai = createOpenAI({ + apiKey: resolved.apiKey, + baseURL: resolved.baseUrl, + }); + return { ok: true, model: openai(resolved.modelId) }; + } + case "google": { + const google = createGoogleGenerativeAI({ apiKey: resolved.apiKey }); + return { ok: true, model: google(resolved.modelId) }; + } + default: + return { + ok: false, + reason: `Unsupported provider type: ${resolved.providerType as string}`, + }; + } + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { ok: false, reason: message }; + } +} diff --git a/packages/database/migrations/0011_cool_captain_america.sql b/packages/database/migrations/0011_cool_captain_america.sql new file mode 100644 index 0000000..033a194 --- /dev/null +++ b/packages/database/migrations/0011_cool_captain_america.sql @@ -0,0 +1,4 @@ +ALTER TABLE "agent_runs" ADD COLUMN "model_alias" varchar(120);--> statement-breakpoint +ALTER TABLE "agent_runs" ADD COLUMN "provider_type" varchar(40);--> statement-breakpoint +ALTER TABLE "agent_runs" ADD COLUMN "model_id" varchar(200);--> statement-breakpoint +ALTER TABLE "agent_runs" ADD COLUMN "model_tier" varchar(20); \ No newline at end of file diff --git a/packages/database/migrations/meta/0011_snapshot.json b/packages/database/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..cf3f64e --- /dev/null +++ b/packages/database/migrations/meta/0011_snapshot.json @@ -0,0 +1,3375 @@ +{ + "id": "1e188bcd-0e55-4da9-bca9-ded2ab16f0ff", + "prevId": "69ed8958-b67e-4ca2-9884-b878c5d40c34", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.workspace_invites": { + "name": "workspace_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now() + interval '14 days'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_invites_workspace_id_idx": { + "name": "workspace_invites_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_token_unique": { + "name": "workspace_invites_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_open_email_unique": { + "name": "workspace_invites_open_email_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_invites\".\"accepted_at\" IS NULL AND \"workspace_invites\".\"revoked_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_invites_workspace_id_workspaces_id_fk": { + "name": "workspace_invites_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invites_invited_by_user_id_users_id_fk": { + "name": "workspace_invites_invited_by_user_id_users_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "users", + "columnsFrom": [ + "invited_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(60)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan_tier": { + "name": "plan_tier", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_slug_unique": { + "name": "workspaces_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_owner_user_id_idx": { + "name": "workspaces_owner_user_id_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_owner_user_id_users_id_fk": { + "name": "workspaces_owner_user_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_assignees": { + "name": "object_assignees", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'assignee'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_assignees_object_id_user_id_unique": { + "name": "object_assignees_object_id_user_id_unique", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_assignees_object_id_idx": { + "name": "object_assignees_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_assignees_user_id_idx": { + "name": "object_assignees_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_assignees_object_id_objects_id_fk": { + "name": "object_assignees_object_id_objects_id_fk", + "tableFrom": "object_assignees", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_assignees_user_id_users_id_fk": { + "name": "object_assignees_user_id_users_id_fk", + "tableFrom": "object_assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.objects": { + "name": "objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "objects_parent_id_idx": { + "name": "objects_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_type_idx": { + "name": "objects_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_workspace_id_idx": { + "name": "objects_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_template_id_idx": { + "name": "objects_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_created_by_idx": { + "name": "objects_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_type_workspace_id_idx": { + "name": "objects_type_workspace_id_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "objects_workspace_id_workspaces_id_fk": { + "name": "objects_workspace_id_workspaces_id_fk", + "tableFrom": "objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "objects_created_by_users_id_fk": { + "name": "objects_created_by_users_id_fk", + "tableFrom": "objects", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "objects_parent_id_objects_id_fk": { + "name": "objects_parent_id_objects_id_fk", + "tableFrom": "objects", + "tableTo": "objects", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "objects_template_id_templates_id_fk": { + "name": "objects_template_id_templates_id_fk", + "tableFrom": "objects", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_members_workspace_id_user_id_unique": { + "name": "workspace_members_workspace_id_user_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_workspace_id_idx": { + "name": "workspace_members_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_user_id_idx": { + "name": "workspace_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_members_workspace_id_workspaces_id_fk": { + "name": "workspace_members_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_user_id_users_id_fk": { + "name": "workspace_members_user_id_users_id_fk", + "tableFrom": "workspace_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_type_defs": { + "name": "object_type_defs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "layout": { + "name": "layout", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "default_properties": { + "name": "default_properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_type_defs_workspace_id_idx": { + "name": "object_type_defs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_type_defs_slug_idx": { + "name": "object_type_defs_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_type_defs_workspace_id_workspaces_id_fk": { + "name": "object_type_defs_workspace_id_workspaces_id_fk", + "tableFrom": "object_type_defs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_definitions": { + "name": "property_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "property_definitions_workspace_id_idx": { + "name": "property_definitions_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_definitions_workspace_id_name_idx": { + "name": "property_definitions_workspace_id_name_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_definitions_workspace_id_workspaces_id_fk": { + "name": "property_definitions_workspace_id_workspaces_id_fk", + "tableFrom": "property_definitions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_values": { + "name": "property_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "property_def_id": { + "name": "property_def_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "property_values_object_id_property_def_id_unique": { + "name": "property_values_object_id_property_def_id_unique", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_def_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_values_object_id_idx": { + "name": "property_values_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_values_property_def_id_idx": { + "name": "property_values_property_def_id_idx", + "columns": [ + { + "expression": "property_def_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_values_object_id_objects_id_fk": { + "name": "property_values_object_id_objects_id_fk", + "tableFrom": "property_values", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "property_values_property_def_id_property_definitions_id_fk": { + "name": "property_values_property_def_id_property_definitions_id_fk", + "tableFrom": "property_values", + "tableTo": "property_definitions", + "columnsFrom": [ + "property_def_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.views": { + "name": "views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "view_type": { + "name": "view_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "views_object_id_idx": { + "name": "views_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "views_object_id_objects_id_fk": { + "name": "views_object_id_objects_id_fk", + "tableFrom": "views", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "accounts_provider_provider_account_id_unique": { + "name": "accounts_provider_provider_account_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_session_token_unique": { + "name": "sessions_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "session_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_email_identities": { + "name": "user_email_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_identities_user_id_idx": { + "name": "user_email_identities_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_email_idx": { + "name": "user_email_identities_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_user_id_email_unique": { + "name": "user_email_identities_user_id_email_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_verified_email_unique": { + "name": "user_email_identities_verified_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_email_identities\".\"verified_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_email_identities_user_id_users_id_fk": { + "name": "user_email_identities_user_id_users_id_fk", + "tableFrom": "user_email_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_lower_unique": { + "name": "users_email_lower_unique", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_relations": { + "name": "object_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "relation_type": { + "name": "relation_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_relations_source_id_idx": { + "name": "object_relations_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_target_id_idx": { + "name": "object_relations_target_id_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_relation_type_idx": { + "name": "object_relations_relation_type_idx", + "columns": [ + { + "expression": "relation_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_source_target_type_unique": { + "name": "object_relations_source_target_type_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "relation_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_relations_source_id_objects_id_fk": { + "name": "object_relations_source_id_objects_id_fk", + "tableFrom": "object_relations", + "tableTo": "objects", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_relations_target_id_objects_id_fk": { + "name": "object_relations_target_id_objects_id_fk", + "tableFrom": "object_relations", + "tableTo": "objects", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_workspace_id_idx": { + "name": "templates_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workspace_id_workspaces_id_fk": { + "name": "templates_workspace_id_workspaces_id_fk", + "tableFrom": "templates", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_responses": { + "name": "form_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "respondent_id": { + "name": "respondent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_object_id": { + "name": "created_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_responses_form_id_idx": { + "name": "form_responses_form_id_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_responses_respondent_id_idx": { + "name": "form_responses_respondent_id_idx", + "columns": [ + { + "expression": "respondent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_responses_form_id_forms_id_fk": { + "name": "form_responses_form_id_forms_id_fk", + "tableFrom": "form_responses", + "tableTo": "forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_responses_respondent_id_users_id_fk": { + "name": "form_responses_respondent_id_users_id_fk", + "tableFrom": "form_responses", + "tableTo": "users", + "columnsFrom": [ + "respondent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "form_responses_created_object_id_objects_id_fk": { + "name": "form_responses_created_object_id_objects_id_fk", + "tableFrom": "form_responses", + "tableTo": "objects", + "columnsFrom": [ + "created_object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forms": { + "name": "forms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "forms_workspace_id_idx": { + "name": "forms_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forms_object_id_idx": { + "name": "forms_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forms_workspace_id_workspaces_id_fk": { + "name": "forms_workspace_id_workspaces_id_fk", + "tableFrom": "forms", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forms_object_id_objects_id_fk": { + "name": "forms_object_id_objects_id_fk", + "tableFrom": "forms", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forms_created_by_users_id_fk": { + "name": "forms_created_by_users_id_fk", + "tableFrom": "forms", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_favorites": { + "name": "user_favorites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_favorites_user_object_idx": { + "name": "user_favorites_user_object_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_favorites_user_id_users_id_fk": { + "name": "user_favorites_user_id_users_id_fk", + "tableFrom": "user_favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_favorites_object_id_objects_id_fk": { + "name": "user_favorites_object_id_objects_id_fk", + "tableFrom": "user_favorites", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.markdown_backlog_items": { + "name": "markdown_backlog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "plan_slug": { + "name": "plan_slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "epic_slug": { + "name": "epic_slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontmatter": { + "name": "frontmatter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_hash": { + "name": "content_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workflow_prompt": { + "name": "workflow_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "markdown_backlog_workspace_repo_path_unique": { + "name": "markdown_backlog_workspace_repo_path_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_workspace_plan_idx": { + "name": "markdown_backlog_workspace_plan_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_parent_id_idx": { + "name": "markdown_backlog_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_workspace_kind_idx": { + "name": "markdown_backlog_workspace_kind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "markdown_backlog_items_workspace_id_workspaces_id_fk": { + "name": "markdown_backlog_items_workspace_id_workspaces_id_fk", + "tableFrom": "markdown_backlog_items", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk": { + "name": "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk", + "tableFrom": "markdown_backlog_items", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cursor_sync_mappings": { + "name": "cursor_sync_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "backlog_item_id": { + "name": "backlog_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cursor_plan_id": { + "name": "cursor_plan_id", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "cursor_item_id": { + "name": "cursor_item_id", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "last_pulled_at": { + "name": "last_pulled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_pushed_at": { + "name": "last_pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_content_hash": { + "name": "sync_content_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cursor_sync_mappings_backlog_item_id_unique": { + "name": "cursor_sync_mappings_backlog_item_id_unique", + "columns": [ + { + "expression": "backlog_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cursor_sync_mappings_workspace_id_idx": { + "name": "cursor_sync_mappings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cursor_sync_mappings_workspace_id_workspaces_id_fk": { + "name": "cursor_sync_mappings_workspace_id_workspaces_id_fk", + "tableFrom": "cursor_sync_mappings", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk": { + "name": "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk", + "tableFrom": "cursor_sync_mappings", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "backlog_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_id_created_at_idx": { + "name": "audit_log_workspace_id_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_user_id_idx": { + "name": "audit_log_actor_user_id_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspaces_id_fk": { + "name": "audit_log_workspace_id_workspaces_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_log_actor_user_id_users_id_fk": { + "name": "audit_log_actor_user_id_users_id_fk", + "tableFrom": "audit_log", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runs": { + "name": "agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "backlog_item_id": { + "name": "backlog_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_input": { + "name": "tokens_input", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_output": { + "name": "tokens_output", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_total": { + "name": "tokens_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_alias": { + "name": "model_alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "model_tier": { + "name": "model_tier", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_runs_workspace_id_started_at_idx": { + "name": "agent_runs_workspace_id_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_backlog_item_id_started_at_idx": { + "name": "agent_runs_backlog_item_id_started_at_idx", + "columns": [ + { + "expression": "backlog_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runs_workspace_id_workspaces_id_fk": { + "name": "agent_runs_workspace_id_workspaces_id_fk", + "tableFrom": "agent_runs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_runs_backlog_item_id_markdown_backlog_items_id_fk": { + "name": "agent_runs_backlog_item_id_markdown_backlog_items_id_fk", + "tableFrom": "agent_runs", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "backlog_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_runs_actor_user_id_users_id_fk": { + "name": "agent_runs_actor_user_id_users_id_fk", + "tableFrom": "agent_runs", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_providers": { + "name": "model_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_env_var": { + "name": "api_key_env_var", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "extra_headers": { + "name": "extra_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "default_params": { + "name": "default_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "model_providers_workspace_id_idx": { + "name": "model_providers_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "model_providers_workspace_id_alias_unique": { + "name": "model_providers_workspace_id_alias_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_providers_workspace_id_workspaces_id_fk": { + "name": "model_providers_workspace_id_workspaces_id_fk", + "tableFrom": "model_providers", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_model_routes": { + "name": "role_model_routes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "provider_alias_id": { + "name": "provider_alias_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "role_model_routes_workspace_id_idx": { + "name": "role_model_routes_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_model_routes_workspace_id_role_id_unique": { + "name": "role_model_routes_workspace_id_role_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_model_routes_provider_alias_id_idx": { + "name": "role_model_routes_provider_alias_id_idx", + "columns": [ + { + "expression": "provider_alias_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_model_routes_workspace_id_workspaces_id_fk": { + "name": "role_model_routes_workspace_id_workspaces_id_fk", + "tableFrom": "role_model_routes", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_model_routes_provider_alias_id_model_providers_id_fk": { + "name": "role_model_routes_provider_alias_id_model_providers_id_fk", + "tableFrom": "role_model_routes", + "tableTo": "model_providers", + "columnsFrom": [ + "provider_alias_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 6ed2ed6..734682c 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1783450794221, "tag": "0010_legal_stryfe", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1783451144455, + "tag": "0011_cool_captain_america", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/database/src/schema/agent_runs.ts b/packages/database/src/schema/agent_runs.ts index c1049b8..c4f603d 100644 --- a/packages/database/src/schema/agent_runs.ts +++ b/packages/database/src/schema/agent_runs.ts @@ -67,6 +67,10 @@ export const agentRuns = pgTable( tokensTotal: integer("tokens_total"), notes: text("notes"), metadata: jsonb("metadata").$type | null>(), + modelAlias: varchar("model_alias", { length: 120 }), + providerType: varchar("provider_type", { length: 40 }), + modelId: varchar("model_id", { length: 200 }), + modelTier: varchar("model_tier", { length: 20 }), }, (table) => ({ workspaceStartedIdx: index("agent_runs_workspace_id_started_at_idx").on( diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md index 3085c46..abf4237 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md @@ -3,7 +3,7 @@ kind: epic slug: model-provider-registry title: Model provider registry and per-role routing plan_slug: automation-foundation -status: ready +status: done priority: P1 tenant_id: global cursor_epic_id: null diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-agent-runs-model-logging.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-agent-runs-model-logging.md index e682db8..ae9125c 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-agent-runs-model-logging.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-agent-runs-model-logging.md @@ -4,7 +4,7 @@ slug: agent-runs-model-logging title: agent_runs model metadata and complete_task logging fields plan_slug: automation-foundation epic_slug: model-provider-registry -status: ready +status: done priority: P1 tenant_id: global owner: unassigned diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-providers-ui.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-providers-ui.md index c19741d..7cbe6dd 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-providers-ui.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-providers-ui.md @@ -4,7 +4,7 @@ slug: settings-ai-providers-ui title: Settings UI — AI model providers plan_slug: automation-foundation epic_slug: model-provider-registry -status: ready +status: done priority: P1 tenant_id: global owner: unassigned diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-role-routing-ui.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-role-routing-ui.md index 2915c9b..bef119f 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-role-routing-ui.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-settings-ai-role-routing-ui.md @@ -4,7 +4,7 @@ slug: settings-ai-role-routing-ui title: Settings UI — per-role model routing plan_slug: automation-foundation epic_slug: model-provider-registry -status: ready +status: done priority: P1 tenant_id: global owner: unassigned diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-trpc-models-router.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-trpc-models-router.md index 0a779b2..53012a7 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-trpc-models-router.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-trpc-models-router.md @@ -4,7 +4,7 @@ slug: trpc-models-router title: tRPC ai.models router — CRUD, role routes, testProvider plan_slug: automation-foundation epic_slug: model-provider-registry -status: ready +status: done priority: P1 tenant_id: global owner: unassigned diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e029dc..15696d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,6 +335,9 @@ importers: '@ai-sdk/anthropic': specifier: ^1.0.0 version: 1.2.12(zod@3.25.76) + '@ai-sdk/google': + specifier: ^1.0.0 + version: 1.2.22(zod@3.25.76) '@ai-sdk/openai': specifier: ^1.0.0 version: 1.3.24(zod@3.25.76) @@ -404,6 +407,12 @@ packages: peerDependencies: zod: ^3.0.0 + '@ai-sdk/google@1.2.22': + resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/openai@1.3.24': resolution: {integrity: sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==} engines: {node: '>=18'} @@ -5394,6 +5403,12 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/google@1.2.22(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/openai@1.3.24(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.1.3