SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
9.5 KB · 205 lines typescript
Raw Blame History
1/**2 * Workspace tables added by the 2026-09 "AI operating system" upgrade:3 * projects (workspaces), project files (reusable context library), prompt library with variables,4 * user model labels, custom OpenAI-compatible endpoints and Arena votes/criteria.5 *6 * Kept in a separate file so parallel feature work never edits `schema.ts` concurrently.7 * `drizzle.config.ts` globs `schema*.ts`; `src/db/index.ts` re-exports everything.8 */9import { pgTable, text, timestamp, boolean, integer, jsonb, index, uniqueIndex, primaryKey } from "drizzle-orm/pg-core";10import { sql } from "drizzle-orm";11import { users } from "./schema";1213const ts = (name: string) => timestamp(name, { withTimezone: true, mode: "date" });14const now = () => sql`now()`;1516// ---------------------------------------------------------------------------17// Projects / workspaces18// ---------------------------------------------------------------------------19export const projects = pgTable(20  "projects",21  {22    id: text("id").primaryKey(),23    userId: text("user_id")24      .notNull()25      .references(() => users.id, { onDelete: "cascade" }),26    name: text("name").notNull(),27    description: text("description"),28    /** Emoji or lucide icon name. */29    icon: text("icon"),30    color: text("color"),31    /** Default system instructions prepended to every chat in the project. */32    instructions: text("instructions"),33    /** Preferred model keys (first = default for new chats). */34    preferredModelKeys: jsonb("preferred_model_keys").$type<string[]>().notNull().default([]),35    /** Default generation settings for new chats. */36    defaultSettings: jsonb("default_settings").$type<Record<string, unknown>>().notNull().default({}),37    /** Free-form markdown notes. */38    notes: text("notes"),39    archived: boolean("archived").notNull().default(false),40    sortOrder: integer("sort_order").notNull().default(0),41    createdAt: ts("created_at").notNull().default(now()),42    updatedAt: ts("updated_at").notNull().default(now()),43  },44  (t) => [index("projects_user_idx").on(t.userId)],45);4647/** Reusable files saved to a project (or to the user's global library when projectId is null). */48export const projectFiles = pgTable(49  "project_files",50  {51    id: text("id").primaryKey(),52    userId: text("user_id")53      .notNull()54      .references(() => users.id, { onDelete: "cascade" }),55    projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),56    kind: text("kind").notNull(), // image | pdf | text | code | csv | json | document57    name: text("name").notNull(),58    mimeType: text("mime_type").notNull(),59    sizeBytes: integer("size_bytes").notNull(),60    /** Base64 payload (same storage strategy as message_attachments). */61    dataBase64: text("data_base64").notNull(),62    width: integer("width"),63    height: integer("height"),64    /** Heuristic token estimate computed at upload. */65    estimatedTokens: integer("estimated_tokens"),66    /** Optional user description used when injecting into prompts. */67    description: text("description"),68    createdAt: ts("created_at").notNull().default(now()),69  },70  (t) => [index("project_files_user_idx").on(t.userId), index("project_files_project_idx").on(t.projectId)],71);7273// ---------------------------------------------------------------------------74// Prompt library (templates with {{variables}})75// ---------------------------------------------------------------------------76export const prompts = pgTable(77  "prompts",78  {79    id: text("id").primaryKey(),80    userId: text("user_id")81      .notNull()82      .references(() => users.id, { onDelete: "cascade" }),83    projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),84    /** system | user | template | structured */85    kind: text("kind").notNull().default("user"),86    name: text("name").notNull(),87    description: text("description"),88    /** Body with `{{variable}}` placeholders. */89    content: text("content").notNull(),90    /** Declared variables (parsed from content, editable defaults). */91    variables: jsonb("variables").$type<{ name: string; label?: string; default?: string; required?: boolean; options?: string[] }[]>().notNull().default([]),92    /** For `structured`: the JSON schema to request. */93    schema: jsonb("schema").$type<Record<string, unknown> | null>(),94    folder: text("folder"),95    tags: jsonb("tags").$type<string[]>().notNull().default([]),96    favorite: boolean("favorite").notNull().default(false),97    defaultModelKey: text("default_model_key"),98    uses: integer("uses").notNull().default(0),99    lastUsedAt: ts("last_used_at"),100    createdAt: ts("created_at").notNull().default(now()),101    updatedAt: ts("updated_at").notNull().default(now()),102  },103  (t) => [index("prompts_user_idx").on(t.userId), index("prompts_user_folder_idx").on(t.userId, t.folder)],104);105106// ---------------------------------------------------------------------------107// Model intelligence — user labels & parameter presets scoped to capability sets108// ---------------------------------------------------------------------------109export const userModelLabels = pgTable(110  "user_model_labels",111  {112    userId: text("user_id")113      .notNull()114      .references(() => users.id, { onDelete: "cascade" }),115    modelKey: text("model_key").notNull(),116    label: text("label").notNull(),117    updatedAt: ts("updated_at").notNull().default(now()),118  },119  (t) => [primaryKey({ columns: [t.userId, t.modelKey] })],120);121122// ---------------------------------------------------------------------------123// Custom OpenAI-compatible endpoints (Ollama, LM Studio, vLLM, llama.cpp, MLX…)124// ---------------------------------------------------------------------------125export const customEndpoints = pgTable(126  "custom_endpoints",127  {128    id: text("id").primaryKey(),129    userId: text("user_id")130      .notNull()131      .references(() => users.id, { onDelete: "cascade" }),132    name: text("name").notNull(),133    /** Base URL up to and including `/v1` (e.g. http://localhost:11434/v1). */134    baseUrl: text("base_url").notNull(),135    /** Optional API key, AES-256-GCM envelope like provider_connections.encrypted_key. */136    encryptedKey: text("encrypted_key"),137    keyHint: text("key_hint"),138    /** Extra headers (values encrypted as a single JSON envelope). */139    encryptedHeaders: text("encrypted_headers"),140    /** Relative path used for discovery, default `/models`. Empty = manual models only. */141    modelsPath: text("models_path").notNull().default("/models"),142    /** Manually declared models when discovery is unavailable. */143    manualModels: jsonb("manual_models").$type<{ id: string; displayName?: string; contextTokens?: number; vision?: boolean; tools?: boolean; reasoning?: boolean }[]>().notNull().default([]),144    status: text("status").notNull().default("unverified"), // unverified | valid | invalid | error145    lastValidatedAt: ts("last_validated_at"),146    lastValidationError: text("last_validation_error"),147    lastLatencyMs: integer("last_latency_ms"),148    modelsAvailable: integer("models_available"),149    /** Snapshot of the last successful discovery (`GET {baseUrl}{modelsPath}`), so /api/models never calls the endpoint. */150    discoveredModels: jsonb("discovered_models").$type<{ id: string; ownedBy?: string; created?: number }[]>().notNull().default([]),151    discoveredAt: ts("discovered_at"),152    createdAt: ts("created_at").notNull().default(now()),153    updatedAt: ts("updated_at").notNull().default(now()),154  },155  (t) => [index("custom_endpoints_user_idx").on(t.userId)],156);157158// ---------------------------------------------------------------------------159// Arena — votes per criterion (ratings on arena_responses stay for compatibility)160// ---------------------------------------------------------------------------161export const arenaVotes = pgTable(162  "arena_votes",163  {164    id: text("id").primaryKey(),165    userId: text("user_id")166      .notNull()167      .references(() => users.id, { onDelete: "cascade" }),168    sessionId: text("session_id").notNull(),169    responseId: text("response_id").notNull(),170    modelKey: text("model_key").notNull(),171    /** best | accurate | writing | coding | value | fastest | custom:<slug> */172    criterion: text("criterion").notNull(),173    /** Task category the prompt was classified into (coding | research | writing | reasoning | general). */174    category: text("category"),175    createdAt: ts("created_at").notNull().default(now()),176  },177  (t) => [uniqueIndex("arena_votes_session_criterion_uq").on(t.sessionId, t.criterion), index("arena_votes_user_model_idx").on(t.userId, t.modelKey)],178);179180// ---------------------------------------------------------------------------181// Public share links for Arena sessions182// ---------------------------------------------------------------------------183export const sharedArenaSessions = pgTable(184  "shared_arena_sessions",185  {186    id: text("id").primaryKey(),187    sessionId: text("session_id").notNull(),188    userId: text("user_id")189      .notNull()190      .references(() => users.id, { onDelete: "cascade" }),191    snapshot: jsonb("snapshot").$type<Record<string, unknown>>().notNull(),192    isPublic: boolean("is_public").notNull().default(true),193    viewCount: integer("view_count").notNull().default(0),194    createdAt: ts("created_at").notNull().default(now()),195    revokedAt: ts("revoked_at"),196  },197  (t) => [index("shared_arena_session_idx").on(t.sessionId)],198);199200export type Project = typeof projects.$inferSelect;201export type ProjectFile = typeof projectFiles.$inferSelect;202export type Prompt = typeof prompts.$inferSelect;203export type CustomEndpoint = typeof customEndpoints.$inferSelect;204export type ArenaVote = typeof arenaVotes.$inferSelect;205