Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/openrouter-video-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-openrouter': minor
---

Add `openRouterVideo`, a video generation adapter for OpenRouter's dedicated async API (`POST /api/v1/videos`) β€” Seedance, Veo 3.1, Wan, Kling, and Sora 2 Pro through one API key. Follows the jobs/polling architecture (`generateVideo()` β†’ `getVideoJobStatus()`), with per-model `size` / `duration` / provider-option types generated from OpenRouter's `GET /api/v1/videos/models` metadata and validated before submit. `duration` is typed per model on the shared typed-duration contract β€” the adapter implements `availableDurations()` and `snapDuration(seconds)` (matching the Veo adapter) to enumerate the valid set and coerce raw UI seconds to the closest supported value. Image-conditioned prompts map `metadata.role` onto the wire: `start_frame` / `end_frame` β†’ `frame_images[]` (`first_frame` / `last_frame`), `reference` / `character` β†’ `input_references[]`; frame roles are validated against each model's `supported_frame_images`. Completed videos are downloaded server-side and returned as `data:` URLs (OpenRouter's download URLs require the API key), and the gateway-reported cost is surfaced as `usage.cost`.

Image adapter fixes from the #624 review: requested `size` is now validated (the `WIDTHxHEIGHT` union previously used a Unicode `Γ—`, so every size except `1024x1024` silently dropped its aspect ratio; unsupported sizes now throw with the supported list), `numberOfImages > 1` throws instead of silently returning one image (verified live: the gateway ignores all count keys in `image_config`), and `image_config.strength` (0.0–1.0 image-to-image influence) is exposed via `modelOptions.strength`.
4 changes: 2 additions & 2 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add packages/ scripts/openrouter.models.json scripts/vercel-gateway.models.json scripts/.sync-models-last-run .changeset/
git add packages/ scripts/openrouter.models.json scripts/openrouter.video-models.json scripts/vercel-gateway.models.json scripts/.sync-models-last-run .changeset/
git commit -m "chore: sync model metadata"
git push --force origin HEAD:automated/sync-models
env:
Expand All @@ -63,7 +63,7 @@ jobs:
BODY=$(cat <<'PRBODY'
Automated daily sync of model metadata from OpenRouter and Vercel AI Gateway.

- Fetches the latest model list from OpenRouter
- Fetches the latest model list from OpenRouter (chat + `GET /api/v1/videos/models`)
- Fetches the latest model list from Vercel AI Gateway
- Converts to the internal adapter format
- Syncs provider-specific model metadata for affected packages
Expand Down
106 changes: 95 additions & 11 deletions docs/adapters/openrouter.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ npm install @tanstack/ai-openrouter
```typescript
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";

const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages: [{ role: "user", content: "Hello!" }],
messages: [{ role: "user", content: "Hello!" }],
});
```

Expand Down Expand Up @@ -67,13 +67,13 @@ See the full list at [openrouter.ai/models](https://openrouter.ai/models).
```typescript
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";

export async function POST(request: Request) {
const { messages } = await request.json();

const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages,
messages,
});

return toServerSentEventsResponse(stream);
Expand Down Expand Up @@ -111,8 +111,6 @@ export async function POST(request: Request) {
return toServerSentEventsResponse(stream);
}
```



## Environment Variables

Expand Down Expand Up @@ -181,10 +179,10 @@ export async function POST(request: Request) {
OpenRouter exposes two OpenAI-compatible wire formats, and the adapter
package ships one of each:

| Adapter | Endpoint | Status | When to use |
| -------------------------- | ------------------------- | -------- | ---------------------------------------------------------------------------- |
| `openRouterText` | `/v1/chat/completions` | Stable | Default for almost everything. Broadest model + tool support. |
| `openRouterResponsesText` | `/v1/responses` | Beta | OpenAI Responses-shaped request/response; richer multi-turn state on OpenAI-style models. |
| Adapter | Endpoint | Status | When to use |
| ------------------------- | ---------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `openRouterText` | `/v1/chat/completions` | Stable | Default for almost everything. Broadest model + tool support. |
| `openRouterResponsesText` | `/v1/responses` | Beta | OpenAI Responses-shaped request/response; richer multi-turn state on OpenAI-style models. |

Both adapters route to any underlying model OpenRouter supports
(`anthropic/...`, `google/...`, `meta-llama/...`, etc.) β€” the wire format
Expand Down Expand Up @@ -278,6 +276,93 @@ attribution headers, just like the chat adapter.
See the [Reranking guide](../rerank/rerank) for object documents, RAG
pipelines, options, and the result shape.

## Image Generation

`openRouterImage` routes image generation through OpenRouter's
chat-completions surface (`modalities: ['image']`). Multimodal prompts are
supported β€” text and image parts are forwarded in order for
image-conditioned generation:

```typescript
import { generateImage } from "@tanstack/ai";
import { openRouterImage } from "@tanstack/ai-openrouter";

const result = await generateImage({
adapter: openRouterImage("google/gemini-2.5-flash-image"),
prompt: "A watercolor lighthouse at dusk",
size: "1344x768", // mapped to image_config.aspect_ratio ('16:9')
modelOptions: {
image_size: "2K", // resolution (Gemini models)
strength: 0.35, // image-to-image influence, i2i-capable models only
},
});
```

Notes:

- The pathway returns **exactly one image per request** β€” `numberOfImages > 1`
throws instead of silently under-delivering. Make multiple requests if you
need multiple candidates.
- `size` must be one of the ten supported `WIDTHxHEIGHT` values (it is
converted to `image_config.aspect_ratio`); anything else throws with the
supported list.

## Video Generation (Experimental)

`openRouterVideo` targets OpenRouter's dedicated **async video API**
(`POST /api/v1/videos`) β€” Seedance, Veo 3.1, Wan, Kling, and Sora 2 Pro
through your one OpenRouter key. It follows the jobs/polling architecture
shared by all TanStack AI video adapters:

```typescript
// Server: create the job, then poll
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openRouterVideo } from "@tanstack/ai-openrouter";

const adapter = openRouterVideo("bytedance/seedance-2.0");

const { jobId } = await generateVideo({
adapter,
prompt: [
{ type: "text", content: "Animate this product shot, slow push-in" },
{
type: "image",
source: { type: "url", value: "https://your-cdn.com/product.png" },
metadata: { role: "start_frame" },
},
],
size: "1280x720",
// `duration` is typed per model from the published metadata; coerce raw
// seconds with adapter.snapDuration() or enumerate via adapter.availableDurations().
duration: 8,
});

let status = await getVideoJobStatus({ adapter, jobId });
while (status.status !== "completed" && status.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
status = await getVideoJobStatus({ adapter, jobId });
}
// status.url is a data: URL (OpenRouter download URLs require the API key,
// so the adapter downloads server-side); status.usage?.cost is the real
// billed cost reported by the gateway.
```

```tsx
// Client: track the job with the useGenerateVideo hook
import { useGenerateVideo, fetchServerSentEvents } from "@tanstack/ai-react";

const { generate, result, videoStatus, isLoading } = useGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
});
// result?.url renders directly: <video src={result.url} controls />
```

Sizes, durations, and per-model options (`resolution`, `aspectRatio`,
`generateAudio`, `seed`, …) are typed and validated per model from
OpenRouter's video model metadata. See
[Video Generation](../media/video-generation.md) for the full lifecycle,
streaming mode, and the image-to-video role-mapping table.

## Next Steps

- [Getting Started](../getting-started/quick-start) - Learn the basics
Expand Down Expand Up @@ -363,4 +448,3 @@ const stream = chat({
```

**Supported models:** all OpenRouter chat models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools).

6 changes: 3 additions & 3 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,13 @@
"label": "Image Generation",
"to": "media/image-generation",
"addedAt": "2026-04-15",
"updatedAt": "2026-08-04"
"updatedAt": "2026-08-13"
},
{
"label": "Video Generation",
"to": "media/video-generation",
"addedAt": "2026-04-15",
"updatedAt": "2026-08-07"
"updatedAt": "2026-08-13"
},
{
"label": "Generation Hooks",
Expand Down Expand Up @@ -841,7 +841,7 @@
"label": "OpenRouter Adapter",
"to": "adapters/openrouter",
"addedAt": "2026-04-15",
"updatedAt": "2026-06-25"
"updatedAt": "2026-08-13"
},
{
"label": "Vercel AI Gateway",
Expand Down
Loading
Loading