@amjadiqbal/tanstack-ai-sdk-bridge 0.0.0-stage → 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,1111 @@
1
+ import { EventType, isFileSource, normalizeSystemPrompts, requireTextOnlyEmbeddingInput, unsupportedFileSourceError } from "@tanstack/ai";
2
+ import { tanstackMetadata, toRunErrorPayload, transformMessagesForReplay } from "@tanstack/ai/adapter-internals";
3
+ import { BaseEmbeddingAdapter, BaseImageAdapter, BaseRerankAdapter, BaseTTSAdapter, BaseTextAdapter, BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
4
+ // src/shared.ts
5
+ function specVersionOf(model, kind, supported) {
6
+ const version = model.specificationVersion;
7
+ if (!supported.includes(version)) throw new Error(`@amjadiqbal/tanstack-ai-sdk-bridge: ${kind} model "${model.provider}/${model.modelId}" uses AI SDK spec "${model.specificationVersion}", supported: ${supported.join(", ")}.`);
8
+ return version;
9
+ }
10
+ function adapterName(model, options) {
11
+ return options?.name ?? (model.provider.split(".")[0] || model.provider);
12
+ }
13
+ function randomId(prefix) {
14
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
15
+ }
16
+ function tokenUsage(input, output, total) {
17
+ const promptTokens = input ?? 0;
18
+ const completionTokens = output ?? 0;
19
+ return {
20
+ promptTokens,
21
+ completionTokens,
22
+ totalTokens: total ?? promptTokens + completionTokens
23
+ };
24
+ }
25
+ function bytesToBase64(bytes) {
26
+ let binary = "";
27
+ const chunk = 32768;
28
+ for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
29
+ return btoa(binary);
30
+ }
31
+ function base64ToBytes(base64) {
32
+ const binary = atob(base64);
33
+ const bytes = new Uint8Array(binary.length);
34
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
35
+ return bytes;
36
+ }
37
+ /** Splits `data:<type>;base64,<data>` into its parts. Returns undefined for anything else. */
38
+ function parseDataUrl(value) {
39
+ const match = /^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/s.exec(value);
40
+ if (!match) return void 0;
41
+ return {
42
+ mediaType: match[1] ?? "application/octet-stream",
43
+ base64: match[2] ?? ""
44
+ };
45
+ }
46
+ /** Pass-through call fields, without keys whose value is undefined. */
47
+ function callExtras(options, abortSignal) {
48
+ return {
49
+ ...options?.providerOptions !== void 0 && { providerOptions: options.providerOptions },
50
+ ...options?.headers !== void 0 && { headers: definedHeaders(options.headers) },
51
+ ...abortSignal !== void 0 && { abortSignal }
52
+ };
53
+ }
54
+ function definedHeaders(headers) {
55
+ return Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== void 0));
56
+ }
57
+ // src/embedding.ts
58
+ const SPECS$5 = [
59
+ "v2",
60
+ "v3",
61
+ "v4"
62
+ ];
63
+ /** TanStack AI embedding adapter around an AI SDK embedding model. Splits large inputs by `maxEmbeddingsPerCall`. */
64
+ var AiSdkEmbeddingAdapter = class extends BaseEmbeddingAdapter {
65
+ name;
66
+ embeddingModel;
67
+ specificationVersion;
68
+ constructor(model, options) {
69
+ super(model.modelId, {});
70
+ this.specificationVersion = specVersionOf(model, "embedding", SPECS$5);
71
+ this.embeddingModel = model;
72
+ this.name = adapterName(model, options);
73
+ }
74
+ async createEmbeddings(options) {
75
+ const { model, logger, modelOptions, dimensions } = options;
76
+ const values = requireTextOnlyEmbeddingInput(options.input, this.name, model);
77
+ if (dimensions !== void 0 && this.specificationVersion !== "v4") throw new Error(`${this.name}: \`dimensions\` needs an AI SDK v4 embedding model; set it through modelOptions.providerOptions instead.`);
78
+ try {
79
+ logger.request(`activity=embed provider=${this.name} model=${model} inputs=${values.length}`, {
80
+ provider: this.name,
81
+ model
82
+ });
83
+ const max = await this.embeddingModel.maxEmbeddingsPerCall ?? values.length;
84
+ const size = Math.max(1, Math.min(max, values.length || 1));
85
+ const vectors = [];
86
+ let tokens;
87
+ for (let start = 0; start < values.length; start += size) {
88
+ const batch = values.slice(start, start + size);
89
+ const result = await this.embeddingModel.doEmbed({
90
+ values: batch,
91
+ ...dimensions !== void 0 && { dimensions },
92
+ ...callExtras(modelOptions, void 0)
93
+ });
94
+ vectors.push(...result.embeddings);
95
+ if (result.usage) tokens = (tokens ?? 0) + result.usage.tokens;
96
+ }
97
+ return {
98
+ id: randomId(this.name),
99
+ model,
100
+ embeddings: vectors.map((vector, index) => ({
101
+ vector,
102
+ index
103
+ })),
104
+ ...tokens !== void 0 && { usage: tokenUsage(tokens, 0) }
105
+ };
106
+ } catch (error) {
107
+ logger.errors(`${this.name}.createEmbeddings fatal`, {
108
+ error: toRunErrorPayload(error, `${this.name}.createEmbeddings failed`),
109
+ source: `${this.name}.createEmbeddings`
110
+ });
111
+ throw error;
112
+ }
113
+ }
114
+ };
115
+ /**
116
+ * Wraps an AI SDK embedding model as a TanStack AI embedding adapter.
117
+ *
118
+ * @example
119
+ * import { embed } from '@tanstack/ai'
120
+ * import { openai } from '@ai-sdk/openai'
121
+ * await embed({ adapter: aiSdkEmbedding(openai.embedding('text-embedding-3-small')), input: ['hello'] })
122
+ */
123
+ function aiSdkEmbedding(model, options) {
124
+ return new AiSdkEmbeddingAdapter(model, options);
125
+ }
126
+ // src/image.ts
127
+ const SPECS$4 = [
128
+ "v2",
129
+ "v3",
130
+ "v4"
131
+ ];
132
+ /**
133
+ * TanStack AI image adapter around an AI SDK image model.
134
+ * Text parts of the prompt become the prompt; image parts become input files (AI SDK v3/v4 models).
135
+ */
136
+ var AiSdkImageAdapter = class extends BaseImageAdapter {
137
+ name;
138
+ imageModel;
139
+ specificationVersion;
140
+ constructor(model, options) {
141
+ super(model.modelId, {});
142
+ this.specificationVersion = specVersionOf(model, "image", SPECS$4);
143
+ this.imageModel = model;
144
+ this.name = adapterName(model, options);
145
+ }
146
+ promptOf(prompt) {
147
+ if (typeof prompt === "string") return {
148
+ text: prompt,
149
+ files: []
150
+ };
151
+ const texts = [];
152
+ const files = [];
153
+ for (const part of prompt) {
154
+ if (part.type === "text") {
155
+ texts.push(part.content);
156
+ continue;
157
+ }
158
+ if (part.type !== "image") throw new Error(`${this.name}: image models take text and image prompt parts only`);
159
+ if (isFileSource(part.source)) throw unsupportedFileSourceError(this.name);
160
+ files.push(part.source.type === "url" ? {
161
+ type: "url",
162
+ url: part.source.value
163
+ } : {
164
+ type: "file",
165
+ mediaType: part.source.mimeType,
166
+ data: part.source.value
167
+ });
168
+ }
169
+ if (files.length > 0 && this.specificationVersion === "v2") throw new Error(`${this.name}: image inputs need an AI SDK v3 or v4 image model`);
170
+ return {
171
+ text: texts.join("\n"),
172
+ files
173
+ };
174
+ }
175
+ async generateImages(options) {
176
+ const { model, logger, modelOptions, abortSignal, size } = options;
177
+ const wanted = options.numberOfImages ?? 1;
178
+ try {
179
+ const { text, files } = this.promptOf(options.prompt);
180
+ const maxSetting = this.imageModel.maxImagesPerCall;
181
+ const max = (typeof maxSetting === "function" ? await maxSetting({ modelId: this.imageModel.modelId }) : maxSetting) ?? 1;
182
+ logger.request(`activity=image provider=${this.name} model=${model} n=${wanted} size=${size ?? "default"}`, {
183
+ provider: this.name,
184
+ model
185
+ });
186
+ const images = [];
187
+ let input = 0;
188
+ let output = 0;
189
+ let sawUsage = false;
190
+ while (images.length < wanted) {
191
+ const n = Math.min(Math.max(1, max), wanted - images.length);
192
+ const result = await this.imageModel.doGenerate({
193
+ prompt: text,
194
+ n,
195
+ size,
196
+ aspectRatio: modelOptions?.aspectRatio,
197
+ seed: modelOptions?.seed,
198
+ files: files.length > 0 ? files : void 0,
199
+ mask: void 0,
200
+ providerOptions: modelOptions?.providerOptions ?? {},
201
+ ...modelOptions?.headers && { headers: modelOptions.headers },
202
+ ...abortSignal && { abortSignal }
203
+ });
204
+ if (result.images.length === 0) break;
205
+ for (const image of result.images) images.push(typeof image === "string" ? image : bytesToBase64(image));
206
+ if (result.usage) {
207
+ sawUsage = true;
208
+ input += result.usage.inputTokens ?? 0;
209
+ output += result.usage.outputTokens ?? 0;
210
+ }
211
+ }
212
+ return {
213
+ id: randomId(this.name),
214
+ model,
215
+ images: images.slice(0, wanted).map((b64Json) => ({ b64Json })),
216
+ ...sawUsage && { usage: tokenUsage(input, output) }
217
+ };
218
+ } catch (error) {
219
+ logger.errors(`${this.name}.generateImages fatal`, {
220
+ error: toRunErrorPayload(error, `${this.name}.generateImages failed`),
221
+ source: `${this.name}.generateImages`
222
+ });
223
+ throw error;
224
+ }
225
+ }
226
+ };
227
+ /**
228
+ * Wraps an AI SDK image model as a TanStack AI image adapter.
229
+ *
230
+ * @example
231
+ * import { generateImage } from '@tanstack/ai'
232
+ * import { openai } from '@ai-sdk/openai'
233
+ * await generateImage({ threadId, adapter: aiSdkImage(openai.image('gpt-image-1')), prompt: 'a lighthouse' })
234
+ */
235
+ function aiSdkImage(model, options) {
236
+ return new AiSdkImageAdapter(model, options);
237
+ }
238
+ // src/rerank.ts
239
+ const SPECS$3 = ["v3", "v4"];
240
+ /** TanStack AI rerank adapter around an AI SDK reranking model. AI SDK reports no token usage for reranking. */
241
+ var AiSdkRerankAdapter = class extends BaseRerankAdapter {
242
+ name;
243
+ rerankingModel;
244
+ specificationVersion;
245
+ constructor(model, options) {
246
+ super({}, model.modelId);
247
+ this.specificationVersion = specVersionOf(model, "reranking", SPECS$3);
248
+ this.rerankingModel = model;
249
+ this.name = adapterName(model, options);
250
+ }
251
+ async rerank(options) {
252
+ const { model, logger, modelOptions, abortSignal, query, documents, topN } = options;
253
+ try {
254
+ logger.request(`activity=rerank provider=${this.name} model=${model} documents=${documents.length}`, {
255
+ provider: this.name,
256
+ model
257
+ });
258
+ const result = await this.rerankingModel.doRerank({
259
+ query,
260
+ documents: {
261
+ type: "text",
262
+ values: documents
263
+ },
264
+ ...topN !== void 0 && { topN },
265
+ ...callExtras(modelOptions, abortSignal)
266
+ });
267
+ return {
268
+ id: result.response?.id ?? randomId(this.name),
269
+ ranking: result.ranking.map(({ index, relevanceScore }) => ({
270
+ index,
271
+ score: relevanceScore
272
+ })),
273
+ usage: tokenUsage(0, 0)
274
+ };
275
+ } catch (error) {
276
+ logger.errors(`${this.name}.rerank fatal`, {
277
+ error: toRunErrorPayload(error, `${this.name}.rerank failed`),
278
+ source: `${this.name}.rerank`
279
+ });
280
+ throw error;
281
+ }
282
+ }
283
+ };
284
+ /**
285
+ * Wraps an AI SDK reranking model as a TanStack AI rerank adapter.
286
+ *
287
+ * @example
288
+ * import { rerank } from '@tanstack/ai'
289
+ * import { cohere } from '@ai-sdk/cohere'
290
+ * await rerank({ adapter: aiSdkRerank(cohere.reranking('rerank-v3.5')), query, documents })
291
+ */
292
+ function aiSdkRerank(model, options) {
293
+ return new AiSdkRerankAdapter(model, options);
294
+ }
295
+ // src/speech.ts
296
+ const SPECS$2 = [
297
+ "v2",
298
+ "v3",
299
+ "v4"
300
+ ];
301
+ const CONTENT_TYPES = {
302
+ mp3: "audio/mpeg",
303
+ opus: "audio/opus",
304
+ aac: "audio/aac",
305
+ flac: "audio/flac",
306
+ wav: "audio/wav",
307
+ pcm: "audio/pcm"
308
+ };
309
+ /** TanStack AI text-to-speech adapter around an AI SDK speech model. */
310
+ var AiSdkSpeechAdapter = class extends BaseTTSAdapter {
311
+ name;
312
+ speechModel;
313
+ specificationVersion;
314
+ constructor(model, options) {
315
+ super(model.modelId, {});
316
+ this.specificationVersion = specVersionOf(model, "speech", SPECS$2);
317
+ this.speechModel = model;
318
+ this.name = adapterName(model, options);
319
+ }
320
+ async generateSpeech(options) {
321
+ const { model, logger, modelOptions, abortSignal, text, voice, format, speed } = options;
322
+ try {
323
+ logger.request(`activity=tts provider=${this.name} model=${model} format=${format ?? "default"} voice=${voice ?? "default"}`, {
324
+ provider: this.name,
325
+ model
326
+ });
327
+ const result = await this.speechModel.doGenerate({
328
+ text,
329
+ ...voice !== void 0 && { voice },
330
+ ...format !== void 0 && { outputFormat: format },
331
+ ...speed !== void 0 && { speed },
332
+ ...modelOptions?.instructions !== void 0 && { instructions: modelOptions.instructions },
333
+ ...modelOptions?.language !== void 0 && { language: modelOptions.language },
334
+ ...callExtras(modelOptions, abortSignal)
335
+ });
336
+ const audio = typeof result.audio === "string" ? result.audio : bytesToBase64(result.audio);
337
+ const outputFormat = format ?? "mp3";
338
+ const headerType = (result.response?.headers)?.["content-type"];
339
+ return {
340
+ id: randomId(this.name),
341
+ model: result.response?.modelId ?? model,
342
+ audio,
343
+ format: outputFormat,
344
+ contentType: headerType?.split(";")[0] || CONTENT_TYPES[outputFormat] || "application/octet-stream"
345
+ };
346
+ } catch (error) {
347
+ logger.errors(`${this.name}.generateSpeech fatal`, {
348
+ error: toRunErrorPayload(error, `${this.name}.generateSpeech failed`),
349
+ source: `${this.name}.generateSpeech`
350
+ });
351
+ throw error;
352
+ }
353
+ }
354
+ };
355
+ /**
356
+ * Wraps an AI SDK speech model as a TanStack AI text-to-speech adapter.
357
+ *
358
+ * @example
359
+ * import { generateSpeech } from '@tanstack/ai'
360
+ * import { openai } from '@ai-sdk/openai'
361
+ * await generateSpeech({ threadId, adapter: aiSdkSpeech(openai.speech('gpt-4o-mini-tts')), text: 'Hello', voice: 'alloy' })
362
+ */
363
+ function aiSdkSpeech(model, options) {
364
+ return new AiSdkSpeechAdapter(model, options);
365
+ }
366
+ // src/messages.ts
367
+ const DEFAULT_MEDIA_TYPE = {
368
+ image: "image/*",
369
+ audio: "audio/*",
370
+ video: "video/*",
371
+ document: "application/pdf"
372
+ };
373
+ function filePart(part, adapterName) {
374
+ const { source } = part;
375
+ if (isFileSource(source)) throw unsupportedFileSourceError(adapterName);
376
+ const mediaType = source.mimeType ?? DEFAULT_MEDIA_TYPE[part.type] ?? "application/octet-stream";
377
+ if (source.type === "url") return {
378
+ type: "file",
379
+ mediaType,
380
+ data: {
381
+ type: "url",
382
+ url: new URL(source.value)
383
+ }
384
+ };
385
+ return {
386
+ type: "file",
387
+ mediaType,
388
+ data: {
389
+ type: "data",
390
+ data: source.value
391
+ }
392
+ };
393
+ }
394
+ function textOf(content) {
395
+ if (typeof content === "string") return content;
396
+ if (!content) return "";
397
+ return content.filter((part) => part.type === "text").map((part) => part.content).join("");
398
+ }
399
+ function parseJson(value) {
400
+ if (value.trim() === "") return {};
401
+ try {
402
+ return JSON.parse(value);
403
+ } catch {
404
+ return value;
405
+ }
406
+ }
407
+ function toolOutput(message) {
408
+ if (message.error !== void 0) return {
409
+ type: "error-text",
410
+ value: message.error || textOf(message.content)
411
+ };
412
+ return {
413
+ type: "text",
414
+ value: textOf(message.content)
415
+ };
416
+ }
417
+ /**
418
+ * Converts TanStack AI system prompts and messages into an AI SDK V4 prompt.
419
+ * `toPromptForSpec` then adapts file parts for V2/V3 models.
420
+ */
421
+ function toAiSdkPrompt(options, adapterName) {
422
+ const prompt = [];
423
+ for (const system of normalizeSystemPrompts(options.systemPrompts)) if (system.content) prompt.push({
424
+ role: "system",
425
+ content: system.content
426
+ });
427
+ const toolNames = /* @__PURE__ */ new Map();
428
+ for (const message of options.messages) for (const call of message.toolCalls ?? []) toolNames.set(call.id, call.function.name);
429
+ for (const message of options.messages) {
430
+ if (message.role === "user") {
431
+ const content = [];
432
+ if (typeof message.content === "string") content.push({
433
+ type: "text",
434
+ text: message.content
435
+ });
436
+ else for (const part of message.content ?? []) content.push(part.type === "text" ? {
437
+ type: "text",
438
+ text: part.content
439
+ } : filePart(part, adapterName));
440
+ if (content.length > 0) prompt.push({
441
+ role: "user",
442
+ content
443
+ });
444
+ continue;
445
+ }
446
+ if (message.role === "assistant") {
447
+ const content = [];
448
+ for (const thinking of message.thinking ?? []) if (thinking.content && !thinking.redacted) content.push({
449
+ type: "reasoning",
450
+ text: thinking.content
451
+ });
452
+ const text = textOf(message.content);
453
+ if (text) content.push({
454
+ type: "text",
455
+ text
456
+ });
457
+ for (const call of message.toolCalls ?? []) content.push({
458
+ type: "tool-call",
459
+ toolCallId: call.id,
460
+ toolName: call.function.name,
461
+ input: parseJson(call.function.arguments)
462
+ });
463
+ if (content.length > 0) prompt.push({
464
+ role: "assistant",
465
+ content
466
+ });
467
+ continue;
468
+ }
469
+ const toolCallId = message.toolCallId ?? "";
470
+ const part = {
471
+ type: "tool-result",
472
+ toolCallId,
473
+ toolName: toolNames.get(toolCallId) ?? message.name ?? "unknown",
474
+ output: toolOutput(message)
475
+ };
476
+ const last = prompt[prompt.length - 1];
477
+ if (last?.role === "tool") last.content.push(part);
478
+ else prompt.push({
479
+ role: "tool",
480
+ content: [part]
481
+ });
482
+ }
483
+ return prompt;
484
+ }
485
+ /**
486
+ * V2/V3 file parts carry raw data (base64 string, bytes or URL) instead of V4's `{ type, ... }` wrapper,
487
+ * and V2's tool result `content` output differs. Everything else in the prompt is the same shape.
488
+ */
489
+ function toPromptForSpec(prompt, version) {
490
+ if (version === "v4") return prompt;
491
+ return prompt.map((message) => {
492
+ if (message.role !== "user" && message.role !== "assistant") return message;
493
+ return {
494
+ ...message,
495
+ content: message.content.map((part) => {
496
+ if (part.type !== "file") return part;
497
+ const data = part.data;
498
+ const raw = data.type === "url" ? data.url : data.type === "data" ? data.data : void 0;
499
+ if (raw === void 0) throw new Error(`File data of type "${data.type}" needs an AI SDK v4 model.`);
500
+ return {
501
+ ...part,
502
+ data: raw
503
+ };
504
+ })
505
+ };
506
+ });
507
+ }
508
+ function toAiSdkTools(tools) {
509
+ if (!tools || tools.length === 0) return void 0;
510
+ return tools.map((tool) => ({
511
+ type: "function",
512
+ name: tool.name,
513
+ description: tool.description,
514
+ inputSchema: tool.inputSchema ?? {
515
+ type: "object",
516
+ properties: {}
517
+ }
518
+ }));
519
+ }
520
+ function toAiSdkToolChoice(choice) {
521
+ if (choice === void 0) return void 0;
522
+ if (typeof choice === "string") return { type: choice };
523
+ return {
524
+ type: "tool",
525
+ toolName: choice.name
526
+ };
527
+ }
528
+ // src/usage.ts
529
+ /** Maps AI SDK language-model usage (any spec version) to TanStack AI `TokenUsage`. */
530
+ function languageModelUsage(usage) {
531
+ if (!usage) return void 0;
532
+ let input;
533
+ let output;
534
+ let total;
535
+ let cached;
536
+ let cacheWrite;
537
+ let reasoning;
538
+ if (typeof usage.inputTokens === "object" && usage.inputTokens !== null) {
539
+ const v3 = usage;
540
+ input = v3.inputTokens.total;
541
+ output = v3.outputTokens.total;
542
+ cached = v3.inputTokens.cacheRead;
543
+ cacheWrite = v3.inputTokens.cacheWrite;
544
+ reasoning = v3.outputTokens.reasoning;
545
+ } else {
546
+ const v2 = usage;
547
+ input = v2.inputTokens;
548
+ output = v2.outputTokens;
549
+ total = v2.totalTokens;
550
+ cached = v2.cachedInputTokens;
551
+ reasoning = v2.reasoningTokens;
552
+ }
553
+ if (input === void 0 && output === void 0 && total === void 0) return void 0;
554
+ const promptTokens = input ?? 0;
555
+ const completionTokens = output ?? 0;
556
+ const result = {
557
+ promptTokens,
558
+ completionTokens,
559
+ totalTokens: total ?? promptTokens + completionTokens
560
+ };
561
+ if (cached !== void 0 || cacheWrite !== void 0) result.promptTokensDetails = {
562
+ ...cached !== void 0 && { cachedTokens: cached },
563
+ ...cacheWrite !== void 0 && { cacheWriteTokens: cacheWrite }
564
+ };
565
+ if (reasoning !== void 0) result.completionTokensDetails = { reasoningTokens: reasoning };
566
+ return result;
567
+ }
568
+ // src/text.ts
569
+ const SPECS$1 = [
570
+ "v2",
571
+ "v3",
572
+ "v4"
573
+ ];
574
+ const REASONING = {
575
+ off: "none",
576
+ minimal: "minimal",
577
+ low: "low",
578
+ medium: "medium",
579
+ high: "high",
580
+ xhigh: "xhigh",
581
+ max: "max"
582
+ };
583
+ function finishReason(reason, sawToolCalls) {
584
+ if (sawToolCalls) return "tool_calls";
585
+ switch (typeof reason === "object" && reason !== null ? reason.unified : reason) {
586
+ case "length": return "length";
587
+ case "content-filter": return "content_filter";
588
+ case "tool-calls": return "tool_calls";
589
+ default: return "stop";
590
+ }
591
+ }
592
+ function parseObject(json) {
593
+ try {
594
+ const value = JSON.parse(json || "{}");
595
+ return value && typeof value === "object" ? value : void 0;
596
+ } catch {
597
+ return;
598
+ }
599
+ }
600
+ /** Parses a model's JSON answer, also when the model wrapped it in a Markdown code fence. */
601
+ function parseJsonAnswer(text) {
602
+ try {
603
+ return JSON.parse(text);
604
+ } catch (error) {
605
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
606
+ if (fenced?.[1]) return JSON.parse(fenced[1]);
607
+ throw error;
608
+ }
609
+ }
610
+ /** Replay-safe tool call ids: letters, digits, `_` and `-`, at most 64 characters. */
611
+ function cleanToolCallId(id, { attempt }) {
612
+ const clean = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
613
+ return attempt === 0 ? clean : `${clean.slice(0, 50)}_${attempt}`;
614
+ }
615
+ /**
616
+ * TanStack AI text adapter around an AI SDK language model.
617
+ * Streams text, reasoning and tool calls as AG-UI events and maps usage and finish reasons.
618
+ */
619
+ var AiSdkTextAdapter = class extends BaseTextAdapter {
620
+ name;
621
+ api = "ai-sdk";
622
+ provider;
623
+ inputModalities = [
624
+ "text",
625
+ "image",
626
+ "audio",
627
+ "video",
628
+ "document"
629
+ ];
630
+ /** The wrapped AI SDK model. */
631
+ languageModel;
632
+ /** AI SDK spec version of the wrapped model. */
633
+ specificationVersion;
634
+ constructor(model, options) {
635
+ super({}, model.modelId);
636
+ this.specificationVersion = specVersionOf(model, "language", SPECS$1);
637
+ this.languageModel = model;
638
+ this.name = adapterName(model, options);
639
+ this.provider = this.name;
640
+ }
641
+ callOptions(options) {
642
+ const { messages } = transformMessagesForReplay(options.messages, {
643
+ provider: this.name,
644
+ api: this.api,
645
+ model: this.model
646
+ }, cleanToolCallId);
647
+ const prompt = toAiSdkPrompt({
648
+ messages,
649
+ systemPrompts: options.systemPrompts
650
+ }, this.name);
651
+ const { providerOptions, headers, ...sampling } = options.modelOptions ?? {};
652
+ const tools = toAiSdkTools(options.tools);
653
+ const toolChoice = tools ? toAiSdkToolChoice(options.toolChoice) : void 0;
654
+ const abortSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
655
+ const call = {
656
+ prompt: toPromptForSpec(prompt, this.specificationVersion),
657
+ ...sampling,
658
+ ...tools && { tools },
659
+ ...toolChoice && { toolChoice },
660
+ ...abortSignal && { abortSignal },
661
+ ...headers && { headers },
662
+ ...providerOptions && { providerOptions }
663
+ };
664
+ if (options.reasoning && this.specificationVersion === "v4") call.reasoning = REASONING[options.reasoning.level];
665
+ return call;
666
+ }
667
+ async *chatStream(options) {
668
+ const { logger } = options;
669
+ const source = {
670
+ provider: this.name,
671
+ api: this.api,
672
+ model: options.model
673
+ };
674
+ try {
675
+ const call = this.callOptions(options);
676
+ logger.request(`activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`, {
677
+ provider: this.name,
678
+ model: this.model
679
+ });
680
+ const stream = (await this.languageModel.doStream(call)).stream;
681
+ for await (const chunk of this.mapStream(stream, options, logger, call.abortSignal)) yield {
682
+ ...chunk,
683
+ metadata: {
684
+ ...chunk.metadata,
685
+ tanstack: {
686
+ ...tanstackMetadata(chunk),
687
+ source
688
+ }
689
+ }
690
+ };
691
+ } catch (error) {
692
+ logger.errors(`${this.name}.chatStream fatal`, {
693
+ error,
694
+ source: `${this.name}.chatStream`
695
+ });
696
+ yield this.runError(error, options.model, source);
697
+ }
698
+ }
699
+ runError(error, model, source) {
700
+ const payload = toRunErrorPayload(error, "An unknown error occurred during the chat stream.");
701
+ return {
702
+ type: EventType.RUN_ERROR,
703
+ metadata: { tanstack: { source } },
704
+ model,
705
+ timestamp: Date.now(),
706
+ message: payload.message,
707
+ code: payload.code,
708
+ error: {
709
+ message: payload.message,
710
+ code: payload.code
711
+ }
712
+ };
713
+ }
714
+ async *mapStream(stream, options, logger, abortSignal) {
715
+ const runId = options.runId ?? randomId("run");
716
+ const threadId = options.threadId;
717
+ const messageId = randomId("msg");
718
+ let model = options.model;
719
+ let text = "";
720
+ let textOpen = false;
721
+ let reasoningId;
722
+ let finished = false;
723
+ const started = /* @__PURE__ */ new Map();
724
+ const skipped = /* @__PURE__ */ new Set();
725
+ let toolCallCount = 0;
726
+ yield {
727
+ type: EventType.RUN_STARTED,
728
+ runId,
729
+ threadId,
730
+ model,
731
+ timestamp: Date.now(),
732
+ parentRunId: options.parentRunId
733
+ };
734
+ const closeReasoning = function* () {
735
+ if (!reasoningId) return;
736
+ yield {
737
+ type: EventType.REASONING_MESSAGE_END,
738
+ messageId: reasoningId,
739
+ model,
740
+ timestamp: Date.now()
741
+ };
742
+ yield {
743
+ type: EventType.REASONING_END,
744
+ messageId: reasoningId,
745
+ model,
746
+ timestamp: Date.now()
747
+ };
748
+ reasoningId = void 0;
749
+ };
750
+ const reader = stream.getReader();
751
+ const onAbort = () => void reader.cancel().catch(() => void 0);
752
+ if (abortSignal?.aborted) onAbort();
753
+ else abortSignal?.addEventListener("abort", onAbort, { once: true });
754
+ try {
755
+ while (true) {
756
+ const { done, value: part } = await reader.read();
757
+ if (done) break;
758
+ logger.provider(`provider=${this.name}`, { chunk: part });
759
+ switch (part.type) {
760
+ case "response-metadata":
761
+ if (typeof part.modelId === "string") model = part.modelId;
762
+ break;
763
+ case "text-delta": {
764
+ const delta = String(part.delta ?? part.textDelta ?? "");
765
+ if (!delta) break;
766
+ yield* closeReasoning();
767
+ if (!textOpen) {
768
+ textOpen = true;
769
+ yield {
770
+ type: EventType.TEXT_MESSAGE_START,
771
+ messageId,
772
+ role: "assistant",
773
+ model,
774
+ timestamp: Date.now()
775
+ };
776
+ }
777
+ text += delta;
778
+ yield {
779
+ type: EventType.TEXT_MESSAGE_CONTENT,
780
+ messageId,
781
+ delta,
782
+ content: text,
783
+ model,
784
+ timestamp: Date.now()
785
+ };
786
+ break;
787
+ }
788
+ case "reasoning-start":
789
+ case "reasoning-delta": {
790
+ if (!reasoningId) {
791
+ reasoningId = randomId("reasoning");
792
+ yield {
793
+ type: EventType.REASONING_START,
794
+ messageId: reasoningId,
795
+ model,
796
+ timestamp: Date.now()
797
+ };
798
+ yield {
799
+ type: EventType.REASONING_MESSAGE_START,
800
+ messageId: reasoningId,
801
+ role: "reasoning",
802
+ model,
803
+ timestamp: Date.now()
804
+ };
805
+ }
806
+ const delta = part.type === "reasoning-delta" ? String(part.delta ?? "") : "";
807
+ if (delta) yield {
808
+ type: EventType.REASONING_MESSAGE_CONTENT,
809
+ messageId: reasoningId,
810
+ delta,
811
+ model,
812
+ timestamp: Date.now()
813
+ };
814
+ break;
815
+ }
816
+ case "reasoning-end":
817
+ yield* closeReasoning();
818
+ break;
819
+ case "tool-input-start": {
820
+ const id = String(part.id);
821
+ if (part.providerExecuted) {
822
+ skipped.add(id);
823
+ break;
824
+ }
825
+ yield* closeReasoning();
826
+ const name = String(part.toolName);
827
+ started.set(id, {
828
+ name,
829
+ args: "",
830
+ index: toolCallCount++
831
+ });
832
+ yield {
833
+ type: EventType.TOOL_CALL_START,
834
+ toolCallId: id,
835
+ toolCallName: name,
836
+ parentMessageId: messageId,
837
+ index: started.get(id)?.index,
838
+ model,
839
+ timestamp: Date.now()
840
+ };
841
+ break;
842
+ }
843
+ case "tool-input-delta": {
844
+ const id = String(part.id);
845
+ const call = started.get(id);
846
+ if (!call || skipped.has(id)) break;
847
+ const delta = String(part.delta ?? "");
848
+ call.args += delta;
849
+ yield {
850
+ type: EventType.TOOL_CALL_ARGS,
851
+ toolCallId: id,
852
+ delta,
853
+ args: call.args,
854
+ model,
855
+ timestamp: Date.now()
856
+ };
857
+ break;
858
+ }
859
+ case "tool-call": {
860
+ const id = String(part.toolCallId);
861
+ if (part.providerExecuted || skipped.has(id)) break;
862
+ const name = String(part.toolName);
863
+ const input = typeof part.input === "string" ? part.input : JSON.stringify(part.input ?? part.args ?? {});
864
+ let call = started.get(id);
865
+ if (!call) {
866
+ yield* closeReasoning();
867
+ call = {
868
+ name,
869
+ args: "",
870
+ index: toolCallCount++
871
+ };
872
+ started.set(id, call);
873
+ yield {
874
+ type: EventType.TOOL_CALL_START,
875
+ toolCallId: id,
876
+ toolCallName: name,
877
+ parentMessageId: messageId,
878
+ index: call.index,
879
+ model,
880
+ timestamp: Date.now()
881
+ };
882
+ }
883
+ if (call.args !== input) {
884
+ const delta = input.startsWith(call.args) ? input.slice(call.args.length) : input;
885
+ call.args = input;
886
+ if (delta) yield {
887
+ type: EventType.TOOL_CALL_ARGS,
888
+ toolCallId: id,
889
+ delta,
890
+ args: input,
891
+ model,
892
+ timestamp: Date.now()
893
+ };
894
+ }
895
+ const parsed = parseObject(input);
896
+ yield {
897
+ type: EventType.TOOL_CALL_END,
898
+ toolCallId: id,
899
+ toolCallName: name,
900
+ args: input,
901
+ ...parsed !== void 0 && { input: parsed },
902
+ model,
903
+ timestamp: Date.now()
904
+ };
905
+ break;
906
+ }
907
+ case "error":
908
+ yield* closeReasoning();
909
+ yield this.runError(part.error, model, {
910
+ provider: this.name,
911
+ api: this.api,
912
+ model
913
+ });
914
+ return;
915
+ case "finish": {
916
+ yield* closeReasoning();
917
+ if (textOpen) {
918
+ textOpen = false;
919
+ yield {
920
+ type: EventType.TEXT_MESSAGE_END,
921
+ messageId,
922
+ model,
923
+ timestamp: Date.now()
924
+ };
925
+ }
926
+ const usage = languageModelUsage(part.usage);
927
+ finished = true;
928
+ yield {
929
+ type: EventType.RUN_FINISHED,
930
+ runId,
931
+ threadId,
932
+ model,
933
+ finishReason: finishReason(part.finishReason, toolCallCount > 0),
934
+ ...usage && { usage },
935
+ timestamp: Date.now()
936
+ };
937
+ break;
938
+ }
939
+ }
940
+ }
941
+ } finally {
942
+ abortSignal?.removeEventListener("abort", onAbort);
943
+ reader.releaseLock();
944
+ }
945
+ if (!finished) {
946
+ yield* closeReasoning();
947
+ if (textOpen) yield {
948
+ type: EventType.TEXT_MESSAGE_END,
949
+ messageId,
950
+ model,
951
+ timestamp: Date.now()
952
+ };
953
+ yield {
954
+ type: EventType.RUN_FINISHED,
955
+ runId,
956
+ threadId,
957
+ model,
958
+ finishReason: toolCallCount > 0 ? "tool_calls" : "stop",
959
+ timestamp: Date.now()
960
+ };
961
+ }
962
+ }
963
+ async structuredOutput(options) {
964
+ const { chatOptions, outputSchema } = options;
965
+ const { logger } = chatOptions;
966
+ try {
967
+ const call = this.callOptions({
968
+ ...chatOptions,
969
+ tools: void 0
970
+ });
971
+ call.responseFormat = {
972
+ type: "json",
973
+ schema: outputSchema
974
+ };
975
+ logger.request(`activity=chat provider=${this.name} model=${this.model} messages=${chatOptions.messages.length} stream=false structured=true`, {
976
+ provider: this.name,
977
+ model: this.model
978
+ });
979
+ const result = await this.languageModel.doGenerate(call);
980
+ const rawText = result.content.filter((part) => part.type === "text").map((part) => part.text).join("");
981
+ if (finishReason(result.finishReason, false) === "length") throw new Error("the response was cut off at the maximum token limit; raise modelOptions.maxOutputTokens");
982
+ let data;
983
+ try {
984
+ data = parseJsonAnswer(rawText);
985
+ } catch {
986
+ throw new Error(`the model did not return valid JSON: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`);
987
+ }
988
+ const usage = languageModelUsage(result.usage);
989
+ return {
990
+ data,
991
+ rawText,
992
+ ...usage && { usage },
993
+ ...result.response?.id && { responseId: result.response.id },
994
+ ...result.response?.modelId && { model: result.response.modelId }
995
+ };
996
+ } catch (error) {
997
+ logger.errors(`${this.name}.structuredOutput fatal`, {
998
+ error,
999
+ source: `${this.name}.structuredOutput`
1000
+ });
1001
+ throw new Error(`Structured output generation failed: ${error?.message ?? String(error)}`, { cause: error });
1002
+ }
1003
+ }
1004
+ };
1005
+ /**
1006
+ * Wraps an AI SDK language model as a TanStack AI text adapter.
1007
+ *
1008
+ * @example
1009
+ * import { chat } from '@tanstack/ai'
1010
+ * import { deepseek } from '@ai-sdk/deepseek'
1011
+ * chat({ adapter: aiSdkText(deepseek('deepseek-chat')), messages, threadId })
1012
+ */
1013
+ function aiSdkText(model, options) {
1014
+ return new AiSdkTextAdapter(model, options);
1015
+ }
1016
+ // src/transcription.ts
1017
+ const SPECS = [
1018
+ "v2",
1019
+ "v3",
1020
+ "v4"
1021
+ ];
1022
+ async function toAudio(audio, fallbackType, signal) {
1023
+ if (typeof audio === "string") {
1024
+ if (/^https?:\/\//.test(audio)) {
1025
+ const response = await fetch(audio, signal ? { signal } : void 0);
1026
+ if (!response.ok) throw new Error(`Could not download audio (${response.status}) from ${audio}`);
1027
+ return {
1028
+ data: new Uint8Array(await response.arrayBuffer()),
1029
+ mediaType: response.headers.get("content-type")?.split(";")[0] || fallbackType
1030
+ };
1031
+ }
1032
+ const dataUrl = parseDataUrl(audio);
1033
+ if (dataUrl) return {
1034
+ data: base64ToBytes(dataUrl.base64),
1035
+ mediaType: dataUrl.mediaType
1036
+ };
1037
+ return {
1038
+ data: base64ToBytes(audio),
1039
+ mediaType: fallbackType
1040
+ };
1041
+ }
1042
+ if (audio instanceof ArrayBuffer) return {
1043
+ data: new Uint8Array(audio),
1044
+ mediaType: fallbackType
1045
+ };
1046
+ return {
1047
+ data: new Uint8Array(await audio.arrayBuffer()),
1048
+ mediaType: audio.type || fallbackType
1049
+ };
1050
+ }
1051
+ /**
1052
+ * TanStack AI transcription adapter around an AI SDK transcription model.
1053
+ * `language` and `prompt` have no AI SDK equivalent: pass them in `modelOptions.providerOptions`.
1054
+ */
1055
+ var AiSdkTranscriptionAdapter = class extends BaseTranscriptionAdapter {
1056
+ name;
1057
+ transcriptionModel;
1058
+ specificationVersion;
1059
+ constructor(model, options) {
1060
+ super(model.modelId, {});
1061
+ this.specificationVersion = specVersionOf(model, "transcription", SPECS);
1062
+ this.transcriptionModel = model;
1063
+ this.name = adapterName(model, options);
1064
+ }
1065
+ async transcribe(options) {
1066
+ const { model, logger, modelOptions, abortSignal } = options;
1067
+ try {
1068
+ const audio = await toAudio(options.audio, modelOptions?.mediaType ?? "audio/mpeg", abortSignal);
1069
+ logger.request(`activity=transcription provider=${this.name} model=${model} bytes=${audio.data.length}`, {
1070
+ provider: this.name,
1071
+ model
1072
+ });
1073
+ const result = await this.transcriptionModel.doGenerate({
1074
+ audio: audio.data,
1075
+ mediaType: audio.mediaType,
1076
+ ...callExtras(modelOptions, abortSignal)
1077
+ });
1078
+ return {
1079
+ id: randomId(this.name),
1080
+ model: result.response?.modelId ?? model,
1081
+ text: result.text,
1082
+ ...result.language !== void 0 && { language: result.language },
1083
+ ...result.durationInSeconds !== void 0 && { duration: result.durationInSeconds },
1084
+ ...result.segments.length > 0 && { segments: result.segments.map((segment, id) => ({
1085
+ id,
1086
+ start: segment.startSecond,
1087
+ end: segment.endSecond,
1088
+ text: segment.text
1089
+ })) }
1090
+ };
1091
+ } catch (error) {
1092
+ logger.errors(`${this.name}.transcribe fatal`, {
1093
+ error: toRunErrorPayload(error, `${this.name}.transcribe failed`),
1094
+ source: `${this.name}.transcribe`
1095
+ });
1096
+ throw error;
1097
+ }
1098
+ }
1099
+ };
1100
+ /**
1101
+ * Wraps an AI SDK transcription model as a TanStack AI transcription adapter.
1102
+ *
1103
+ * @example
1104
+ * import { generateTranscription } from '@tanstack/ai'
1105
+ * import { openai } from '@ai-sdk/openai'
1106
+ * await generateTranscription({ threadId, adapter: aiSdkTranscription(openai.transcription('whisper-1')), audio: file })
1107
+ */
1108
+ function aiSdkTranscription(model, options) {
1109
+ return new AiSdkTranscriptionAdapter(model, options);
1110
+ }
1111
+ export { AiSdkEmbeddingAdapter, AiSdkImageAdapter, AiSdkRerankAdapter, AiSdkSpeechAdapter, AiSdkTextAdapter, AiSdkTranscriptionAdapter, aiSdkEmbedding, aiSdkImage, aiSdkRerank, aiSdkSpeech, aiSdkText, aiSdkTranscription };