Skip to content

feat(cloudflare): sync full Workers AI catalog + mirror into AI Gateway#3313

Open
michaeldwan wants to merge 4 commits into
anomalyco:devfrom
michaeldwan:cloudflare-ai-gateway-wai-catalog
Open

feat(cloudflare): sync full Workers AI catalog + mirror into AI Gateway#3313
michaeldwan wants to merge 4 commits into
anomalyco:devfrom
michaeldwan:cloudflare-ai-gateway-wai-catalog

Conversation

@michaeldwan

Copy link
Copy Markdown

1) Fix drift between Workers AI and AI Gateway

Cloudflare Workers AI models can be accessed directly, or through Cloudflare AI Gateway, but models.dev maintains them as two independent providers that have drifted apart for over a year.

Previously cloudflare-workers-ai kept Workers AI in sync each hour; AI Gateway had a hand-run shell pipeline under providers/cloudflare-ai-gateway/scripts/ that was never wired into the hourly workflow.

cloudflare-ai-gateway/models/workers-ai/@cf is now a symlink to cloudflare-workers-ai/models/@cf, so AIG's Workers AI catalog is the WAI catalog. The scripts/ + data/ pipeline is deleted, and the AIG README is rewritten to describe the symlink layout.

2) Sync the complete Workers AI model catalog

The WAI sync now covers Cloudflare's complete Workers AI catalog -- 39 new TOMLs for embeddings, ASR, TTS, translation, classification, and image gen. Previously it was only fetching text generation models due to the format=openrouter filter. Now it also fetches and reshapes models from the native ai/models/search endpoint into the same OpenRouterModel the pipeline already builds from. Where both APIs report the same model, openrouter wins, so the 22 existing chat entries stay byte-identical.

Any future WAI expansion (new task type, new model) lands in AIG on the next deploy through the same file.

Notes

