Skip to content
Open
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
19 changes: 19 additions & 0 deletions .changeset/initial-ai-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai-schemas': minor
---

Initial release of `@tanstack/ai-schemas` β€” JSON Schema and Zod schemas for AI provider generation endpoints, generated nightly from upstream OpenAPI specs.

Covers OpenAI, Anthropic, Gemini, ElevenLabs, xAI Grok, OpenRouter, and FAL. Endpoints are grouped per provider by activity β€” `chat`, `image`, `video`, `audio` (TTS, transcription, music, sound effects in one group), `embeddings`, `moderation` β€” behind `@tanstack/ai-schemas/{provider}/{activity}/{json-schema,zod}` subpaths. Platform/admin endpoints are excluded from generation. Architecture ported from fal-ai/fal-js PR #212; extended to every provider that publishes an OpenAPI spec.

Sources:

- OpenAI: `github.com/openai/openai-openapi`
- Anthropic: Stainless OpenAPI resolved via `anthropic-sdk-typescript/.stats.yml`
- Gemini: Google Generative Language Discovery doc, converted to OpenAPI in-pipeline
- ElevenLabs: `api.elevenlabs.io/openapi.json`
- xAI Grok: `docs.x.ai/openapi.json`
- OpenRouter: `openrouter.ai/openapi.json` + `api/v1/videos/models` metadata (per-video-model constrained schemas)
- FAL: per-model OpenAPI from the FAL models API (requires `FAL_KEY`)

The `.github/workflows/sync-schemas.yml` workflow runs daily and opens a PR when any provider's spec changes. Resolves #619.
108 changes: 108 additions & 0 deletions .github/workflows/sync-schemas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Sync Provider Schemas

on:
schedule:
# 05:00 UTC daily β€” one hour before sync-models.yml so the two automated
# PRs don't contend on the changeset / branch push at the same minute.
- cron: '0 5 * * *'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write

jobs:
sync:
name: Sync Schemas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: true

- name: Setup Tools
uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main

- name: Fetch provider OpenAPI specs
run: pnpm --filter @tanstack/ai-schemas fetch-schemas
env:
FAL_KEY: ${{ secrets.FAL_KEY }}

- name: Generate JSON Schemas + Zod
run: pnpm --filter @tanstack/ai-schemas generate-schemas

- name: Generate endpoint maps and barrels
run: pnpm --filter @tanstack/ai-schemas generate-endpoint-maps

- name: Verify generated output is data-only
run: pnpm --filter @tanstack/ai-schemas verify-generated

