---
title: "AI SDK structured notes: adapters, schemas, and business checks"
description: Read ChatCrystal’s AI SDK 7 calls, distinguish provider adaptation from output shape and factual validity, and test description limits with offline Zod.
image: "https://zengliangyi.com/og/blog/en/ai-sdk-structured-notes.png"
---

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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json) pins `ai: 7.0.97`; the project requires Node 24+. The [summary service](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/summarize.ts) 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:



```ts
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](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) 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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/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](https://ai-sdk.dev/providers/ai-sdk-providers/openai) 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](/en/blog/cloud-llm-configuration/) for switching settings and compatibility checks, and [embedding model configuration](/en/blog/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.



```js
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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/relations.ts) 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](/en/blog/note-relations-and-graph/) 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](/en/blog/llm-summary-evaluation/).

## Retry counts do not guarantee correct output

The summary call sets maxRetries to 3 and the relation call to 2; the [generateText reference](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) 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.

Share:

##  Related Posts

[##  Building an Embedding Service: Text, Chunks, and Commits

Read ChatCrystal’s actual embedding pipeline, separate text selection, soft chunking, vector validation, and commits, and verify boundaries offline.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/embedding-service-design)    [##  Structured JSON Prompts: Field Design & Verifiable Evidence

Use ChatCrystal’s summaries and relations to define fields, visible evidence, and counterexamples, then distinguish shape and evidence checks with offline Zod.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/structured-json-prompts)    [##  Vectra local search: fixed vectors, filters, and persistence

Run a deterministic Vectra 0.15.0 exercise in a temporary directory to check cosine scores, metadata filters, reopening, and canceled updates.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/vectra-local-search)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "AI SDK structured notes: adapters, schemas, and business checks",
  "description": "Read ChatCrystal’s AI SDK 7 calls, distinguish provider adaptation from output shape and factual validity, and test description limits with offline Zod.",
  "url": "https://zengliangyi.com/en/blog/ai-sdk-structured-notes/",
  "image": "https://zengliangyi.com/images/blog/ai-sdk-structured-notes.svg",
  "datePublished": "2026-10-05T00:00:00.000Z",
  "dateModified": "2026-10-05T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/ai-sdk-structured-notes/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "AI SDK structured notes: adapters, schemas, and business checks",
      "item": "https://zengliangyi.com/en/blog/ai-sdk-structured-notes/"
    }
  ]
}
```
