@amjadiqbal/tanstack-ai-sdk-bridge 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.1] - 2026-10-23
9
+
10
+ ### Fixed
11
+
12
+ - `aiSdkSpeech`: the result's `format` and `contentType` now come from what the provider returned (its `content-type` header, else the audio bytes), then the requested format, else `unknown`. Before, it reported `mp3` whenever no format was requested.
13
+
14
+ ### Added
15
+
16
+ - `aiSdkImage`: `modelOptions.mask` is sent to AI SDK v3/v4 image models (it was never sent before).
17
+
18
+ ### Documentation
19
+
20
+ - README states exactly which activities can be aborted (not `embed()`), what `outputSchema` does in the bridge (one non-streaming call without tools, JSON parsing, error cases; `chat()` validates), how `dimensions` behaves on v2/v3 embedding models, which transcription options are not sent, how speech reports its format, the image options and defaults, why `@ai-sdk/provider` is a dependency, and the `@tanstack/ai/adapter-internals` import.
21
+ - Removed the untested `deepgram` keyword.
22
+
8
23
  ## [0.1.0] - 2026-10-23
9
24
 
10
25
  ### Added
package/README.md CHANGED
@@ -27,10 +27,10 @@ const stream = chat({
27
27
  })
28
28
  ```
29
29
 
30
- - **Text**: streaming chat, tool calls (TanStack's agent loop runs your tools), reasoning, structured output (`outputSchema`), token usage, abort.
30
+ - **Text**: streaming chat, tool calls (TanStack's agent loop runs your tools), reasoning, structured output (`outputSchema`), token usage, abort (see [Abort](#abort)).
31
31
  - **Embeddings, transcription, speech, image generation, rerank**: one adapter function each.
32
32
  - **AI SDK 5, 6 and 7 providers**: provider spec v2, v3 and v4 are all read.
33
- - **No extra runtime code from AI SDK**: only the provider package you already use, plus `@ai-sdk/provider` types. No `ai` package needed.
33
+ - **No AI SDK runtime code**: the built package imports only `@tanstack/ai`. `@ai-sdk/provider` is a dependency because the published type declarations use its model types; nothing from it runs. No `ai` package needed.
34
34
 
35
35
  ## Install
36
36
 
@@ -112,7 +112,13 @@ const product = await chat({
112
112
  // product: { name: string; price: number }
113
113
  ```
114
114
 
115
- The bridge asks the model for JSON with the schema (AI SDK `responseFormat: { type: 'json', schema }`) and TanStack AI validates the result. A JSON answer wrapped in a Markdown code fence is also accepted.
115
+ What the bridge does for `outputSchema`, exactly:
116
+
117
+ - After the agent loop, TanStack AI calls the adapter's `structuredOutput()`. The bridge makes **one non-streaming** `doGenerate` call with the conversation, **without tools**, and `responseFormat: { type: 'json', schema }` (the JSON Schema TanStack AI made from your schema). Whether the provider enforces the schema depends on the provider.
118
+ - It joins the text parts of the answer and parses them as JSON. An answer wrapped in a Markdown code fence is accepted.
119
+ - It throws `Structured output generation failed: ...` (original error as `cause`) when the answer was cut off at the token limit, when the text is not JSON, or when the provider call fails.
120
+ - The bridge does not check the data against the schema; `chat()` does that with your schema after the bridge returns.
121
+ - `structuredOutputStream` and combined tools-plus-schema in one request are not implemented, so TanStack AI uses its non-streaming path.
116
122
 
117
123
  ### Reasoning
118
124
 
@@ -141,7 +147,7 @@ const { embeddings } = await embed({
141
147
  })
