@jterrazz/intelligence 4.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,115 +1,150 @@
1
1
  # @jterrazz/intelligence
2
2
 
3
- Lightweight, composable utilities for AI SDK apps - middleware for logging and observability, structured output parsing, result handling, and provider helpers.
3
+ A thin composition layer over [AI SDK v7](https://ai-sdk.dev) provider factories, cost/fallback/logging middleware, and a config-driven factory that wires them together. Observability goes through OpenTelemetry: the host app registers an OTel Node SDK (e.g. via [`@jterrazz/telemetry`](https://www.npmjs.com/package/@jterrazz/telemetry)), and this package emits AI SDK spans and a `gen_ai.usage.cost` attribute into it.
4
+
5
+ The public surface is AI SDK's own `LanguageModel` type — every factory here returns a model you can pass straight into `generateText`, `streamText`, `generateObject`, etc.
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm install @jterrazz/intelligence ai zod
10
+ npm install @jterrazz/intelligence ai
9
11
  ```
10
12
 
11
- ## Generation
12
-
13
- ### `generateStructured` - Type-safe structured generation with error handling
13
+ `ai` (^7.0.0) is a peer dependency — bring your own version.
14
14
 
15
- Combines `generateText` + `parseObject` + error classification into a single function that returns a discriminated union result.
15
+ ## Quick start
16
16
 
17
17
  ```typescript
18
- import { generateStructured, withObservability } from '@jterrazz/intelligence';
19
- import { z } from 'zod';
20
-
21
- const schema = z.object({
22
- sentiment: z.string(),
23
- score: z.number(),
24
- });
18
+ import { createIntelligence } from '@jterrazz/intelligence';
19
+ import { generateText } from 'ai';
25
20
 
26
- const result = await generateStructured({
27
- model,
28
- prompt: 'Analyze this article...',
29
- schema,
30
- providerOptions: withObservability({ traceId: 'trace-123' }),
21
+ const intelligence = createIntelligence({
22
+ providers: {
23
+ openrouter: {
24
+ type: 'openrouter',
25
+ apiKey: process.env.OPENROUTER_API_KEY,
26
+ metadata: { application: 'my-app', website: 'https://example.com' },
27
+ },
28
+ localProxy: {
29
+ type: 'gateway',
30
+ baseURL: 'https://my-gateway.example.com/v1',
31
+ apiKey: process.env.PROXY_API_KEY,
32
+ },
33
+ },
34
+ agents: {
35
+ summarizer: {
36
+ provider: 'openrouter',
37
+ model: 'google/gemini-2.5-flash-lite',
38
+ fallback: { provider: 'openrouter', model: 'openai/gpt-4o-mini' },
39
+ },
40
+ localAgent: {
41
+ provider: 'localProxy',
42
+ model: 'some-local-model',
43
+ },
44
+ },
45
+ pricing: {
46
+ // USD per million tokens — used only when the provider doesn't report actual cost
47
+ 'openrouter/openai/gpt-4o-mini': { input: 0.15, output: 0.6 },
48
+ },
49
+ logger, // LoggerPort from @jterrazz/telemetry, optional
31
50
  });
32
51
 
33
- if (result.success) {
34
- console.log(result.data.sentiment, result.data.score);
35
- } else {
36
- // Typed error with code: TIMEOUT | RATE_LIMITED | PARSING_FAILED | etc.
37
- console.error(result.error.code, result.error.message);
38
- }
52
+ const model = intelligence.model('summarizer');
53
+ const { text } = await generateText({ model, prompt: 'Summarize this article...' });
39
54
  ```
40
55
 
41
- ## Result Utilities
56
+ Each agent has a `provider` (a key into `providers`) and a `model` (the technical model id, passed through to the provider as-is — e.g. `'anthropic/claude-sonnet-4'` for a model id that itself contains a `/`). `pricing` is keyed by `"<provider>/<model>"`, joining those same two fields.
42
57
 
43
- Discriminated union result type for explicit error handling.
58
+ `intelligence.model(agentName)` builds each model lazily and caches it — calling it twice for the same agent returns the same instance.
59
+
60
+ ## What `createIntelligence` wires up
61
+
62
+ For each resolved model reference:
63
+
64
+ 1. The provider's base model (via `createOpenRouterProvider` or `createGatewayProvider`).
65
+ 2. `createCostMiddleware` — records the generation's USD cost on the active OpenTelemetry span.
66
+ 3. If the agent has a `fallback`, `createFallbackModel` wraps primary + fallback with automatic retry-on-failure.
67
+ 4. If a `logger` is configured, `createLoggingMiddleware` wraps the whole thing.
68
+
69
+ On first use, `createIntelligence` registers the AI SDK's OpenTelemetry integration (`@ai-sdk/otel`) globally. This is idempotent and best-effort — if the host app hasn't set up an OpenTelemetry SDK, this is a no-op rather than an error.
70
+
71
+ ## Building blocks
72
+
73
+ Each piece is also exported individually if you want to compose things yourself instead of using `createIntelligence`.
74
+
75
+ ### Providers
44
76
 
45
77
  ```typescript
46
- import {
47
- generationSuccess,
48
- generationFailure,
49
- isSuccess,
50
- isFailure,
51
- unwrap,
52
- unwrapOr,
53
- classifyError,
54
- type GenerationResult,
55
- } from '@jterrazz/intelligence';
56
-
57
- // Create results
58
- const success = generationSuccess({ data: 'value' });
59
- const failure = generationFailure('TIMEOUT', 'Request timed out');
60
-
61
- // Type guards
62
- if (isSuccess(result)) {
63
- console.log(result.data);
64
- }
78
+ import { createOpenRouterProvider, createGatewayProvider } from '@jterrazz/intelligence';
65
79
 
66
- // Unwrap with default
67
- const value = unwrapOr(result, defaultValue);
80
+ const openrouter = createOpenRouterProvider({
81
+ apiKey: process.env.OPENROUTER_API_KEY,
82
+ metadata: { application: 'my-app', website: 'https://example.com' },
83
+ });
84
+ const model = openrouter.model('anthropic/claude-sonnet-4-20250514');
68
85
 
69
- // Classify errors automatically
70
- try {
71
- await someOperation();
72
- } catch (error) {
73
- const code = classifyError(error); // TIMEOUT, RATE_LIMITED, PARSING_FAILED, etc.
74
- }
86
+ // Per-call options (reasoning effort, max tokens, ...) now go through providerOptions
87
+ // at the call site instead of the provider factory:
88
+ await generateText({
89
+ model,
90
+ prompt: 'Hello!',
91
+ providerOptions: { openrouter: { reasoning: { effort: 'high' } } },
92
+ });
75
93
  ```
76
94
 
77
- ## Middleware
95
+ ```typescript
96
+ const proxy = createGatewayProvider({
97
+ baseURL: 'https://my-gateway.example.com/v1',
98
+ apiKey: process.env.PROXY_API_KEY,
99
+ });
100
+ const model = proxy.model('some-model-id');
101
+ ```
78
102
 
79
- Composable middlewares that wrap AI SDK models. Stack them together for logging, observability, and more.
103
+ `createGatewayProvider` targets the chat completions endpoint (`.chat()`, not the Responses API) for maximum compatibility with gateways exposing any API implementing the OpenAI chat completions spec, and wraps every model with AI SDK's `extractJsonMiddleware` — a safety net that strips markdown code fences from responses. This matters for gateways that sometimes wrap JSON output in ` ```json ` fences even when structured output was requested.
80
104
 
81
- ### Composing Middlewares
105
+ ### Cost middleware
82
106
 
83
107
  ```typescript
84
108
  import { wrapLanguageModel } from 'ai';
85
- import {
86
- createLoggingMiddleware,
87
- createObservabilityMiddleware,
88
- LangfuseAdapter,
89
- OpenRouterMetadataAdapter,
90
- } from '@jterrazz/intelligence';
109
+ import { createCostMiddleware } from '@jterrazz/intelligence';
91
110
 
92
111
  const model = wrapLanguageModel({
93
- model: provider.model('anthropic/claude-sonnet-4-20250514'),
112
+ model: provider.model('google/gemini-2.5-flash-lite'),
94
113
  middleware: [
95
- createLoggingMiddleware({ logger, include: { usage: true } }),
96
- createObservabilityMiddleware({
97
- observability: new LangfuseAdapter({
98
- secretKey: process.env.LANGFUSE_SECRET_KEY,
99
- publicKey: process.env.LANGFUSE_PUBLIC_KEY,
100
- }),
101
- providerMetadata: new OpenRouterMetadataAdapter(),
114
+ createCostMiddleware({
115
+ modelRef: 'openrouter/google/gemini-2.5-flash-lite',
116
+ // Only used as a fallback when the provider doesn't report actual cost
117
+ pricing: { input: 0.1, output: 0.4 }, // USD per million tokens
102
118
  }),
103
119
  ],
104
120
  });
105
121
  ```
106
122
 
107
- ### Logging Middleware
123
+ Cost resolution order:
124
+
125
+ 1. Actual cost reported by the provider (currently: OpenRouter's `providerMetadata.openrouter.usage.cost`), when present and greater than zero.
126
+ 2. Otherwise, an estimate from `pricing` and the reported token usage.
127
+
128
+ When a cost is determined, it's set as the `gen_ai.usage.cost` attribute on `trace.getActiveSpan()`. This is the attribute Langfuse's OpenTelemetry ingestion prioritizes over its own cost inference — `langfuse.observation.cost_details` is buggy on ingestion, so this package deliberately avoids it. All enrichment is best-effort: it never throws, even with no active span or a broken telemetry backend.
129
+
130
+ ### Fallback model
131
+
132
+ ```typescript
133
+ import { createFallbackModel } from '@jterrazz/intelligence';
134
+
135
+ const model = createFallbackModel({
136
+ primary: provider.model('anthropic/claude-sonnet-4'),
137
+ fallback: provider.model('openai/gpt-4o-mini'),
138
+ logger, // optional — logs 'ai.fallback.triggered' when the switch happens
139
+ });
140
+ ```
141
+
142
+ `createFallbackModel` returns a model (implementing `LanguageModelV4`), not a middleware — a middleware can't swap the underlying model. It retries on the fallback only for retryable errors: HTTP 429, 5xx, and network errors (connection refused/reset, timeouts). Non-retryable errors (400s, validation errors, aborts) propagate unchanged.
108
143
 
109
- Logs AI SDK requests with timing, usage, and optional content.
144
+ ### Logging middleware
110
145
 
111
146
  ```typescript
112
- import { wrapLanguageModel, generateText } from 'ai';
147
+ import { wrapLanguageModel } from 'ai';
113
148
  import { createLoggingMiddleware } from '@jterrazz/intelligence';
114
149
 
115
150
  const model = wrapLanguageModel({
@@ -123,194 +158,175 @@ const model = wrapLanguageModel({
123
158
  },
124
159
  }),
125
160
  });
126
-
127
- await generateText({ model, prompt: 'Hello!' });
128
- // Logs: ai.generate.start, ai.generate.complete (with durationMs, usage, etc.)
129
161
  ```
130
162
 
131
- ### Observability Middleware
163
+ Logs `ai.generate.start` / `ai.generate.complete` / `ai.generate.error` (and the `ai.stream.*` equivalents) with timing and usage.
132
164
 
133
- Sends generation data to observability platforms (Langfuse, etc.).
165
+ ### `cleanAiText` / `toSentenceCase` text formatting utilities
166
+
167
+ Dependency-free — import them from `@jterrazz/intelligence/formatting` to avoid installing `ai`.
134
168
 
135
169
  ```typescript
136
- import { wrapLanguageModel, generateText } from 'ai';
137
- import {
138
- createObservabilityMiddleware,
139
- withObservability,
140
- LangfuseAdapter,
141
- } from '@jterrazz/intelligence';
142
-
143
- const observability = new LangfuseAdapter({
144
- secretKey: process.env.LANGFUSE_SECRET_KEY,
145
- publicKey: process.env.LANGFUSE_PUBLIC_KEY,
146
- });
170
+ import { cleanAiText, toSentenceCase } from '@jterrazz/intelligence/formatting';
147
171
 
148
- const model = wrapLanguageModel({
149
- model: provider.model('anthropic/claude-sonnet-4-20250514'),
150
- middleware: createObservabilityMiddleware({ observability }),
151
- });
172
+ const clean = cleanAiText(messyAiOutput);
173
+ // Removes: BOM, zero-width chars, citation markers
174
+ // Normalizes: smart quotes, em dashes, ellipsis
152
175
 
153
- // Use withObservability() helper for type-safe metadata
154
- await generateText({
155
- model,
156
- prompt: 'Analyze this...',
157
- providerOptions: withObservability({
158
- traceId: 'trace-123',
159
- name: 'analyzer',
160
- metadata: { userId: 'user-1' },
161
- }),
162
- });
176
+ const headline = toSentenceCase('Your Next AI Skill Is Worldbuilding');
177
+ // -> "Your next AI skill is worldbuilding"
163
178
  ```
164
179
 
165
- ### Custom Adapters
180
+ ## Agent & prompt conventions
166
181
 
167
- Implement ports to integrate with any platform:
168
-
169
- ```typescript
170
- import type { ObservabilityPort, ProviderMetadataPort } from "@jterrazz/intelligence";
171
-
172
- // Observability adapter (Datadog, etc.)
173
- class DatadogAdapter implements ObservabilityPort {
174
- trace(params) { /* ... */ }
175
- generation(params) { /* ... */ }
176
- async flush() { /* ... */ }
177
- async shutdown() { /* ... */ }
178
- }
182
+ `@jterrazz/intelligence` gives you the `LanguageModel`; how you organize the agents that call it is up to the host repo, but here's the folder-per-agent convention we've converged on across `@jterrazz` projects:
179
183
 
180
- // Provider metadata adapter (extract usage/cost)
181
- class AnthropicMetadataAdapter implements ProviderMetadataPort {
182
- extract(metadata) {
183
- return { usage: { ... }, cost: { ... } };
184
- }
185
- }
184
+ ```
185
+ agents/
186
+ article-composer/
187
+ article-composer.ts # the agent class canonical, framework-visible
188
+ article-composer.prompt.ts # the prompt, and only the prompt
189
+ event-assigner/
190
+ event-assigner.ts
191
+ event-assigner.prompt.ts
192
+ _shared/
193
+ category-taxonomy.prompt.ts # sections shared across agents
186
194
  ```
187
195
 
188
- ## Parsing Utilities
189
-
190
- ### `parseObject` - Extract structured data from AI responses
191
-
192
- Extracts and validates JSON from messy AI outputs (markdown blocks, malformed syntax).
193
-
194
- ````typescript
195
- import { parseObject } from '@jterrazz/intelligence';
196
- import { z } from 'zod';
197
-
198
- const schema = z.object({
199
- title: z.string(),
200
- tags: z.array(z.string()),
201
- });
196
+ Large agents can split their prompt across several `<name>-<section>.prompt.ts` files instead of one.
202
197
 
203
- const text = '```json\n{"title": "Hello", "tags": ["ai"]}\n```';
204
- const result = parseObject(text, schema);
205
- // { title: "Hello", tags: ["ai"] }
206
- ````
198
+ The hard rule: **no multi-line natural-language literal outside `*.prompt.ts`.** The class only shapes data — filter, sort, map to flat records, `JSON.stringify` — and hands the builder plain values, never assembled prose. `generateText`/`Output.object` stay visible in `run()`, unabstracted:
207
199
 
208
- ### `createSchemaPrompt` - Generate schema instructions
200
+ ```typescript
201
+ export class ArticleComposer implements ArticleComposerPort {
202
+ static readonly SCHEMA = z.object({/* ... */});
203
+
204
+ constructor(
205
+ private readonly model: LanguageModel,
206
+ private readonly logger: LoggerPort,
207
+ ) {}
208
+
209
+ async run(input: ArticleCompositionInput): Promise<ArticleCompositionResult> {
210
+ const { output } = await generateText({
211
+ model: this.model,
212
+ output: Output.object({ schema: ArticleComposer.SCHEMA }),
213
+ prompt: buildPrompt({
214
+ angle: input.angle,
215
+ factsJson: ArticleComposer.buildFactsJson(input.facts),
216
+ history: ArticleComposer.buildHistoryRecords(input.previousArticles),
217
+ }),
218
+ });
219
+ return output;
220
+ }
221
+
222
+ // Data prep only — map/sort/JSON.stringify/flatten to records. No prose.
223
+ private static buildFactsJson(facts: Fact[]): string {
224
+ /* ... */
225
+ }
226
+
227
+ private static buildHistoryRecords(articles?: PreviousArticle[]): HistoryArticle[] {
228
+ /* sort chronologically, map to { angle, body, date, headline }[] */
229
+ }
230
+ }
231
+ ```
209
232
 
210
- Creates system prompt instructions for models without native structured output.
233
+ The sibling `*.prompt.ts` holds all the prose. It's not limited to one `buildPrompt` — it can export several builders, a main one plus section builders it calls internally, each owning its own empty-case handling:
211
234
 
212
235
  ```typescript
213
- import { generateText } from 'ai';
214
- import { createSchemaPrompt, parseObject } from '@jterrazz/intelligence';
215
- import { z } from 'zod';
236
+ // article-composer.prompt.ts
237
+ interface HistoryArticle {
238
+ angle: string;
239
+ body: string;
240
+ date: string;
241
+ headline: string;
242
+ }
216
243
 
217
- const schema = z.object({ summary: z.string(), score: z.number() });
244
+ interface Variables {
245
+ angle: string;
246
+ factsJson: string;
247
+ history: HistoryArticle[];
248
+ }
218
249
 
219
- const { text } = await generateText({
220
- model,
221
- prompt: 'Analyze this article...',
222
- system: createSchemaPrompt(schema),
223
- });
250
+ export const buildHistorySection = (articles: HistoryArticle[]): string => {
251
+ if (articles.length === 0) {
252
+ return '';
253
+ }
224
254
 
225
- const result = parseObject(text, schema);
226
- ```
255
+ return `\n\n## Previously Published Articles\n\n${articles
256
+ .map((a) => `### ${a.headline}\n${a.body}`)
257
+ .join('\n\n')}`;
258
+ };
227
259
 
228
- ### `parseText` - Sanitize AI-generated text
260
+ export const buildPrompt = (v: Variables): string => {
261
+ return `# Article Composition
229
262
 
230
- Removes invisible characters, normalizes typography, cleans AI artifacts.
263
+ Your angle for this article: **${v.angle}**.
231
264
 
232
- ```typescript
233
- import { parseText } from '@jterrazz/intelligence';
265
+ ## Facts
234
266
 
235
- const clean = parseText(messyAiOutput);
236
- // Removes: BOM, zero-width chars, citation markers
237
- // Normalizes: smart quotes, em dashes, ellipsis
267
+ ${v.factsJson}${buildHistorySection(v.history)}`;
268
+ };
238
269
  ```
239
270
 
240
- ## Provider
271
+ Sections reused across agents (a category taxonomy needed by both an assigner and an ingester, say) live in `_shared/<name>.prompt.ts` as `export const <name> = (): string => { ... }` and get imported wherever they're needed — never copy-pasted between prompt files.
241
272
 
242
- ### `createOpenRouterProvider` - OpenRouter for AI SDK
273
+ Why: this is the shape most reference TypeScript AI codebases converge on, and it gives the prompt↔variables contract native TypeScript typing instead of loose string interpolation.
243
274
 
244
- ```typescript
245
- import { generateText } from 'ai';
246
- import { createOpenRouterProvider } from '@jterrazz/intelligence';
275
+ ### Enforcing it — `@jterrazz/intelligence/oxlint`
247
276
 
248
- const provider = createOpenRouterProvider({
249
- apiKey: process.env.OPENROUTER_API_KEY,
250
- });
277
+ These conventions are mechanized as an oxlint plugin, shaped like `@jterrazz/test`'s own `oxlint` export: a `LintPlugin` of `intelligence/*` rules, a manifest of `RULE_DOCS`, and a composable `intelligence` fragment.
251
278
 
252
- const { text } = await generateText({
253
- model: provider.model('anthropic/claude-sonnet-4-20250514'),
254
- prompt: 'Hello!',
255
- });
279
+ ```typescript
280
+ import { compose, node } from '@jterrazz/typescript/oxlint';
281
+ import { intelligence } from '@jterrazz/intelligence/oxlint';
256
282
 
257
- // With reasoning models
258
- const reasoningModel = provider.model('anthropic/claude-sonnet-4-20250514', {
259
- maxTokens: 16000,
260
- reasoning: { effort: 'high' },
261
- });
283
+ export default compose(node, intelligence);
262
284
  ```
263
285
 
264
- ## API Reference
286
+ `compose()` (from `@jterrazz/typescript/oxlint`) merges any number of fragments — add `@jterrazz/test`'s `testing` fragment, `hexagonal`, or your own overrides the same way: `compose(node, hexagonal, testing, intelligence, { rules: {...} })`.
265
287
 
266
- ### Generation
288
+ | Rule | Severity | Enforces |
289
+ | ----------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
290
+ | `intelligence/p1-prose-in-prompt-files` | error | No multi-line natural-language template literal outside `*.prompt.ts` — the flagship rule for this whole convention. |
291
+ | `intelligence/p2-prompt-file-exports` | error | A `*.prompt.ts` file exports only const string-builder functions and types/interfaces. |
292
+ | `intelligence/p3-agent-prompt-sibling` | error | An agent file imports `./<name>.prompt.js`; a non-`_shared/` prompt file has its `<name>.ts` sibling on disk. |
293
+ | `intelligence/g1-agent-class-shape` | error | An agent file exports exactly one class with `static readonly SCHEMA`, a `run` method, and `constructor(model, ...)`. |
294
+ | `intelligence/m1-model-resolution-in-container` | error | `createIntelligence`/`createGatewayProvider`/`createOpenRouterProvider` and `.model('…')` resolution are DI/container-only. |
295
+ | `intelligence/m2w-no-hardcoded-model-id` | warning | A string literal shaped like a model id outside config/test/fixture files — model ids belong in configuration. |
267
296
 
268
- | Export | Description |
269
- | ----------------------------- | ------------------------------------------------------ |
270
- | `generateStructured(options)` | Generate and parse structured data with error handling |
297
+ Each rule is deliberately best-effort where full static verification isn't possible (documented per rule via `meta.docs` / the manifest) — the goal is catching the common slip, not a type checker.
298
+
299
+ ## API Reference
271
300
 
272
- ### Result
301
+ ### Factory
273
302
 
274
- | Export | Description |
275
- | ------------------------------------------ | -------------------------------------------------------- |
276
- | `GenerationResult<T>` | Discriminated union result type |
277
- | `generationSuccess(data)` | Create success result |
278
- | `generationFailure(code, message, cause?)` | Create failure result |
279
- | `isSuccess(result)` | Type guard for success |
280
- | `isFailure(result)` | Type guard for failure |
281
- | `unwrap(result)` | Extract data or throw |
282
- | `unwrapOr(result, default)` | Extract data or return default |
283
- | `classifyError(error)` | Classify error into error code |
284
- | `GenerationErrorCode` | Error codes: TIMEOUT, RATE_LIMITED, PARSING_FAILED, etc. |
303
+ | Export | Description |
304
+ | ---------------------------- | ------------------------------------------------------------ |
305
+ | `createIntelligence(config)` | Config-driven factory: agents → fully wired `LanguageModel`s |
285
306
 
286
307
  ### Middleware
287
308
 
288
- | Export | Description |
289
- | ---------------------------------------- | -------------------------------------------- |
290
- | `createLoggingMiddleware(options)` | Creates logging middleware |
291
- | `createObservabilityMiddleware(options)` | Creates observability middleware |
292
- | `withObservability(meta)` | Helper for type-safe observability metadata |
293
- | `LangfuseAdapter` | Langfuse implementation of ObservabilityPort |
294
- | `NoopObservabilityAdapter` | No-op adapter for testing/development |
295
- | `OpenRouterMetadataAdapter` | Extract usage/cost from OpenRouter |
309
+ | Export | Description |
310
+ | ---------------------------------- | --------------------------------------------- |
311
+ | `createCostMiddleware(options)` | Records USD cost on the active OTel span |
312
+ | `createLoggingMiddleware(options)` | Logs requests/responses with timing and usage |
296
313
 
297
- ### Ports
314
+ ### Model
298
315
 
299
- | Export | Description |
300
- | ---------------------- | ------------------------------------------ |
301
- | `ObservabilityPort` | Interface for observability adapters |
302
- | `ProviderMetadataPort` | Interface for provider metadata extraction |
316
+ | Export | Description |
317
+ | ------------------------------ | -------------------------------------------------- |
318
+ | `createFallbackModel(options)` | A `LanguageModel` that retries on a fallback model |
303
319
 
304
- ### Parsing
320
+ ### Providers
305
321
 
306
- | Export | Description |
307
- | ---------------------------- | ---------------------------------------- |
308
- | `parseObject(text, schema)` | Parse and validate JSON from AI output |
309
- | `createSchemaPrompt(schema)` | Generate schema instructions for prompts |
310
- | `parseText(text, options?)` | Sanitize AI-generated text |
322
+ | Export | Description |
323
+ | ---------------------------------- | -------------------------------------------------------- |
324
+ | `createOpenRouterProvider(config)` | OpenRouter provider for AI SDK |
325
+ | `createGatewayProvider(config)` | Provider for any gateway exposing a chat-completions API |
311
326
 
312
- ### Provider
327
+ ### Formatting
313
328
 
314
- | Export | Description |
315
- | ---------------------------------- | ------------------------------------- |
316
- | `createOpenRouterProvider(config)` | Create OpenRouter provider for AI SDK |
329
+ | Export | Description |
330
+ | -------------------------------- | -------------------------------------------------------------------------- |
331
+ | `cleanAiText(text, options?)` | Sanitize AI-generated text (also at `/formatting`) |
332
+ | `toSentenceCase(text, options?)` | Normalize Title Case overuse back to sentence case (also at `/formatting`) |