@cloudflare/tanstack-ai 0.1.1 → 0.1.3

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 (86) hide show
  1. package/README.md +9 -0
  2. package/dist/_tsup-dts-rollup.d.cts +35 -18
  3. package/dist/_tsup-dts-rollup.d.ts +35 -18
  4. package/dist/adapters/anthropic.cjs +3 -3
  5. package/dist/adapters/anthropic.js +2 -2
  6. package/dist/adapters/gemini.cjs +2 -2
  7. package/dist/adapters/gemini.js +1 -1
  8. package/dist/adapters/grok.cjs +3 -3
  9. package/dist/adapters/grok.js +2 -2
  10. package/dist/adapters/openai.cjs +3 -3
  11. package/dist/adapters/openai.js +2 -2
  12. package/dist/adapters/openrouter.cjs +3 -3
  13. package/dist/adapters/openrouter.js +2 -2
  14. package/dist/adapters/workers-ai-image.cjs +3 -3
  15. package/dist/adapters/workers-ai-image.js +2 -2
  16. package/dist/adapters/workers-ai-summarize.cjs +3 -3
  17. package/dist/adapters/workers-ai-summarize.js +2 -2
  18. package/dist/adapters/workers-ai-transcription.cjs +3 -3
  19. package/dist/adapters/workers-ai-transcription.js +2 -2
  20. package/dist/adapters/workers-ai-tts.cjs +3 -3
  21. package/dist/adapters/workers-ai-tts.js +2 -2
  22. package/dist/adapters/workers-ai.cjs +3 -3
  23. package/dist/adapters/workers-ai.js +2 -2
  24. package/dist/{chunk-4ACSLQDI.cjs → chunk-45DEJBFY.cjs} +7 -5
  25. package/dist/chunk-45DEJBFY.cjs.map +1 -0
  26. package/dist/{chunk-S3ALRROX.js → chunk-4HOZDNW6.js} +2 -2
  27. package/dist/{chunk-6FBIXTAL.cjs → chunk-53RO5I22.cjs} +46 -37
  28. package/dist/chunk-53RO5I22.cjs.map +1 -0
  29. package/dist/{chunk-VV3JFKAN.js → chunk-627RVSSB.js} +5 -3
  30. package/dist/chunk-627RVSSB.js.map +1 -0
  31. package/dist/{chunk-UUFEOQ6B.js → chunk-727MRLUF.js} +38 -29
  32. package/dist/chunk-727MRLUF.js.map +1 -0
  33. package/dist/{chunk-AHXFO2BB.cjs → chunk-7YINQQCY.cjs} +3 -3
  34. package/dist/chunk-7YINQQCY.cjs.map +1 -0
  35. package/dist/{chunk-SIOQQHXS.js → chunk-AJMCHPPR.js} +2 -2
  36. package/dist/chunk-AJMCHPPR.js.map +1 -0
  37. package/dist/{chunk-GL2EQLMI.cjs → chunk-CPOXZIMA.cjs} +7 -5
  38. package/dist/chunk-CPOXZIMA.cjs.map +1 -0
  39. package/dist/{chunk-SFZAUNHJ.cjs → chunk-DQGO754X.cjs} +5 -5
  40. package/dist/{chunk-SFZAUNHJ.cjs.map → chunk-DQGO754X.cjs.map} +1 -1
  41. package/dist/{chunk-VZJHRPOT.cjs → chunk-DTGQYEJ2.cjs} +50 -32
  42. package/dist/chunk-DTGQYEJ2.cjs.map +1 -0
  43. package/dist/{chunk-ALUCJNDE.js → chunk-GST6AT3R.js} +5 -3
  44. package/dist/chunk-GST6AT3R.js.map +1 -0
  45. package/dist/{chunk-5YEJ5ZRQ.js → chunk-HDDZEGLC.js} +49 -31
  46. package/dist/chunk-HDDZEGLC.js.map +1 -0
  47. package/dist/{chunk-F75IZQCM.js → chunk-HFZK4C5B.js} +2 -2
  48. package/dist/{chunk-AN23SOZX.cjs → chunk-IL6PTKJE.cjs} +7 -5
  49. package/dist/chunk-IL6PTKJE.cjs.map +1 -0
  50. package/dist/{chunk-7AEFXYJG.js → chunk-J5DSSZTO.js} +9 -3
  51. package/dist/chunk-J5DSSZTO.js.map +1 -0
  52. package/dist/{chunk-OV65IEEY.cjs → chunk-MPVUETJY.cjs} +3 -3
  53. package/dist/{chunk-OV65IEEY.cjs.map → chunk-MPVUETJY.cjs.map} +1 -1
  54. package/dist/{chunk-R2MRGLZ4.js → chunk-PMPGIUVA.js} +5 -3
  55. package/dist/chunk-PMPGIUVA.js.map +1 -0
  56. package/dist/{chunk-6OXP4IVS.cjs → chunk-QUS7INYA.cjs} +5 -5
  57. package/dist/{chunk-6OXP4IVS.cjs.map → chunk-QUS7INYA.cjs.map} +1 -1
  58. package/dist/{chunk-PAVBM57P.js → chunk-RKBJLYVO.js} +2 -2
  59. package/dist/{chunk-RGDUK5KX.cjs → chunk-V4LKHWJA.cjs} +9 -3
  60. package/dist/chunk-V4LKHWJA.cjs.map +1 -0
  61. package/dist/{chunk-2RO3A3R4.js → chunk-XCLZJGUI.js} +5 -3
  62. package/dist/chunk-XCLZJGUI.js.map +1 -0
  63. package/dist/{chunk-M64PETK7.cjs → chunk-Y2PUBTP5.cjs} +7 -5
  64. package/dist/chunk-Y2PUBTP5.cjs.map +1 -0
  65. package/dist/index.cjs +12 -12
  66. package/dist/index.js +11 -11
  67. package/package.json +11 -10
  68. package/dist/chunk-2RO3A3R4.js.map +0 -1
  69. package/dist/chunk-4ACSLQDI.cjs.map +0 -1
  70. package/dist/chunk-5YEJ5ZRQ.js.map +0 -1
  71. package/dist/chunk-6FBIXTAL.cjs.map +0 -1
  72. package/dist/chunk-7AEFXYJG.js.map +0 -1
  73. package/dist/chunk-AHXFO2BB.cjs.map +0 -1
  74. package/dist/chunk-ALUCJNDE.js.map +0 -1
  75. package/dist/chunk-AN23SOZX.cjs.map +0 -1
  76. package/dist/chunk-GL2EQLMI.cjs.map +0 -1
  77. package/dist/chunk-M64PETK7.cjs.map +0 -1
  78. package/dist/chunk-R2MRGLZ4.js.map +0 -1
  79. package/dist/chunk-RGDUK5KX.cjs.map +0 -1
  80. package/dist/chunk-SIOQQHXS.js.map +0 -1
  81. package/dist/chunk-UUFEOQ6B.js.map +0 -1
  82. package/dist/chunk-VV3JFKAN.js.map +0 -1
  83. package/dist/chunk-VZJHRPOT.cjs.map +0 -1
  84. /package/dist/{chunk-S3ALRROX.js.map → chunk-4HOZDNW6.js.map} +0 -0
  85. /package/dist/{chunk-F75IZQCM.js.map → chunk-HFZK4C5B.js.map} +0 -0
  86. /package/dist/{chunk-PAVBM57P.js.map → chunk-RKBJLYVO.js.map} +0 -0
@@ -9,7 +9,8 @@ var _chunk7HSUHP63cjs = require('./chunk-7HSUHP63.cjs');
9
9
 
10
10
 
11
11
 
12
- var _chunk6FBIXTALcjs = require('./chunk-6FBIXTAL.cjs');
12
+
13
+ var _chunk53RO5I22cjs = require('./chunk-53RO5I22.cjs');
13
14
 
14
15
 
15
16
  var _chunk4DE2IREAcjs = require('./chunk-4DE2IREA.cjs');
@@ -21,6 +22,7 @@ var WorkersAiTranscriptionAdapter = class extends _adapters.BaseTranscriptionAda
21
22
  super({}, model);
22
23
  _chunk4DE2IREAcjs.__publicField.call(void 0, this, "name", "workers-ai-transcription");
23
24
  _chunk4DE2IREAcjs.__publicField.call(void 0, this, "adapterConfig");
25
+ _chunk53RO5I22cjs.validateWorkersAiConfig.call(void 0, config);
24
26
  this.adapterConfig = config;
25
27
  }