142
148
  ```
143
149
 
144
- Large inputs are split by the model's `maxEmbeddingsPerCall`. `dimensions` works with AI SDK 7 models; older ones take it through `modelOptions.providerOptions`.
150
+ Large inputs are split by the model's `maxEmbeddingsPerCall`. `dimensions` is sent to AI SDK 7 (spec v4) embedding models. On spec v2/v3 models the bridge throws if `dimensions` is set, because their spec has no such field: leave it unset and pass the provider's own option instead, for example `modelOptions: { providerOptions: { openai: { dimensions: 512 } } }`.
145
151
 
146
152
  ### Transcription, speech, image, rerank
147
153
 
@@ -168,8 +174,24 @@ await rerank({ adapter: aiSdkRerank(cohere.reranking('rerank-v3.5')), query: 're
168
174
  | `generateImage()` | `aiSdkImage(model)` | `ImageModel` | v2, v3, v4 |
169
175
  | `rerank()` | `aiSdkRerank(model)` | `RerankingModel` | v3, v4 |
170
176
 
177
+ Activity-specific `modelOptions`, on top of `providerOptions` and `headers`:
178
+
179
+ | Adapter | `modelOptions` | Notes |
180
+ |---|---|---|
181
+ | `aiSdkTranscription` | `mediaType` | Media type of the audio when it can't be read from a `Blob`/`File` type or a data URL. Default `audio/mpeg`. `language`, `prompt` and `responseFormat` are not sent (the AI SDK spec has no such fields); the result has `text`, plus `segments`, `language` and `duration` when the provider returns them. An `http(s)` URL as `audio` is downloaded with the global `fetch`, not the provider's `fetch`. |
182
+ | `aiSdkSpeech` | `instructions`, `language` | Sent as the AI SDK `instructions` and `language` call options. The result's `format` and `contentType` come from the provider's `content-type` header, else from the audio bytes (MP3, WAV, Ogg, FLAC, AAC), else the `format` you asked for, else `unknown`. |
183
+ | `aiSdkImage` | `aspectRatio`, `seed`, `mask` | Sent as the AI SDK call options. `mask` is an AI SDK image file (`{ type: 'file', mediaType, data }` or `{ type: 'url', url }`). Image parts in the prompt become input `files`. Input images and masks need a spec v3/v4 model; v2 models throw. When the model does not report `maxImagesPerCall`, one image is requested per call (the AI SDK's own default) until `numberOfImages` is reached. |
184
+
171
185
  Every function takes an optional second argument `{ name }`, the adapter name TanStack AI shows in devtools and usage. It defaults to the provider id before the first dot (`deepseek.chat` becomes `deepseek`).
172
186
 
187
+ ### Abort
188
+
189
+ | Activity | How to cancel | Passed to the AI SDK model |
190
+ |---|---|---|
191
+ | `chat()` | `abortController` (or the `request` signal) | yes: `abortSignal`, and the stream is cancelled |
192
+ | `generateTranscription()`, `generateSpeech()`, `generateImage()`, `rerank()` | `abortSignal` | yes |
193
+ | `embed()` | none: TanStack AI's `embed()` takes no signal | no |
194
+
173
195
  ## Migrating from the Vercel AI SDK
174
196
 
175
197
  TanStack's own [migration guide](https://tanstack.com/ai/latest/docs/migration/migration-from-vercel-ai) swaps your provider packages for TanStack adapters. With the bridge you can skip that step, and keep providers that have no TanStack adapter:
@@ -215,7 +237,9 @@ Everything below ran on 2026-10-23 with `@tanstack/ai` 1.0.0. **No live provider
215
237
  - **Reasoning signatures** are not sent back, so providers that need signed thinking blocks in later turns (Anthropic extended thinking with tools) receive the reasoning as plain text or drop it.
216
238
  - `chat({ wrapFetch })` is not applied; pass a custom `fetch` to the provider factory (`createDeepSeek({ fetch })`).
217
239
  - Provider file handles (`{ type: 'file' }` sources) are rejected; send images and documents as data or URLs.
218
- - Transcription `language` and `prompt` have no AI SDK equivalent; pass them in `modelOptions.providerOptions`.
240
+ - Transcription `language`, `prompt` and `responseFormat` are not sent; see the options table.
241
+ - Embeddings cannot be aborted (TanStack AI's `embed()` has no signal).
242
+ - **TanStack AI internals:** like TanStack's own adapters, the bridge imports helpers (`transformMessagesForReplay`, `tanstackMetadata`, `toRunErrorPayload`) from `@tanstack/ai/adapter-internals`, the subpath TanStack's [community adapter guide](https://tanstack.com/ai/latest/docs/community-adapters/guide) points adapters to. It is not covered by the same stability promise as the main entry. Tested with `@tanstack/ai` 1.0.0; the peer range is `^1.0.0`. If a 1.x release breaks it, please open an issue.
219
243
  - AI SDK reranking models report no token usage, so `rerank()` usage is zero.
220
244
  - Video, audio generation and realtime models are not bridged yet.
221
245
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AdapterYieldChunk, EmbeddingOptions, EmbeddingResult, ImageGenerationOptions, ImageGenerationResult, ReasoningLevel, RerankAdapterResult, RerankOptions, TTSOptions, TTSResult, TextOptions, TranscriptionOptions, TranscriptionResult } from "@tanstack/ai";
2
2
  import { BaseEmbeddingAdapter, BaseImageAdapter, BaseRerankAdapter, BaseTTSAdapter, BaseTextAdapter, BaseTranscriptionAdapter, StructuredOutputOptions, StructuredOutputResult } from "@tanstack/ai/adapters";
3
- import { EmbeddingModelV2, EmbeddingModelV3, EmbeddingModelV4, ImageModelV2, ImageModelV3, ImageModelV4, LanguageModelV2, LanguageModelV3, LanguageModelV4, RerankingModelV3, RerankingModelV4, SpeechModelV2, SpeechModelV3, SpeechModelV4, TranscriptionModelV2, TranscriptionModelV3, TranscriptionModelV4 } from "@ai-sdk/provider";
3
+ import { EmbeddingModelV2, EmbeddingModelV3, EmbeddingModelV4, ImageModelV2, ImageModelV3, ImageModelV4, ImageModelV4File, LanguageModelV2, LanguageModelV3, LanguageModelV4, RerankingModelV3, RerankingModelV4, SpeechModelV2, SpeechModelV3, SpeechModelV4, TranscriptionModelV2, TranscriptionModelV3, TranscriptionModelV4 } from "@ai-sdk/provider";
4
4
  /** AI SDK provider spec versions this package reads. */
5
5
  type SpecVersion = 'v2' | 'v3' | 'v4';
6
6
  /** Options every bridge adapter takes besides the model. */
@@ -45,7 +45,10 @@ type AiSdkImageModel = ImageModelV2 | ImageModelV3 | ImageModelV4;
45
45
  interface AiSdkImageModelOptions extends AiSdkCallOptions {
46
46
  /** Aspect ratio such as `16:9`, for models that take one instead of a size. */
47
47
  aspectRatio?: `${number}:${number}`;
48
+ /** Seed, for models that take one. */
48
49
  seed?: number;
50
+ /** Mask for inpainting (AI SDK v3/v4 image models that support masks), as an AI SDK image file. */
51
+ mask?: ImageModelV4File;
49
52
  }
50
53
  /**
51
54
  * TanStack AI image adapter around an AI SDK image model.
@@ -98,7 +101,11 @@ interface AiSdkSpeechModelOptions extends AiSdkCallOptions {
98
101
  /** Language of the text, e.g. `en`. */
99
102
  language?: string;
100
103
  }
101
- /** TanStack AI text-to-speech adapter around an AI SDK speech model. */
104
+ /**
105
+ * TanStack AI text-to-speech adapter around an AI SDK speech model.
106
+ * `format` in the result is what the provider returned (content-type header or audio bytes),
107
+ * falling back to the requested format, else `unknown`.
108
+ */
102
109
  export declare class AiSdkSpeechAdapter<TModel extends string = string> extends BaseTTSAdapter<TModel, AiSdkSpeechModelOptions> {
103
110
  readonly name: string;
104
111
  readonly speechModel: AiSdkSpeechModel;
@@ -177,7 +184,9 @@ interface AiSdkTranscriptionModelOptions extends AiSdkCallOptions {
177
184
  }
178
185
  /**
179
186
  * TanStack AI transcription adapter around an AI SDK transcription model.
180
- * `language` and `prompt` have no AI SDK equivalent: pass them in `modelOptions.providerOptions`.
187
+ * `language`, `prompt` and `responseFormat` have no AI SDK equivalent and are not sent: pass the provider's own
188
+ * options in `modelOptions.providerOptions`. The result always has `text`, plus `segments`, `language` and `duration`
189
+ * when the provider returns them. An `http(s)` URL as `audio` is downloaded with the global `fetch`, not the provider's.
181
190
  */
182
191
  export declare class AiSdkTranscriptionAdapter<TModel extends string = string> extends BaseTranscriptionAdapter<TModel, AiSdkTranscriptionModelOptions> {
183
192
  readonly name: string;
package/dist/index.js CHANGED
@@ -177,6 +177,7 @@ var AiSdkImageAdapter = class extends BaseImageAdapter {
177
177
  const wanted = options.numberOfImages ?? 1;
178
178
  try {
179
179
  const { text, files } = this.promptOf(options.prompt);
180
+ if (modelOptions?.mask && this.specificationVersion === "v2") throw new Error(`${this.name}: a mask needs an AI SDK v3 or v4 image model`);
180
181
  const maxSetting = this.imageModel.maxImagesPerCall;
181
182
  const max = (typeof maxSetting === "function" ? await maxSetting({ modelId: this.imageModel.modelId }) : maxSetting) ?? 1;
182
183
  logger.request(`activity=image provider=${this.name} model=${model} n=${wanted} size=${size ?? "default"}`, {
@@ -196,7 +197,7 @@ var AiSdkImageAdapter = class extends BaseImageAdapter {
196
197
  aspectRatio: modelOptions?.aspectRatio,
197
198
  seed: modelOptions?.seed,
198
199
  files: files.length > 0 ? files : void 0,
199
- mask: void 0,
200
+ mask: modelOptions?.mask,
200
201
  providerOptions: modelOptions?.providerOptions ?? {},
201
202
  ...modelOptions?.headers && { headers: modelOptions.headers },
202
203
  ...abortSignal && { abortSignal }
@@ -306,7 +307,43 @@ const CONTENT_TYPES = {
306
307
  wav: "audio/wav",
307
308
  pcm: "audio/pcm"
308
309
  };
309
- /** TanStack AI text-to-speech adapter around an AI SDK speech model. */
310
+ const FORMATS_BY_TYPE = {
311
+ "audio/mpeg": "mp3",
312
+ "audio/mp3": "mp3",
313
+ "audio/wav": "wav",
314
+ "audio/x-wav": "wav",
315
+ "audio/wave": "wav",
316
+ "audio/opus": "opus",
317
+ "audio/ogg": "opus",
318
+ "audio/aac": "aac",
319
+ "audio/flac": "flac",
320
+ "audio/x-flac": "flac",
321
+ "audio/pcm": "pcm",
322
+ "audio/l16": "pcm"
323
+ };
324
+ function headerValue(headers, name) {
325
+ if (!headers || typeof headers !== "object") return void 0;
326
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === name && typeof value === "string") return value;
327
+ }
328
+ function formatFromContentType(type) {
329
+ return type ? FORMATS_BY_TYPE[type.toLowerCase()] : void 0;
330
+ }
331
+ /** Recognises MP3, WAV, Ogg/Opus, FLAC and ADTS AAC from their first bytes. */
332
+ function formatFromBytes(bytes) {
333
+ const ascii = (start, text) => [...text].every((char, i) => bytes[start + i] === char.charCodeAt(0));
334
+ if (ascii(0, "RIFF") && ascii(8, "WAVE")) return "wav";
335
+ if (ascii(0, "OggS")) return "opus";
336
+ if (ascii(0, "fLaC")) return "flac";
337
+ if (ascii(0, "ID3")) return "mp3";
338
+ const b0 = bytes[0] ?? 0;
339
+ const b1 = bytes[1] ?? 0;
340
+ if (b0 === 255 && (b1 & 224) === 224) return (b1 & 6) === 0 ? "aac" : "mp3";
341
+ }
342
+ /**
343
+ * TanStack AI text-to-speech adapter around an AI SDK speech model.
344
+ * `format` in the result is what the provider returned (content-type header or audio bytes),
345
+ * falling back to the requested format, else `unknown`.
346
+ */
310
347
  var AiSdkSpeechAdapter = class extends BaseTTSAdapter {
311
348
  name;
312
349
  speechModel;
@@ -333,15 +370,16 @@ var AiSdkSpeechAdapter = class extends BaseTTSAdapter {
333
370
  ...modelOptions?.language !== void 0 && { language: modelOptions.language },
334
371
  ...callExtras(modelOptions, abortSignal)
335
372
  });
373
+ const bytes = typeof result.audio === "string" ? base64ToBytes(result.audio) : result.audio;
336
374
  const audio = typeof result.audio === "string" ? result.audio : bytesToBase64(result.audio);
337
- const outputFormat = format ?? "mp3";
338
- const headerType = (result.response?.headers)?.["content-type"];
375
+ const headerType = headerValue(result.response?.headers, "content-type")?.split(";")[0]?.trim();
376
+ const outputFormat = formatFromContentType(headerType) ?? formatFromBytes(bytes) ?? format ?? "unknown";
339
377
  return {
340
378
  id: randomId(this.name),
341
379
  model: result.response?.modelId ?? model,
342
380
  audio,
343
381
  format: outputFormat,
344
- contentType: headerType?.split(";")[0] || CONTENT_TYPES[outputFormat] || "application/octet-stream"
382
+ contentType: headerType || CONTENT_TYPES[outputFormat] || "application/octet-stream"
345
383
  };
346
384
  } catch (error) {
347
385
  logger.errors(`${this.name}.generateSpeech fatal`, {
@@ -1050,7 +1088,9 @@ async function toAudio(audio, fallbackType, signal) {
1050
1088
  }
1051
1089
  /**
1052
1090
  * TanStack AI transcription adapter around an AI SDK transcription model.
1053
- * `language` and `prompt` have no AI SDK equivalent: pass them in `modelOptions.providerOptions`.
1091
+ * `language`, `prompt` and `responseFormat` have no AI SDK equivalent and are not sent: pass the provider's own
1092
+ * options in `modelOptions.providerOptions`. The result always has `text`, plus `segments`, `language` and `duration`
1093
+ * when the provider returns them. An `http(s)` URL as `audio` is downloaded with the global `fetch`, not the provider's.
1054
1094
  */
1055
1095
  var AiSdkTranscriptionAdapter = class extends BaseTranscriptionAdapter {
1056
1096
  name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amjadiqbal/tanstack-ai-sdk-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Use any Vercel AI SDK provider (@ai-sdk/* and community providers) as a TanStack AI adapter: streaming chat with tools, reasoning and structured output, plus embeddings, transcription, speech, image generation and rerank. Supports AI SDK provider spec v2, v3 and v4.",
5
5
  "keywords": [
6
6
  "tanstack",
@@ -13,7 +13,6 @@
13
13
  "cerebras",
14
14
  "togetherai",
15
15
  "fireworks",
16
- "deepgram",
17
16
  "embeddings",
18
17
  "tool-calling",
19
18
  "migration"