Skip to content

Commit 9c68bcc

Browse files
tombeckenhamautofix-ci[bot]claude
authored
feat(ai-openrouter): upgrade openrouter SDK to 0.12.14, add builtin structured output support, and refresh models (#312)
* Update openrouter package, models, and scripts to generate openrouter models Fixes #310 * ci: apply automated fixes * Added compare script and fixed key name issue in fetch models Fixes #310 * resolved coderabbit issues * ci: apply automated fixes * Added support for openrouter structured output * Address PR #312 review feedback: improve error handling and cleanup - Add explicit guard for empty content in structuredOutput before JSON.parse - Remove redundant Sets in compare script (Map.has() is already O(1)) - Suppress stderr leaks from execSync in compare script - Update example to use valid model name Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update model metadata and pricing in OpenRouter, add new models, and refactor parameter handling - Introduced new models: AI21 Jamba Large 1.7, AionLabs Aion-1.0, AionLabs Aion-1.0 Mini, AionLabs Aion-2.0, AionLabs Aion-RP Llama 3.1 8B, AlfredPros CodeLLaMa 7B Instruct Solidity, and Tongyi DeepResearch 30B A3B. - Updated existing model parameters, including context windows and max output tokens. - Refactored parameter handling in scripts to improve consistency and readability, including the introduction of a mapping function for API parameters. - Adjusted pricing structures for several models to reflect updated costs. - Ensured all model entries are sorted for better organization. * ci: apply automated fixes * Update openrouter package models and added prettier to fetch script Fixes #310 * Updated models and script Fixes #310 * ci: apply automated fixes * Update openrouter package to 0.9.11 Fixes #310 * Refactor OpenRouter options passthrough and bump fal client Spread modelOptions first in request construction so provider-specific options pass through correctly, only override with explicit options when defined. Remove unused InternalTextProviderOptions import. Bump @fal-ai/client to ^1.9.4. Fixes #310 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Refactor text-provider-options to derive types from SDK's ChatGenerationParams Replace hand-written interfaces with type aliases derived from @openrouter/sdk's ChatGenerationParams, eliminating type drift and keeping provider options aligned with the SDK automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: apply automated fixes * Changeset updated Fixes #310 * ci: apply automated fixes * chore(ai-openrouter): upgrade @openrouter/sdk to 0.12.13 The SDK renamed every chat-related exported type (ChatGenerationParams → ChatRequest, ChatResponse → ChatResult, etc.) and renamed the request wrapper key on chat.send from chatGenerationParams to chatRequest. Migrate adapters and tests to the new names. The SDK also narrowed ChatRequest to OpenAI-compatible fields, so Zod strips topK/topA/minP/repetitionPenalty/includeReasoning/verbosity/ webSearchOptions from outbound requests. Drop these keys from OpenRouterBaseOptions and the model catalog so callers get a TS error instead of silent no-op behavior, and add them to excludedParams in the catalog generator so future syncs stay honest. Also restore the SDK-derived shape of text-provider-options that was lost to an upstream merge, re-keyed off ChatRequest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * chore(ai-openrouter): expand changeset, wire format into generate:models Rewrite the ai-openrouter changeset to reflect the full scope of the PR: the 0.12.13 SDK bump (not 0.9.11), the ChatGenerationParams -> ChatRequest rename, and the deliberate removal of topK/topA/minP/repetitionPenalty/ includeReasoning/verbosity/webSearchOptions from OpenRouterBaseOptions so callers get a TS error instead of silent stripping. Append pnpm format to generate:models so regenerated model-meta and provider files land formatted in a single command. Remove the now-unused compare-openrouter-models.ts script — the models file is sorted alphabetically so a plain git diff is sufficient. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Corrected openrouter params in example Fixes #310 * chore(ai-openrouter): bump @openrouter/sdk to 0.12.14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ai-openrouter): clarify changeset, tidy image error check, document common options Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ai-openrouter): explain modelOptions-first spread ordering in text adapter Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai-openrouter): apply OpenAI-strict transformation to structuredOutput schemas OpenRouter forwards `json_schema` requests with `strict: true` to upstream providers (notably OpenAI), which reject schemas that don't mark every property required and set `additionalProperties: false`. Run the schema through `convertSchemaToJsonSchema(..., { forStructuredOutput: true })` before sending so Zod / ArkType / Valibot schemas work out of the box. Adds adapter-level regression tests covering the transformation (nested objects, arrays, optional-to-nullable widening) and a structured-output example page in `ts-react-chat` to exercise the path end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cb07e01 commit 9c68bcc

File tree

22 files changed

+17138
-9534
lines changed

22 files changed

+17138
-9534
lines changed

.changeset/giant-garlics-crash.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@tanstack/ai-openrouter': patch
3+
---
4+
5+
- Upgrade `@openrouter/sdk` to 0.12.14 (from 0.3.15)
6+
- Migrate adapters and tests to the SDK's renamed chat types (`ChatGenerationParams``ChatRequest`, `ChatResponse``ChatResult`) and the renamed `chatRequest` key on `chat.send`
7+
- Derive `text-provider-options` types from the SDK's `ChatRequest` so provider options stay in lockstep with the SDK surface
8+
- Drop `topK` / `topA` / `minP` / `repetitionPenalty` / `includeReasoning` / `verbosity` / `webSearchOptions` from `OpenRouterBaseOptions` now that the SDK narrows `ChatRequest` to OpenAI-compatible fields — callers passing these previously saw them silently stripped at runtime, and now get a TypeScript error instead
9+
- Add those same keys to `excludedParams` in the model-catalog generator so future syncs don't reintroduce them
10+
- Switch `structuredOutput` to the SDK's native `responseFormat: { type: 'json_schema' }` instead of coercing the model via a forced `structured_output` tool call — the schema now flows straight to the provider, yielding cleaner responses and dropping the dummy-tool round-trip
11+
- Apply the OpenAI-strict schema transformation (`additionalProperties: false`, all properties required, optional fields widened to include `null`) inside `structuredOutput` before forwarding — without this, upstream OpenAI rejected the request with "Provider returned error" for schemas generated by Zod / ArkType / Valibot
12+
- Refactor options passthrough to use camelCase naming convention
13+
- Refresh the model catalog with the latest OpenRouter models (Opus 4.6, Sonnet 4.6, Gemini 3.1 Pro, etc.) and remove the deprecated `openrouter/auto` model
14+
- Unify the outbound request payload envelope
15+
- Improve error handling and add tests for nested payloads, structured output parsing, and error cases

.changeset/sync-models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
'@tanstack/ai-anthropic': patch
23
'@tanstack/ai-openrouter': patch
34
---
45

examples/ts-react-chat/src/components/Header.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Link } from '@tanstack/react-router'
22

