@tanstack/ai-cloudflare 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/esm/adapters/embedding.d.ts +19 -0
  4. package/dist/esm/adapters/embedding.js +60 -0
  5. package/dist/esm/adapters/embedding.js.map +1 -0
  6. package/dist/esm/adapters/image.d.ts +26 -0
  7. package/dist/esm/adapters/image.js +62 -0
  8. package/dist/esm/adapters/image.js.map +1 -0
  9. package/dist/esm/adapters/summarize.d.ts +15 -0
  10. package/dist/esm/adapters/summarize.js +22 -0
  11. package/dist/esm/adapters/summarize.js.map +1 -0
  12. package/dist/esm/adapters/text.d.ts +71 -0
  13. package/dist/esm/adapters/text.js +88 -0
  14. package/dist/esm/adapters/text.js.map +1 -0
  15. package/dist/esm/adapters/transcription.d.ts +21 -0
  16. package/dist/esm/adapters/transcription.js +134 -0
  17. package/dist/esm/adapters/transcription.js.map +1 -0
  18. package/dist/esm/adapters/tts.d.ts +26 -0
  19. package/dist/esm/adapters/tts.js +68 -0
  20. package/dist/esm/adapters/tts.js.map +1 -0
  21. package/dist/esm/byok.d.ts +9 -0
  22. package/dist/esm/byok.js +24 -0
  23. package/dist/esm/byok.js.map +1 -0
  24. package/dist/esm/gateway.d.ts +30 -0
  25. package/dist/esm/gateway.js +45 -0
  26. package/dist/esm/gateway.js.map +1 -0
  27. package/dist/esm/index.d.ts +16 -0
  28. package/dist/esm/index.js +8 -0
  29. package/dist/esm/utils/config.d.ts +60 -0
  30. package/dist/esm/utils/config.js +48 -0
  31. package/dist/esm/utils/config.js.map +1 -0
  32. package/dist/esm/utils/fetch.d.ts +27 -0
  33. package/dist/esm/utils/fetch.js +94 -0
  34. package/dist/esm/utils/fetch.js.map +1 -0
  35. package/dist/esm/utils/models.d.ts +16 -0
  36. package/dist/esm/utils/run.d.ts +21 -0
  37. package/dist/esm/utils/run.js +62 -0
  38. package/dist/esm/utils/run.js.map +1 -0
  39. package/package.json +73 -4
  40. package/src/adapters/embedding.ts +84 -0
  41. package/src/adapters/image.ts +97 -0
  42. package/src/adapters/summarize.ts +46 -0
  43. package/src/adapters/text.ts +150 -0
  44. package/src/adapters/transcription.ts +227 -0
  45. package/src/adapters/tts.ts +100 -0
  46. package/src/byok.ts +21 -0
  47. package/src/gateway.ts +57 -0
  48. package/src/index.ts +68 -0
  49. package/src/utils/config.ts +129 -0
  50. package/src/utils/fetch.ts +131 -0
  51. package/src/utils/models.ts +41 -0
  52. package/src/utils/run.ts +116 -0