The Cloudflare APIs don't provide all the data model TOMLs expose. I'll see if we can fix this on our (Cloudflare's) end:

  • Output token limit and open-weights aren't in either API, so they're placeholders (0 / false) -- same fallback digitalocean.ts, chutes.ts, and wandb.ts use when a source doesn't report them. The old AIG shell pipeline this replaces incorrectly hardcoded context=128000 / output=16384 on every model.
  • Unpriced tokens (audio-priced ASR/TTS, free models) use -1 (OpenRouter's own sentinel) rather than a fake 0, so price() doesn't invent cost. Audio-priced models get input_audio / output_audio applied in a follow-up pass.
  • API endpoints require authentication and reflect models accessible to the authenticated user. We probably need a public unauthenticated API for use cases like this.

Sources

The scripts/ + data/ pipeline hit gateway.ai.cloudflare.com/.../compat/models
and hand-generated TOMLs under models/workers-ai/. It was never wired into
the hourly model sync workflow, so its output drifted from
providers/cloudflare-workers-ai/ by a year of merges. models/workers-ai/@cf
still contains those stale, hand-generated TOMLs for now.
Delete the 43 hand-generated TOMLs under
providers/cloudflare-ai-gateway/models/workers-ai/@cf and replace the
subtree with a symlink to ../../../cloudflare-workers-ai/models/@cf so
AIG's Workers AI catalog is the Cloudflare Workers AI catalog by
construction.

generate.ts, sync/index.ts, packages/web/src/render.tsx, and
compare-model-migrations.ts all glob TOMLs with followSymlinks: true,
so the symlink resolves at build/validate time and AIG's provider
catalog materializes with WAI's contents. Merging a WAI sync PR now
updates AIG's catalog on the next deploy too, through the same file.

Net catalog change for AIG's workers-ai/:
  - 43 -> 22 models
  - drops 15 non-chat entries unique to AIG (BGE, Deepgram, MeloTTS,
    m2m100, distilbert, etc.) and 9 deprecated chat models
  - picks up 3 net-new WAI models (kimi-k2.7-code, glm-5.2,
    gemma-4-26b-a4b-it)

Also rewrites providers/cloudflare-ai-gateway/README.md to describe the
symlink and drop the (now-removed) shell pipeline docs.
Cloudflare's Workers AI models API returns two shapes: format=openrouter
(rich, but chat-models only) and a native format that covers everything
else it serves (embeddings, ASR, TTS, translation, classification,
image gen, etc). The WAI sync only ever fetched the openrouter shape, so
those other models were invisible to models.dev even though Cloudflare
serves them live.

This teaches the sync to also fetch the native format, and reshape each
native record into the same OpenRouterModel shape the existing pipeline
already builds from -- mapping task type to input/output modality,
converting per-million-token pricing to the per-token format the schema
expects, and reading capability flags (tool calling, reasoning) out of
the native properties array. Native pagination has no page count to
trust (total_count reflects Cloudflare's whole catalog, not this
account's view), so it pages until an empty result instead.

Two fields -- output token limit and open-weights status -- have no
source in either API shape for any model, so they're faked (0 / false).
Guessing these two fields when a provider's API doesn't report them is
already normal in this codebase (e.g. digitalocean.ts, chutes.ts, and
wandb.ts all fall back to 0 for a missing output limit; chutes defaults
open_weights to true, digitalocean/pioneer/llmgateway/vercel default it
to false) -- and the old hand-run AIG pipeline this branch replaces did
exactly this too, hardcoding context=128000 and output=16384 as shell
constants on every model it generated.

Where both formats report the same model, the richer openrouter record
wins, keeping fields native lacks -- max_completion_tokens,
structured_output, hugging_face_id -- on the 22 existing chat models.
…ch surfaces

These 39 TOMLs are the sync's output against the live Workers AI API:
every model Cloudflare serves that only reports through the native
shape, not format=openrouter -- 5 embedding models, 3 TTS voices, ASR
(whisper), translation (m2m100), image classification, several
text-to-image models, a couple of LoRA chat variants, and others. They
existed on Cloudflare's side already; the sync had no path to them
until now.

The 22 existing chat models came out byte-identical.
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] providers/cloudflare-workers-ai/models/@cf/openai/whisper-large-v3-turbo.toml:1 - Check: base_model must be used when a matching models/<provider>/<model>.toml entry exists. Why: models/openai/whisper-large-v3-turbo.toml exists with open_weights = true, limit.context = 448, limit.output = 448, and name = "Whisper Large v3 Turbo". The new WAI TOML is a full inline definition that duplicates all of this provider-agnostic metadata and fabricates incorrect values: open_weights = false (Whisper IS open-weight), limit.context = 0, limit.output = 0, and name = "@cf/openai/whisper-large-v3-turbo" (raw slug). The resolveCloudflareBaseModel heuristic failed to resolve the match because both whisper-large-v3 and whisper-large-v3-turbo satisfy the identity-token check, producing 2 matches instead of 1. Action: Use base_model = "openai/whisper-large-v3-turbo" and retain only provider-specific overrides (cost, attachment = true, release_date if different). Either fix the resolveCloudflareBaseModel heuristic to prefer exact filename matches over substring identity matches, or hand-add the base_model pointer (the sync runner preserves existing base_model on subsequent runs).

  • [medium] [violation] packages/core/src/sync/providers/cloudflare-workers-ai.ts:applyAudioPricing - Check: cost.input_audio and cost.output_audio are documented as "Cost per million audio input/output tokens (USD)" per the README schema reference. Why: applyAudioPricing takes the native API's "per audio minute" price and puts it directly into input_audio, and takes "per 1k characters" or "per audio minute" and puts it into output_audio. These are different units from per-million-tokens. The resulting 9 new TOMLs (all ASR and TTS models: whisper, whisper-large-v3-turbo, nova-3, flux, aura-1, aura-2-en, aura-2-es, melotts, smart-turn-v2) contain audio pricing values that are not comparable with any other provider in the catalog (e.g. OpenAI input_audio = 32.00 is per-million-tokens; Cloudflare input_audio = 0.000338 is per-audio-minute). No TOML comment documents the unit discrepancy. Action: Either convert per-minute/per-character pricing to per-million-token pricing, or document the unit in a leading comment block on each affected TOML (the sync preserves leading comments), or clarify with maintainers whether input_audio/output_audio may carry non-per-million-token units.

  • [medium] [possible mistake] providers/cloudflare-workers-ai/models/@cf/deepgram/flux.toml:2-3 - Check: Factual model data must be internally consistent. Why: The TOML has description = "Image model for prompt-driven generation, editing, and visual design workflows" and family = "flux" (the Black Forest Labs image-generation family), but the model's own modalities are input = ["audio"], output = ["text"] and it carries input_audio = 0.0077 — all indicating an ASR model, not an image model. The "flux" substring in the model name triggers false-positive image-model heuristics in describeModel (has(target, /\bflux\b/) → "Image model…") and inferFamily (matches "flux" in ModelFamilyValues). The native API fixture in the PR shows that the API provides an accurate description field per model, but reshapeNative discards it and buildOpenRouterModel synthesizes a generic description instead. Action: Override the description and family for @cf/deepgram/flux to reflect its ASR nature (e.g., family should not be "flux"; description should reference speech transcription). Consider preserving the native API's description field in reshapeNative so the generated TOMLs carry provider-accurate descriptions instead of heuristic ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant