Files
template-app/config/src/ai.ts
T

124 lines
6.5 KiB
TypeScript

// 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<string, string | undefined> = 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<string, string | undefined> = 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<string, string | undefined> = 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<string, string | undefined> = 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<string, string | undefined> = 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<string, string | undefined> = process.env): string | null {
return vectorStoreOf(environment) === "pgvector" ? "0006_vectors" : null
}