// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The one AI setting of this app: which model the platform's AI gateway is asked for. The gateway's // token and URL are the deployment contract (@conusai-kit/platform); the model is a choice, so it has // a default that works and a variable that changes it, per environment, without a rebuild. /** A model the ConusAI AI gateway serves; the extraction prompt of `modules/documents` is written against this family. */ export const DEFAULT_AI_MODEL = "anthropic/claude-sonnet-5" export const AI_MODEL_VAR = "AI_MODEL" /** The model id the app asks the gateway for: `AI_MODEL` when set, otherwise the default. */ export function aiModelOf(env: Record = process.env): string { const chosen = env[AI_MODEL_VAR]?.trim() return chosen ? chosen : DEFAULT_AI_MODEL } // The knowledge module's deployment choices: which search backend serves it, which embeddings // model the vector backend asks for, and the vector size the store is built with. // // KNOWLEDGE_BACKEND "vectors" (default): chunks and queries are embedded through the AI // gateway and stored in the app's vector store (KNOWLEDGE_VECTOR_STORE // below: a linked Qdrant, or pgvector when chosen explicitly). // "platform": the platform's managed documents-index service does the // indexing and the retrieval. // EMBEDDING_MODEL the gateway's embeddings model for the vectors backend (required when the // backend is vectors; AI_EMBEDDING_MODEL overrides). // VECTOR_DIMENSIONS the embedding size the vector store is built with; the pgvector column and // the Qdrant collection both take it (default 1536). import { z } from "zod" export const KNOWLEDGE_BACKEND_VAR = "KNOWLEDGE_BACKEND" export const KNOWLEDGE_BACKENDS = ["vectors", "platform"] as const export type KnowledgeBackendChoice = (typeof KNOWLEDGE_BACKENDS)[number] export const knowledgeBackendSchema = z.enum(KNOWLEDGE_BACKENDS) /** Which search backend this deployment binds; "vectors" unless the operator says otherwise. */ export function knowledgeBackendOf(env: Record = process.env): KnowledgeBackendChoice { const raw = env[KNOWLEDGE_BACKEND_VAR]?.trim().toLowerCase() if (raw === undefined || raw === "") return "vectors" const parsed = knowledgeBackendSchema.safeParse(raw) if (!parsed.success) { throw new Error(`${KNOWLEDGE_BACKEND_VAR} must be one of ${KNOWLEDGE_BACKENDS.join(", ")}`) } return parsed.data } export const EMBEDDING_MODEL_VAR = "AI_EMBEDDING_MODEL" /** The embeddings model the vector backend asks the gateway for. */ export function embeddingModelOf(env: Record = process.env): string | undefined { const chosen = env[EMBEDDING_MODEL_VAR]?.trim() return chosen ? chosen : undefined } export const VECTOR_DIMENSIONS_VAR = "VECTOR_DIMENSIONS" export const DEFAULT_VECTOR_DIMENSIONS = 1536 /** The vector size the store is built with; must match the embeddings model's output. */ export function vectorDimensionsOf(env: Record = process.env): number { const raw = env[VECTOR_DIMENSIONS_VAR]?.trim() if (raw === undefined || raw === "") return DEFAULT_VECTOR_DIMENSIONS const value = Number(raw) if (!Number.isInteger(value) || value < 1 || value > 16_000) { throw new Error(`${VECTOR_DIMENSIONS_VAR} must be an integer between 1 and 16000`) } return value } // Where the vectors backend keeps its vectors (decision 2026-10-25, managed-AI closeout): // // KNOWLEDGE_VECTOR_STORE unset (default): Qdrant when a Qdrant service is linked (QDRANT_URL), // otherwise NO store — the knowledge module reports itself unavailable // with the reason below, and the migrate job creates nothing. // "pgvector": the app's own Postgres, opted into explicitly. The // platform's default managed Postgres image does not ship the extension; // pgvector needs a Postgres created from the pgvector image. // "qdrant": Qdrant, required — unavailable until QDRANT_URL is set. // // pgvector used to be the silent default whenever no Qdrant was linked, so a fresh app on the // default managed Postgres failed its migrate job (`extension "vector" is not available`) before // anyone had asked for document AI. Enabling document AI links a managed Qdrant, which is the // product path; pgvector stays a deliberate choice for databases that offer it. export const KNOWLEDGE_VECTOR_STORE_VAR = "KNOWLEDGE_VECTOR_STORE" export const VECTOR_STORES = ["qdrant", "pgvector"] as const export type VectorStoreChoice = (typeof VECTOR_STORES)[number] const vectorStoreSchema = z.enum(VECTOR_STORES) /** Why the vectors backend has no store, for the knowledge status a person reads. */ export const NO_VECTOR_STORE_REASON = "no vector store is configured: enable document AI to link a managed Qdrant, set KNOWLEDGE_VECTOR_STORE=pgvector on a Postgres that has the pgvector extension, or set KNOWLEDGE_BACKEND=platform" /** * The vector store this deployment binds, or null when the vectors backend has none (or the * backend is the platform's). Explicit pgvector only: never inferred from a missing Qdrant. */ export function vectorStoreOf(environment: Record = process.env): VectorStoreChoice | null { if (knowledgeBackendOf(environment) !== "vectors") return null const raw = environment[KNOWLEDGE_VECTOR_STORE_VAR]?.trim().toLowerCase() const linkedQdrant = Boolean(environment.QDRANT_URL?.trim()) if (raw === undefined || raw === "") return linkedQdrant ? "qdrant" : null const parsed = vectorStoreSchema.safeParse(raw) if (!parsed.success) { throw new Error(`${KNOWLEDGE_VECTOR_STORE_VAR} must be one of ${VECTOR_STORES.join(", ")}`) } if (parsed.data === "qdrant" && !linkedQdrant) return null return parsed.data } /** * The runtime-built migration that creates the pgvector table, for a deployment that chose the * pgvector store. The migrate job applies it; the web readiness probe expects exactly its name, so * the decision lives here, in the one place that reads the same environment. */ export function vectorStoreMigrationName(environment: Record = process.env): string | null { return vectorStoreOf(environment) === "pgvector" ? "0006_vectors" : null }