26
28
  async transcribe(options) {
@@ -30,10 +32,10 @@ var WorkersAiTranscriptionAdapter = class extends _adapters.BaseTranscriptionAda
30
32
  if (language) extra.language = language;
31
33
  if (prompt) extra.initial_prompt = prompt;
32
34
  const audioPayload = this.buildAudioPayload(audioBytes, audio);
33
- if (_chunk6FBIXTALcjs.isDirectBindingConfig.call(void 0, this.adapterConfig)) {
35
+ if (_chunk53RO5I22cjs.isDirectBindingConfig.call(void 0, this.adapterConfig)) {
34
36
  return this.transcribeViaBinding(audioPayload, extra);
35
37
  }
36
- if (_chunk6FBIXTALcjs.isDirectCredentialsConfig.call(void 0, this.adapterConfig)) {
38
+ if (_chunk53RO5I22cjs.isDirectCredentialsConfig.call(void 0, this.adapterConfig)) {
37
39
  if (this.model === "@cf/deepgram/nova-3") {
38
40
  return this.transcribeViaRestBinary(audioBytes, audio, extra);
39
41
  }
@@ -104,7 +106,7 @@ var WorkersAiTranscriptionAdapter = class extends _adapters.BaseTranscriptionAda
104
106
  }
105
107
  async transcribeViaGateway(audioPayload, options) {
106
108
  const gatewayConfig = this.adapterConfig;
107
- const gatewayFetch = _chunk6FBIXTALcjs.createGatewayFetch.call(void 0, "workers-ai", gatewayConfig);
109
+ const gatewayFetch = _chunk53RO5I22cjs.createGatewayFetch.call(void 0, "workers-ai", gatewayConfig);
108
110
  const response = await gatewayFetch("https://api.cloudflare.com/v1/audio/transcriptions", {
109
111
  method: "POST",
110
112
  body: JSON.stringify({
@@ -213,4 +215,4 @@ function detectAudioContentType(audio) {
213
215
 
214
216
 
215
217
  exports.WorkersAiTranscriptionAdapter = WorkersAiTranscriptionAdapter; exports.createWorkersAiTranscription = createWorkersAiTranscription;
216
- //# sourceMappingURL=chunk-4ACSLQDI.cjs.map
218
+ //# sourceMappingURL=chunk-45DEJBFY.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/ai/ai/packages/tanstack-ai/dist/chunk-45DEJBFY.cjs","../src/adapters/workers-ai-transcription.ts"],"names":[],"mappings":"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","file":"/home/runner/work/ai/ai/packages/tanstack-ai/dist/chunk-45DEJBFY.cjs","sourcesContent":[null,"import { BaseTranscriptionAdapter } from \"@tanstack/ai/adapters\";\nimport type { TranscriptionOptions, TranscriptionResult } from \"@tanstack/ai\";\nimport {\n\ttype WorkersAiAdapterConfig,\n\ttype WorkersAiDirectBindingConfig,\n\ttype WorkersAiDirectCredentialsConfig,\n\ttype AiGatewayAdapterConfig,\n\tcreateGatewayFetch,\n\tisDirectBindingConfig,\n\tisDirectCredentialsConfig,\n\tvalidateWorkersAiConfig,\n} from \"../utils/create-fetcher\";\nimport { workersAiRestFetch, workersAiRestFetchBinary } from \"../utils/workers-ai-rest\";\nimport { uint8ArrayToBase64 } from \"../utils/binary\";\n\n// ---------------------------------------------------------------------------\n// Model types\n// ---------------------------------------------------------------------------\n\n/**\n * Workers AI models that support speech-to-text transcription.\n *\n * Note: the typed `AiModels` interface in `@cloudflare/workers-types` may lag\n * behind what's deployed. We use a string union here that matches the known\n * models including Deepgram partner models.\n *\n * **Nova-3 note:** `@cf/deepgram/nova-3` uses a different input format than the\n * Whisper models. Via binding it accepts `{ audio: { body: base64, contentType } }`.\n * Via REST it requires multipart form data (not JSON). The adapter handles both.\n */\nexport type WorkersAiTranscriptionModel =\n\t| \"@cf/openai/whisper\"\n\t| \"@cf/openai/whisper-tiny-en\"\n\t| \"@cf/openai/whisper-large-v3-turbo\"\n\t| \"@cf/deepgram/nova-3\"\n\t| (string & {});\n\n// ---------------------------------------------------------------------------\n// WorkersAiTranscriptionAdapter\n// ---------------------------------------------------------------------------\n\nexport class WorkersAiTranscriptionAdapter extends BaseTranscriptionAdapter<WorkersAiTranscriptionModel> {\n\treadonly name = \"workers-ai-transcription\" as const;\n\tprivate adapterConfig: WorkersAiAdapterConfig;\n\n\tconstructor(config: WorkersAiAdapterConfig, model: WorkersAiTranscriptionModel) {\n\t\tsuper({}, model);\n\t\tvalidateWorkersAiConfig(config);\n\t\tthis.adapterConfig = config;\n\t}\n\n\tasync transcribe(options: TranscriptionOptions): Promise<TranscriptionResult> {\n\t\tconst { audio, language, prompt, modelOptions } = options;\n\n\t\t// Normalize audio to raw bytes\n\t\tconst audioBytes = await normalizeAudioToBytes(audio);\n\n\t\tconst extra: Record<string, unknown> = { ...modelOptions };\n\t\tif (language) extra.language = language;\n\t\tif (prompt) extra.initial_prompt = prompt;\n\n\t\t// Build the model-specific audio payload:\n\t\t// - Deepgram Nova-3 (binding): { audio: { body: base64, contentType: \"audio/...\" } }\n\t\t// - Deepgram Nova-3 (REST): multipart FormData (handled separately)\n\t\t// - Whisper Large v3 Turbo (REST/gateway): { audio: base64string }\n\t\t// - Other Whisper models (binding): { audio: number[] }\n\t\tconst audioPayload = this.buildAudioPayload(audioBytes, audio);\n\n\t\tif (isDirectBindingConfig(this.adapterConfig)) {\n\t\t\treturn this.transcribeViaBinding(audioPayload, extra);\n\t\t}\n\n\t\tif (isDirectCredentialsConfig(this.adapterConfig)) {\n\t\t\t// Nova-3 REST requires raw binary audio, not JSON\n\t\t\tif (this.model === \"@cf/deepgram/nova-3\") {\n\t\t\t\treturn this.transcribeViaRestBinary(audioBytes, audio, extra);\n\t\t\t}\n\t\t\treturn this.transcribeViaRest(audioPayload, extra);\n\t\t}\n\n\t\treturn this.transcribeViaGateway(audioPayload, extra);\n\t}\n\n\t/**\n\t * Build the audio field for the request payload, handling model-specific formats.\n\t *\n\t * - `@cf/deepgram/nova-3` requires `{ body: base64, contentType: \"audio/...\" }`\n\t * - `@cf/openai/whisper-large-v3-turbo` REST/gateway accepts a base64 string\n\t * - Other Whisper models accept `number[]` (binding) or base64 (REST)\n\t */\n\tprivate buildAudioPayload(\n\t\taudioBytes: number[],\n\t\toriginalAudio: string | File | Blob | ArrayBuffer,\n\t): Record<string, unknown> {\n\t\tif (this.model === \"@cf/deepgram/nova-3\") {\n\t\t\tconst b64 = uint8ArrayToBase64(new Uint8Array(audioBytes));\n\t\t\tconst contentType = detectAudioContentType(originalAudio);\n\t\t\treturn { audio: { body: b64, contentType } };\n\t\t}\n\n\t\tif (this.model === \"@cf/openai/whisper-large-v3-turbo\") {\n\t\t\treturn { audio: uint8ArrayToBase64(new Uint8Array(audioBytes)) };\n\t\t}\n\n\t\treturn { audio: audioBytes };\n\t}\n\n\tprivate async transcribeViaBinding(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst ai = (this.adapterConfig as WorkersAiDirectBindingConfig).binding;\n\t\tconst result = (await ai.run(this.model, {\n\t\t\t...audioPayload,\n\t\t\t...options,\n\t\t})) as Record<string, unknown>;\n\t\treturn this.normalizeResult(result);\n\t}\n\n\tprivate async transcribeViaRest(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst config = this.adapterConfig as WorkersAiDirectCredentialsConfig;\n\n\t\tconst response = await workersAiRestFetch(\n\t\t\tconfig,\n\t\t\tthis.model,\n\t\t\t{ ...audioPayload, ...options },\n\t\t\t{\n\t\t\t\tlabel: \"Workers AI transcription\",\n\t\t\t\tsignal: (options as { signal?: AbortSignal }).signal,\n\t\t\t},\n\t\t);\n\n\t\tconst data = (await response.json()) as {\n\t\t\tresult?: Record<string, unknown>;\n\t\t} & Record<string, unknown>;\n\n\t\t// Cloudflare REST API wraps responses in { success, result: {...} }.\n\t\t// Use `data.result` when present, fall back to `data` for direct responses.\n\t\treturn this.normalizeResult(data.result ?? data);\n\t}\n\n\t/**\n\t * Transcribe via REST using raw binary audio.\n\t * Required for models like Deepgram Nova-3 that expect raw audio bytes\n\t * with a Content-Type header (e.g. \"audio/wav\") instead of JSON.\n\t */\n\tprivate async transcribeViaRestBinary(\n\t\taudioBytes: number[],\n\t\toriginalAudio: string | File | Blob | ArrayBuffer,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst config = this.adapterConfig as WorkersAiDirectCredentialsConfig;\n\t\tconst contentType = detectAudioContentType(originalAudio);\n\n\t\tconst response = await workersAiRestFetchBinary(\n\t\t\tconfig,\n\t\t\tthis.model,\n\t\t\tnew Uint8Array(audioBytes),\n\t\t\tcontentType,\n\t\t\t{\n\t\t\t\tlabel: \"Workers AI transcription\",\n\t\t\t\tsignal: (options as { signal?: AbortSignal }).signal,\n\t\t\t},\n\t\t);\n\n\t\tconst data = (await response.json()) as {\n\t\t\tresult?: Record<string, unknown>;\n\t\t} & Record<string, unknown>;\n\n\t\treturn this.normalizeResult(data.result ?? data);\n\t}\n\n\tprivate async transcribeViaGateway(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst gatewayConfig = this.adapterConfig as AiGatewayAdapterConfig;\n\t\tconst gatewayFetch = createGatewayFetch(\"workers-ai\", gatewayConfig);\n\n\t\t// The URL here is a placeholder — createGatewayFetch for \"workers-ai\" extracts\n\t\t// the model from the body, sets it as the endpoint, and routes through the gateway.\n\t\t// The actual URL path is not used.\n\t\tconst response = await gatewayFetch(\"https://api.cloudflare.com/v1/audio/transcriptions\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: this.model,\n\t\t\t\t...audioPayload,\n\t\t\t\t...options,\n\t\t\t}),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text();\n\t\t\tthrow new Error(\n\t\t\t\t`Workers AI transcription gateway request failed (${response.status}): ${errorText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await response.json()) as Record<string, unknown>;\n\t\treturn this.normalizeResult(data);\n\t}\n\n\t/**\n\t * Normalize Workers AI transcription results into the standard\n\t * TanStack AI TranscriptionResult shape.\n\t *\n\t * Handles three response formats:\n\t * - Whisper: `{ text, words?, vtt? }`\n\t * - Whisper v3-turbo: `{ text, segments?, transcription_info? }`\n\t * - Deepgram Nova-3: `{ results: { channels: [{ alternatives: [{ transcript, words }] }] } }`\n\t */\n\tprivate normalizeResult(raw: Record<string, unknown>): TranscriptionResult {\n\t\t// Deepgram Nova-3 format: { results: { channels: [{ alternatives: [{ transcript, words }] }] } }\n\t\tconst results = raw.results as Record<string, unknown> | undefined;\n\t\tif (results?.channels) {\n\t\t\tconst channels = results.channels as Array<{\n\t\t\t\talternatives?: Array<{\n\t\t\t\t\ttranscript?: string;\n\t\t\t\t\tconfidence?: number;\n\t\t\t\t\twords?: Array<{ word: string; start: number; end: number; confidence: number }>;\n\t\t\t\t}>;\n\t\t\t}>;\n\t\t\tconst alt = channels?.[0]?.alternatives?.[0];\n\t\t\tconst text = alt?.transcript ?? \"\";\n\t\t\tconst result: TranscriptionResult = {\n\t\t\t\tid: this.generateId(),\n\t\t\t\tmodel: this.model,\n\t\t\t\ttext,\n\t\t\t};\n\t\t\tif (alt?.words && Array.isArray(alt.words)) {\n\t\t\t\tresult.words = alt.words.map((w) => ({\n\t\t\t\t\tword: w.word ?? \"\",\n\t\t\t\t\tstart: w.start ?? 0,\n\t\t\t\t\tend: w.end ?? 0,\n\t\t\t\t}));\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\t// Whisper format: { text, words?, vtt? }\n\t\t// Whisper v3-turbo format: { text, segments?, transcription_info? }\n\t\tconst result: TranscriptionResult = {\n\t\t\tid: this.generateId(),\n\t\t\tmodel: this.model,\n\t\t\ttext: (raw.text as string) ?? \"\",\n\t\t};\n\n\t\t// Language from transcription_info (whisper-large-v3-turbo)\n\t\tconst transcriptionInfo = raw.transcription_info as Record<string, unknown> | undefined;\n\t\tif (transcriptionInfo?.language) {\n\t\t\tresult.language = transcriptionInfo.language as string;\n\t\t}\n\n\t\t// Duration\n\t\tif (transcriptionInfo?.duration != null) {\n\t\t\tresult.duration = transcriptionInfo.duration as number;\n\t\t}\n\n\t\t// Segments (whisper-large-v3-turbo returns these)\n\t\tif (raw.segments && Array.isArray(raw.segments)) {\n\t\t\tresult.segments = raw.segments.map((seg: Record<string, unknown>, idx: number) => ({\n\t\t\t\tid: idx,\n\t\t\t\ttext: (seg.text as string) ?? \"\",\n\t\t\t\tstart: (seg.start as number) ?? 0,\n\t\t\t\tend: (seg.end as number) ?? 0,\n\t\t\t}));\n\t\t}\n\n\t\t// Words — basic whisper returns top-level words[], v3-turbo nests them in segments\n\t\tif (raw.words && Array.isArray(raw.words)) {\n\t\t\tresult.words = raw.words.map((w: Record<string, unknown>) => ({\n\t\t\t\tword: (w.word as string) ?? \"\",\n\t\t\t\tstart: (w.start as number) ?? 0,\n\t\t\t\tend: (w.end as number) ?? 0,\n\t\t\t}));\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Workers AI transcription adapter for speech-to-text.\n *\n * Works with TanStack AI's `generateTranscription()` activity function:\n * ```ts\n * import { generateTranscription } from \"@tanstack/ai\";\n * import { createWorkersAiTranscription } from \"@cloudflare/tanstack-ai\";\n *\n * const adapter = createWorkersAiTranscription(\n * \"@cf/openai/whisper-large-v3-turbo\",\n * { binding: env.AI },\n * );\n *\n * const result = await generateTranscription({ adapter, audio: audioData });\n * // result.text — the transcribed text\n * ```\n *\n * Note: Factory takes `(model, config)` for ergonomics — the class constructor\n * uses `(config, model)` to match TanStack AI's upstream convention.\n */\nexport function createWorkersAiTranscription(\n\tmodel: WorkersAiTranscriptionModel,\n\tconfig: WorkersAiAdapterConfig,\n) {\n\treturn new WorkersAiTranscriptionAdapter(config, model);\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize various audio input formats into a number[] (raw bytes).\n *\n * Note: `File extends Blob`, so `File` instances are handled by the\n * `instanceof Blob` branch. `Blob.arrayBuffer()` always reads the full\n * contents regardless of any prior reads — there's no cursor to worry about.\n */\nasync function normalizeAudioToBytes(audio: string | File | Blob | ArrayBuffer): Promise<number[]> {\n\tif (audio instanceof ArrayBuffer) {\n\t\treturn Array.from(new Uint8Array(audio));\n\t}\n\n\tif (audio instanceof Blob) {\n\t\t// This also handles `File` (which extends Blob)\n\t\tconst buffer = await audio.arrayBuffer();\n\t\treturn Array.from(new Uint8Array(buffer));\n\t}\n\n\tif (typeof audio === \"string\") {\n\t\t// Assume base64 string — decode to bytes\n\t\tconst binary = atob(audio);\n\t\tconst bytes = new Uint8Array(binary.length);\n\t\tfor (let i = 0; i < binary.length; i++) {\n\t\t\tbytes[i] = binary.charCodeAt(i);\n\t\t}\n\t\treturn Array.from(bytes);\n\t}\n\n\tthrow new Error(\"Unsupported audio format. Expected string, File, Blob, or ArrayBuffer.\");\n}\n\n/**\n * Detect the MIME type of the audio input for models that require it\n * (e.g., Deepgram Nova-3).\n *\n * - `File` / `Blob`: use the `.type` property (e.g., \"audio/wav\")\n * - `ArrayBuffer` / `string`: defaults to \"audio/wav\"\n */\nfunction detectAudioContentType(audio: string | File | Blob | ArrayBuffer): string {\n\t// File and Blob carry their own MIME type\n\tif (audio instanceof Blob && audio.type) {\n\t\treturn audio.type;\n\t}\n\n\t// For raw bytes, default to audio/wav — this is the most common\n\t// format for transcription inputs and what the E2E tests use.\n\treturn \"audio/wav\";\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createGatewayFetch
3
- } from "./chunk-UUFEOQ6B.js";
3
+ } from "./chunk-727MRLUF.js";
4
4
 
5
5
  // src/adapters/openrouter.ts
6
6
  import {
@@ -54,4 +54,4 @@ export {
54
54
  createOpenRouterImage,
55
55
  createOpenRouterSummarize
56
56
  };
57
- //# sourceMappingURL=chunk-S3ALRROX.js.map
57
+ //# sourceMappingURL=chunk-4HOZDNW6.js.map
@@ -5,6 +5,17 @@ function isDirectBindingConfig(config) {
5
5
  function isDirectCredentialsConfig(config) {
6
6
  return "accountId" in config && "apiKey" in config && !("gatewayId" in config);
7
7
  }
8
+ function isGatewayConfig(config) {
9
+ if ("gatewayId" in config) return true;
10
+ return "binding" in config && !isDirectBindingConfig(config);
11
+ }
12
+ function validateWorkersAiConfig(config) {
13
+ if (!isDirectBindingConfig(config) && !isDirectCredentialsConfig(config) && !isGatewayConfig(config)) {
14
+ throw new Error(
15
+ "Invalid Workers AI configuration: you must provide either a binding (e.g. { binding: env.AI }), credentials ({ accountId, apiKey }), or a gateway configuration ({ binding: env.AI.gateway(id) } or { accountId, gatewayId })."
16
+ );
17
+ }
18
+ }
8
19
  function createGatewayFetch(provider, config, headers = {}) {
9
20
  return (input, init) => {
10
21
  let query = {};
@@ -43,7 +54,9 @@ function createGatewayFetch(provider, config, headers = {}) {
43
54
  query
44
55
  };
45
56
  if (provider === "workers-ai") {
46
- request.endpoint = query.model;
57
+ if (!request.endpoint.startsWith("run/")) {
58
+ request.endpoint = `run/${query.model}`;
59
+ }
47
60
  delete query.model;
48
61
  delete query.instructions;
49
62
  }
@@ -74,27 +87,10 @@ function normalizeMessagesForBinding(messages) {
74
87
  if (normalized.content === null || normalized.content === void 0) {
75
88
  normalized.content = "";
76
89
  }
77
- if (normalized.tool_call_id && typeof normalized.tool_call_id === "string") {
78
- normalized.tool_call_id = sanitizeToolCallId(normalized.tool_call_id);
79
- }
80
- if (Array.isArray(normalized.tool_calls)) {
81
- normalized.tool_calls = normalized.tool_calls.map(
82
- (tc) => {
83
- if (tc.id && typeof tc.id === "string") {
84
- return { ...tc, id: sanitizeToolCallId(tc.id) };
85
- }
86
- return tc;
87
- }
88
- );
89
- }
90
90
  return normalized;
91
91
  });
92
92
  }
93
- function sanitizeToolCallId(id) {
94
- const alphanumeric = id.replace(/[^a-zA-Z0-9]/g, "");
95
- return alphanumeric.slice(0, 9).padEnd(9, "0");
96
- }
97
- function createWorkersAiBindingFetch(binding) {
93
+ function createWorkersAiBindingFetch(binding, options) {
98
94
  return async (_input, init) => {
99
95
  if (!_optionalChain([init, 'optionalAccess', _3 => _3.body])) {
100
96
  return new Response("No body", { status: 400 });
@@ -118,7 +114,11 @@ function createWorkersAiBindingFetch(binding) {
118
114
  if (typeof body.max_tokens === "number") inputs.max_tokens = body.max_tokens;
119
115
  if (body.response_format) inputs.response_format = body.response_format;
120
116
  if (stream) inputs.stream = true;
121
- const result = await binding.run(model, inputs);
117
+ const result = await binding.run(
118
+ model,
119
+ inputs,
120
+ _optionalChain([options, 'optionalAccess', _4 => _4.extraHeaders]) ? { extraHeaders: options.extraHeaders } : void 0
121
+ );
122
122
  if (stream && result instanceof ReadableStream) {
123
123
  const transformed = transformWorkersAiStream(
124
124
  result,
@@ -142,11 +142,11 @@ function createWorkersAiBindingFetch(binding) {
142
142
  finishReason = "tool_calls";
143
143
  message.tool_calls = responseObj.tool_calls.map(
144
144
  (tc) => ({
145
- id: sanitizeToolCallId(tc.id || crypto.randomUUID()),
145
+ id: tc.id || crypto.randomUUID(),
146
146
  type: "function",
147
147
  function: {
148
- name: _optionalChain([tc, 'access', _4 => _4.function, 'optionalAccess', _5 => _5.name]) || tc.name || "",
149
- arguments: typeof (_nullishCoalesce(_optionalChain([tc, 'access', _6 => _6.function, 'optionalAccess', _7 => _7.arguments]), () => ( tc.arguments))) === "string" ? _nullishCoalesce(_optionalChain([tc, 'access', _8 => _8.function, 'optionalAccess', _9 => _9.arguments]), () => ( tc.arguments)) : JSON.stringify(_nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _10 => _10.function, 'optionalAccess', _11 => _11.arguments]), () => ( tc.arguments)), () => ( {})))
148
+ name: _optionalChain([tc, 'access', _5 => _5.function, 'optionalAccess', _6 => _6.name]) || tc.name || "",
149
+ arguments: typeof (_nullishCoalesce(_optionalChain([tc, 'access', _7 => _7.function, 'optionalAccess', _8 => _8.arguments]), () => ( tc.arguments))) === "string" ? _nullishCoalesce(_optionalChain([tc, 'access', _9 => _9.function, 'optionalAccess', _10 => _10.arguments]), () => ( tc.arguments)) : JSON.stringify(_nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _11 => _11.function, 'optionalAccess', _12 => _12.arguments]), () => ( tc.arguments)), () => ( {})))
150
150
  }
151
151
  })
152
152
  );
@@ -171,7 +171,7 @@ function transformWorkersAiStream(source, model) {
171
171
  let buffer = "";
172
172
  let hasToolCalls = false;
173
173
  let isOpenAiFormat = false;
174
- const emittedToolCallStart = /* @__PURE__ */ new Set();
174
+ const toolCallState = /* @__PURE__ */ new Map();
175
175
  return source.pipeThrough(
176
176
  new TransformStream({
177
177
  transform(chunk, controller) {
@@ -187,16 +187,24 @@ function transformWorkersAiStream(source, model) {
187
187
  const parsed = JSON.parse(data);
188
188
  if (parsed.choices !== void 0) {
189
189
  isOpenAiFormat = true;
190
- const choice = _optionalChain([parsed, 'access', _12 => _12.choices, 'optionalAccess', _13 => _13[0]]);
191
- if (_optionalChain([choice, 'optionalAccess', _14 => _14.delta, 'optionalAccess', _15 => _15.tool_calls])) {
190
+ const choice = _optionalChain([parsed, 'access', _13 => _13.choices, 'optionalAccess', _14 => _14[0]]);
191
+ if (_optionalChain([choice, 'optionalAccess', _15 => _15.delta, 'optionalAccess', _16 => _16.tool_calls])) {
192
192
  hasToolCalls = true;
193
193
  for (const tc of choice.delta.tool_calls) {
194
- if (tc.id && typeof tc.id === "string") {
195
- tc.id = sanitizeToolCallId(tc.id);
194
+ const tcIndex = _nullishCoalesce(tc.index, () => ( 0));
195
+ if (!toolCallState.has(tcIndex)) {
196
+ const id = tc.id || `call${streamId}${tcIndex}`;
197
+ toolCallState.set(tcIndex, {
198
+ id,
199
+ name: _optionalChain([tc, 'access', _17 => _17.function, 'optionalAccess', _18 => _18.name]) || ""
200
+ });
201
+ tc.id = id;
202
+ } else {
203
+ delete tc.id;
196
204
  }
197
205
  }
198
206
  }
199
- if (_optionalChain([choice, 'optionalAccess', _16 => _16.finish_reason]) === "tool_calls") {
207
+ if (_optionalChain([choice, 'optionalAccess', _19 => _19.finish_reason]) === "tool_calls") {
200
208
  hasToolCalls = true;
201
209
  }
202
210
  controller.enqueue(
@@ -229,18 +237,18 @@ function transformWorkersAiStream(source, model) {
229
237
  if (Array.isArray(parsed.tool_calls) && parsed.tool_calls.length > 0) {
230
238
  for (const tc of parsed.tool_calls) {
231
239
  const tcIndex = _nullishCoalesce(tc.index, () => ( 0));
232
- const tcName = _nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _17 => _17.function, 'optionalAccess', _18 => _18.name]), () => ( tc.name)), () => ( null));
233
- const tcArgs = _nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _19 => _19.function, 'optionalAccess', _20 => _20.arguments]), () => ( tc.arguments)), () => ( null));
240
+ const tcName = _nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _20 => _20.function, 'optionalAccess', _21 => _21.name]), () => ( tc.name)), () => ( null));
241
+ const tcArgs = _nullishCoalesce(_nullishCoalesce(_optionalChain([tc, 'access', _22 => _22.function, 'optionalAccess', _23 => _23.arguments]), () => ( tc.arguments)), () => ( null));
234
242
  const tcId = _nullishCoalesce(tc.id, () => ( null));
235
243
  if (!tcId && !tcName && (!tcArgs || tcArgs === "")) continue;
236
244
  hasToolCalls = true;
237
245
  const toolCallDelta = {
238
246
  index: tcIndex
239
247
  };
240
- if (!emittedToolCallStart.has(tcIndex)) {
241
- emittedToolCallStart.add(tcIndex);
242
- const rawId = tcId || `call${streamId}${tcIndex}`;
243
- toolCallDelta.id = sanitizeToolCallId(rawId);
248
+ if (!toolCallState.has(tcIndex)) {
249
+ const id = tcId || `call${streamId}${tcIndex}`;
250
+ toolCallState.set(tcIndex, { id, name: tcName || "" });
251
+ toolCallDelta.id = id;
244
252
  toolCallDelta.type = "function";
245
253
  toolCallDelta.function = {
246
254
  name: tcName || "",
@@ -311,5 +319,6 @@ function transformWorkersAiStream(source, model) {
311
319
 
312
320
 
313
321
 
314
- exports.isDirectBindingConfig = isDirectBindingConfig; exports.isDirectCredentialsConfig = isDirectCredentialsConfig; exports.createGatewayFetch = createGatewayFetch; exports.createWorkersAiBindingFetch = createWorkersAiBindingFetch;
315
- //# sourceMappingURL=chunk-6FBIXTAL.cjs.map
322
+
323
+ exports.isDirectBindingConfig = isDirectBindingConfig; exports.isDirectCredentialsConfig = isDirectCredentialsConfig; exports.validateWorkersAiConfig = validateWorkersAiConfig; exports.createGatewayFetch = createGatewayFetch; exports.createWorkersAiBindingFetch = createWorkersAiBindingFetch;
324
+ //# sourceMappingURL=chunk-53RO5I22.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/ai/ai/packages/tanstack-ai/dist/chunk-53RO5I22.cjs","../src/utils/create-fetcher.ts"],"names":[],"mappings":"AAAA;AC4HO,SAAS,qBAAA,CACf,MAAA,EACyC;AAGzC,EAAA,OACC,UAAA,GAAa,OAAA,GACb,OAAQ,MAAA,CAAO,OAAA,CAA+C,QAAA,IAAY,UAAA;AAE5E;AAGO,SAAS,yBAAA,CACf,MAAA,EAC6C;AAC7C,EAAA,OAAO,YAAA,GAAe,OAAA,GAAU,SAAA,GAAY,OAAA,GAAU,CAAA,CAAE,YAAA,GAAe,MAAA,CAAA;AACxE;AAGO,SAAS,eAAA,CAAgB,MAAA,EAAkE;AACjG,EAAA,GAAA,CAAI,YAAA,GAAe,MAAA,EAAQ,OAAO,IAAA;AAElC,EAAA,OAAO,UAAA,GAAa,OAAA,GAAU,CAAC,qBAAA,CAAsB,MAAM,CAAA;AAC5D;AAWO,SAAS,uBAAA,CAAwB,MAAA,EAAsC;AAC7E,EAAA,GAAA,CACC,CAAC,qBAAA,CAAsB,MAAM,EAAA,GAC7B,CAAC,yBAAA,CAA0B,MAAM,EAAA,GACjC,CAAC,eAAA,CAAgB,MAAM,CAAA,EACtB;AACD,IAAA,MAAM,IAAI,KAAA;AAAA,MACT;AAAA,IAGD,CAAA;AAAA,EACD;AACD;AAMO,SAAS,kBAAA,CACf,QAAA,EACA,MAAA,EACA,QAAA,EAAkC,CAAC,CAAA,EACpB;AACf,EAAA,OAAO,CAAC,KAAA,EAAO,IAAA,EAAA,GAAS;AACvB,IAAA,IAAI,MAAA,EAAiC,CAAC,CAAA;AAEtC,IAAA,MAAM,IAAA,EACL,OAAO,MAAA,IAAU,SAAA,EAAW,MAAA,EAAQ,MAAA,WAAiB,IAAA,EAAM,KAAA,CAAM,KAAA,EAAO,KAAA,CAAM,GAAA;AAC/E,IAAA,MAAM,OAAA,EAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAG1B,IAAA,MAAM,SAAA,EAAW,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,EAAA,EAAI,MAAA,CAAO,MAAA;AAEpF,IAAA,GAAA,iBAAI,IAAA,2BAAM,MAAA,EAAM;AACf,MAAA,IAAI;AACH,QAAA,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAc,CAAA;AAAA,MACvC,EAAA,WAAQ;AACP,QAAA,MAAA,EAAQ,EAAE,IAAA,EAAM,IAAA,CAAK,KAAK,CAAA;AAAA,MAC3B;AAAA,IACD;AAEA,IAAA,MAAM,aAAA,EAAuC,CAAC,CAAA;AAE9C,IAAA,GAAA,CAAI,YAAA,GAAe,OAAA,GAAU,MAAA,CAAO,SAAA,EAAW;AAC9C,MAAA,YAAA,CAAa,mBAAmB,EAAA,EAAI,MAAA;AAAA,IACrC;AAEA,IAAA,GAAA,CAAI,OAAO,MAAA,CAAO,SAAA,IAAa,QAAA,EAAU;AACxC,MAAA,YAAA,CAAa,kBAAkB,EAAA,EAAI,MAAA,CAAO,MAAA,CAAO,QAAQ,CAAA;AAAA,IAC1D;AAEA,IAAA,GAAA,CAAI,OAAO,MAAA,CAAO,eAAA,IAAmB,QAAA,EAAU;AAC9C,MAAA,YAAA,CAAa,kBAAkB,EAAA,EAAI,MAAA,CAAO,cAAA;AAAA,IAC3C;AAEA,IAAA,GAAA,CAAI,OAAO,MAAA,CAAO,SAAA,IAAa,QAAA,EAAU;AACxC,MAAA,YAAA,CAAa,iBAAiB,EAAA,EAAI,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,IACjE;AAEA,IAAA,MAAM,QAAA,EAKF;AAAA,MACH,QAAA;AAAA,MACA,QAAA;AAAA,MACA,OAAA,EAAS;AAAA,QACR,mBAAI,IAAA,6BAAM,SAAA;AAAA,QACV,GAAG,OAAA;AAAA,QACH,GAAG,YAAA;AAAA,QACH,cAAA,EAAgB;AAAA,MACjB,CAAA;AAAA,MACA;AAAA,IACD,CAAA;AAEA,IAAA,GAAA,CAAI,SAAA,IAAa,YAAA,EAAc;AAC9B,MAAA,GAAA,CAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,UAAA,CAAW,MAAM,CAAA,EAAG;AACzC,QAAA,OAAA,CAAQ,SAAA,EAAW,CAAA,IAAA,EAAO,KAAA,CAAM,KAAK,CAAA,CAAA;AACtC,MAAA;AACa,MAAA;AACA,MAAA;AACd,IAAA;AAEmB,IAAA;AACiB,MAAA;AACpC,IAAA;AAEyB,IAAA;AACS,MAAA;AAClC,IAAA;AAEO,IAAA;AACkC,MAAA;AACxC,MAAA;AACI,QAAA;AACM,QAAA;AACQ,UAAA;AACb,UAAA;AACA,UAAA;AAEE,UAAA;AAEN,QAAA;AAC4B,QAAA;AAC7B,MAAA;AACD,IAAA;AACD,EAAA;AACD;AAaC;AAE6B,EAAA;AACA,IAAA;AAGO,IAAA;AACb,MAAA;AACtB,IAAA;AAEO,IAAA;AACP,EAAA;AACF;AAaC;AAE+B,EAAA;AACb,IAAA;AACiB,MAAA;AAClC,IAAA;AAEI,IAAA;AACA,IAAA;AACkC,MAAA;AAC9B,IAAA;AACa,MAAA;AACrB,IAAA;AAEmB,IAAA;AACC,IAAA;AAGqB,IAAA;AACtB,IAAA;AACA,MAAA;AACZ,QAAA;AACN,MAAA;AACD,IAAA;AACoC,IAAA;AACM,IAAA;AACD,IAAA;AACR,IAAA;AACL,IAAA;AAEC,IAAA;AAC5B,MAAA;AACA,MAAA;AACwC,sBAAA;AACzC,IAAA;AAEgC,IAAA;AAGX,MAAA;AACnB,QAAA;AACA,QAAA;AACD,MAAA;AACiC,MAAA;AACvB,QAAA;AACQ,UAAA;AACC,UAAA;AAClB,QAAA;AACA,MAAA;AACF,IAAA;AAUmB,IAAA;AAIqB,IAAA;AAEC,IAAA;AAClC,MAAA;AACG,MAAA;AACV,IAAA;AACmB,IAAA;AAGqB,IAAA;AACxB,MAAA;AACkB,MAAA;AAMzB,QAAA;AACyB,UAAA;AACzB,UAAA;AACI,UAAA;AACqB,YAAA;AAER,YAAA;AAGvB,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AAEuB,IAAA;AACe,MAAA;AAC7B,MAAA;AAC6B,MAAA;AACrC,MAAA;AAC+B,MAAA;AAChC,IAAA;AAEmC,IAAA;AACP,MAAA;AAC3B,IAAA;AACF,EAAA;AACD;AAkBC;AAEgC,EAAA;AACA,EAAA;AAGM,EAAA;AACM,EAAA;AAC/B,EAAA;AACM,EAAA;AAIE,EAAA;AAGK,EAAA;AAEZ,EAAA;AAC+B,IAAA;AACd,MAAA;AACM,QAAA;AACH,QAAA;AACP,QAAA;AAEE,QAAA;AACC,UAAA;AACU,UAAA;AACR,UAAA;AAGL,UAAA;AAEnB,UAAA;AAC2B,YAAA;AAKI,YAAA;AAGhB,cAAA;AACe,cAAA;AACD,cAAA;AACf,gBAAA;AACe,gBAAA;AACD,kBAAA;AACL,kBAAA;AAEF,oBAAA;AACF,oBAAA;AACjB,sBAAA;AACmB,sBAAA;AACnB,oBAAA;AACO,oBAAA;AACF,kBAAA;AAGI,oBAAA;AACX,kBAAA;AACD,gBAAA;AACD,cAAA;AAC8B,cAAA;AACd,gBAAA;AAChB,cAAA;AACW,cAAA;AACmB,gBAAA;AAAiB;AAAM;AACrD,cAAA;AACA,cAAA;AACD,YAAA;AAK+B,YAAA;AACV,cAAA;AACf,gBAAA;AACI,gBAAA;AACR,gBAAA;AACA,gBAAA;AACS,gBAAA;AACR,kBAAA;AACQ,oBAAA;AACkB,oBAAA;AACV,oBAAA;AAChB,kBAAA;AACD,gBAAA;AACD,cAAA;AACW,cAAA;AACmB,gBAAA;AAAsB;AAAM;AAC1D,cAAA;AACD,YAAA;AAOyB,YAAA;AACA,cAAA;AACK,gBAAA;AAGA,gBAAA;AACA,gBAAA;AACN,gBAAA;AAGI,gBAAA;AAEX,gBAAA;AAGgC,gBAAA;AACvC,kBAAA;AACR,gBAAA;AAE8B,gBAAA;AAEH,kBAAA;AACC,kBAAA;AACR,kBAAA;AACE,kBAAA;AACI,kBAAA;AACR,oBAAA;AAAA;AAGL,oBAAA;AAKZ,kBAAA;AACM,gBAAA;AAEgB,kBAAA;AACI,oBAAA;AAEhB,sBAAA;AAGT,oBAAA;AACM,kBAAA;AACN,oBAAA;AACD,kBAAA;AACD,gBAAA;AAEkB,gBAAA;AACb,kBAAA;AACI,kBAAA;AACR,kBAAA;AACA,kBAAA;AACS,kBAAA;AACR,oBAAA;AACQ,sBAAA;AACe,sBAAA;AACP,sBAAA;AAChB,oBAAA;AACD,kBAAA;AACD,gBAAA;AACW,gBAAA;AACc,kBAAA;AAAyB;AAAM;AACxD,gBAAA;AACD,cAAA;AACD,YAAA;AACW,UAAA;AAEE,YAAA;AACd,UAAA;AACD,QAAA;AACD,MAAA;AACkB,MAAA;AACI,QAAA;AAED,UAAA;AACd,YAAA;AACI,YAAA;AACR,YAAA;AACA,YAAA;AACS,YAAA;AACR,cAAA;AACQ,gBAAA;AACC,gBAAA;AACsB,gBAAA;AAC/B,cAAA;AACD,YAAA;AACD,UAAA;AACkC,UAAA;AAAmC;AAAO;AAC7E,QAAA;AAGkC,QAAA;AACnC,MAAA;AACA,IAAA;AACF,EAAA;AACD;ADrT8C;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/ai/ai/packages/tanstack-ai/dist/chunk-53RO5I22.cjs","sourcesContent":[null,"// ---------------------------------------------------------------------------\n// AI Gateway types (for third-party providers + Workers AI through gateway)\n// ---------------------------------------------------------------------------\n\nexport interface CloudflareAiGateway {\n\trun(request: unknown): Promise<Response>;\n}\n\nexport interface AiGatewayBindingConfig {\n\t/**\n\t * The AI Gateway binding\n\t * @example\n\t * env.AI.gateway('my-gateway-id')\n\t */\n\tbinding: CloudflareAiGateway;\n\t/**\n\t * The Provider API Key if you want to manually pass it, ignore if using Unified Billing or BYOK.\n\t */\n\tapiKey?: string;\n}\n\nexport type AiGatewayCredentialsConfig = {\n\t/**\n\t * The Cloudflare account ID\n\t */\n\taccountId: string;\n\t/**\n\t * The AI Gateway ID\n\t */\n\tgatewayId: string;\n} & (\n\t| {\n\t\t\t/** Cloudflare API Key for AI Gateway */\n\t\t\tcfApiKey: string;\n\t\t\tapiKey?: string;\n\t }\n\t| {\n\t\t\t/** Provider API Key */\n\t\t\tapiKey: string;\n\t\t\t/** Cloudflare API Key for AI Gateway */\n\t\t\tcfApiKey?: string;\n\t }\n);\n\nexport interface AiGatewayConfig {\n\tskipCache?: boolean;\n\tcacheTtl?: number;\n\tcustomCacheKey?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type AiGatewayAdapterConfig = (AiGatewayBindingConfig | AiGatewayCredentialsConfig) &\n\tAiGatewayConfig;\n\n// ---------------------------------------------------------------------------\n// Plain Workers AI types (direct binding or REST, no gateway)\n// ---------------------------------------------------------------------------\n\n/**\n * The Workers AI binding interface (env.AI).\n * Accepts a model name and inputs, returns results directly.\n * Includes `gateway()` which is present on `env.AI` but not on `env.AI.gateway(id)`,\n * enabling structural discrimination from `CloudflareAiGateway`.\n */\nexport interface WorkersAiBinding {\n\trun(\n\t\tmodel: string,\n\t\tinputs: Record<string, unknown>,\n\t\toptions?: Record<string, unknown>,\n\t): Promise<unknown>;\n\tgateway(gatewayId: string): CloudflareAiGateway;\n}\n\nexport interface WorkersAiDirectBindingConfig {\n\t/**\n\t * The Workers AI binding (env.AI).\n\t * @example\n\t * { binding: env.AI }\n\t */\n\tbinding: WorkersAiBinding;\n}\n\nexport interface WorkersAiDirectCredentialsConfig {\n\t/**\n\t * The Cloudflare account ID\n\t */\n\taccountId: string;\n\t/**\n\t * The Cloudflare API key for Workers AI\n\t */\n\tapiKey: string;\n}\n\n/**\n * Config for Workers AI adapters. Supports four modes:\n * - Plain binding: `{ binding: env.AI }`\n * - Plain REST: `{ accountId, apiKey }`\n * - AI Gateway binding: `{ binding: env.AI.gateway(id) }`\n * - AI Gateway REST: `{ accountId, gatewayId, ... }`\n *\n * The third union member intersects `AiGatewayAdapterConfig` with `{ apiKey?: string }`.\n * For the gateway binding variant, `AiGatewayBindingConfig` already includes `apiKey?`,\n * so the intersection is redundant there. For the gateway credentials variant, this\n * `apiKey` represents the Workers AI token (used in the `Authorization` header to the\n * upstream provider), distinct from `cfApiKey` (used in the `cf-aig-authorization`\n * header for authenticated gateways).\n */\nexport type WorkersAiAdapterConfig = (\n\t| WorkersAiDirectBindingConfig\n\t| WorkersAiDirectCredentialsConfig\n\t| (AiGatewayAdapterConfig & { apiKey?: string })\n) & {\n\t/**\n\t * Session affinity key for prefix-cache optimization.\n\t * Routes requests with the same key to the same backend replica.\n\t */\n\tsessionAffinity?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Config detection helpers\n// ---------------------------------------------------------------------------\n\n/** Returns true if this is a plain Workers AI binding config (`{ binding: env.AI }`) */\nexport function isDirectBindingConfig(\n\tconfig: WorkersAiAdapterConfig,\n): config is WorkersAiDirectBindingConfig {\n\t// env.AI has a .gateway() method; env.AI.gateway(id) does not.\n\t// This distinguishes direct bindings from AI Gateway bindings.\n\treturn (\n\t\t\"binding\" in config &&\n\t\ttypeof (config.binding as unknown as Record<string, unknown>).gateway === \"function\"\n\t);\n}\n\n/** Returns true if this is a plain Workers AI REST config (accountId + apiKey, no gatewayId) */\nexport function isDirectCredentialsConfig(\n\tconfig: WorkersAiAdapterConfig,\n): config is WorkersAiDirectCredentialsConfig {\n\treturn \"accountId\" in config && \"apiKey\" in config && !(\"gatewayId\" in config);\n}\n\n/** Returns true if this is an AI Gateway config (has gateway binding or `gatewayId`) */\nexport function isGatewayConfig(config: WorkersAiAdapterConfig): config is AiGatewayAdapterConfig {\n\tif (\"gatewayId\" in config) return true;\n\t// Has `binding` but NOT a direct Workers AI binding (no .gateway method)\n\treturn \"binding\" in config && !isDirectBindingConfig(config);\n}\n\n// ---------------------------------------------------------------------------\n// Config validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates that a WorkersAiAdapterConfig contains a valid configuration.\n * Throws an error if neither a binding, credentials (accountId + apiKey),\n * nor a gateway configuration is provided.\n */\nexport function validateWorkersAiConfig(config: WorkersAiAdapterConfig): void {\n\tif (\n\t\t!isDirectBindingConfig(config) &&\n\t\t!isDirectCredentialsConfig(config) &&\n\t\t!isGatewayConfig(config)\n\t) {\n\t\tthrow new Error(\n\t\t\t\"Invalid Workers AI configuration: you must provide either a binding (e.g. { binding: env.AI }), \" +\n\t\t\t\t\"credentials ({ accountId, apiKey }), or a gateway configuration ({ binding: env.AI.gateway(id) } \" +\n\t\t\t\t\"or { accountId, gatewayId }).\",\n\t\t);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// createGatewayFetch -- for routing through AI Gateway\n// ---------------------------------------------------------------------------\n\nexport function createGatewayFetch(\n\tprovider: string,\n\tconfig: AiGatewayAdapterConfig,\n\theaders: Record<string, string> = {},\n): typeof fetch {\n\treturn (input, init) => {\n\t\tlet query: Record<string, unknown> = {};\n\n\t\tconst url =\n\t\t\ttypeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\t\tconst urlObj = new URL(url);\n\n\t\t// Extract endpoint path (remove /v1/ prefix if present)\n\t\tconst endpoint = urlObj.pathname.replace(/^\\/v1\\//, \"\").replace(/^\\//, \"\") + urlObj.search;\n\n\t\tif (init?.body) {\n\t\t\ttry {\n\t\t\t\tquery = JSON.parse(init.body as string);\n\t\t\t} catch {\n\t\t\t\tquery = { _raw: init.body };\n\t\t\t}\n\t\t}\n\n\t\tconst cacheHeaders: Record<string, string> = {};\n\n\t\tif (\"skipCache\" in config && config.skipCache) {\n\t\t\tcacheHeaders[\"cf-aig-skip-cache\"] = \"true\";\n\t\t}\n\n\t\tif (typeof config.cacheTtl === \"number\") {\n\t\t\tcacheHeaders[\"cf-aig-cache-ttl\"] = String(config.cacheTtl);\n\t\t}\n\n\t\tif (typeof config.customCacheKey === \"string\") {\n\t\t\tcacheHeaders[\"cf-aig-cache-key\"] = config.customCacheKey;\n\t\t}\n\n\t\tif (typeof config.metadata === \"object\") {\n\t\t\tcacheHeaders[\"cf-aig-metadata\"] = JSON.stringify(config.metadata);\n\t\t}\n\n\t\tconst request: {\n\t\t\tprovider: string;\n\t\t\tendpoint: string;\n\t\t\theaders: Record<string, string>;\n\t\t\tquery: Record<string, unknown>;\n\t\t} = {\n\t\t\tprovider,\n\t\t\tendpoint,\n\t\t\theaders: {\n\t\t\t\t...(init?.headers as Record<string, string> | undefined),\n\t\t\t\t...headers,\n\t\t\t\t...cacheHeaders,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tquery,\n\t\t};\n\n\t\tif (provider === \"workers-ai\") {\n\t\t\tif (!request.endpoint.startsWith(\"run/\")) {\n\t\t\t\trequest.endpoint = `run/${query.model}`;\n\t\t\t}\n\t\t\tdelete query.model;\n\t\t\tdelete query.instructions;\n\t\t}\n\n\t\tif (config.apiKey) {\n\t\t\trequest.headers[\"authorization\"] = `Bearer ${config.apiKey}`;\n\t\t}\n\n\t\tif (\"binding\" in config) {\n\t\t\treturn config.binding.run(request);\n\t\t}\n\n\t\treturn fetch(\n\t\t\t`https://gateway.ai.cloudflare.com/v1/${config.accountId}/${config.gatewayId}`,\n\t\t\t{\n\t\t\t\t...init,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t...headers,\n\t\t\t\t\t...cacheHeaders,\n\t\t\t\t\t...(config.cfApiKey\n\t\t\t\t\t\t? { \"cf-aig-authorization\": `Bearer ${config.cfApiKey}` }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(request),\n\t\t\t},\n\t\t);\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// createWorkersAiBindingFetch -- shim that makes env.AI look like an OpenAI endpoint\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize messages before passing to Workers AI binding.\n *\n * The binding has strict schema validation that may differ from the OpenAI API:\n * - `content` must be a string (not null)\n */\nfunction normalizeMessagesForBinding(\n\tmessages: Record<string, unknown>[],\n): Record<string, unknown>[] {\n\treturn messages.map((msg) => {\n\t\tconst normalized = { ...msg };\n\n\t\t// content: null → content: \"\"\n\t\tif (normalized.content === null || normalized.content === undefined) {\n\t\t\tnormalized.content = \"\";\n\t\t}\n\n\t\treturn normalized;\n\t});\n}\n\n/**\n * Creates a fetch function that intercepts OpenAI SDK requests and translates them\n * to Workers AI binding calls (env.AI.run). This allows the WorkersAiTextAdapter\n * to use the OpenAI SDK against a plain Workers AI binding.\n *\n * NOTE: The `input` URL parameter is intentionally ignored. The model name and all\n * request parameters are extracted from the JSON body, matching Workers AI's\n * `binding.run(model, inputs)` calling convention.\n */\nexport function createWorkersAiBindingFetch(\n\tbinding: WorkersAiBinding,\n\toptions?: { extraHeaders?: Record<string, string> },\n): typeof fetch {\n\treturn async (_input, init) => {\n\t\tif (!init?.body) {\n\t\t\treturn new Response(\"No body\", { status: 400 });\n\t\t}\n\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = JSON.parse(init.body as string);\n\t\t} catch {\n\t\t\treturn new Response(\"Invalid JSON body\", { status: 400 });\n\t\t}\n\n\t\tconst model = body.model as string;\n\t\tconst stream = body.stream as boolean | undefined;\n\n\t\t// Build Workers AI inputs from OpenAI format\n\t\tconst inputs: Record<string, unknown> = {};\n\t\tif (body.messages) {\n\t\t\tinputs.messages = normalizeMessagesForBinding(\n\t\t\t\tbody.messages as Record<string, unknown>[],\n\t\t\t);\n\t\t}\n\t\tif (body.tools) inputs.tools = body.tools;\n\t\tif (typeof body.temperature === \"number\") inputs.temperature = body.temperature;\n\t\tif (typeof body.max_tokens === \"number\") inputs.max_tokens = body.max_tokens;\n\t\tif (body.response_format) inputs.response_format = body.response_format;\n\t\tif (stream) inputs.stream = true;\n\n\t\tconst result = await binding.run(\n\t\t\tmodel,\n\t\t\tinputs,\n\t\t\toptions?.extraHeaders ? { extraHeaders: options.extraHeaders } : undefined,\n\t\t);\n\n\t\tif (stream && result instanceof ReadableStream) {\n\t\t\t// Workers AI returns an SSE stream with `data: {\"response\":\"chunk\"}` format.\n\t\t\t// Transform it to OpenAI-compatible SSE format.\n\t\t\tconst transformed = transformWorkersAiStream(\n\t\t\t\tresult as ReadableStream<Uint8Array>,\n\t\t\t\tmodel,\n\t\t\t);\n\t\t\treturn new Response(transformed, {\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Graceful degradation: some models return a complete (non-streaming)\n\t\t// response even when `stream: true` is requested. Fall through to the\n\t\t// non-streaming wrapper which produces a valid OpenAI Chat Completion\n\t\t// response that the SDK can consume.\n\n\t\t// Non-streaming: Workers AI returns { response: \"text\", tool_calls?: [...] }\n\t\t// Wrap into OpenAI Chat Completion format.\n\t\tconst responseObj =\n\t\t\ttypeof result === \"object\" && result !== null\n\t\t\t\t? (result as Record<string, unknown>)\n\t\t\t\t: { response: String(result) };\n\n\t\tconst responseText = typeof responseObj.response === \"string\" ? responseObj.response : \"\";\n\n\t\tconst message: Record<string, unknown> = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: responseText,\n\t\t};\n\t\tlet finishReason = \"stop\";\n\n\t\t// Handle tool calls if present in Workers AI response\n\t\tif (Array.isArray(responseObj.tool_calls) && responseObj.tool_calls.length > 0) {\n\t\t\tfinishReason = \"tool_calls\";\n\t\t\tmessage.tool_calls = responseObj.tool_calls.map(\n\t\t\t\t(tc: {\n\t\t\t\t\tid?: string;\n\t\t\t\t\tname?: string;\n\t\t\t\t\targuments: unknown;\n\t\t\t\t\tfunction?: { name: string; arguments?: unknown };\n\t\t\t\t}) => ({\n\t\t\t\t\tid: tc.id || crypto.randomUUID(),\n\t\t\t\t\ttype: \"function\",\n\t\t\t\t\tfunction: {\n\t\t\t\t\t\tname: tc.function?.name || tc.name || \"\",\n\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\ttypeof (tc.function?.arguments ?? tc.arguments) === \"string\"\n\t\t\t\t\t\t\t\t? ((tc.function?.arguments ?? tc.arguments) as string)\n\t\t\t\t\t\t\t\t: JSON.stringify(tc.function?.arguments ?? tc.arguments ?? {}),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst openAiResponse = {\n\t\t\tid: `workers-ai-${crypto.randomUUID()}`,\n\t\t\tobject: \"chat.completion\",\n\t\t\tcreated: Math.floor(Date.now() / 1000),\n\t\t\tmodel,\n\t\t\tchoices: [{ index: 0, message, finish_reason: finishReason }],\n\t\t};\n\n\t\treturn new Response(JSON.stringify(openAiResponse), {\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t});\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Stream transformer: Workers AI SSE -> OpenAI-compatible SSE\n// Uses TransformStream for proper backpressure.\n// ---------------------------------------------------------------------------\n\n/**\n * Transforms a Workers AI SSE stream (data: {\"response\":\"chunk\"}) into\n * an OpenAI-compatible SSE stream (data: {\"choices\":[{\"delta\":{\"content\":\"chunk\"}}]}).\n *\n * Workers AI binding streams tool calls in an OpenAI-like nested format:\n * { tool_calls: [{ id, type, index, function: { name, arguments } }] }\n * Arguments are streamed incrementally across multiple SSE chunks, so the\n * transformer must forward them as incremental deltas rather than a single blob.\n */\nfunction transformWorkersAiStream(\n\tsource: ReadableStream<Uint8Array>,\n\tmodel: string,\n): ReadableStream<Uint8Array> {\n\tconst decoder = new TextDecoder();\n\tconst encoder = new TextEncoder();\n\t// Generate a stable ID and timestamp for the entire stream, matching OpenAI's\n\t// convention where all chunks in a single response share the same id/created.\n\tconst streamId = `workers-ai-${crypto.randomUUID()}`;\n\tconst created = Math.floor(Date.now() / 1000);\n\tlet buffer = \"\";\n\tlet hasToolCalls = false;\n\t// When true, the source stream is already in OpenAI format (some models\n\t// like Qwen3, Kimi K2.5 stream OpenAI-compatible SSE through the binding).\n\t// In that case, flush() should only emit [DONE] and skip the finish chunk.\n\tlet isOpenAiFormat = false;\n\t// Track tool call state per index: store the generated/assigned ID so that\n\t// subsequent argument deltas use the same ID (matching the working streaming.ts pattern).\n\tconst toolCallState = new Map<number, { id: string; name: string }>();\n\n\treturn source.pipeThrough(\n\t\tnew TransformStream<Uint8Array, Uint8Array>({\n\t\t\ttransform(chunk, controller) {\n\t\t\t\tbuffer += decoder.decode(chunk, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tconst trimmed = line.trim();\n\t\t\t\t\tif (!trimmed || !trimmed.startsWith(\"data: \")) continue;\n\t\t\t\t\tconst data = trimmed.slice(6);\n\n\t\t\t\t\t// Swallow source [DONE]; we emit our own in flush()\n\t\t\t\t\tif (data === \"[DONE]\") continue;\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\n\t\t\t\t\t\t// Some models (Qwen3, Kimi K2.5) return OpenAI-compatible format\n\t\t\t\t\t\t// directly through the binding, with `choices[].delta.content` and\n\t\t\t\t\t\t// optional `reasoning_content`. Detect this and pass through as-is.\n\t\t\t\t\t\tif (parsed.choices !== undefined) {\n\t\t\t\t\t\t\t// Already OpenAI format — pass through but ensure each tool call\n\t\t\t\t\t\t\t// index gets a unique, stable ID across all chunks.\n\t\t\t\t\t\t\tisOpenAiFormat = true;\n\t\t\t\t\t\t\tconst choice = parsed.choices?.[0];\n\t\t\t\t\t\t\tif (choice?.delta?.tool_calls) {\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t\t\tfor (const tc of choice.delta.tool_calls) {\n\t\t\t\t\t\t\t\t\tconst tcIndex = tc.index ?? 0;\n\t\t\t\t\t\t\t\t\tif (!toolCallState.has(tcIndex)) {\n\t\t\t\t\t\t\t\t\t\t// First chunk for this index — generate/store unique ID\n\t\t\t\t\t\t\t\t\t\tconst id = tc.id || `call${streamId}${tcIndex}`;\n\t\t\t\t\t\t\t\t\t\ttoolCallState.set(tcIndex, {\n\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\tname: tc.function?.name || \"\",\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\ttc.id = id;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Subsequent chunk — reuse stored ID, remove id from delta\n\t\t\t\t\t\t\t\t\t\t// (OpenAI format only sends id in first chunk)\n\t\t\t\t\t\t\t\t\t\tdelete tc.id;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (choice?.finish_reason === \"tool_calls\") {\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(parsed)}\\n\\n`),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// --- Workers AI native format handling below ---\n\n\t\t\t\t\t\t// Text content\n\t\t\t\t\t\tif (parsed.response != null && parsed.response !== \"\") {\n\t\t\t\t\t\t\tconst openAiChunk = {\n\t\t\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\t\t\tcreated,\n\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\t\t\tdelta: { content: parsed.response },\n\t\t\t\t\t\t\t\t\t\tfinish_reason: null,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(openAiChunk)}\\n\\n`),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Tool calls — Workers AI binding streams these incrementally:\n\t\t\t\t\t\t// Chunk A: { id, type, index, function: { name } } — start\n\t\t\t\t\t\t// Chunk B: { index, function: { arguments: \"partial...\" } } — args delta\n\t\t\t\t\t\t// Chunk C: { index, function: { arguments: \"rest...\" } } — args delta\n\t\t\t\t\t\t// Chunk D: { id: null, type: null, index, function: { name: null, arguments: \"\" } } — finalize (skip)\n\t\t\t\t\t\tif (Array.isArray(parsed.tool_calls) && parsed.tool_calls.length > 0) {\n\t\t\t\t\t\t\tfor (const tc of parsed.tool_calls) {\n\t\t\t\t\t\t\t\tconst tcIndex = tc.index ?? 0;\n\n\t\t\t\t\t\t\t\t// Resolve name and arguments from either nested or flat format\n\t\t\t\t\t\t\t\tconst tcName = tc.function?.name ?? tc.name ?? null;\n\t\t\t\t\t\t\t\tconst tcArgs = tc.function?.arguments ?? tc.arguments ?? null;\n\t\t\t\t\t\t\t\tconst tcId = tc.id ?? null;\n\n\t\t\t\t\t\t\t\t// Skip finalization chunks where everything is null/empty\n\t\t\t\t\t\t\t\tif (!tcId && !tcName && (!tcArgs || tcArgs === \"\")) continue;\n\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\n\t\t\t\t\t\t\t\t// Build the OpenAI-compatible tool_calls delta\n\t\t\t\t\t\t\t\tconst toolCallDelta: Record<string, unknown> = {\n\t\t\t\t\t\t\t\t\tindex: tcIndex,\n\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\tif (!toolCallState.has(tcIndex)) {\n\t\t\t\t\t\t\t\t\t// First chunk for this tool call index — emit id, type, name.\n\t\t\t\t\t\t\t\t\tconst id = tcId || `call${streamId}${tcIndex}`;\n\t\t\t\t\t\t\t\t\ttoolCallState.set(tcIndex, { id, name: tcName || \"\" });\n\t\t\t\t\t\t\t\t\ttoolCallDelta.id = id;\n\t\t\t\t\t\t\t\t\ttoolCallDelta.type = \"function\";\n\t\t\t\t\t\t\t\t\ttoolCallDelta.function = {\n\t\t\t\t\t\t\t\t\t\tname: tcName || \"\",\n\t\t\t\t\t\t\t\t\t\t// Include arguments if they arrive in the same chunk\n\t\t\t\t\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\t\t\t\t\ttcArgs != null\n\t\t\t\t\t\t\t\t\t\t\t\t? typeof tcArgs === \"string\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t? tcArgs\n\t\t\t\t\t\t\t\t\t\t\t\t\t: JSON.stringify(tcArgs)\n\t\t\t\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Subsequent chunks — only include arguments delta\n\t\t\t\t\t\t\t\t\tif (tcArgs != null && tcArgs !== \"\") {\n\t\t\t\t\t\t\t\t\t\ttoolCallDelta.function = {\n\t\t\t\t\t\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\t\t\t\t\t\ttypeof tcArgs === \"string\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t? tcArgs\n\t\t\t\t\t\t\t\t\t\t\t\t\t: JSON.stringify(tcArgs),\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tcontinue; // Nothing useful to forward\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tconst toolChunk = {\n\t\t\t\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\t\t\t\tcreated,\n\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\t\t\t\tdelta: { tool_calls: [toolCallDelta] },\n\t\t\t\t\t\t\t\t\t\t\tfinish_reason: null,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(toolChunk)}\\n\\n`),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t// Log malformed SSE events for debugging; don't break the stream.\n\t\t\t\t\t\tconsole.warn(\"[tanstack-ai] failed to parse SSE event:\", data, e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tflush(controller) {\n\t\t\t\tif (!isOpenAiFormat) {\n\t\t\t\t\t// Workers AI native format: emit a finish chunk with stop/tool_calls\n\t\t\t\t\tconst finalChunk = {\n\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\tdelta: {},\n\t\t\t\t\t\t\t\tfinish_reason: hasToolCalls ? \"tool_calls\" : \"stop\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t};\n\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\\n\\n`));\n\t\t\t\t}\n\t\t\t\t// OpenAI format already includes its own finish_reason in the stream.\n\t\t\t\t// Either way, emit a [DONE] sentinel.\n\t\t\t\tcontroller.enqueue(encoder.encode(\"data: [DONE]\\n\\n\"));\n\t\t\t},\n\t\t}),\n\t);\n}\n"]}
@@ -8,8 +8,9 @@ import {
8
8
  import {
9
9
  createGatewayFetch,
10
10
  isDirectBindingConfig,
11
- isDirectCredentialsConfig
12
- } from "./chunk-UUFEOQ6B.js";
11
+ isDirectCredentialsConfig,
12
+ validateWorkersAiConfig
13
+ } from "./chunk-727MRLUF.js";
13
14
  import {
14
15
  __publicField
15
16
  } from "./chunk-V6TY7KAL.js";
@@ -21,6 +22,7 @@ var WorkersAiTranscriptionAdapter = class extends BaseTranscriptionAdapter {
21
22
  super({}, model);
22
23
  __publicField(this, "name", "workers-ai-transcription");
23
24
  __publicField(this, "adapterConfig");
25
+ validateWorkersAiConfig(config);
24
26
  this.adapterConfig = config;
25
27
  }
26
28
  async transcribe(options) {
@@ -213,4 +215,4 @@ export {
213
215
  WorkersAiTranscriptionAdapter,
214
216
  createWorkersAiTranscription
215
217
  };
216
- //# sourceMappingURL=chunk-VV3JFKAN.js.map
218
+ //# sourceMappingURL=chunk-627RVSSB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/workers-ai-transcription.ts"],"sourcesContent":["import { BaseTranscriptionAdapter } from \"@tanstack/ai/adapters\";\nimport type { TranscriptionOptions, TranscriptionResult } from \"@tanstack/ai\";\nimport {\n\ttype WorkersAiAdapterConfig,\n\ttype WorkersAiDirectBindingConfig,\n\ttype WorkersAiDirectCredentialsConfig,\n\ttype AiGatewayAdapterConfig,\n\tcreateGatewayFetch,\n\tisDirectBindingConfig,\n\tisDirectCredentialsConfig,\n\tvalidateWorkersAiConfig,\n} from \"../utils/create-fetcher\";\nimport { workersAiRestFetch, workersAiRestFetchBinary } from \"../utils/workers-ai-rest\";\nimport { uint8ArrayToBase64 } from \"../utils/binary\";\n\n// ---------------------------------------------------------------------------\n// Model types\n// ---------------------------------------------------------------------------\n\n/**\n * Workers AI models that support speech-to-text transcription.\n *\n * Note: the typed `AiModels` interface in `@cloudflare/workers-types` may lag\n * behind what's deployed. We use a string union here that matches the known\n * models including Deepgram partner models.\n *\n * **Nova-3 note:** `@cf/deepgram/nova-3` uses a different input format than the\n * Whisper models. Via binding it accepts `{ audio: { body: base64, contentType } }`.\n * Via REST it requires multipart form data (not JSON). The adapter handles both.\n */\nexport type WorkersAiTranscriptionModel =\n\t| \"@cf/openai/whisper\"\n\t| \"@cf/openai/whisper-tiny-en\"\n\t| \"@cf/openai/whisper-large-v3-turbo\"\n\t| \"@cf/deepgram/nova-3\"\n\t| (string & {});\n\n// ---------------------------------------------------------------------------\n// WorkersAiTranscriptionAdapter\n// ---------------------------------------------------------------------------\n\nexport class WorkersAiTranscriptionAdapter extends BaseTranscriptionAdapter<WorkersAiTranscriptionModel> {\n\treadonly name = \"workers-ai-transcription\" as const;\n\tprivate adapterConfig: WorkersAiAdapterConfig;\n\n\tconstructor(config: WorkersAiAdapterConfig, model: WorkersAiTranscriptionModel) {\n\t\tsuper({}, model);\n\t\tvalidateWorkersAiConfig(config);\n\t\tthis.adapterConfig = config;\n\t}\n\n\tasync transcribe(options: TranscriptionOptions): Promise<TranscriptionResult> {\n\t\tconst { audio, language, prompt, modelOptions } = options;\n\n\t\t// Normalize audio to raw bytes\n\t\tconst audioBytes = await normalizeAudioToBytes(audio);\n\n\t\tconst extra: Record<string, unknown> = { ...modelOptions };\n\t\tif (language) extra.language = language;\n\t\tif (prompt) extra.initial_prompt = prompt;\n\n\t\t// Build the model-specific audio payload:\n\t\t// - Deepgram Nova-3 (binding): { audio: { body: base64, contentType: \"audio/...\" } }\n\t\t// - Deepgram Nova-3 (REST): multipart FormData (handled separately)\n\t\t// - Whisper Large v3 Turbo (REST/gateway): { audio: base64string }\n\t\t// - Other Whisper models (binding): { audio: number[] }\n\t\tconst audioPayload = this.buildAudioPayload(audioBytes, audio);\n\n\t\tif (isDirectBindingConfig(this.adapterConfig)) {\n\t\t\treturn this.transcribeViaBinding(audioPayload, extra);\n\t\t}\n\n\t\tif (isDirectCredentialsConfig(this.adapterConfig)) {\n\t\t\t// Nova-3 REST requires raw binary audio, not JSON\n\t\t\tif (this.model === \"@cf/deepgram/nova-3\") {\n\t\t\t\treturn this.transcribeViaRestBinary(audioBytes, audio, extra);\n\t\t\t}\n\t\t\treturn this.transcribeViaRest(audioPayload, extra);\n\t\t}\n\n\t\treturn this.transcribeViaGateway(audioPayload, extra);\n\t}\n\n\t/**\n\t * Build the audio field for the request payload, handling model-specific formats.\n\t *\n\t * - `@cf/deepgram/nova-3` requires `{ body: base64, contentType: \"audio/...\" }`\n\t * - `@cf/openai/whisper-large-v3-turbo` REST/gateway accepts a base64 string\n\t * - Other Whisper models accept `number[]` (binding) or base64 (REST)\n\t */\n\tprivate buildAudioPayload(\n\t\taudioBytes: number[],\n\t\toriginalAudio: string | File | Blob | ArrayBuffer,\n\t): Record<string, unknown> {\n\t\tif (this.model === \"@cf/deepgram/nova-3\") {\n\t\t\tconst b64 = uint8ArrayToBase64(new Uint8Array(audioBytes));\n\t\t\tconst contentType = detectAudioContentType(originalAudio);\n\t\t\treturn { audio: { body: b64, contentType } };\n\t\t}\n\n\t\tif (this.model === \"@cf/openai/whisper-large-v3-turbo\") {\n\t\t\treturn { audio: uint8ArrayToBase64(new Uint8Array(audioBytes)) };\n\t\t}\n\n\t\treturn { audio: audioBytes };\n\t}\n\n\tprivate async transcribeViaBinding(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst ai = (this.adapterConfig as WorkersAiDirectBindingConfig).binding;\n\t\tconst result = (await ai.run(this.model, {\n\t\t\t...audioPayload,\n\t\t\t...options,\n\t\t})) as Record<string, unknown>;\n\t\treturn this.normalizeResult(result);\n\t}\n\n\tprivate async transcribeViaRest(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst config = this.adapterConfig as WorkersAiDirectCredentialsConfig;\n\n\t\tconst response = await workersAiRestFetch(\n\t\t\tconfig,\n\t\t\tthis.model,\n\t\t\t{ ...audioPayload, ...options },\n\t\t\t{\n\t\t\t\tlabel: \"Workers AI transcription\",\n\t\t\t\tsignal: (options as { signal?: AbortSignal }).signal,\n\t\t\t},\n\t\t);\n\n\t\tconst data = (await response.json()) as {\n\t\t\tresult?: Record<string, unknown>;\n\t\t} & Record<string, unknown>;\n\n\t\t// Cloudflare REST API wraps responses in { success, result: {...} }.\n\t\t// Use `data.result` when present, fall back to `data` for direct responses.\n\t\treturn this.normalizeResult(data.result ?? data);\n\t}\n\n\t/**\n\t * Transcribe via REST using raw binary audio.\n\t * Required for models like Deepgram Nova-3 that expect raw audio bytes\n\t * with a Content-Type header (e.g. \"audio/wav\") instead of JSON.\n\t */\n\tprivate async transcribeViaRestBinary(\n\t\taudioBytes: number[],\n\t\toriginalAudio: string | File | Blob | ArrayBuffer,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst config = this.adapterConfig as WorkersAiDirectCredentialsConfig;\n\t\tconst contentType = detectAudioContentType(originalAudio);\n\n\t\tconst response = await workersAiRestFetchBinary(\n\t\t\tconfig,\n\t\t\tthis.model,\n\t\t\tnew Uint8Array(audioBytes),\n\t\t\tcontentType,\n\t\t\t{\n\t\t\t\tlabel: \"Workers AI transcription\",\n\t\t\t\tsignal: (options as { signal?: AbortSignal }).signal,\n\t\t\t},\n\t\t);\n\n\t\tconst data = (await response.json()) as {\n\t\t\tresult?: Record<string, unknown>;\n\t\t} & Record<string, unknown>;\n\n\t\treturn this.normalizeResult(data.result ?? data);\n\t}\n\n\tprivate async transcribeViaGateway(\n\t\taudioPayload: Record<string, unknown>,\n\t\toptions: Record<string, unknown>,\n\t): Promise<TranscriptionResult> {\n\t\tconst gatewayConfig = this.adapterConfig as AiGatewayAdapterConfig;\n\t\tconst gatewayFetch = createGatewayFetch(\"workers-ai\", gatewayConfig);\n\n\t\t// The URL here is a placeholder — createGatewayFetch for \"workers-ai\" extracts\n\t\t// the model from the body, sets it as the endpoint, and routes through the gateway.\n\t\t// The actual URL path is not used.\n\t\tconst response = await gatewayFetch(\"https://api.cloudflare.com/v1/audio/transcriptions\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: this.model,\n\t\t\t\t...audioPayload,\n\t\t\t\t...options,\n\t\t\t}),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text();\n\t\t\tthrow new Error(\n\t\t\t\t`Workers AI transcription gateway request failed (${response.status}): ${errorText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await response.json()) as Record<string, unknown>;\n\t\treturn this.normalizeResult(data);\n\t}\n\n\t/**\n\t * Normalize Workers AI transcription results into the standard\n\t * TanStack AI TranscriptionResult shape.\n\t *\n\t * Handles three response formats:\n\t * - Whisper: `{ text, words?, vtt? }`\n\t * - Whisper v3-turbo: `{ text, segments?, transcription_info? }`\n\t * - Deepgram Nova-3: `{ results: { channels: [{ alternatives: [{ transcript, words }] }] } }`\n\t */\n\tprivate normalizeResult(raw: Record<string, unknown>): TranscriptionResult {\n\t\t// Deepgram Nova-3 format: { results: { channels: [{ alternatives: [{ transcript, words }] }] } }\n\t\tconst results = raw.results as Record<string, unknown> | undefined;\n\t\tif (results?.channels) {\n\t\t\tconst channels = results.channels as Array<{\n\t\t\t\talternatives?: Array<{\n\t\t\t\t\ttranscript?: string;\n\t\t\t\t\tconfidence?: number;\n\t\t\t\t\twords?: Array<{ word: string; start: number; end: number; confidence: number }>;\n\t\t\t\t}>;\n\t\t\t}>;\n\t\t\tconst alt = channels?.[0]?.alternatives?.[0];\n\t\t\tconst text = alt?.transcript ?? \"\";\n\t\t\tconst result: TranscriptionResult = {\n\t\t\t\tid: this.generateId(),\n\t\t\t\tmodel: this.model,\n\t\t\t\ttext,\n\t\t\t};\n\t\t\tif (alt?.words && Array.isArray(alt.words)) {\n\t\t\t\tresult.words = alt.words.map((w) => ({\n\t\t\t\t\tword: w.word ?? \"\",\n\t\t\t\t\tstart: w.start ?? 0,\n\t\t\t\t\tend: w.end ?? 0,\n\t\t\t\t}));\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\t// Whisper format: { text, words?, vtt? }\n\t\t// Whisper v3-turbo format: { text, segments?, transcription_info? }\n\t\tconst result: TranscriptionResult = {\n\t\t\tid: this.generateId(),\n\t\t\tmodel: this.model,\n\t\t\ttext: (raw.text as string) ?? \"\",\n\t\t};\n\n\t\t// Language from transcription_info (whisper-large-v3-turbo)\n\t\tconst transcriptionInfo = raw.transcription_info as Record<string, unknown> | undefined;\n\t\tif (transcriptionInfo?.language) {\n\t\t\tresult.language = transcriptionInfo.language as string;\n\t\t}\n\n\t\t// Duration\n\t\tif (transcriptionInfo?.duration != null) {\n\t\t\tresult.duration = transcriptionInfo.duration as number;\n\t\t}\n\n\t\t// Segments (whisper-large-v3-turbo returns these)\n\t\tif (raw.segments && Array.isArray(raw.segments)) {\n\t\t\tresult.segments = raw.segments.map((seg: Record<string, unknown>, idx: number) => ({\n\t\t\t\tid: idx,\n\t\t\t\ttext: (seg.text as string) ?? \"\",\n\t\t\t\tstart: (seg.start as number) ?? 0,\n\t\t\t\tend: (seg.end as number) ?? 0,\n\t\t\t}));\n\t\t}\n\n\t\t// Words — basic whisper returns top-level words[], v3-turbo nests them in segments\n\t\tif (raw.words && Array.isArray(raw.words)) {\n\t\t\tresult.words = raw.words.map((w: Record<string, unknown>) => ({\n\t\t\t\tword: (w.word as string) ?? \"\",\n\t\t\t\tstart: (w.start as number) ?? 0,\n\t\t\t\tend: (w.end as number) ?? 0,\n\t\t\t}));\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Workers AI transcription adapter for speech-to-text.\n *\n * Works with TanStack AI's `generateTranscription()` activity function:\n * ```ts\n * import { generateTranscription } from \"@tanstack/ai\";\n * import { createWorkersAiTranscription } from \"@cloudflare/tanstack-ai\";\n *\n * const adapter = createWorkersAiTranscription(\n * \"@cf/openai/whisper-large-v3-turbo\",\n * { binding: env.AI },\n * );\n *\n * const result = await generateTranscription({ adapter, audio: audioData });\n * // result.text — the transcribed text\n * ```\n *\n * Note: Factory takes `(model, config)` for ergonomics — the class constructor\n * uses `(config, model)` to match TanStack AI's upstream convention.\n */\nexport function createWorkersAiTranscription(\n\tmodel: WorkersAiTranscriptionModel,\n\tconfig: WorkersAiAdapterConfig,\n) {\n\treturn new WorkersAiTranscriptionAdapter(config, model);\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize various audio input formats into a number[] (raw bytes).\n *\n * Note: `File extends Blob`, so `File` instances are handled by the\n * `instanceof Blob` branch. `Blob.arrayBuffer()` always reads the full\n * contents regardless of any prior reads — there's no cursor to worry about.\n */\nasync function normalizeAudioToBytes(audio: string | File | Blob | ArrayBuffer): Promise<number[]> {\n\tif (audio instanceof ArrayBuffer) {\n\t\treturn Array.from(new Uint8Array(audio));\n\t}\n\n\tif (audio instanceof Blob) {\n\t\t// This also handles `File` (which extends Blob)\n\t\tconst buffer = await audio.arrayBuffer();\n\t\treturn Array.from(new Uint8Array(buffer));\n\t}\n\n\tif (typeof audio === \"string\") {\n\t\t// Assume base64 string — decode to bytes\n\t\tconst binary = atob(audio);\n\t\tconst bytes = new Uint8Array(binary.length);\n\t\tfor (let i = 0; i < binary.length; i++) {\n\t\t\tbytes[i] = binary.charCodeAt(i);\n\t\t}\n\t\treturn Array.from(bytes);\n\t}\n\n\tthrow new Error(\"Unsupported audio format. Expected string, File, Blob, or ArrayBuffer.\");\n}\n\n/**\n * Detect the MIME type of the audio input for models that require it\n * (e.g., Deepgram Nova-3).\n *\n * - `File` / `Blob`: use the `.type` property (e.g., \"audio/wav\")\n * - `ArrayBuffer` / `string`: defaults to \"audio/wav\"\n */\nfunction detectAudioContentType(audio: string | File | Blob | ArrayBuffer): string {\n\t// File and Blob carry their own MIME type\n\tif (audio instanceof Blob && audio.type) {\n\t\treturn audio.type;\n\t}\n\n\t// For raw bytes, default to audio/wav — this is the most common\n\t// format for transcription inputs and what the E2E tests use.\n\treturn \"audio/wav\";\n}\n"],"mappings":"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","names":["result"]}
@@ -5,6 +5,17 @@ function isDirectBindingConfig(config) {
5
5
  function isDirectCredentialsConfig(config) {
6
6
  return "accountId" in config && "apiKey" in config && !("gatewayId" in config);
7
7
  }
8
+ function isGatewayConfig(config) {
9
+ if ("gatewayId" in config) return true;
10
+ return "binding" in config && !isDirectBindingConfig(config);
11
+ }
12
+ function validateWorkersAiConfig(config) {
13
+ if (!isDirectBindingConfig(config) && !isDirectCredentialsConfig(config) && !isGatewayConfig(config)) {
14
+ throw new Error(
15
+ "Invalid Workers AI configuration: you must provide either a binding (e.g. { binding: env.AI }), credentials ({ accountId, apiKey }), or a gateway configuration ({ binding: env.AI.gateway(id) } or { accountId, gatewayId })."
16
+ );
17
+ }
18
+ }
8
19
  function createGatewayFetch(provider, config, headers = {}) {
9
20
  return (input, init) => {
10
21
  let query = {};
@@ -43,7 +54,9 @@ function createGatewayFetch(provider, config, headers = {}) {
43
54
  query
44
55
  };
45
56
  if (provider === "workers-ai") {
46
- request.endpoint = query.model;
57
+ if (!request.endpoint.startsWith("run/")) {
58
+ request.endpoint = `run/${query.model}`;
59
+ }
47
60
  delete query.model;
48
61
  delete query.instructions;
49
62
  }
@@ -74,27 +87,10 @@ function normalizeMessagesForBinding(messages) {
74
87
  if (normalized.content === null || normalized.content === void 0) {
75
88
  normalized.content = "";
76
89
  }
77
- if (normalized.tool_call_id && typeof normalized.tool_call_id === "string") {
78
- normalized.tool_call_id = sanitizeToolCallId(normalized.tool_call_id);
79
- }
80
- if (Array.isArray(normalized.tool_calls)) {
81
- normalized.tool_calls = normalized.tool_calls.map(
82
- (tc) => {
83
- if (tc.id && typeof tc.id === "string") {
84
- return { ...tc, id: sanitizeToolCallId(tc.id) };
85
- }
86
- return tc;
87
- }
88
- );
89
- }
90
90
  return normalized;
91
91
  });
92
92
  }
93
- function sanitizeToolCallId(id) {
94
- const alphanumeric = id.replace(/[^a-zA-Z0-9]/g, "");
95
- return alphanumeric.slice(0, 9).padEnd(9, "0");
96
- }
97
- function createWorkersAiBindingFetch(binding) {
93
+ function createWorkersAiBindingFetch(binding, options) {
98
94
  return async (_input, init) => {
99
95
  if (!init?.body) {
100
96
  return new Response("No body", { status: 400 });
@@ -118,7 +114,11 @@ function createWorkersAiBindingFetch(binding) {
118
114
  if (typeof body.max_tokens === "number") inputs.max_tokens = body.max_tokens;
119
115
  if (body.response_format) inputs.response_format = body.response_format;
120
116
  if (stream) inputs.stream = true;
121
- const result = await binding.run(model, inputs);
117
+ const result = await binding.run(
118
+ model,
119
+ inputs,
120
+ options?.extraHeaders ? { extraHeaders: options.extraHeaders } : void 0
121
+ );
122
122
  if (stream && result instanceof ReadableStream) {
123
123
  const transformed = transformWorkersAiStream(
124
124
  result,
@@ -142,7 +142,7 @@ function createWorkersAiBindingFetch(binding) {
142
142
  finishReason = "tool_calls";
143
143
  message.tool_calls = responseObj.tool_calls.map(
144
144
  (tc) => ({
145
- id: sanitizeToolCallId(tc.id || crypto.randomUUID()),
145
+ id: tc.id || crypto.randomUUID(),
146
146
  type: "function",
147
147
  function: {
148
148
  name: tc.function?.name || tc.name || "",
@@ -171,7 +171,7 @@ function transformWorkersAiStream(source, model) {
171
171
  let buffer = "";
172
172
  let hasToolCalls = false;
173
173
  let isOpenAiFormat = false;
174
- const emittedToolCallStart = /* @__PURE__ */ new Set();
174
+ const toolCallState = /* @__PURE__ */ new Map();
175
175
  return source.pipeThrough(
176
176
  new TransformStream({
177
177
  transform(chunk, controller) {
@@ -191,8 +191,16 @@ function transformWorkersAiStream(source, model) {
191
191
  if (choice?.delta?.tool_calls) {
192
192
  hasToolCalls = true;
193
193
  for (const tc of choice.delta.tool_calls) {
194
- if (tc.id && typeof tc.id === "string") {
195
- tc.id = sanitizeToolCallId(tc.id);
194
+ const tcIndex = tc.index ?? 0;
195
+ if (!toolCallState.has(tcIndex)) {
196
+ const id = tc.id || `call${streamId}${tcIndex}`;
197
+ toolCallState.set(tcIndex, {
198
+ id,
199
+ name: tc.function?.name || ""
200
+ });
201
+ tc.id = id;
202
+ } else {
203
+ delete tc.id;
196
204
  }
197
205
  }
198
206
  }
@@ -237,10 +245,10 @@ function transformWorkersAiStream(source, model) {
237
245
  const toolCallDelta = {
238
246
  index: tcIndex
239
247
  };
240
- if (!emittedToolCallStart.has(tcIndex)) {
241
- emittedToolCallStart.add(tcIndex);
242
- const rawId = tcId || `call${streamId}${tcIndex}`;
243
- toolCallDelta.id = sanitizeToolCallId(rawId);
248
+ if (!toolCallState.has(tcIndex)) {
249
+ const id = tcId || `call${streamId}${tcIndex}`;
250
+ toolCallState.set(tcIndex, { id, name: tcName || "" });
251
+ toolCallDelta.id = id;
244
252
  toolCallDelta.type = "function";
245
253
  toolCallDelta.function = {
246
254
  name: tcName || "",
@@ -309,7 +317,8 @@ function transformWorkersAiStream(source, model) {
309
317
  export {
310
318
  isDirectBindingConfig,
311
319
  isDirectCredentialsConfig,
320
+ validateWorkersAiConfig,
312
321
  createGatewayFetch,
313
322
  createWorkersAiBindingFetch
314
323
  };
315
- //# sourceMappingURL=chunk-UUFEOQ6B.js.map
324
+ //# sourceMappingURL=chunk-727MRLUF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/create-fetcher.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// AI Gateway types (for third-party providers + Workers AI through gateway)\n// ---------------------------------------------------------------------------\n\nexport interface CloudflareAiGateway {\n\trun(request: unknown): Promise<Response>;\n}\n\nexport interface AiGatewayBindingConfig {\n\t/**\n\t * The AI Gateway binding\n\t * @example\n\t * env.AI.gateway('my-gateway-id')\n\t */\n\tbinding: CloudflareAiGateway;\n\t/**\n\t * The Provider API Key if you want to manually pass it, ignore if using Unified Billing or BYOK.\n\t */\n\tapiKey?: string;\n}\n\nexport type AiGatewayCredentialsConfig = {\n\t/**\n\t * The Cloudflare account ID\n\t */\n\taccountId: string;\n\t/**\n\t * The AI Gateway ID\n\t */\n\tgatewayId: string;\n} & (\n\t| {\n\t\t\t/** Cloudflare API Key for AI Gateway */\n\t\t\tcfApiKey: string;\n\t\t\tapiKey?: string;\n\t }\n\t| {\n\t\t\t/** Provider API Key */\n\t\t\tapiKey: string;\n\t\t\t/** Cloudflare API Key for AI Gateway */\n\t\t\tcfApiKey?: string;\n\t }\n);\n\nexport interface AiGatewayConfig {\n\tskipCache?: boolean;\n\tcacheTtl?: number;\n\tcustomCacheKey?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type AiGatewayAdapterConfig = (AiGatewayBindingConfig | AiGatewayCredentialsConfig) &\n\tAiGatewayConfig;\n\n// ---------------------------------------------------------------------------\n// Plain Workers AI types (direct binding or REST, no gateway)\n// ---------------------------------------------------------------------------\n\n/**\n * The Workers AI binding interface (env.AI).\n * Accepts a model name and inputs, returns results directly.\n * Includes `gateway()` which is present on `env.AI` but not on `env.AI.gateway(id)`,\n * enabling structural discrimination from `CloudflareAiGateway`.\n */\nexport interface WorkersAiBinding {\n\trun(\n\t\tmodel: string,\n\t\tinputs: Record<string, unknown>,\n\t\toptions?: Record<string, unknown>,\n\t): Promise<unknown>;\n\tgateway(gatewayId: string): CloudflareAiGateway;\n}\n\nexport interface WorkersAiDirectBindingConfig {\n\t/**\n\t * The Workers AI binding (env.AI).\n\t * @example\n\t * { binding: env.AI }\n\t */\n\tbinding: WorkersAiBinding;\n}\n\nexport interface WorkersAiDirectCredentialsConfig {\n\t/**\n\t * The Cloudflare account ID\n\t */\n\taccountId: string;\n\t/**\n\t * The Cloudflare API key for Workers AI\n\t */\n\tapiKey: string;\n}\n\n/**\n * Config for Workers AI adapters. Supports four modes:\n * - Plain binding: `{ binding: env.AI }`\n * - Plain REST: `{ accountId, apiKey }`\n * - AI Gateway binding: `{ binding: env.AI.gateway(id) }`\n * - AI Gateway REST: `{ accountId, gatewayId, ... }`\n *\n * The third union member intersects `AiGatewayAdapterConfig` with `{ apiKey?: string }`.\n * For the gateway binding variant, `AiGatewayBindingConfig` already includes `apiKey?`,\n * so the intersection is redundant there. For the gateway credentials variant, this\n * `apiKey` represents the Workers AI token (used in the `Authorization` header to the\n * upstream provider), distinct from `cfApiKey` (used in the `cf-aig-authorization`\n * header for authenticated gateways).\n */\nexport type WorkersAiAdapterConfig = (\n\t| WorkersAiDirectBindingConfig\n\t| WorkersAiDirectCredentialsConfig\n\t| (AiGatewayAdapterConfig & { apiKey?: string })\n) & {\n\t/**\n\t * Session affinity key for prefix-cache optimization.\n\t * Routes requests with the same key to the same backend replica.\n\t */\n\tsessionAffinity?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Config detection helpers\n// ---------------------------------------------------------------------------\n\n/** Returns true if this is a plain Workers AI binding config (`{ binding: env.AI }`) */\nexport function isDirectBindingConfig(\n\tconfig: WorkersAiAdapterConfig,\n): config is WorkersAiDirectBindingConfig {\n\t// env.AI has a .gateway() method; env.AI.gateway(id) does not.\n\t// This distinguishes direct bindings from AI Gateway bindings.\n\treturn (\n\t\t\"binding\" in config &&\n\t\ttypeof (config.binding as unknown as Record<string, unknown>).gateway === \"function\"\n\t);\n}\n\n/** Returns true if this is a plain Workers AI REST config (accountId + apiKey, no gatewayId) */\nexport function isDirectCredentialsConfig(\n\tconfig: WorkersAiAdapterConfig,\n): config is WorkersAiDirectCredentialsConfig {\n\treturn \"accountId\" in config && \"apiKey\" in config && !(\"gatewayId\" in config);\n}\n\n/** Returns true if this is an AI Gateway config (has gateway binding or `gatewayId`) */\nexport function isGatewayConfig(config: WorkersAiAdapterConfig): config is AiGatewayAdapterConfig {\n\tif (\"gatewayId\" in config) return true;\n\t// Has `binding` but NOT a direct Workers AI binding (no .gateway method)\n\treturn \"binding\" in config && !isDirectBindingConfig(config);\n}\n\n// ---------------------------------------------------------------------------\n// Config validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates that a WorkersAiAdapterConfig contains a valid configuration.\n * Throws an error if neither a binding, credentials (accountId + apiKey),\n * nor a gateway configuration is provided.\n */\nexport function validateWorkersAiConfig(config: WorkersAiAdapterConfig): void {\n\tif (\n\t\t!isDirectBindingConfig(config) &&\n\t\t!isDirectCredentialsConfig(config) &&\n\t\t!isGatewayConfig(config)\n\t) {\n\t\tthrow new Error(\n\t\t\t\"Invalid Workers AI configuration: you must provide either a binding (e.g. { binding: env.AI }), \" +\n\t\t\t\t\"credentials ({ accountId, apiKey }), or a gateway configuration ({ binding: env.AI.gateway(id) } \" +\n\t\t\t\t\"or { accountId, gatewayId }).\",\n\t\t);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// createGatewayFetch -- for routing through AI Gateway\n// ---------------------------------------------------------------------------\n\nexport function createGatewayFetch(\n\tprovider: string,\n\tconfig: AiGatewayAdapterConfig,\n\theaders: Record<string, string> = {},\n): typeof fetch {\n\treturn (input, init) => {\n\t\tlet query: Record<string, unknown> = {};\n\n\t\tconst url =\n\t\t\ttypeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\t\tconst urlObj = new URL(url);\n\n\t\t// Extract endpoint path (remove /v1/ prefix if present)\n\t\tconst endpoint = urlObj.pathname.replace(/^\\/v1\\//, \"\").replace(/^\\//, \"\") + urlObj.search;\n\n\t\tif (init?.body) {\n\t\t\ttry {\n\t\t\t\tquery = JSON.parse(init.body as string);\n\t\t\t} catch {\n\t\t\t\tquery = { _raw: init.body };\n\t\t\t}\n\t\t}\n\n\t\tconst cacheHeaders: Record<string, string> = {};\n\n\t\tif (\"skipCache\" in config && config.skipCache) {\n\t\t\tcacheHeaders[\"cf-aig-skip-cache\"] = \"true\";\n\t\t}\n\n\t\tif (typeof config.cacheTtl === \"number\") {\n\t\t\tcacheHeaders[\"cf-aig-cache-ttl\"] = String(config.cacheTtl);\n\t\t}\n\n\t\tif (typeof config.customCacheKey === \"string\") {\n\t\t\tcacheHeaders[\"cf-aig-cache-key\"] = config.customCacheKey;\n\t\t}\n\n\t\tif (typeof config.metadata === \"object\") {\n\t\t\tcacheHeaders[\"cf-aig-metadata\"] = JSON.stringify(config.metadata);\n\t\t}\n\n\t\tconst request: {\n\t\t\tprovider: string;\n\t\t\tendpoint: string;\n\t\t\theaders: Record<string, string>;\n\t\t\tquery: Record<string, unknown>;\n\t\t} = {\n\t\t\tprovider,\n\t\t\tendpoint,\n\t\t\theaders: {\n\t\t\t\t...(init?.headers as Record<string, string> | undefined),\n\t\t\t\t...headers,\n\t\t\t\t...cacheHeaders,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tquery,\n\t\t};\n\n\t\tif (provider === \"workers-ai\") {\n\t\t\tif (!request.endpoint.startsWith(\"run/\")) {\n\t\t\t\trequest.endpoint = `run/${query.model}`;\n\t\t\t}\n\t\t\tdelete query.model;\n\t\t\tdelete query.instructions;\n\t\t}\n\n\t\tif (config.apiKey) {\n\t\t\trequest.headers[\"authorization\"] = `Bearer ${config.apiKey}`;\n\t\t}\n\n\t\tif (\"binding\" in config) {\n\t\t\treturn config.binding.run(request);\n\t\t}\n\n\t\treturn fetch(\n\t\t\t`https://gateway.ai.cloudflare.com/v1/${config.accountId}/${config.gatewayId}`,\n\t\t\t{\n\t\t\t\t...init,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t...headers,\n\t\t\t\t\t...cacheHeaders,\n\t\t\t\t\t...(config.cfApiKey\n\t\t\t\t\t\t? { \"cf-aig-authorization\": `Bearer ${config.cfApiKey}` }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(request),\n\t\t\t},\n\t\t);\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// createWorkersAiBindingFetch -- shim that makes env.AI look like an OpenAI endpoint\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize messages before passing to Workers AI binding.\n *\n * The binding has strict schema validation that may differ from the OpenAI API:\n * - `content` must be a string (not null)\n */\nfunction normalizeMessagesForBinding(\n\tmessages: Record<string, unknown>[],\n): Record<string, unknown>[] {\n\treturn messages.map((msg) => {\n\t\tconst normalized = { ...msg };\n\n\t\t// content: null → content: \"\"\n\t\tif (normalized.content === null || normalized.content === undefined) {\n\t\t\tnormalized.content = \"\";\n\t\t}\n\n\t\treturn normalized;\n\t});\n}\n\n/**\n * Creates a fetch function that intercepts OpenAI SDK requests and translates them\n * to Workers AI binding calls (env.AI.run). This allows the WorkersAiTextAdapter\n * to use the OpenAI SDK against a plain Workers AI binding.\n *\n * NOTE: The `input` URL parameter is intentionally ignored. The model name and all\n * request parameters are extracted from the JSON body, matching Workers AI's\n * `binding.run(model, inputs)` calling convention.\n */\nexport function createWorkersAiBindingFetch(\n\tbinding: WorkersAiBinding,\n\toptions?: { extraHeaders?: Record<string, string> },\n): typeof fetch {\n\treturn async (_input, init) => {\n\t\tif (!init?.body) {\n\t\t\treturn new Response(\"No body\", { status: 400 });\n\t\t}\n\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = JSON.parse(init.body as string);\n\t\t} catch {\n\t\t\treturn new Response(\"Invalid JSON body\", { status: 400 });\n\t\t}\n\n\t\tconst model = body.model as string;\n\t\tconst stream = body.stream as boolean | undefined;\n\n\t\t// Build Workers AI inputs from OpenAI format\n\t\tconst inputs: Record<string, unknown> = {};\n\t\tif (body.messages) {\n\t\t\tinputs.messages = normalizeMessagesForBinding(\n\t\t\t\tbody.messages as Record<string, unknown>[],\n\t\t\t);\n\t\t}\n\t\tif (body.tools) inputs.tools = body.tools;\n\t\tif (typeof body.temperature === \"number\") inputs.temperature = body.temperature;\n\t\tif (typeof body.max_tokens === \"number\") inputs.max_tokens = body.max_tokens;\n\t\tif (body.response_format) inputs.response_format = body.response_format;\n\t\tif (stream) inputs.stream = true;\n\n\t\tconst result = await binding.run(\n\t\t\tmodel,\n\t\t\tinputs,\n\t\t\toptions?.extraHeaders ? { extraHeaders: options.extraHeaders } : undefined,\n\t\t);\n\n\t\tif (stream && result instanceof ReadableStream) {\n\t\t\t// Workers AI returns an SSE stream with `data: {\"response\":\"chunk\"}` format.\n\t\t\t// Transform it to OpenAI-compatible SSE format.\n\t\t\tconst transformed = transformWorkersAiStream(\n\t\t\t\tresult as ReadableStream<Uint8Array>,\n\t\t\t\tmodel,\n\t\t\t);\n\t\t\treturn new Response(transformed, {\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Graceful degradation: some models return a complete (non-streaming)\n\t\t// response even when `stream: true` is requested. Fall through to the\n\t\t// non-streaming wrapper which produces a valid OpenAI Chat Completion\n\t\t// response that the SDK can consume.\n\n\t\t// Non-streaming: Workers AI returns { response: \"text\", tool_calls?: [...] }\n\t\t// Wrap into OpenAI Chat Completion format.\n\t\tconst responseObj =\n\t\t\ttypeof result === \"object\" && result !== null\n\t\t\t\t? (result as Record<string, unknown>)\n\t\t\t\t: { response: String(result) };\n\n\t\tconst responseText = typeof responseObj.response === \"string\" ? responseObj.response : \"\";\n\n\t\tconst message: Record<string, unknown> = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: responseText,\n\t\t};\n\t\tlet finishReason = \"stop\";\n\n\t\t// Handle tool calls if present in Workers AI response\n\t\tif (Array.isArray(responseObj.tool_calls) && responseObj.tool_calls.length > 0) {\n\t\t\tfinishReason = \"tool_calls\";\n\t\t\tmessage.tool_calls = responseObj.tool_calls.map(\n\t\t\t\t(tc: {\n\t\t\t\t\tid?: string;\n\t\t\t\t\tname?: string;\n\t\t\t\t\targuments: unknown;\n\t\t\t\t\tfunction?: { name: string; arguments?: unknown };\n\t\t\t\t}) => ({\n\t\t\t\t\tid: tc.id || crypto.randomUUID(),\n\t\t\t\t\ttype: \"function\",\n\t\t\t\t\tfunction: {\n\t\t\t\t\t\tname: tc.function?.name || tc.name || \"\",\n\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\ttypeof (tc.function?.arguments ?? tc.arguments) === \"string\"\n\t\t\t\t\t\t\t\t? ((tc.function?.arguments ?? tc.arguments) as string)\n\t\t\t\t\t\t\t\t: JSON.stringify(tc.function?.arguments ?? tc.arguments ?? {}),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst openAiResponse = {\n\t\t\tid: `workers-ai-${crypto.randomUUID()}`,\n\t\t\tobject: \"chat.completion\",\n\t\t\tcreated: Math.floor(Date.now() / 1000),\n\t\t\tmodel,\n\t\t\tchoices: [{ index: 0, message, finish_reason: finishReason }],\n\t\t};\n\n\t\treturn new Response(JSON.stringify(openAiResponse), {\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t});\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Stream transformer: Workers AI SSE -> OpenAI-compatible SSE\n// Uses TransformStream for proper backpressure.\n// ---------------------------------------------------------------------------\n\n/**\n * Transforms a Workers AI SSE stream (data: {\"response\":\"chunk\"}) into\n * an OpenAI-compatible SSE stream (data: {\"choices\":[{\"delta\":{\"content\":\"chunk\"}}]}).\n *\n * Workers AI binding streams tool calls in an OpenAI-like nested format:\n * { tool_calls: [{ id, type, index, function: { name, arguments } }] }\n * Arguments are streamed incrementally across multiple SSE chunks, so the\n * transformer must forward them as incremental deltas rather than a single blob.\n */\nfunction transformWorkersAiStream(\n\tsource: ReadableStream<Uint8Array>,\n\tmodel: string,\n): ReadableStream<Uint8Array> {\n\tconst decoder = new TextDecoder();\n\tconst encoder = new TextEncoder();\n\t// Generate a stable ID and timestamp for the entire stream, matching OpenAI's\n\t// convention where all chunks in a single response share the same id/created.\n\tconst streamId = `workers-ai-${crypto.randomUUID()}`;\n\tconst created = Math.floor(Date.now() / 1000);\n\tlet buffer = \"\";\n\tlet hasToolCalls = false;\n\t// When true, the source stream is already in OpenAI format (some models\n\t// like Qwen3, Kimi K2.5 stream OpenAI-compatible SSE through the binding).\n\t// In that case, flush() should only emit [DONE] and skip the finish chunk.\n\tlet isOpenAiFormat = false;\n\t// Track tool call state per index: store the generated/assigned ID so that\n\t// subsequent argument deltas use the same ID (matching the working streaming.ts pattern).\n\tconst toolCallState = new Map<number, { id: string; name: string }>();\n\n\treturn source.pipeThrough(\n\t\tnew TransformStream<Uint8Array, Uint8Array>({\n\t\t\ttransform(chunk, controller) {\n\t\t\t\tbuffer += decoder.decode(chunk, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tconst trimmed = line.trim();\n\t\t\t\t\tif (!trimmed || !trimmed.startsWith(\"data: \")) continue;\n\t\t\t\t\tconst data = trimmed.slice(6);\n\n\t\t\t\t\t// Swallow source [DONE]; we emit our own in flush()\n\t\t\t\t\tif (data === \"[DONE]\") continue;\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\n\t\t\t\t\t\t// Some models (Qwen3, Kimi K2.5) return OpenAI-compatible format\n\t\t\t\t\t\t// directly through the binding, with `choices[].delta.content` and\n\t\t\t\t\t\t// optional `reasoning_content`. Detect this and pass through as-is.\n\t\t\t\t\t\tif (parsed.choices !== undefined) {\n\t\t\t\t\t\t\t// Already OpenAI format — pass through but ensure each tool call\n\t\t\t\t\t\t\t// index gets a unique, stable ID across all chunks.\n\t\t\t\t\t\t\tisOpenAiFormat = true;\n\t\t\t\t\t\t\tconst choice = parsed.choices?.[0];\n\t\t\t\t\t\t\tif (choice?.delta?.tool_calls) {\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t\t\tfor (const tc of choice.delta.tool_calls) {\n\t\t\t\t\t\t\t\t\tconst tcIndex = tc.index ?? 0;\n\t\t\t\t\t\t\t\t\tif (!toolCallState.has(tcIndex)) {\n\t\t\t\t\t\t\t\t\t\t// First chunk for this index — generate/store unique ID\n\t\t\t\t\t\t\t\t\t\tconst id = tc.id || `call${streamId}${tcIndex}`;\n\t\t\t\t\t\t\t\t\t\ttoolCallState.set(tcIndex, {\n\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\tname: tc.function?.name || \"\",\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\ttc.id = id;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Subsequent chunk — reuse stored ID, remove id from delta\n\t\t\t\t\t\t\t\t\t\t// (OpenAI format only sends id in first chunk)\n\t\t\t\t\t\t\t\t\t\tdelete tc.id;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (choice?.finish_reason === \"tool_calls\") {\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(parsed)}\\n\\n`),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// --- Workers AI native format handling below ---\n\n\t\t\t\t\t\t// Text content\n\t\t\t\t\t\tif (parsed.response != null && parsed.response !== \"\") {\n\t\t\t\t\t\t\tconst openAiChunk = {\n\t\t\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\t\t\tcreated,\n\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\t\t\tdelta: { content: parsed.response },\n\t\t\t\t\t\t\t\t\t\tfinish_reason: null,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(openAiChunk)}\\n\\n`),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Tool calls — Workers AI binding streams these incrementally:\n\t\t\t\t\t\t// Chunk A: { id, type, index, function: { name } } — start\n\t\t\t\t\t\t// Chunk B: { index, function: { arguments: \"partial...\" } } — args delta\n\t\t\t\t\t\t// Chunk C: { index, function: { arguments: \"rest...\" } } — args delta\n\t\t\t\t\t\t// Chunk D: { id: null, type: null, index, function: { name: null, arguments: \"\" } } — finalize (skip)\n\t\t\t\t\t\tif (Array.isArray(parsed.tool_calls) && parsed.tool_calls.length > 0) {\n\t\t\t\t\t\t\tfor (const tc of parsed.tool_calls) {\n\t\t\t\t\t\t\t\tconst tcIndex = tc.index ?? 0;\n\n\t\t\t\t\t\t\t\t// Resolve name and arguments from either nested or flat format\n\t\t\t\t\t\t\t\tconst tcName = tc.function?.name ?? tc.name ?? null;\n\t\t\t\t\t\t\t\tconst tcArgs = tc.function?.arguments ?? tc.arguments ?? null;\n\t\t\t\t\t\t\t\tconst tcId = tc.id ?? null;\n\n\t\t\t\t\t\t\t\t// Skip finalization chunks where everything is null/empty\n\t\t\t\t\t\t\t\tif (!tcId && !tcName && (!tcArgs || tcArgs === \"\")) continue;\n\n\t\t\t\t\t\t\t\thasToolCalls = true;\n\n\t\t\t\t\t\t\t\t// Build the OpenAI-compatible tool_calls delta\n\t\t\t\t\t\t\t\tconst toolCallDelta: Record<string, unknown> = {\n\t\t\t\t\t\t\t\t\tindex: tcIndex,\n\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\tif (!toolCallState.has(tcIndex)) {\n\t\t\t\t\t\t\t\t\t// First chunk for this tool call index — emit id, type, name.\n\t\t\t\t\t\t\t\t\tconst id = tcId || `call${streamId}${tcIndex}`;\n\t\t\t\t\t\t\t\t\ttoolCallState.set(tcIndex, { id, name: tcName || \"\" });\n\t\t\t\t\t\t\t\t\ttoolCallDelta.id = id;\n\t\t\t\t\t\t\t\t\ttoolCallDelta.type = \"function\";\n\t\t\t\t\t\t\t\t\ttoolCallDelta.function = {\n\t\t\t\t\t\t\t\t\t\tname: tcName || \"\",\n\t\t\t\t\t\t\t\t\t\t// Include arguments if they arrive in the same chunk\n\t\t\t\t\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\t\t\t\t\ttcArgs != null\n\t\t\t\t\t\t\t\t\t\t\t\t? typeof tcArgs === \"string\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t? tcArgs\n\t\t\t\t\t\t\t\t\t\t\t\t\t: JSON.stringify(tcArgs)\n\t\t\t\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Subsequent chunks — only include arguments delta\n\t\t\t\t\t\t\t\t\tif (tcArgs != null && tcArgs !== \"\") {\n\t\t\t\t\t\t\t\t\t\ttoolCallDelta.function = {\n\t\t\t\t\t\t\t\t\t\t\targuments:\n\t\t\t\t\t\t\t\t\t\t\t\ttypeof tcArgs === \"string\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t? tcArgs\n\t\t\t\t\t\t\t\t\t\t\t\t\t: JSON.stringify(tcArgs),\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tcontinue; // Nothing useful to forward\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tconst toolChunk = {\n\t\t\t\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\t\t\t\tcreated,\n\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\t\t\t\tdelta: { tool_calls: [toolCallDelta] },\n\t\t\t\t\t\t\t\t\t\t\tfinish_reason: null,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tcontroller.enqueue(\n\t\t\t\t\t\t\t\t\tencoder.encode(`data: ${JSON.stringify(toolChunk)}\\n\\n`),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t// Log malformed SSE events for debugging; don't break the stream.\n\t\t\t\t\t\tconsole.warn(\"[tanstack-ai] failed to parse SSE event:\", data, e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tflush(controller) {\n\t\t\t\tif (!isOpenAiFormat) {\n\t\t\t\t\t// Workers AI native format: emit a finish chunk with stop/tool_calls\n\t\t\t\t\tconst finalChunk = {\n\t\t\t\t\t\tid: streamId,\n\t\t\t\t\t\tobject: \"chat.completion.chunk\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tindex: 0,\n\t\t\t\t\t\t\t\tdelta: {},\n\t\t\t\t\t\t\t\tfinish_reason: hasToolCalls ? \"tool_calls\" : \"stop\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t};\n\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\\n\\n`));\n\t\t\t\t}\n\t\t\t\t// OpenAI format already includes its own finish_reason in the stream.\n\t\t\t\t// Either way, emit a [DONE] sentinel.\n\t\t\t\tcontroller.enqueue(encoder.encode(\"data: [DONE]\\n\\n\"));\n\t\t\t},\n\t\t}),\n\t);\n}\n"],"mappings":"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","names":[]}