@@ -0,0 +1,62 @@
1
+ import { CLOUDFLARE_API_BASE, gatewayHeaders, isBindingConfig } from "./config.js";
2
+ import { arrayBufferToBase64 } from "@tanstack/ai-utils";
3
+ //#region src/utils/run.ts
4
+ /**
5
+ * Runs a Workers AI model with its native task inputs (embeddings, image,
6
+ * speech, transcription) through the binding or the REST `/ai/run` endpoint.
7
+ *
8
+ * Returns the model's decoded output: an object for JSON tasks, or bytes
9
+ * (`Uint8Array` / `ReadableStream`) for binary media outputs.
10
+ */
11
+ async function runModel(config, model, inputs, options) {
12
+ if (isBindingConfig(config)) return await config.binding.run.bind(config.binding)(model, options?.binary ? {
13
+ ...inputs,
14
+ [options.binary.field]: {
15
+ body: new Response(options.binary.body).body,
16
+ contentType: options.binary.contentType
17
+ }
18
+ } : inputs, config.gateway ? { gateway: config.gateway } : void 0);
19
+ const url = new URL(`${CLOUDFLARE_API_BASE}/accounts/${config.accountId}/ai/run/${model}`);
20
+ const headers = {
21
+ Authorization: `Bearer ${config.apiKey}`,
22
+ ...gatewayHeaders(config.gateway)
23
+ };
24
+ let body;
25
+ if (options?.binary) {
26
+ for (const [key, value] of Object.entries(inputs)) if (value !== void 0) url.searchParams.set(key, String(value));
27
+ headers["Content-Type"] = options.binary.contentType;
28
+ body = options.binary.body;
29
+ } else {
30
+ headers["Content-Type"] = "application/json";
31
+ body = JSON.stringify(inputs);
32
+ }
33
+ const response = await (config.fetch ?? fetch)(url, {
34
+ method: "POST",
35
+ headers,
36
+ body,
37
+ signal: options?.signal
38
+ });
39
+ if (!response.ok) throw new Error(`Workers AI request for ${model} failed (${response.status}): ${await response.text()}`);
40
+ if (response.headers.get("content-type")?.includes("application/json")) {
41
+ const json = await response.json();
42
+ if (json.success === false) throw new Error(`Workers AI request for ${model} failed: ${json.errors?.map((e) => e.message).join("; ")}`);
43
+ return "result" in json ? json.result : json;
44
+ }
45
+ return new Uint8Array(await response.arrayBuffer());
46
+ }
47
+ /** Base64-encodes a binary model output, whatever shape it arrived in. */
48
+ async function outputToBase64(output) {
49
+ if (typeof output === "string") return output;
50
+ let bytes;
51
+ if (output instanceof Uint8Array) bytes = output;
52
+ else if (output instanceof ArrayBuffer) bytes = new Uint8Array(output);
53
+ else if (output instanceof ReadableStream) bytes = new Uint8Array(await new Response(output).arrayBuffer());
54
+ else throw new Error(`Unexpected Workers AI output type: ${Object.prototype.toString.call(output)}`);
55
+ const copy = new Uint8Array(bytes.byteLength);
56
+ copy.set(bytes);
57
+ return arrayBufferToBase64(copy.buffer);
58
+ }
59
+ //#endregion
60
+ export { outputToBase64, runModel };
61
+
62
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","names":[],"sources":["../../../src/utils/run.ts"],"sourcesContent":["import { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport { CLOUDFLARE_API_BASE, gatewayHeaders, isBindingConfig } from './config'\nimport type { CloudflareConfig } from './config'\n\nexport type RunInputs = Record<string, unknown>\n\nexport interface RunBinary {\n /** Input field that carries the bytes on the binding path. */\n field: string\n body: Uint8Array | ArrayBuffer | Blob\n contentType: string\n}\n\n/**\n * Runs a Workers AI model with its native task inputs (embeddings, image,\n * speech, transcription) through the binding or the REST `/ai/run` endpoint.\n *\n * Returns the model's decoded output: an object for JSON tasks, or bytes\n * (`Uint8Array` / `ReadableStream`) for binary media outputs.\n */\nexport async function runModel(\n config: CloudflareConfig,\n model: string,\n inputs: RunInputs,\n options?: { signal?: AbortSignal; binary?: RunBinary },\n): Promise<unknown> {\n if (isBindingConfig(config)) {\n const run = config.binding.run.bind(config.binding) as (\n model: string,\n inputs: RunInputs,\n options?: Record<string, unknown>,\n ) => Promise<unknown>\n // The binding serializes inputs as JSON; binary bodies must travel as a\n // ReadableStream, which it forwards as the raw request body.\n const bindingInputs = options?.binary\n ? {\n ...inputs,\n [options.binary.field]: {\n body: new Response(options.binary.body as BodyInit).body,\n contentType: options.binary.contentType,\n },\n }\n : inputs\n return await run(\n model,\n bindingInputs,\n config.gateway ? { gateway: config.gateway } : undefined,\n )\n }\n\n const url = new URL(\n `${CLOUDFLARE_API_BASE}/accounts/${config.accountId}/ai/run/${model}`,\n )\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.apiKey}`,\n ...gatewayHeaders(config.gateway),\n }\n let body: BodyInit\n if (options?.binary) {\n // Binary tasks take the bytes as the body and the other inputs as query.\n for (const [key, value] of Object.entries(inputs)) {\n if (value !== undefined) url.searchParams.set(key, String(value))\n }\n headers['Content-Type'] = options.binary.contentType\n body = options.binary.body as BodyInit\n } else {\n headers['Content-Type'] = 'application/json'\n body = JSON.stringify(inputs)\n }\n const fetchImpl = config.fetch ?? fetch\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n signal: options?.signal,\n })\n if (!response.ok) {\n throw new Error(\n `Workers AI request for ${model} failed (${response.status}): ${await response.text()}`,\n )\n }\n if (response.headers.get('content-type')?.includes('application/json')) {\n const json = (await response.json()) as {\n success?: boolean\n result?: unknown\n errors?: Array<{ message?: string }>\n }\n if (json.success === false) {\n throw new Error(\n `Workers AI request for ${model} failed: ${json.errors?.map((e) => e.message).join('; ')}`,\n )\n }\n return 'result' in json ? json.result : json\n }\n return new Uint8Array(await response.arrayBuffer())\n}\n\n/** Base64-encodes a binary model output, whatever shape it arrived in. */\nexport async function outputToBase64(output: unknown): Promise<string> {\n if (typeof output === 'string') return output\n let bytes: Uint8Array\n if (output instanceof Uint8Array) {\n bytes = output\n } else if (output instanceof ArrayBuffer) {\n bytes = new Uint8Array(output)\n } else if (output instanceof ReadableStream) {\n bytes = new Uint8Array(await new Response(output).arrayBuffer())\n } else {\n throw new Error(\n `Unexpected Workers AI output type: ${Object.prototype.toString.call(output)}`,\n )\n }\n const copy = new Uint8Array(bytes.byteLength)\n copy.set(bytes)\n return arrayBufferToBase64(copy.buffer)\n}\n"],"mappings":";;;;;;;;;;AAoBA,eAAsB,SACpB,QACA,OACA,QACA,SACkB;CAClB,IAAI,gBAAgB,MAAM,GAiBxB,OAAO,MAhBK,OAAO,QAAQ,IAAI,KAAK,OAAO,OAgB9B,CAAA,CACX,OAVoB,SAAS,SAC3B;EACE,GAAG;GACF,QAAQ,OAAO,QAAQ;GACtB,MAAM,IAAI,SAAS,QAAQ,OAAO,IAAgB,CAAC,CAAC;GACpD,aAAa,QAAQ,OAAO;EAC9B;CACF,IACA,QAIF,OAAO,UAAU,EAAE,SAAS,OAAO,QAAQ,IAAI,KAAA,CACjD;CAGF,MAAM,MAAM,IAAI,IACd,GAAG,oBAAoB,YAAY,OAAO,UAAU,UAAU,OAChE;CACA,MAAM,UAAkC;EACtC,eAAe,UAAU,OAAO;EAChC,GAAG,eAAe,OAAO,OAAO;CAClC;CACA,IAAI;CACJ,IAAI,SAAS,QAAQ;EAEnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,UAAU,KAAA,GAAW,IAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;EAElE,QAAQ,kBAAkB,QAAQ,OAAO;EACzC,OAAO,QAAQ,OAAO;CACxB,OAAO;EACL,QAAQ,kBAAkB;EAC1B,OAAO,KAAK,UAAU,MAAM;CAC9B;CAEA,MAAM,WAAW,OADC,OAAO,SAAS,MAAA,CACD,KAAK;EACpC,QAAQ;EACR;EACA;EACA,QAAQ,SAAS;CACnB,CAAC;CACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,0BAA0B,MAAM,WAAW,SAAS,OAAO,KAAK,MAAM,SAAS,KAAK,GACtF;CAEF,IAAI,SAAS,QAAQ,IAAI,cAAc,CAAC,EAAE,SAAS,kBAAkB,GAAG;EACtE,MAAM,OAAQ,MAAM,SAAS,KAAK;EAKlC,IAAI,KAAK,YAAY,OACnB,MAAM,IAAI,MACR,0BAA0B,MAAM,WAAW,KAAK,QAAQ,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,GACzF;EAEF,OAAO,YAAY,OAAO,KAAK,SAAS;CAC1C;CACA,OAAO,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC;AACpD;;AAGA,eAAsB,eAAe,QAAkC;CACrE,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,IAAI;CACJ,IAAI,kBAAkB,YACpB,QAAQ;MACH,IAAI,kBAAkB,aAC3B,QAAQ,IAAI,WAAW,MAAM;MACxB,IAAI,kBAAkB,gBAC3B,QAAQ,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,YAAY,CAAC;MAE/D,MAAM,IAAI,MACR,sCAAsC,OAAO,UAAU,SAAS,KAAK,MAAM,GAC7E;CAEF,MAAM,OAAO,IAAI,WAAW,MAAM,UAAU;CAC5C,KAAK,IAAI,KAAK;CACd,OAAO,oBAAoB,KAAK,MAAM;AACxC"}
package/package.json CHANGED
@@ -1,11 +1,80 @@
1
- {
1
+ {
2
2
  "name": "@tanstack/ai-cloudflare",
3
- "version": "0.0.0",
4
- "description": "Placeholder for npm trusted publishing setup. Not a functional package.",
3
+ "version": "0.1.1",
4
+ "description": "Cloudflare adapter for TanStack AI — Workers AI chat, embeddings, images, speech, transcription, and summarization over the AI binding or REST, with AI Gateway routing for any provider.",
5
+ "author": "Tanner Linsley",
5
6
  "license": "MIT",
7
+ "homepage": "https://tanstack.com/ai",
6
8
  "repository": {
7
9
  "type": "git",
8
10
  "url": "git+https://github.com/TanStack/ai.git",
9
11
  "directory": "packages/ai-cloudflare"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/TanStack/ai/issues"
15
+ },
16
+ "funding": {
17
+ "type": "github",
18
+ "url": "https://github.com/sponsors/tannerlinsley"
19
+ },
20
+ "type": "module",
21
+ "module": "./dist/esm/index.js",
22
+ "types": "./dist/esm/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/esm/index.d.ts",
26
+ "import": "./dist/esm/index.js"
27
+ },
28
+ "./byok": {
29
+ "types": "./dist/esm/byok.d.ts",
30
+ "import": "./dist/esm/byok.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src"
36
+ ],
37
+ "keywords": [
38
+ "ai",
39
+ "ai-sdk",
40
+ "typescript",
41
+ "tanstack",
42
+ "cloudflare",
43
+ "workers-ai",
44
+ "ai-gateway",
45
+ "workers",
46
+ "adapter",
47
+ "chat",
48
+ "streaming",
49
+ "tool-calling",
50
+ "structured-outputs",
51
+ "embeddings",
52
+ "image-generation",
53
+ "text-to-speech",
54
+ "transcription"
55
+ ],
56
+ "dependencies": {
57
+ "@cloudflare/workers-types": "^4.20260317.1",
58
+ "openai": "^6.41.0",
59
+ "@tanstack/ai-utils": "^0.4.0",
60
+ "@tanstack/openai-base": "^0.10.11"
61
+ },
62
+ "devDependencies": {
63
+ "@vitest/coverage-v8": "4.1.10",
64
+ "vite": "^8.2.1",
65
+ "@tanstack/ai": "0.54.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@tanstack/ai": "^0.54.0"
69
+ },
70
+ "scripts": {
71
+ "build": "vite build",
72
+ "clean": "premove ./build ./dist",
73
+ "lint:fix": "oxlint src --type-aware --fix",
74
+ "test:build": "publint --strict",
75
+ "test:oxlint": "oxlint src --type-aware",
76
+ "test:lib": "vitest run",
77
+ "test:lib:dev": "pnpm test:lib --watch",
78
+ "test:types": "tsc"
10
79
  }
11
- }
80
+ }
@@ -0,0 +1,84 @@
1
+ import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters'
2
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
3
+ import { generateId } from '@tanstack/ai-utils'
4
+ import { requireTextOnlyEmbeddingInput } from '@tanstack/ai'
5
+ import { resolveConfigFromEnv } from '../utils/config'
6
+ import { runModel } from '../utils/run'
7
+ import type { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai'
8
+ import type { CloudflareConfig, CloudflareConfigInput } from '../utils/config'
9
+ import type { CloudflareEmbeddingModel } from '../utils/models'
10
+
11
+ /** Extra inputs forwarded to the embedding model (model specific). */
12
+ export type CloudflareEmbeddingProviderOptions = Record<string, unknown>
13
+
14
+ /**
15
+ * Cloudflare embedding adapter. Runs Workers AI text-embedding models
16
+ * (`{ text: [...] }` in, `{ data: number[][] }` out) through the binding or
17
+ * the REST API.
18
+ */
19
+ export class CloudflareEmbeddingAdapter<
20
+ TModel extends CloudflareEmbeddingModel,
21
+ > extends BaseEmbeddingAdapter<TModel, CloudflareEmbeddingProviderOptions> {
22
+ readonly name = 'cloudflare' as const
23
+
24
+ constructor(
25
+ private readonly cfConfig: CloudflareConfig,
26
+ model: TModel,
27
+ ) {
28
+ super(model, {})
29
+ }
30
+
31
+ async createEmbeddings(
32
+ options: EmbeddingOptions<CloudflareEmbeddingProviderOptions>,
33
+ ): Promise<EmbeddingResult> {
34
+ const { model, logger } = options
35
+ const texts = requireTextOnlyEmbeddingInput(options.input, this.name, model)
36
+ if (options.dimensions !== undefined) {
37
+ throw new Error(
38
+ 'Workers AI embedding models have fixed dimensions; do not set `dimensions`',
39
+ )
40
+ }
41
+ try {
42
+ logger.request(
43
+ `activity=embed provider=${this.name} model=${model} inputs=${texts.length}`,
44
+ { provider: this.name, model },
45
+ )
46
+ const output = (await runModel(this.cfConfig, model, {
47
+ ...options.modelOptions,
48
+ text: texts,
49
+ })) as { data?: Array<Array<number>> }
50
+ if (!Array.isArray(output.data) || output.data.length !== texts.length) {
51
+ throw new Error(
52
+ `Workers AI ${model} returned ${output.data?.length ?? 0} embeddings for ${texts.length} inputs`,
53
+ )
54
+ }
55
+ return {
56
+ id: generateId(this.name),
57
+ model,
58
+ embeddings: output.data.map((vector, index) => ({
59
+ vector,
60
+ index,
61
+ })),
62
+ }
63
+ } catch (error: unknown) {
64
+ logger.errors(`${this.name}.createEmbeddings fatal`, {
65
+ error: toRunErrorPayload(error, `${this.name}.createEmbeddings failed`),
66
+ source: `${this.name}.createEmbeddings`,
67
+ })
68
+ throw error
69
+ }
70
+ }
71
+ }
72
+
73
+ export function createCloudflareEmbedding<
74
+ TModel extends CloudflareEmbeddingModel,
75
+ >(model: TModel, config: CloudflareConfig): CloudflareEmbeddingAdapter<TModel> {
76
+ return new CloudflareEmbeddingAdapter(config, model)
77
+ }
78
+
79
+ export function cloudflareEmbedding<TModel extends CloudflareEmbeddingModel>(
80
+ model: TModel,
81
+ config?: CloudflareConfigInput,
82
+ ): CloudflareEmbeddingAdapter<TModel> {
83
+ return new CloudflareEmbeddingAdapter(resolveConfigFromEnv(config), model)
84
+ }
@@ -0,0 +1,97 @@
1
+ import { BaseImageAdapter } from '@tanstack/ai/adapters'
2
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
3
+ import { generateId } from '@tanstack/ai-utils'
4
+ import { resolveMediaPrompt } from '@tanstack/ai'
5
+ import { resolveConfigFromEnv } from '../utils/config'
6
+ import { outputToBase64, runModel } from '../utils/run'
7
+ import type {
8
+ GeneratedImage,
9
+ ImageGenerationOptions,
10
+ ImageGenerationResult,
11
+ } from '@tanstack/ai'
12
+ import type { CloudflareConfig, CloudflareConfigInput } from '../utils/config'
13
+ import type { CloudflareImageModel } from '../utils/models'
14
+
15
+ /** Text-to-image inputs forwarded to the model (`steps`, `guidance`, ...). */
16
+ export interface CloudflareImageProviderOptions {
17
+ negative_prompt?: string
18
+ steps?: number
19
+ num_steps?: number
20
+ guidance?: number
21
+ seed?: number
22
+ [key: string]: unknown
23
+ }
24
+
25
+ /**
26
+ * Cloudflare image adapter. Runs Workers AI text-to-image models and returns
27
+ * base64 images, whether the model answers with `{ image }` JSON (Flux,
28
+ * Leonardo) or raw PNG bytes (Stable Diffusion).
29
+ */
30
+ export class CloudflareImageAdapter<
31
+ TModel extends CloudflareImageModel,
32
+ > extends BaseImageAdapter<TModel, CloudflareImageProviderOptions> {
33
+ readonly name = 'cloudflare' as const
34
+
35
+ constructor(
36
+ private readonly cfConfig: CloudflareConfig,
37
+ model: TModel,
38
+ ) {
39
+ super(model, {})
40
+ }
41
+
42
+ async generateImages(
43
+ options: ImageGenerationOptions<CloudflareImageProviderOptions>,
44
+ ): Promise<ImageGenerationResult> {
45
+ const { model, logger, numberOfImages = 1 } = options
46
+ const prompt = resolveMediaPrompt(options.prompt)
47
+ const [width, height] = options.size?.split('x').map(Number) ?? []
48
+ const inputs = {
49
+ ...(width && { width }),
50
+ ...(height && { height }),
51
+ ...options.modelOptions,
52
+ prompt: prompt.text,
53
+ }
54
+ try {
55
+ logger.request(
56
+ `activity=image provider=${this.name} model=${model} n=${numberOfImages}`,
57
+ { provider: this.name, model },
58
+ )
59
+ const images = await Promise.all(
60
+ Array.from(
61
+ { length: numberOfImages },
62
+ async (): Promise<GeneratedImage> => {
63
+ const output = await runModel(this.cfConfig, model, inputs, {
64
+ signal: options.abortSignal,
65
+ })
66
+ const image =
67
+ output && typeof output === 'object' && 'image' in output
68
+ ? (output as { image: string }).image
69
+ : await outputToBase64(output)
70
+ return { b64Json: image }
71
+ },
72
+ ),
73
+ )
74
+ return { id: generateId(this.name), model, images }
75
+ } catch (error: unknown) {
76
+ logger.errors(`${this.name}.generateImages fatal`, {
77
+ error: toRunErrorPayload(error, `${this.name}.generateImages failed`),
78
+ source: `${this.name}.generateImages`,
79
+ })
80
+ throw error
81
+ }
82
+ }
83
+ }
84
+
85
+ export function createCloudflareImage<TModel extends CloudflareImageModel>(
86
+ model: TModel,
87
+ config: CloudflareConfig,
88
+ ): CloudflareImageAdapter<TModel> {
89
+ return new CloudflareImageAdapter(config, model)
90
+ }
91
+
92
+ export function cloudflareImage<TModel extends CloudflareImageModel>(
93
+ model: TModel,
94
+ config?: CloudflareConfigInput,
95
+ ): CloudflareImageAdapter<TModel> {
96
+ return new CloudflareImageAdapter(resolveConfigFromEnv(config), model)
97
+ }
@@ -0,0 +1,46 @@
1
+ import { ChatStreamSummarizeAdapter } from '@tanstack/ai/adapters'
2
+ import { resolveConfigFromEnv } from '../utils/config'
3
+ import { CloudflareTextAdapter } from './text'
4
+ import type { InferTextProviderOptions } from '@tanstack/ai/adapters'
5
+ import type {
6
+ CloudflareConfigInput,
7
+ CloudflareTextConfig,
8
+ CloudflareTextRestConfig,
9
+ } from '../utils/config'
10
+ import type { CloudflareTextModel } from '../utils/models'
11
+
12
+ export type CloudflareSummarizeModel = CloudflareTextModel
13
+
14
+ /**
15
+ * Creates a Cloudflare summarize adapter. Summaries run as a chat request
16
+ * against the given model.
17
+ */
18
+ export function createCloudflareSummarize<
19
+ TModel extends CloudflareSummarizeModel,
20
+ >(
21
+ model: TModel,
22
+ config: CloudflareTextConfig,
23
+ ): ChatStreamSummarizeAdapter<
24
+ TModel,
25
+ InferTextProviderOptions<CloudflareTextAdapter<TModel>>
26
+ > {
27
+ return new ChatStreamSummarizeAdapter(
28
+ new CloudflareTextAdapter(config, model),
29
+ model,
30
+ 'cloudflare',
31
+ )
32
+ }
33
+
34
+ /**
35
+ * Creates a Cloudflare summarize adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
36
+ * `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
37
+ */
38
+ export function cloudflareSummarize<TModel extends CloudflareSummarizeModel>(
39
+ model: TModel,
40
+ config?: CloudflareConfigInput<CloudflareTextRestConfig>,
41
+ ): ChatStreamSummarizeAdapter<
42
+ TModel,
43
+ InferTextProviderOptions<CloudflareTextAdapter<TModel>>
44
+ > {
45
+ return createCloudflareSummarize(model, resolveConfigFromEnv(config))
46
+ }
@@ -0,0 +1,150 @@
1
+ import OpenAI from 'openai'
2
+ import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
3
+ import {
4
+ gatewayHeaders,
5
+ isBindingConfig,
6
+ resolveConfigFromEnv,
7
+ restChatBaseURL,
8
+ } from '../utils/config'
9
+ import { createBindingFetch, createRestFetch } from '../utils/fetch'
10
+ import type {
11
+ ChatCompletionChunk,
12
+ ChatCompletionMessageParam,
13
+ } from 'openai/resources/chat/completions/completions'
14
+ import type {
15
+ CloudflareConfigInput,
16
+ CloudflareTextConfig,
17
+ CloudflareTextRestConfig,
18
+ } from '../utils/config'
19
+ import type { CloudflareTextModel } from '../utils/models'
20
+ import type { ModelMessage } from '@tanstack/ai'
21
+
22
+ /**
23
+ * Chat Completions parameters forwarded verbatim to Workers AI. Reasoning
24
+ * models (GLM, Kimi, gpt-oss, QwQ) read `reasoning_effort` and
25
+ * `chat_template_kwargs`; `null` for `reasoning_effort` turns reasoning off.
26
+ */
27
+ export interface CloudflareTextProviderOptions {
28
+ temperature?: number
29
+ max_tokens?: number
30
+ top_p?: number
31
+ top_k?: number
32
+ seed?: number
33
+ repetition_penalty?: number
34
+ frequency_penalty?: number
35
+ presence_penalty?: number
36
+ reasoning_effort?: 'low' | 'medium' | 'high' | null
37
+ chat_template_kwargs?: {
38
+ enable_thinking?: boolean
39
+ clear_thinking?: boolean
40
+ }
41
+ }
42
+
43
+ function createClient(config: CloudflareTextConfig): OpenAI {
44
+ if (isBindingConfig(config)) {
45
+ return new OpenAI({
46
+ // The binding authenticates by itself; the SDK only requires a value.
47
+ apiKey: 'cloudflare-binding',
48
+ fetch: createBindingFetch(config.binding, config.gateway),
49
+ })
50
+ }
51
+ const {
52
+ accountId: _accountId,
53
+ binding: _binding,
54
+ gateway,
55
+ ...clientOptions
56
+ } = config
57
+ return new OpenAI({
58
+ ...clientOptions,
59
+ baseURL: restChatBaseURL(config),
60
+ defaultHeaders: {
61
+ ...gatewayHeaders(gateway),
62
+ ...clientOptions.defaultHeaders,
63
+ },
64
+ fetch: createRestFetch(clientOptions.fetch),
65
+ })
66
+ }
67
+
68
+ /**
69
+ * Cloudflare text (chat) adapter.
70
+ *
71
+ * Drives Workers AI's OpenAI-compatible Chat Completions surface with the
72
+ * OpenAI SDK. Inside a Worker pass `{ binding: env.AI }`; anywhere else pass
73
+ * `{ accountId, apiKey }`. Add `gateway` to route through AI Gateway. Any
74
+ * catalog model works, including third-party `provider/model` ids billed
75
+ * through AI Gateway.
76
+ */
77
+ export class CloudflareTextAdapter<
78
+ TModel extends CloudflareTextModel,
79
+ TProviderOptions extends Record<string, any> = CloudflareTextProviderOptions,
80
+ > extends OpenAIBaseChatCompletionsTextAdapter<TModel, TProviderOptions> {
81
+ override readonly kind = 'text' as const
82
+ override readonly name = 'cloudflare' as const
83
+
84
+ constructor(config: CloudflareTextConfig, model: TModel) {
85
+ super(model, 'cloudflare', createClient(config))
86
+ }
87
+
88
+ /**
89
+ * Workers AI validates `messages[].content` as a string, so a tool-call-only
90
+ * assistant turn (which OpenAI accepts as `content: null`) is sent as `''`.
91
+ */
92
+ protected override convertMessage(
93
+ message: ModelMessage,
94
+ ): ChatCompletionMessageParam {
95
+ const converted = super.convertMessage(message)
96
+ if (converted.role === 'assistant' && converted.content == null) {
97
+ return { ...converted, content: '' }
98
+ }
99
+ return converted
100
+ }
101
+
102
+ /**
103
+ * Workers AI accepts `response_format` next to `tools` but its models answer
104
+ * the tool follow-up turn in prose, so structured output with tools runs as
105
+ * a separate finalization request instead.
106
+ */
107
+ override supportsCombinedToolsAndSchema(): boolean {
108
+ return false
109
+ }
110
+
111
+ /** Workers AI reasoning models stream thinking as `reasoning_content` (some as `reasoning`). */
112
+ protected override extractReasoning(
113
+ chunk: ChatCompletionChunk,
114
+ ): { text: string } | undefined {
115
+ const delta = chunk.choices[0]?.delta as
116
+ | { reasoning?: unknown; reasoning_content?: unknown }
117
+ | undefined
118
+ const raw = delta?.reasoning_content ?? delta?.reasoning
119
+ return typeof raw === 'string' && raw.length > 0 ? { text: raw } : undefined
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Creates a Cloudflare text adapter with explicit configuration.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * // Inside a Worker
129
+ * const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { binding: env.AI })
130
+ * // Anywhere, over REST
131
+ * const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { accountId, apiKey })
132
+ * ```
133
+ */
134
+ export function createCloudflareText<TModel extends CloudflareTextModel>(
135
+ model: TModel,
136
+ config: CloudflareTextConfig,
137
+ ): CloudflareTextAdapter<TModel> {
138
+ return new CloudflareTextAdapter(config, model)
139
+ }
140
+
141
+ /**
142
+ * Creates a Cloudflare text adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
143
+ * `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
144
+ */
145
+ export function cloudflareText<TModel extends CloudflareTextModel>(
146
+ model: TModel,
147
+ config?: CloudflareConfigInput<CloudflareTextRestConfig>,
148
+ ): CloudflareTextAdapter<TModel> {
149
+ return new CloudflareTextAdapter(resolveConfigFromEnv(config), model)
150
+ }