AI Integration Architecture

Prev Next

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() registers OpenAIProvider once using OPENAI_API_KEY.
  • First registered provider becomes default.
  • resolveProvider() returns default provider.
  • OpenAIProvider.complete() calls OpenAI chat completions (gpt-4o-mini default, overridable by params).

Image

Domain Boundary and Validation

The content domain path (domain/content/generate.ts) enforces:

  1. At least one field requested (NO_FIELDS_REQUESTED otherwise).
  2. Prompt built from typed input (buildGeneratePrompt).
  3. Provider response must be valid JSON (INVALID_JSON_RESPONSE on parse failure).
  4. Output validated with Zod schema (ContentSchema) for requested keys only.
  5. 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.ts normalizes to full GeneratedContent.

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() and duration()

Current emission state in route:

  • Emits AI_GENERATION_CREATED on successful generate.
  • Emits AI_GENERATION_FAILED on generate failure.
  • Logs payloads with console.debug as temporary sink.
  • Does not emit AI_OUTPUT_ACCEPTED in current apply flow.

Adding a New Provider

  1. Implement AIProvider in src/lib/external/ai/providers/<provider>/.
  2. Register provider in initAI() (or bootstrap composition point).
  3. Optionally set default via setDefaultProvider(name).
  4. 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.