21775ee2 feat(ai-cloudflare): add Cloudflare Workers AI and AI Gateway adapter (#1309)
* feat(ai-cloudflare): add Cloudflare Workers AI + AI Gateway adapter
Adds @tanstack/ai-cloudflare: chat (binding or REST, OpenAI-compatible surface
via openai-base), summarize, embeddings, image, TTS, and transcription over
env.AI or the REST /ai/run endpoint, with AI Gateway routing via a gateway
option and a cloudflareGateway() helper for other providers. Registers the
provider in the e2e matrix and docs, and adds the summarize max_tokens key.
Closes #1307
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* chore(e2e): sort ai-cloudflare dependency entry
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): send empty assistant content, surface Cloudflare errors, stream Nova audio
Workers AI rejects content: null on tool-call assistant turns, so the adapter
sends ''. Error bodies are rewrapped into the OpenAI { error } shape so the
SDK message carries Cloudflare's text. Binary inputs on the binding path are
sent as a ReadableStream, which Deepgram Nova requires.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(skills): list the Cloudflare adapter in adapter-configuration and structured-outputs
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): finalize structured output separately when tools are present
Workers AI models answer the tool follow-up turn in prose even with
response_format set, so the adapter opts out of combined tools + schema mode
and uses the separate finalization request, which the models honor.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-cloudflare): add byok entry and wire Cloudflare into ts-react-chat
Adds @tanstack/ai-cloudflare/byok (cloudflareByok) so a user's Cloudflare
token can come from the browser store while the account id stays server
side. The ts-react-chat example gains a Cloudflare provider (Workers AI
models plus a gateway-routed openai/gpt-5.5 option) and, when
CLOUDFLARE_AI_GATEWAY_ID is set, sends its OpenAI, Anthropic, and Groq
options through that AI Gateway via cloudflareGateway().
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(cloudflare): note AI Gateway Run permission for authenticated gateways
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* refactor(ts-react-chat): validate provider and model with zod instead of casts
The chat route parses forwardedProps.provider/model once through a
discriminated-union schema. Unknown providers fall back to OpenAI and
unknown models to each provider's default, and every adapter factory now
receives a model already narrowed to its literal union. Removes the ten
`(model || 'x') as 'x'` casts.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-cloudflare): add cloudflareAccountByok so users bring their own account
A user-supplied Cloudflare token belongs to that user's account, so BYOK
carries two values: cloudflareByok (token) and cloudflareAccountByok
(account id), each with its own env fallback. The ts-react-chat route
resolves both before building the adapter.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ts-react-chat): group multi-value BYOK credentials and make the key dialog scroll
The key dialog renders Cloudflare's account id and API token as one card
with two inputs and a single Save, while the store keeps one entry per id.
The dialog panel is capped to the viewport and scrolls internally instead
of extending past the frame.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ts-react-chat): save grouped BYOK fields sequentially
Passkey-backed storage runs a WebAuthn ceremony per write; concurrent
updates made the browser reject the second with "A request is already
pending".
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-client): let byokProvider return several ids for multi-value credentials
Some credentials are more than one stored value (Cloudflare account id +
API token). byokProvider may now return a list of ids and the relay
prepares and stamps one x-byok-<id> header per entry. A single-id selector
behaves exactly as before. The ts-react-chat example returns both
Cloudflare ids from its selector.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* chore(ai-client): drop unused single-id BYOK resolver
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): keep the /v1 segment in cloudflareGateway() base URL for xAI
Cloudflare serves xAI at /grok/v1/..., while the OpenAI SDK appends only
/chat/completions, so the helper now returns .../grok/v1. Other documented
providers are unchanged.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): address review findings on the native /ai/run adapters
- TTS: send `wav` as linear16 in a wav container instead of an invalid encoding
- Embedding: reject `dimensions` and throw on a wrong output shape instead of returning no vectors
- Transcription: throw on a missing transcript, check the audio URL fetch status, honour the abort signal on the Whisper path, and use the config `fetch`
- Config: split the REST config so only text and summarize accept OpenAI client options, and discriminate binding vs REST so a mixed config no longer compiles
- Reattach the `cloudflareGateway()` JSDoc, fix the README `Ai` import and the token-permission wording in the env example
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-client): export ByokProviderSelector and accept it in every generation hook
The chat hooks already took a list of ids through ChatClientBaseOptions; the generation and video hooks in react, solid, vue, svelte, angular, and remix still narrowed byokProvider to one id.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai): add getByokKeys() for credentials made of several values
`getByokKeys(request, { apiKey: cloudflareByok, accountId: cloudflareAccountByok })` reads one value per name with the same header-then-env rules as `getByokKey`.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(byok): use provider ids instead of bare slugs in grouped byokProvider samples
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(byok): declare companion credentials on the descriptor instead of returning several ids
`defineByokProvider({ with: [...] })` names the other values a credential needs, and `defineByok({ providers })` expands them: `headers()` sends every header and `prepare()` prompts for each missing one. `cloudflareByok` declares `cloudflareAccountByok`. This reverts the `byokProvider` list return and the hook type changes, since `body.provider` now resolves the whole credential.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i 21775ee2 feat(ai-cloudflare): add Cloudflare Workers AI and AI Gateway adapter (#1309)
* feat(ai-cloudflare): add Cloudflare Workers AI + AI Gateway adapter
Adds @tanstack/ai-cloudflare: chat (binding or REST, OpenAI-compatible surface
via openai-base), summarize, embeddings, image, TTS, and transcription over
env.AI or the REST /ai/run endpoint, with AI Gateway routing via a gateway
option and a cloudflareGateway() helper for other providers. Registers the
provider in the e2e matrix and docs, and adds the summarize max_tokens key.
Closes #1307
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* chore(e2e): sort ai-cloudflare dependency entry
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): send empty assistant content, surface Cloudflare errors, stream Nova audio
Workers AI rejects content: null on tool-call assistant turns, so the adapter
sends ''. Error bodies are rewrapped into the OpenAI { error } shape so the
SDK message carries Cloudflare's text. Binary inputs on the binding path are
sent as a ReadableStream, which Deepgram Nova requires.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(skills): list the Cloudflare adapter in adapter-configuration and structured-outputs
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): finalize structured output separately when tools are present
Workers AI models answer the tool follow-up turn in prose even with
response_format set, so the adapter opts out of combined tools + schema mode
and uses the separate finalization request, which the models honor.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-cloudflare): add byok entry and wire Cloudflare into ts-react-chat
Adds @tanstack/ai-cloudflare/byok (cloudflareByok) so a user's Cloudflare
token can come from the browser store while the account id stays server
side. The ts-react-chat example gains a Cloudflare provider (Workers AI
models plus a gateway-routed openai/gpt-5.5 option) and, when
CLOUDFLARE_AI_GATEWAY_ID is set, sends its OpenAI, Anthropic, and Groq
options through that AI Gateway via cloudflareGateway().
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(cloudflare): note AI Gateway Run permission for authenticated gateways
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* refactor(ts-react-chat): validate provider and model with zod instead of casts
The chat route parses forwardedProps.provider/model once through a
discriminated-union schema. Unknown providers fall back to OpenAI and
unknown models to each provider's default, and every adapter factory now
receives a model already narrowed to its literal union. Removes the ten
`(model || 'x') as 'x'` casts.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-cloudflare): add cloudflareAccountByok so users bring their own account
A user-supplied Cloudflare token belongs to that user's account, so BYOK
carries two values: cloudflareByok (token) and cloudflareAccountByok
(account id), each with its own env fallback. The ts-react-chat route
resolves both before building the adapter.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ts-react-chat): group multi-value BYOK credentials and make the key dialog scroll
The key dialog renders Cloudflare's account id and API token as one card
with two inputs and a single Save, while the store keeps one entry per id.
The dialog panel is capped to the viewport and scrolls internally instead
of extending past the frame.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ts-react-chat): save grouped BYOK fields sequentially
Passkey-backed storage runs a WebAuthn ceremony per write; concurrent
updates made the browser reject the second with "A request is already
pending".
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-client): let byokProvider return several ids for multi-value credentials
Some credentials are more than one stored value (Cloudflare account id +
API token). byokProvider may now return a list of ids and the relay
prepares and stamps one x-byok-<id> header per entry. A single-id selector
behaves exactly as before. The ts-react-chat example returns both
Cloudflare ids from its selector.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* chore(ai-client): drop unused single-id BYOK resolver
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): keep the /v1 segment in cloudflareGateway() base URL for xAI
Cloudflare serves xAI at /grok/v1/..., while the OpenAI SDK appends only
/chat/completions, so the helper now returns .../grok/v1. Other documented
providers are unchanged.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* fix(ai-cloudflare): address review findings on the native /ai/run adapters
- TTS: send `wav` as linear16 in a wav container instead of an invalid encoding
- Embedding: reject `dimensions` and throw on a wrong output shape instead of returning no vectors
- Transcription: throw on a missing transcript, check the audio URL fetch status, honour the abort signal on the Whisper path, and use the config `fetch`
- Config: split the REST config so only text and summarize accept OpenAI client options, and discriminate binding vs REST so a mixed config no longer compiles
- Reattach the `cloudflareGateway()` JSDoc, fix the README `Ai` import and the token-permission wording in the env example
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai-client): export ByokProviderSelector and accept it in every generation hook
The chat hooks already took a list of ids through ChatClientBaseOptions; the generation and video hooks in react, solid, vue, svelte, angular, and remix still narrowed byokProvider to one id.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(ai): add getByokKeys() for credentials made of several values
`getByokKeys(request, { apiKey: cloudflareByok, accountId: cloudflareAccountByok })` reads one value per name with the same header-then-env rules as `getByokKey`.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* docs(byok): use provider ids instead of bare slugs in grouped byokProvider samples
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i
* feat(byok): declare companion credentials on the descriptor instead of returning several ids
`defineByokProvider({ with: [...] })` names the other values a credential needs, and `defineByok({ providers })` expands them: `headers()` sends every header and `prepare()` prompts for each missing one. `cloudflareByok` declares `cloudflareAccountByok`. This reverts the `byokProvider` list return and the hook type changes, since `body.provider` now resolves the whole credential.
Claude-Session: https://claude.ai/code/session_01VMo1BZ95JB5tjWtfwpmN9i