AI Integration Architecture
This page documents the AI infrastructure boundaries and contracts. It focuses on architecture ownership, not product UX.
Layers
| Layer | Owns | Must not own |
|---|---|---|
core/ |
Runtime abstraction, shared AI types, common errors, bootstrap | Domain prompts, business tasks, CMS data fetching |
providers/ |
Concrete provider implementation and provider resolution | CMS module logic, content schema validation |
domain/content/ |
Prompt construction, output parsing, schema validation | Provider SDK calls, direct CMS service/GraphQL access |
audit/ |
Structured event payload creation and timing helpers | Persistence policy, product-level orchestration |
Runtime Contract
aiRuntime is intentionally minimal:
aiRuntime.complete(params)resolves active provider and executes completion.aiRuntime.getProviderName()exposes provider identity for observability/audit.
Non-goals of core/runtime.ts:
- No task registry (no
runTask()surface). - No prompt generation logic.
- No content-type business branching.
Provider Model
AIProvider contract (core/types.ts):
export interface AIProvider {
name: string;
complete(params: AICompletionParams): Promise<AICompletionResult>;
}
Current runtime behavior:
initAI()registersOpenAIProvideronce usingOPENAI_API_KEY.- First registered provider becomes default.
resolveProvider()returns default provider.OpenAIProvider.complete()calls OpenAI chat completions (gpt-4o-minidefault, overridable by params).

Domain Boundary and Validation
The content domain path (domain/content/generate.ts) enforces:
- At least one field requested (
NO_FIELDS_REQUESTEDotherwise). - Prompt built from typed input (
buildGeneratePrompt). - Provider response must be valid JSON (
INVALID_JSON_RESPONSEon parse failure). - Output validated with Zod schema (
ContentSchema) for requested keys only. - Missing requested keys raise
MISSING_REQUIRED_FIELD:<key>.
schema.ts behavior:
- Parses as
ContentSchema.partial()to allow request-specific subsets. - Strips unknown keys.
- Verifies each requested key exists after parse.
- Returns typed partial data, then
generate.tsnormalizes to fullGeneratedContent.
Error Model
| Error code | Where raised | Route response |
|---|---|---|
NO_PROVIDER_REGISTERED |
Provider registry / runtime | 400 |
NO_FIELDS_REQUESTED |
Domain generate precondition | 400 |
INVALID_JSON_RESPONSE |
JSON parse or schema parse | 400 |
MISSING_REQUIRED_FIELD:<field> |
Domain requested-field check | 400 |
PROVIDER_ERROR |
Provider SDK execution | 400 |
UNKNOWN_AI_ERROR |
Fallback in route catch path | 500 (non-AIError path) |
Audit Model
AiAudit defines event and payload contracts:
- Events:
AI_GENERATION_CREATED,AI_GENERATION_FAILED,AI_OUTPUT_ACCEPTED - Shared metadata:
provider,status,requestedFields,durationMs,site,siteId,requesterId,timestamp - Helper methods:
start()andduration()
Current emission state in route:
- Emits
AI_GENERATION_CREATEDon successful generate. - Emits
AI_GENERATION_FAILEDon generate failure. - Logs payloads with
console.debugas temporary sink. - Does not emit
AI_OUTPUT_ACCEPTEDin current apply flow.
Adding a New Provider
- Implement
AIProviderinsrc/lib/external/ai/providers/<provider>/. - Register provider in
initAI()(or bootstrap composition point). - Optionally set default via
setDefaultProvider(name). - Keep domain and module code unchanged unless provider-specific capabilities are intentionally added at domain level.
Provider changes should not require prompt/schema rewrites unless output behavior differs.