A shared model interface reduces integration code without removing differences in protocols and capabilities. Structured output validates shape without establishing factual correctness. This article explains both boundaries through ChatCrystal’s pinned implementation. It is not a model ranking, and the exercise does not require a paid service call.
Current summarization uses AI SDK 7
server/package.json pins ai: 7.0.97; the project requires Node 24+. The summary service uses generateText with Output.object and reads output from the result. The original draft’s generateObject/system/object combination does not describe this revision.
This actual call excerpt depends on the application’s existing model factory, schema, instructions, and transcript. It cannot run as a standalone copied program:
const { output } = await generateText({
model: getLanguageModel(),
output: Output.object({ schema: SummarizeSchema }),
instructions: SYSTEM_PROMPT,
prompt: transcript,
maxOutputTokens: 4096,
maxRetries: 3,
});
instructions supplies task requirements and prompt supplies the visible transcript. The five fields are title, summary, key_conclusions, code_snippets, and tags. The application subsequently lowercases tags and stores the original output JSON as raw_response. The official structured-data guide describes Output.object as structural validation. That does not guarantee successful generation or source-grounded conclusions.
The factory shares types, not every protocol
providers.ts registers seven generation providers: Ollama, OpenAI, OrcaRouter, Anthropic, Google, Azure, and custom. Five support embeddings: Ollama, OpenAI, Google, Azure, and custom. A generation model is not automatically an embedding model.
getLanguageModel selects its factory from llm configuration; embeddings use separate embedding configuration. Ollama trims trailing slashes, appends /v1, and explicitly uses .chat(model) for generation. OpenAI/custom call openai(model), using the SDK’s default Responses path. The OpenAI adapter documentation explains the different model factories. An OpenAI-compatible integration therefore needs checks of endpoint, capabilities, authentication, and adapter version, beyond a plausible URL.
See cloud LLM configuration for switching settings and compatibility checks, and embedding model configuration for vectors. This article focuses on adaptation and validation responsibilities instead of repeating each service’s setup.
describe expresses expectations without enforcing size
The actual SummarizeSchema uses z.string() and z.array(...), with descriptions such as a title within 20 characters and 3–5 conclusions. It has no corresponding max/min count constraints. The following offline lesson reduces the schema to three fields; it is not the full application schema. Install Zod 4.6.2 in a new temporary directory, save lesson.mjs, and run with Node 24+. It calls no model and accesses no database.
import assert from 'node:assert/strict';
import { z } from 'zod';
const noteSchema = z.object({
title: z.string().describe('A concise title within 20 characters'),
summary: z.string(),
key_conclusions: z.array(z.string()).describe('3–5 conclusions'),
});
const constructed = {
title: 'x'.repeat(80),
summary: 'Invented example: this code has not been checked against a source.',
key_conclusions: [],
};
assert.equal(noteSchema.safeParse(constructed).success, true);
assert.equal(noteSchema.safeParse({ ...constructed, title: 123 }).success, false);
assert.equal(
noteSchema.safeParse({ ...constructed, key_conclusions: 'one conclusion' }).success,
false
);
const stricter = noteSchema.extend({
title: z.string().max(20),
key_conclusions: z.array(z.string()).min(3).max(5),
});
assert.equal(stricter.safeParse(constructed).success, false);
The constructed object’s long title, empty array, and unsupported summary all pass the first lesson schema because their field types match. Adding max/min makes size executable; factual grounding still needs another check. Deliberately add max(20) to the first title definition and the first assertion should fail. Restore description-only behavior and it should pass. This demonstrates validation responsibilities rather than prescribing a direct production schema change.
Business filtering and persistence follow generation
Relation discovery uses Output.array, eight relation types, and a numeric 0–1 constraint. It then keeps targets in the candidate ID set with confidence at least 0.5 and takes at most five. Candidate existence, business thresholds, and result count are not implemented only through prompt descriptions. Confidence is not a calibrated probability of correctness. See note relations and graphs for projection details.
Summarization checks for an existing note before the experience gate. Accepted conversations generate and save a note before embeddings and relations run. Embedding errors mark embedding failed; relation errors are logged. They do not automatically turn a successful summary into failure. Clicking the same conversation again also does not overwrite an existing note to reevaluate a model. See summary quality evaluation.
Retry counts do not guarantee correct output
The summary call sets maxRetries to 3 and the relation call to 2; the generateText reference defines this parameter. It does not promise three new generations for every schema-validation failure. Error type, adapter, and failure stage matter; output parsing errors and retryable transport/API errors need separate handling.
The application queue also has a task-start interval and some 429 retries, but one task can make gate, summary, multiple embedding, and relation calls. One task per second does not imply one model request per second across the system. Validate protocol, output shape, source grounding, business filtering, persistence state, and failure type separately; a pass in one does not establish the rest.