@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 +15 -0
- package/README.md +29 -5
- package/dist/index.d.ts +12 -3
- package/dist/index.js +46 -6
- package/package.json +1 -2
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
|
|
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
|
-
|
|
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`
|
|
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 `
|
|
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
|
-
/**
|
|
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 `
|
|
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:
|
|
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
|
-
|
|
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
|
|
338
|
-
const
|
|
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
|
|
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 `
|
|
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.
|
|
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"
|