- name: Check for package changes
id: changes
run: |
# Generation must only touch the sync-owned paths β€” these are the
# only paths the PR-side gate (verify-sync-schemas.yml) accepts.
UNEXPECTED=$(git status --porcelain -- packages/ai-schemas/ \
| grep -vE '^.. packages/ai-schemas/(src/providers|scripts/specs)/' || true)
if [ -n "$UNEXPECTED" ]; then
echo "::error::Generation changed files outside the sync allowlist:"
echo "$UNEXPECTED"
exit 1
fi
# git status (not git diff) so brand-new untracked files also count.
if [ -n "$(git status --porcelain -- packages/ai-schemas/src/providers packages/ai-schemas/scripts/specs)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi

- name: Write changeset and commit
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
mkdir -p .changeset
cat > .changeset/automated-sync-schemas.md <<'EOF'
---
"@tanstack/ai-schemas": patch
---

Nightly sync of provider OpenAPI schemas.
EOF
git add packages/ai-schemas/src/providers packages/ai-schemas/scripts/specs .changeset/automated-sync-schemas.md
git commit -m "chore(schemas): sync provider OpenAPI schemas"
git push --force origin HEAD:automated/sync-schemas

- name: Create or update PR
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
BRANCH="automated/sync-schemas"
EXISTING_PR=$(gh pr list --head "$BRANCH" --base main --json number --jq '.[0].number' 2>/dev/null || true)
if [ -z "$EXISTING_PR" ] || [ "$EXISTING_PR" = "null" ]; then
BODY=$(cat <<'PRBODY'
Automated nightly sync of provider OpenAPI schemas.

- Fetches upstream specs (OpenAI, Anthropic, Gemini, ElevenLabs, FAL).
- Runs `@hey-api/openapi-ts` to regenerate JSON Schemas + Zod.
- Bundles each schema's `$defs` closure and regenerates endpoint maps.
- Patch changeset for `@tanstack/ai-schemas`.

Providers with missing secrets are skipped; the diff reflects only the
providers whose specs the workflow could fetch.
PRBODY
)
gh pr create \
--title "chore(schemas): sync provider OpenAPI schemas" \
--body "$BODY" \
--base main \
--head "$BRANCH"
fi
51 changes: 51 additions & 0 deletions .github/workflows/verify-sync-schemas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Verify Schema Sync PR

# Security gate for the nightly schema-sync PRs (automated/sync-schemas).
# The sync pipeline runs third-party-controlled OpenAPI specs (notably
# per-model FAL metadata) through codegen into modules that execute on
# import, so before such a PR can merge β€” especially with auto-merge β€”
# this workflow enforces:
# 1. the diff touches only sync-owned paths, and
# 2. every generated module is data-only TypeScript (no executable code).

on:
pull_request:
branches: [main]

permissions: {}

jobs:
verify:
name: Verify Sync Diff
# Only the sync bot's PRs; on all other PRs this job is skipped, which
# branch protection treats as passing if the check is made required.
if: github.head_ref == 'automated/sync-schemas'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Enforce path allowlist
env:
BASE_REF: ${{ github.base_ref }}
run: |
CHANGED=$(git diff --name-only "origin/${BASE_REF}...HEAD")
DISALLOWED=$(echo "$CHANGED" | grep -v '^$' \
| grep -vE '^(packages/ai-schemas/(src/providers|scripts/specs)/|\.changeset/automated-sync-schemas\.md$)' || true)
if [ -n "$DISALLOWED" ]; then
echo "::error::Sync PR touches files outside the allowlist; manual review required:"
echo "$DISALLOWED"
exit 1
fi
echo "All changed files are within the sync allowlist."

- name: Setup Tools
uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main

- name: Verify generated output is data-only
run: pnpm --filter @tanstack/ai-schemas verify-generated
144 changes: 144 additions & 0 deletions docs/advanced/ai-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
title: '@tanstack/ai-schemas: provider endpoint schemas'
id: ai-schemas
---

`@tanstack/ai-schemas` is a separate package that ships **JSON Schema** and **Zod** schemas for the generation endpoints of every supported provider (OpenAI, Anthropic, Gemini, ElevenLabs, xAI Grok, OpenRouter, FAL). The schemas are generated nightly from each provider's official OpenAPI spec, so they track upstream changes automatically.

It complements the `model-meta.ts` shipped with each provider adapter: where `model-meta` describes coarse facts (context window, modalities, pricing), `ai-schemas` describes the rich per-endpoint constraint surface β€” allowed video durations, image sizes, voice IDs, prompt-length caps, etc.

## When to reach for it

- **Runtime validation before submitting a request** β€” surface bad inputs client-side instead of paying a round-trip and waiting on a vague upstream error.
- **Discovering allowed values** β€” populate dropdowns or pick lists with the exact enum a model accepts.
- **Feeding an LLM tool API** β€” each JSON Schema is self-contained (its `$ref` closure is bundled under `$defs`), so it can be passed straight to OpenAI / Anthropic / Gemini tool-use APIs without extra resolution.
- **Optimising prompts across models** β€” the schemas expose every model's constraints in a comparable shape, which is the foundation for prompt-portability helpers.

## Install

```bash
pnpm add @tanstack/ai-schemas
# Optional: only required if you import from `@tanstack/ai-schemas/{provider}/{activity}/zod`
pnpm add zod
```

## Activity groups

Each provider's endpoints are grouped by **activity**, matching the core library's activities (chat, generateImage, generateVideo, and the audio family β€” collapsed into a single `audio` group):

| Activity | Covers |
| ------------ | ----------------------------------------------------------------------- |
| `chat` | Text generation: chat/completions, responses, messages, generateContent |
| `image` | Image generation and edits |
| `video` | Video generation, edits, extensions |
| `audio` | All audio: TTS, transcription, music, sound effects, voices, dubbing |
| `embeddings` | Embedding endpoints |
| `moderation` | Moderation endpoints |

Platform/admin endpoints (projects, invites, certificates, fine-tuning jobs, file stores, …) are excluded from generation entirely β€” they aren't generation constraint surface.

## Subpath imports

Provider-first β€” pick the provider, then the activity, then `json-schema` or `zod`:

```ts
// JSON Schemas (no `zod` peer required).
import { geminiChatEndpointSchemaMap } from '@tanstack/ai-schemas/gemini/chat/json-schema'

// Zod (requires `zod ^4`).
import { openaiChatEndpointZodMap } from '@tanstack/ai-schemas/openai/chat/zod'
import { elevenlabsAudioEndpointZodMap } from '@tanstack/ai-schemas/elevenlabs/audio/zod'
import { falVideoEndpointZodMap } from '@tanstack/ai-schemas/fal/video/zod'
```

There is **no aggregator barrel**. `import … from '@tanstack/ai-schemas/gemini/chat/json-schema'` only ships Gemini's chat JSON Schemas β€” no other provider's or activity's bytes leak into the consumer's bundle.

Endpoints that stream binary media (e.g. ElevenLabs text-to-speech) map only an `input` schema β€” there is no JSON output to describe.

## Validate a video-generation request

```ts
import { falVideoEndpointZodMap } from '@tanstack/ai-schemas/fal/video/zod'

const result = falVideoEndpointZodMap[
'fal-ai/kling-video/o3/pro/text-to-video'
].input.safeParse({
prompt: 'A mecha lands on the ground to save the city, in anime style',
duration: '8',
aspect_ratio: '9:16',
})

if (!result.success) console.error(result.error.issues)
```

## Discover what a model supports

```ts
import { KlingVideoO3ProTextToVideoInputSchema } from '@tanstack/ai-schemas/fal/video/json-schema'

KlingVideoO3ProTextToVideoInputSchema.properties.duration.enum
// ['3', '4', …, '15']

KlingVideoO3ProTextToVideoInputSchema.properties.aspect_ratio.enum
// ['16:9', '9:16', '1:1']
```

## Per-model OpenRouter video constraints

OpenRouter exposes one generic `/videos` endpoint, but each video model accepts different durations, resolutions, aspect ratios, and sizes. The package synthesises one model-constrained schema per video model from OpenRouter's public `videos/models` metadata, keyed `videos/{model-id}` alongside the generic `videos` entry. Capabilities a model lacks (audio generation, deterministic seed, frame images) are absent from its schema entirely:

```ts
import { openrouterVideoEndpointZodMap } from '@tanstack/ai-schemas/openrouter/video/zod'

const result = openrouterVideoEndpointZodMap[
'videos/x-ai/grok-imagine-video'
].input.safeParse({
model: 'x-ai/grok-imagine-video',
prompt: 'A mecha lands on the ground to save the city',
duration: 8, // validated against the model's supported durations
resolution: '720p', // validated against the model's supported resolutions
})

if (!result.success) console.error(result.error.issues)
```

## OpenAI structured-outputs strict mode

```ts
import { toOpenAIStrict } from '@tanstack/ai-schemas/openai-strict'
import { Veo3InputSchema } from '@tanstack/ai-schemas/fal/video/json-schema'

await openai.chat.completions.create({
model: 'gpt-5',
messages: [...],
response_format: {
type: 'json_schema',
json_schema: {
name: 'veo3_input',
schema: toOpenAIStrict(Veo3InputSchema),
strict: true,
},
},
})
```

## How it stays current

The `.github/workflows/sync-schemas.yml` workflow runs daily and:

1. Fetches upstream OpenAPI specs for every provider.
2. Re-runs `@hey-api/openapi-ts` to regenerate JSON Schemas + Zod.
3. Bundles each schema's `$ref` closure, regenerates endpoint maps.
4. If anything changed: bumps the package patch version, writes a changeset, opens an automated PR.

Provider sources:

| Provider | Source | Notes |
| ---------- | ------------------------------------------------------------------------------- | ---------------------------------------------------- |
| OpenAI | `github.com/openai/openai-openapi` | Public, no API key required. |
| Anthropic | Stainless-generated OpenAPI (resolved via `anthropic-sdk-typescript/.stats.yml`) | Public. |
| Gemini | `generativelanguage.googleapis.com/$discovery/rest?version=v1beta` | Google Discovery doc converted to OpenAPI in pipeline. |
| ElevenLabs | `api.elevenlabs.io/openapi.json` | Public. |
| xAI Grok | `docs.x.ai/openapi.json` | Public. |
| OpenRouter | `openrouter.ai/openapi.json` + `api/v1/videos/models` metadata | Public. Per-video-model schemas synthesised from the metadata. |
| FAL | `api.fal.ai/v1/models?status=active&expand=openapi-3.0` (per-model) | Needs `FAL_KEY`. Model categories regroup into the shared activities. |
5 changes: 5 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@
"label": "Typed Pre-Configured Options",
"to": "advanced/typed-options",
"addedAt": "2026-05-25"
},
{
"label": "Provider Endpoint Schemas",
"to": "advanced/ai-schemas",
"addedAt": "2026-06-11"
}
]
},
Expand Down
8 changes: 8 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
},
"packages/ai-vue-ui": {
"ignore": ["src/use-chat-context.ts"]
},
"packages/ai-schemas": {
"ignore": [
"src/providers/**/*.gen.ts",
"scripts/**",
"openapi-ts.config.ts"
],
"ignoreDependencies": ["zod", "vite"]
}
}
}
Loading
Loading