33
import { useState } from 'react'
44
import {
5+
Braces,
56
FileAudio,
67
FileText,
78
Guitar,
@@ -138,6 +139,19 @@ export default function Header() {
138139
<span className="font-medium">Video Generation</span>
139140
</Link>
140141

142+
<Link
143+
to="/generations/structured-output"
144+
onClick={() => setIsOpen(false)}
145+
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-1"
146+
activeProps={{
147+
className:
148+
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-1',
149+
}}
150+
>
151+
<Braces size={20} />
152+
<span className="font-medium">Structured Output (OpenRouter)</span>
153+
</Link>
154+
141155
<hr className="border-gray-700 my-2" />
142156

143157
<p className="text-xs text-gray-500 uppercase tracking-wider px-3 pt-2 pb-1">

examples/ts-react-chat/src/routeTree.gen.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import { Route as IndexRouteImport } from './routes/index'
1515
import { Route as GenerationsVideoRouteImport } from './routes/generations.video'
1616
import { Route as GenerationsTranscriptionRouteImport } from './routes/generations.transcription'
1717
import { Route as GenerationsSummarizeRouteImport } from './routes/generations.summarize'
18+
import { Route as GenerationsStructuredOutputRouteImport } from './routes/generations.structured-output'
1819
import { Route as GenerationsSpeechRouteImport } from './routes/generations.speech'
1920
import { Route as GenerationsImageRouteImport } from './routes/generations.image'
2021
import { Route as ApiTranscribeRouteImport } from './routes/api.transcribe'
2122
import { Route as ApiTanchatRouteImport } from './routes/api.tanchat'
2223
import { Route as ApiSummarizeRouteImport } from './routes/api.summarize'
24+
import { Route as ApiStructuredOutputRouteImport } from './routes/api.structured-output'
2325
import { Route as ApiImageGenRouteImport } from './routes/api.image-gen'
2426
import { Route as ExampleGuitarsIndexRouteImport } from './routes/example.guitars/index'
2527
import { Route as ExampleGuitarsGuitarIdRouteImport } from './routes/example.guitars/$guitarId'
@@ -58,6 +60,12 @@ const GenerationsSummarizeRoute = GenerationsSummarizeRouteImport.update({
5860
path: '/generations/summarize',
5961
getParentRoute: () => rootRouteImport,
6062
} as any)
63+
const GenerationsStructuredOutputRoute =
64+
GenerationsStructuredOutputRouteImport.update({
65+
id: '/generations/structured-output',
66+
path: '/generations/structured-output',
67+
getParentRoute: () => rootRouteImport,
68+
} as any)
6169
const GenerationsSpeechRoute = GenerationsSpeechRouteImport.update({
6270
id: '/generations/speech',
6371
path: '/generations/speech',
@@ -83,6 +91,11 @@ const ApiSummarizeRoute = ApiSummarizeRouteImport.update({
8391
path: '/api/summarize',
8492
getParentRoute: () => rootRouteImport,
8593
} as any)
94+
const ApiStructuredOutputRoute = ApiStructuredOutputRouteImport.update({
95+
id: '/api/structured-output',
96+
path: '/api/structured-output',
97+
getParentRoute: () => rootRouteImport,
98+
} as any)
8699
const ApiImageGenRoute = ApiImageGenRouteImport.update({
87100
id: '/api/image-gen',
88101
path: '/api/image-gen',
@@ -119,11 +132,13 @@ export interface FileRoutesByFullPath {
119132
'/image-gen': typeof ImageGenRoute
120133
'/realtime': typeof RealtimeRoute
121134
'/api/image-gen': typeof ApiImageGenRoute
135+
'/api/structured-output': typeof ApiStructuredOutputRoute
122136
'/api/summarize': typeof ApiSummarizeRoute
123137
'/api/tanchat': typeof ApiTanchatRoute
124138
'/api/transcribe': typeof ApiTranscribeRoute
125139
'/generations/image': typeof GenerationsImageRoute
126140
'/generations/speech': typeof GenerationsSpeechRoute
141+
'/generations/structured-output': typeof GenerationsStructuredOutputRoute
127142
'/generations/summarize': typeof GenerationsSummarizeRoute
128143
'/generations/transcription': typeof GenerationsTranscriptionRoute
129144
'/generations/video': typeof GenerationsVideoRoute
@@ -138,11 +153,13 @@ export interface FileRoutesByTo {
138153
'/image-gen': typeof ImageGenRoute
139154
'/realtime': typeof RealtimeRoute
140155
'/api/image-gen': typeof ApiImageGenRoute
156+
'/api/structured-output': typeof ApiStructuredOutputRoute
141157
'/api/summarize': typeof ApiSummarizeRoute
142158
'/api/tanchat': typeof ApiTanchatRoute
143159
'/api/transcribe': typeof ApiTranscribeRoute
144160
'/generations/image': typeof GenerationsImageRoute
145161
'/generations/speech': typeof GenerationsSpeechRoute
162+
'/generations/structured-output': typeof GenerationsStructuredOutputRoute
146163
'/generations/summarize': typeof GenerationsSummarizeRoute
147164
'/generations/transcription': typeof GenerationsTranscriptionRoute
148165
'/generations/video': typeof GenerationsVideoRoute
@@ -158,11 +175,13 @@ export interface FileRoutesById {
158175
'/image-gen': typeof ImageGenRoute
159176
'/realtime': typeof RealtimeRoute
160177
'/api/image-gen': typeof ApiImageGenRoute
178+
'/api/structured-output': typeof ApiStructuredOutputRoute
161179
'/api/summarize': typeof ApiSummarizeRoute
162180
'/api/tanchat': typeof ApiTanchatRoute
163181
'/api/transcribe': typeof ApiTranscribeRoute
164182
'/generations/image': typeof GenerationsImageRoute
165183
'/generations/speech': typeof GenerationsSpeechRoute
184+
'/generations/structured-output': typeof GenerationsStructuredOutputRoute
166185
'/generations/summarize': typeof GenerationsSummarizeRoute
167186
'/generations/transcription': typeof GenerationsTranscriptionRoute
168187
'/generations/video': typeof GenerationsVideoRoute
@@ -179,11 +198,13 @@ export interface FileRouteTypes {
179198
| '/image-gen'
180199
| '/realtime'
181200
| '/api/image-gen'
201+
| '/api/structured-output'
182202
| '/api/summarize'
183203
| '/api/tanchat'
184204
| '/api/transcribe'
185205
| '/generations/image'
186206
| '/generations/speech'
207+
| '/generations/structured-output'
187208
| '/generations/summarize'
188209
| '/generations/transcription'
189210
| '/generations/video'
@@ -198,11 +219,13 @@ export interface FileRouteTypes {
198219
| '/image-gen'
199220
| '/realtime'
200221
| '/api/image-gen'
222+
| '/api/structured-output'
201223
| '/api/summarize'
202224
| '/api/tanchat'
203225
| '/api/transcribe'
204226
| '/generations/image'
205227
| '/generations/speech'
228+
| '/generations/structured-output'
206229
| '/generations/summarize'
207230
| '/generations/transcription'
208231
| '/generations/video'
@@ -217,11 +240,13 @@ export interface FileRouteTypes {
217240
| '/image-gen'
218241
| '/realtime'
219242
| '/api/image-gen'
243+
| '/api/structured-output'
220244
| '/api/summarize'
221245
| '/api/tanchat'
222246
| '/api/transcribe'
223247
| '/generations/image'
224248
| '/generations/speech'
249+
| '/generations/structured-output'
225250
| '/generations/summarize'
226251
| '/generations/transcription'
227252
| '/generations/video'
@@ -237,11 +262,13 @@ export interface RootRouteChildren {
237262
ImageGenRoute: typeof ImageGenRoute
238263
RealtimeRoute: typeof RealtimeRoute
239264
ApiImageGenRoute: typeof ApiImageGenRoute
265+
ApiStructuredOutputRoute: typeof ApiStructuredOutputRoute
240266
ApiSummarizeRoute: typeof ApiSummarizeRoute
241267
ApiTanchatRoute: typeof ApiTanchatRoute
242268
ApiTranscribeRoute: typeof ApiTranscribeRoute
243269
GenerationsImageRoute: typeof GenerationsImageRoute
244270
GenerationsSpeechRoute: typeof GenerationsSpeechRoute
271+
GenerationsStructuredOutputRoute: typeof GenerationsStructuredOutputRoute
245272
GenerationsSummarizeRoute: typeof GenerationsSummarizeRoute
246273
GenerationsTranscriptionRoute: typeof GenerationsTranscriptionRoute
247274
GenerationsVideoRoute: typeof GenerationsVideoRoute
@@ -296,6 +323,13 @@ declare module '@tanstack/react-router' {
296323
preLoaderRoute: typeof GenerationsSummarizeRouteImport
297324
parentRoute: typeof rootRouteImport
298325
}
326+
'/generations/structured-output': {
327+
id: '/generations/structured-output'
328+
path: '/generations/structured-output'
329+
fullPath: '/generations/structured-output'
330+
preLoaderRoute: typeof GenerationsStructuredOutputRouteImport
331+
parentRoute: typeof rootRouteImport
332+
}
299333
'/generations/speech': {
300334
id: '/generations/speech'
301335
path: '/generations/speech'
@@ -331,6 +365,13 @@ declare module '@tanstack/react-router' {
331365
preLoaderRoute: typeof ApiSummarizeRouteImport
332366
parentRoute: typeof rootRouteImport
333367
}
368+
'/api/structured-output': {
369+
id: '/api/structured-output'
370+
path: '/api/structured-output'
371+
fullPath: '/api/structured-output'
372+
preLoaderRoute: typeof ApiStructuredOutputRouteImport
373+
parentRoute: typeof rootRouteImport
374+
}
334375
'/api/image-gen': {
335376
id: '/api/image-gen'
336377
path: '/api/image-gen'
@@ -381,11 +422,13 @@ const rootRouteChildren: RootRouteChildren = {
381422
ImageGenRoute: ImageGenRoute,
382423
RealtimeRoute: RealtimeRoute,
383424
ApiImageGenRoute: ApiImageGenRoute,
425+
ApiStructuredOutputRoute: ApiStructuredOutputRoute,
384426
ApiSummarizeRoute: ApiSummarizeRoute,
385427
ApiTanchatRoute: ApiTanchatRoute,
386428
ApiTranscribeRoute: ApiTranscribeRoute,
387429
GenerationsImageRoute: GenerationsImageRoute,
388430
GenerationsSpeechRoute: GenerationsSpeechRoute,
431+
GenerationsStructuredOutputRoute: GenerationsStructuredOutputRoute,
389432
GenerationsSummarizeRoute: GenerationsSummarizeRoute,
390433
GenerationsTranscriptionRoute: GenerationsTranscriptionRoute,
391434
GenerationsVideoRoute: GenerationsVideoRoute,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { chat } from '@tanstack/ai'
3+
import { openRouterText } from '@tanstack/ai-openrouter'
4+
import { z } from 'zod'
5+
6+
const GuitarRecommendationSchema = z.object({
7+
title: z.string().describe('Short headline for the recommendation'),
8+
summary: z.string().describe('One paragraph summary'),
9+
recommendations: z
10+
.array(
11+
z.object({
12+
name: z.string(),
13+
brand: z.string(),
14+
type: z.enum(['acoustic', 'electric', 'bass', 'classical']),
15+
priceRangeUsd: z.object({ min: z.number(), max: z.number() }),
16+
reason: z.string(),
17+
}),
18+
)
19+
.min(1)
20+
.describe('Guitar recommendations with reasons'),
21+
nextSteps: z.array(z.string()).describe('Practical follow-up actions'),
22+
})
23+
24+
export const Route = createFileRoute('/api/structured-output')({
25+
server: {
26+
handlers: {
27+
POST: async ({ request }) => {
28+
const body = await request.json()
29+
const { prompt, model } = body as {
30+
prompt: string
31+
model?: string
32+
}
33+
34+
try {
35+
const result = await chat({
36+
adapter: openRouterText(
37+
(model || 'openai/gpt-5.2') as 'openai/gpt-5.2',
38+
),
39+
messages: [{ role: 'user', content: prompt }],
40+
outputSchema: GuitarRecommendationSchema,
41+
})
42+
43+
return new Response(JSON.stringify({ data: result }), {
44+
headers: { 'Content-Type': 'application/json' },
45+
})
46+
} catch (error: unknown) {
47+
const message =
48+
error instanceof Error ? error.message : 'An error occurred'
49+
console.error('[api/structured-output] Error:', error)
50+
return new Response(JSON.stringify({ error: message }), {
51+
status: 500,
52+
headers: { 'Content-Type': 'application/json' },
53+
})
54+
}
55+
},
56+
},
57+
},
58+
})

examples/ts-react-chat/src/routes/api.tanchat.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { anthropicText } from '@tanstack/ai-anthropic'
1111
import { geminiText } from '@tanstack/ai-gemini'
1212
import { openRouterText } from '@tanstack/ai-openrouter'
1313
import { grokText } from '@tanstack/ai-grok'
14-
import type { AnyTextAdapter, ChatMiddleware } from '@tanstack/ai'
1514
import { groqText } from '@tanstack/ai-groq'
15+
import type { AnyTextAdapter, ChatMiddleware } from '@tanstack/ai'
1616
import {
1717
addToCartToolDef,
1818
addToWishListToolDef,
@@ -146,8 +146,6 @@ export const Route = createFileRoute('/api/tanchat')({
146146
createChatOptions({
147147
adapter: openRouterText('openai/gpt-5.1'),
148148
modelOptions: {
149-
models: ['openai/chatgpt-4o-latest'],
150-
route: 'fallback',
151149
reasoning: {
152150
effort: 'medium',
153151
},

0 commit comments

Comments
 (0)