Skip to content

Generate provider HTTP clients from modelschemas with Kubb #2104

Open

Description

@tombeckenham

Problem

Provider HTTP calls are hand-written fetch clients (merged in #1973). Each package keeps its own request types, URL map, auth, and body encoding. Updating a provider means editing that client by hand.

modelschemas now publishes a generation-only OpenAPI document per provider:

GET https://modelschemas.com/v1/openapi/{provider}

with Accept: application/openapi+json.

That document is the source we should generate from. A script must be able to refetch it and show whether the committed client is stale.

This is not #1788 (that issue only moves the model catalog off models.dev). It is not #619 / PR #622 (a separate @tanstack/ai-schemas validation package, closed, not merged). Leave World Labs on its existing hey-api client.

Generate with Kubb

Use Kubb v5 @kubb/plugin-ts and @kubb/plugin-fetch. One compact client per provider that has a document, not one file per schema.

import { defineConfig } from 'kubb/config'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginFetch } from '@kubb/plugin-fetch'

export default defineConfig({
  input: './openapi.json',
  output: { path: './src/gen', clean: true, barrel: false, format: 'oxfmt' },
  plugins: [
    pluginTs({
      output: { path: 'types.ts', barrel: false },
      operations: 'inline',
    }),
    pluginFetch({
      output: { path: 'sdk.ts', barrel: false },
      sdk: { name: 'OpenAI', mode: 'flat' },
    }),
  ],
})

A trial of the live OpenAI document (38 operations) wrote 6 files: types.ts, sdk.ts, and a .kubb fetch runtime. output.format: 'oxfmt' runs after generation. This repo already depends on oxfmt.

Share one .kubb runtime across packages with pluginFetch({ importPath }) when that stays simpler than copying the runtime into every package.

What the scripts must do

  1. List providers from modelschemas.
  2. Fetch each generation OpenAPI document.
  3. Skip a provider when the document is not available (missing_connect, no_endpoints, unknown_provider). Do not fail the whole run for those.
  4. Run Kubb, then oxfmt.
  5. A check command (used in CI and locally) refetches the documents and fails when the generated client would change. Follow the drift style of .github/workflows/sync-models.yml, but keep this pipeline separate from model-id sync.

Commit the generated client. Do not commit a second hand-written copy of the same calls.

Spec shape

Keep one path and one method. A call that returns JSON or text/event-stream lists both content types on that operation. Do not add a #stream path. # is a fragment, fetch drops it, and it is not a valid OpenAPI path.

Fal stays one queue client from the untyped queue document (POST /{endpoint_id} on https://queue.fal.run). ?category= is not a selector. Do not generate one client per Fal model. Providers that still require ?model= (Reactor, Cloudflare AI Gateway) stay out of this pass until they have a provider-level document.

Known generator limits

Kubb does not turn a combined JSON plus event-stream operation into both a JSON method and a parsed SSE loop. The body field stream: true selects streaming. The runtime returns response.body for text/event-stream and does not parse event: / data: lines.

audio/* is not mapped to a blob (only image/* and application/octet-stream are), so speech audio can fall through to JSON.parse.

Keep a small hand-written layer for those cases: stream: true, SSE parsing, and audio bytes. Do not use Stainless. Do not switch this work to hey-api.

Auth that a generated client cannot perform (AWS SigV4) stays hand-written. Bearer tokens and the Azure api-key header can go through the generated client.

Acceptance

  • pnpm script generates every available provider client from modelschemas with Kubb, in the compact layout above, formatted with oxfmt.
  • A check script exits non-zero when a live document would change committed output.
  • Provider adapters call the generated client instead of the hand-written fetch maps from perf(providers): call provider APIs with fetch instead of the SDK runtime #1973, except for the hand-written stream, audio, and SigV4 cases above.
  • Packages that only speak another provider's API reuse that generated client instead of generating a duplicate.
  • Unit tests cover generation drift detection and one JSON call plus one streaming call per generated provider that has both.
  • Docs and a changeset ship with the change if a published package's public behavior changes.

Metadata

Metadata

Assignees

Labels

has-prAn open PR references this issuewaiting-on: maintainerThe ball is in the maintainers’ court

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions