Repository navigation
feat(providers): generate type-checked provider HTTP clients from modelschemas with Kubb - #2105
tombeckenham wants to merge 13 commits into
Conversation
|
View your CI Pipeline Execution ↗ for commit 80cb9c0
💡 With Ultracache enabled, Nx Cloud learns each task's file inputs in CI and uses them on later runs. Turn on Ultracache in workspace settings ↗ · Read docs ↗ ☁️ Nx Cloud last updated this comment at |
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: truePre-merge checks:
|
🦋 Changeset detectedLatest commit: 80cb9c0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Coverage❌ Coverage fell under 60% in 1 package(s). Each package is measured twice in this job — on this PR and on its merge-base with
|
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-cloudflare
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-computer-local
@tanstack/ai-computer-playwright
@tanstack/ai-computer-sandbox
@tanstack/ai-computer-use
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-effect
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-harness
@tanstack/ai-harness-cli
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-e2b
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-muse
@tanstack/ai-neon
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-ollaya
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-reactor
@tanstack/ai-remix
@tanstack/ai-sandbox
@tanstack/ai-sandbox-blaxel
@tanstack/ai-sandbox-boxd
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-e2b
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-railway
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-svelte
@tanstack/ai-typesafe
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-worldlabs
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
…h Kubb Adds scripts/provider-clients (generate + --check drift mode), the shared Kubb fetch runtime at @tanstack/ai-utils/kubb with sendWith(), the generated clients for openai, anthropic, gemini, mistral, openrouter, elevenlabs and fal, and routes the openai-base client through the generated OpenAI operations.
…labs and fal through the generated clients Each client sends the operations that its modelschemas OpenAPI document describes through the generated Kubb functions. Auth, retries, errors, SSE parsing and audio bytes stay hand-written, as do calls the documents do not have. The shared runtime now merges every provider's Kubb runtime, so the SSE helpers that ElevenLabs needs are exported.
The generator fetches each provider's ?activity= documents. An activity
with per-model schemas gets a types-only src/generated/{activity} with a
{Provider}{Activity}Endpoints map of model id to { input, output }.
Only fal has per-model schemas today.
…types Adapters and clients build each body as the generated modelschemas type, with no casts. Lines where the document or a public SDK type does not fit carry a @ts-expect-error that names the gap, so the build fails once it is fixed. - openai-base: OpenAIClientLike takes the generated bodies (breaking: an SDK instance no longer fits; use createOpenAIClient). - generator: drop format: byte, which Kubb types as Blob instead of base64. - mistral: stream json_schema response format in snake_case. - elevenlabs: send string entityDetection/entityRedaction.
model-meta's EndpointTypeMap now extends the generated image, video, audio and chat maps instead of @fal-ai/client's single map. The image field overrides and the video duration table are generated from the same activity documents, replacing scripts/generate-fal-image-field-map.ts and the hand-curated duration list. Blocked on TanStack/schemas#605: the activity documents drop ~235 indexed endpoints (veo3.1, kling v1.6/v2.6, sora-2), so 4 tests fail.
…e a Blob The fal activity documents now hold every indexed endpoint, so the typed endpoint map covers veo3.1, kling and whisper. FalModelInput lets *_url and *_urls fields take a Blob, which the fal client uploads, as @fal-ai/client did.
…e and Bedrock clients - cohere: embed goes through the generated client; rerank stays hand-written. - ollama: chat and embed go through the generated client with an NDJSON reader. - worldlabs: the Kubb client replaces hey-api and the copied openapi.yaml. - ollaya, typesafe: /v1/systemone goes through the generated client. - bedrock: Converse bodies are checked against the generated types (types only; the AWS SDK still sends). - generator: typesOnly providers; knip ignores generated types in any folder. - fal: drop the unused per-activity model id types.
modelschemas#643 added POST /tokens to the Reactor document and marked the model commands as realtime-only. Also picks up upstream Fal, Gemini and OpenRouter document changes.
4b90839 to
b68b154
Compare
…ed types Also fixes a test from main that passes an OpenAI SDK instance as the client, and picks up upstream Fal, OpenRouter and OpenAI document changes.
…ra and Grok video durations sync-provider-models.ts reads GET /v1/models?provider=…&pricing=1 for OpenAI, Anthropic, Gemini and Grok instead of the OpenRouter catalogue. Native ids come from rawId, so the OpenRouter id mapping is gone. Models with no price are skipped for OpenAI and Anthropic, whose ModelMeta needs one. The video duration step of generate.ts now also writes OPENAI_VIDEO_DURATIONS and GROK_VIDEO_DURATIONS from each provider's create operation. Gemini, BytePlus and Lovable stay hand-written until TanStack/schemas#657, #658 and #659 land. Refs #2104
Review
packages/openai-base/src/client/client.tsfirst. The other six clients copy its pattern. The provider clients now build their requests with code that Kubb generates from the modelschemas OpenAPI documents. TypeScript checks every request body against the generated types.pnpm generate:provider-clients:checkfails when a live document would change the committed code.🎯 Changes
scripts/provider-clients/lists the providers on modelschemas, fetches each OpenAPI document, and runs Kubb and oxfmt.missing_connect,no_endpoints,unknown_provider).--checkcompares the output with the committed files.format: bytebefore Kubb runs. Kubb typesbyteasBlob, but the APIs take a base64 string.openai-base), Anthropic, Gemini, Mistral, OpenRouter, ElevenLabs, Fal, Cohere, Ollama, World Labs, Ollaya, TypeSafe, and Reactor each getsrc/generated/client/{sdk,types}.ts. Groq, Grok, Azure, and the gateways use the OpenAI client.openapi.yaml.types.tsonly. Its Converse bodies are checked against it, and the AWS SDK still sends them.types.tsonly. It sends through the shared OpenAI Chat Completions path, and its request is checked against Meta's own body type./v2/rerankis not in the document.FalModelInputandFalModelOutputcome from the per-activity fal documents (?activity=image|video|audio|chat), not from@fal-ai/client. The image field overrides and video durations are generated from the same documents.*_urlfields still take aBlobto upload.@ts-expect-errorthat names the mismatch. The build fails when the document is fixed, so the marker cannot go stale.@tanstack/ai-utils/kubbholds the Kubb fetch runtime andsendWith().sendWith()sends a generated request through the provider's own code (auth, retries, errors, SSE, audio).OPENAI_VIDEO_DURATIONS) and Grok Imagine (GROK_VIDEO_DURATIONS) are generated from each provider's video create operation, like fal's. Gemini, BytePlus and Lovable stay hand-written until modelschemas has the data.scripts/sync-provider-models.tsreadsGET /v1/models?provider=…&pricing=1for OpenAI, Anthropic, Gemini and Grok, not the OpenRouter catalogue. Ids are native (rawId), so the OpenRouter id mapping is gone.sync-provider-clients.ymlruns the check, regenerates, and opens a PR.Bugs fixed on the way:
json_schemaresponse format in camelCase. It now sends snake_case.entityDetectionorentityRedaction. It now sends it.Hand-written code that stays: calls that the documents do not have (files, job status, model lists, rerank), SigV4 signing, and the Vertex and Enterprise paths of Gemini.
Differences from the plan in the issue:
@kubb/plugin-client, notplugin-fetch. Onlyplugin-clientacceptsimportPath, so there is one runtime, not one per package. Its plain functions also tree-shake.operations: 'export', not'inline'. In Kubb 5.16,'inline'writes Gemini types that do not compile.x-modelschemas-encode: raw, so Fal passes its own path serializer.POST /tokensthrough its client. The model commands go over the SDK realtime channel, and the document marks themx-modelschemas-transport: realtime(Reactor: add POST /tokens; command paths aren't HTTP endpoints schemas#643).POST /search, the only call the adapter makes (Perplexity: add POST /search schemas#660). BytePlus: the video status call isPOSTand audio is missing from the main document (BytePlus: video status is GET, durations only in prose, audio missing from main document schemas#657). Vertex: requests go through the@google/genaiSDK, so there is no generated body to check until Vertex gets a fetch path.Document gaps found
These lines carry a
modelschemas gapmarker. Each one needs an upstream fix in TanStack/modelschemas.temperature,top_kortop_p. Gemini has noGoogleMaps.authConfigand no videowebhookConfig. ElevenLabs operations have no query parameters.top_logprobs, and OpenRouteruser_locationand reasoningcontent, do not acceptnull.text.verbosityhas no'max', and its responsesinputhas noopenrouter:mcpitem. Ollamakeep_aliveon embed takes no number, and itsToolCallhas noid.durationSecondslimits (Gemini: per-model video schemas with durationSeconds limits (Veo, Omni) schemas#658). BytePlus has its limits in prose only (fix(scripts): retain web_fetch in OpenRouter tool-capability codegen #657). Lovable copies the Sorasecondsenum, but its Veo models take 4, 6 or 8 (ci: Version Packages #659).chat, and 26 models have noreleasedAt(Gemini catalogue: non-chat models tagged chat, missing releasedAt and pricing schemas#661).GETwith no body, as before.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Testing
Commands run
tsc --noEmit,oxlint src --type-aware, andvitest runin the 26 changed packages, andnx run-many -t test:typeson all of them. All pass, and the SDK parity tests are green.pnpm test:knippasses.pnpm generate:provider-clients:checkexits 0.pnpm test:kiirapasses (2149 doc snippets).vitest run scripts/provider-clientspasses (12 tests).pnpm test:prand the E2E suite. CI runs them.Manual test
pnpm generate:provider-clients:check. It exits 0.packages/ai-fal/src/generated/client/sdk.ts. Run the check again. It names the file and exits 1.pnpm generate:provider-clients. The edit is gone.packages/ai-mistral/src/adapters/text.ts, remove a@ts-expect-error modelschemas gapline.pnpm exec tsc --noEmit -p packages/ai-mistralfails on that line.How this PR makes testing easy
Each migrated package has
tests/generated-client.test.ts. It injectsfetchand checks one JSON call and one streaming or audio call.scripts/provider-clients/files.test.tscovers the drift check and thebytefix.Linked issues
Closes #2104
Risk / rollback
new OpenAI()to anopenai-baseadapter no longer compiles. The runtime is the same.ollama-jsUser-Agent. In Node, an image string that is a file path is sent as is.Public API change
OpenAIClientLikein@tanstack/openai-basetakes the generated body types, so an SDK instance no longer fits it.@tanstack/openai-baseexports the generated body types.@tanstack/ai-utilshas a new@tanstack/ai-utils/kubbentry point, andcreateElevenLabsClient()accepts an optionalfetch.Before
After