@amjadiqbal/tanstack-ai-sdk-bridge 0.1.0 → 0.1.2
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 +27 -0
- package/README.md +65 -16
- package/dist/index.d.ts +13 -3
- package/dist/index.js +49 -7
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-10-23
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Positioned as a migration stepping stone, following TanStack AI maintainers' feedback ([TanStack/ai#1884](https://github.com/TanStack/ai/pull/1884)): the README now starts with TanStack AI's migration guide, a table of the native adapter for each tested provider, a third-party notice, and the list of what the bridge loses versus a native adapter.
|
|
13
|
+
- **Peer range narrowed to `@tanstack/ai >=1.0.0 <1.1.0`** (the versions tested), because the bridge imports `@tanstack/ai/adapter-internals`, which has no stability promise. Newer TanStack AI versions are flagged at install time until a tested release widens the range.
|
|
14
|
+
- `aiSdkSpeech`: Ogg audio is reported as `opus` only when the stream is Opus, otherwise as `ogg`; an `audio/ogg` header maps to `ogg`, and the content type for `opus` and `ogg` is `audio/ogg`.
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- The `application/octet-stream` fallback for speech `contentType` is documented.
|
|
19
|
+
|
|
20
|
+
## [0.1.1] - 2026-10-23
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `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.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `aiSdkImage`: `modelOptions.mask` is sent to AI SDK v3/v4 image models (it was never sent before).
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- 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.
|
|
33
|
+
- Removed the untested `deepgram` keyword.
|
|
34
|
+
|
|
8
35
|
## [0.1.0] - 2026-10-23
|
|
9
36
|
|
|
10
37
|
### Added
|
package/README.md
CHANGED
|
@@ -5,14 +5,35 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
# TanStack AI SDK Bridge:
|
|
8
|
+
# TanStack AI SDK Bridge: a migration stepping stone from Vercel AI SDK providers to TanStack AI
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Run your existing Vercel AI SDK provider setup (`@ai-sdk/*` and community providers) inside [TanStack AI](https://tanstack.com/ai) **while you move to native TanStack AI adapters**. Wrap a model, e.g. `aiSdkText(deepseek('deepseek-chat'))`, and `chat()`, `embed()`, `generateTranscription()`, `generateSpeech()`, `generateImage()` and `rerank()` accept it. You can move your server routes and client to TanStack AI first, ship, and then swap each provider for its native adapter.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Use native TanStack AI adapters wherever one exists.** Start from TanStack AI's [migration guide from the Vercel AI SDK](https://tanstack.com/ai/latest/docs/migration/migration-from-vercel-ai) and its [adapter list](https://tanstack.com/ai/latest/docs/adapters/openai). For the providers this package was tested with:
|
|
13
|
+
|
|
14
|
+
| AI SDK provider | Native TanStack AI adapter |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `@ai-sdk/openai` | `openaiText`, `openaiEmbedding`, `openaiImage`, `openaiSpeech`, `openaiTranscription` from `@tanstack/ai-openai` |
|
|
17
|
+
| `@ai-sdk/anthropic` | `anthropicText` from `@tanstack/ai-anthropic` |
|
|
18
|
+
| `ollama-ai-provider-v2` | `ollamaText`, `ollamaEmbedding` from `@tanstack/ai-ollama` |
|
|
19
|
+
| `@ai-sdk/cohere` | `cohereRerank`, `cohereEmbedding` from `@tanstack/ai-cohere` |
|
|
20
|
+
| `@ai-sdk/deepseek`, `@ai-sdk/togetherai`, `@ai-sdk/cerebras`, `@ai-sdk/fireworks`, `@ai-sdk/openai-compatible` | `openaiCompatible` from `@tanstack/ai-openai/compatible` ([docs](https://tanstack.com/ai/latest/docs/adapters/openai-compatible)) |
|
|
21
|
+
|
|
22
|
+
> [!IMPORTANT]
|
|
23
|
+
> **Third-party package.** It is not made, listed or endorsed by TanStack; TanStack AI's maintainers [declined to list it](https://github.com/TanStack/ai/pull/1884) in their docs. Problems with a bridged provider are this package's problems, not TanStack AI's: please [open an issue here](https://github.com/amjadiqbal/tanstack-ai-sdk-bridge/issues).
|
|
24
|
+
|
|
25
|
+
### What you lose versus a native adapter
|
|
26
|
+
|
|
27
|
+
- **No per-model typing**: `modelOptions` and input modalities are not typed per model; you get one generic set of AI SDK call settings.
|
|
28
|
+
- **Reasoning only on AI SDK 7 providers** (spec v4). On AI SDK 5 and 6 providers `chat({ reasoning })` is not sent; reasoning output still streams if the provider returns it.
|
|
29
|
+
- **No provider-executed tools** (a provider's own web search, code execution and the like); only your function tools.
|
|
30
|
+
- **No reasoning signatures across turns**: signed or redacted thinking is not sent back, so providers that need it in later turns get plain text or nothing.
|
|
31
|
+
- **No `wrapFetch`**: TanStack AI's fetch middleware stack does not apply; pass a custom `fetch` to the provider factory instead.
|
|
32
|
+
- **No streaming structured output**: `outputSchema` runs one non-streaming call after the agent loop.
|
|
33
|
+
- **Depends on TanStack AI internals**: it imports `@tanstack/ai/adapter-internals`, which has **no stability promise**. The peer range is pinned to the versions tested (`@tanstack/ai >=1.0.0 <1.1.0`), so an untested TanStack AI version is flagged at install time (npm reports a peer conflict, pnpm a warning) instead of surfacing as a run-time error. A release that widens the range follows after testing.
|
|
13
34
|
|
|
14
35
|
[](https://www.npmjs.com/package/@amjadiqbal/tanstack-ai-sdk-bridge)
|
|
15
|
-
[](https://tanstack.com/ai)
|
|
36
|
+
[](https://tanstack.com/ai)
|
|
16
37
|
[](LICENSE.md)
|
|
17
38
|
|
|
18
39
|
```ts
|
|
@@ -20,25 +41,29 @@ import { chat } from '@tanstack/ai'
|
|
|
20
41
|
import { aiSdkText } from '@amjadiqbal/tanstack-ai-sdk-bridge'
|
|
21
42
|
import { deepseek } from '@ai-sdk/deepseek'
|
|
22
43
|
|
|
44
|
+
// Step 1 of a migration: keep the AI SDK provider, move to TanStack AI's chat().
|
|
23
45
|
const stream = chat({
|
|
24
46
|
adapter: aiSdkText(deepseek('deepseek-chat')),
|
|
25
47
|
messages: [{ role: 'user', content: 'Hello!' }],
|
|
26
48
|
threadId: 'thread-1',
|
|
27
49
|
})
|
|
50
|
+
// Step 2: swap to the native adapter (openaiCompatible from @tanstack/ai-openai/compatible for DeepSeek).
|
|
28
51
|
```
|
|
29
52
|
|
|
30
|
-
- **Text**: streaming chat, tool calls (TanStack's agent loop runs your tools), reasoning, structured output (`outputSchema`), token usage, abort.
|
|
53
|
+
- **Text**: streaming chat, tool calls (TanStack's agent loop runs your tools), reasoning, structured output (`outputSchema`), token usage, abort (see [Abort](#abort)).
|
|
31
54
|
- **Embeddings, transcription, speech, image generation, rerank**: one adapter function each.
|
|
32
55
|
- **AI SDK 5, 6 and 7 providers**: provider spec v2, v3 and v4 are all read.
|
|
33
|
-
- **No
|
|
56
|
+
- **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.
|
|
57
|
+
|
|
58
|
+
Tested with (local wire-level mock, no live APIs): `@ai-sdk/deepseek`, `@ai-sdk/anthropic`, `@ai-sdk/openai`, `@ai-sdk/togetherai`, `@ai-sdk/cerebras`, `@ai-sdk/fireworks`, `@ai-sdk/cohere` (rerank), `ollama-ai-provider-v2` and `@ai-sdk/openai-compatible` 1.x, 2.x and 3.x (AI SDK 5, 6 and 7). See [What is tested](#what-is-tested).
|
|
34
59
|
|
|
35
60
|
## Install
|
|
36
61
|
|
|
37
62
|
```bash
|
|
38
|
-
npm i @amjadiqbal/tanstack-ai-sdk-bridge @tanstack/ai
|
|
63
|
+
npm i @amjadiqbal/tanstack-ai-sdk-bridge @tanstack/ai@1.0
|
|
39
64
|
```
|
|
40
65
|
|
|
41
|
-
Then the AI SDK provider you
|
|
66
|
+
Then the AI SDK provider you already use, for example `npm i @ai-sdk/deepseek`. Requires `@tanstack/ai` 1.0.x (tested: 1.0.0) and Node 20+.
|
|
42
67
|
|
|
43
68
|
## Usage
|
|
44
69
|
|
|
@@ -112,7 +137,13 @@ const product = await chat({
|
|
|
112
137
|
// product: { name: string; price: number }
|
|
113
138
|
```
|
|
114
139
|
|
|
115
|
-
|
|
140
|
+
What the bridge does for `outputSchema`, exactly:
|
|
141
|
+
|
|
142
|
+
- 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.
|
|
143
|
+
- It joins the text parts of the answer and parses them as JSON. An answer wrapped in a Markdown code fence is accepted.
|
|
144
|
+
- 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.
|
|
145
|
+
- The bridge does not check the data against the schema; `chat()` does that with your schema after the bridge returns.
|
|
146
|
+
- `structuredOutputStream` and combined tools-plus-schema in one request are not implemented, so TanStack AI uses its non-streaming path.
|
|
116
147
|
|
|
117
148
|
### Reasoning
|
|
118
149
|
|
|
@@ -141,7 +172,7 @@ const { embeddings } = await embed({
|
|
|
141
172
|
})
|
|
142
173
|
```
|
|
143
174
|
|
|
144
|
-
Large inputs are split by the model's `maxEmbeddingsPerCall`. `dimensions`
|
|
175
|
+
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
176
|
|
|
146
177
|
### Transcription, speech, image, rerank
|
|
147
178
|
|
|
@@ -168,11 +199,27 @@ await rerank({ adapter: aiSdkRerank(cohere.reranking('rerank-v3.5')), query: 're
|
|
|
168
199
|
| `generateImage()` | `aiSdkImage(model)` | `ImageModel` | v2, v3, v4 |
|
|
169
200
|
| `rerank()` | `aiSdkRerank(model)` | `RerankingModel` | v3, v4 |
|
|
170
201
|
|
|
202
|
+
Activity-specific `modelOptions`, on top of `providerOptions` and `headers`:
|
|
203
|
+
|
|
204
|
+
| Adapter | `modelOptions` | Notes |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `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`. |
|
|
207
|
+
| `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, FLAC, AAC, and Ogg: `opus` when the Ogg stream is Opus, else `ogg`), else the `format` you asked for, else `unknown`. When the format is `unknown` (or a format with no known media type), `contentType` is `application/octet-stream`. |
|
|
208
|
+
| `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. |
|
|
209
|
+
|
|
171
210
|
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
211
|
|
|
212
|
+
### Abort
|
|
213
|
+
|
|
214
|
+
| Activity | How to cancel | Passed to the AI SDK model |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `chat()` | `abortController` (or the `request` signal) | yes: `abortSignal`, and the stream is cancelled |
|
|
217
|
+
| `generateTranscription()`, `generateSpeech()`, `generateImage()`, `rerank()` | `abortSignal` | yes |
|
|
218
|
+
| `embed()` | none: TanStack AI's `embed()` takes no signal | no |
|
|
219
|
+
|
|
173
220
|
## Migrating from the Vercel AI SDK
|
|
174
221
|
|
|
175
|
-
TanStack's own [migration guide](https://tanstack.com/ai/latest/docs/migration/migration-from-vercel-ai)
|
|
222
|
+
Follow TanStack's own [migration guide](https://tanstack.com/ai/latest/docs/migration/migration-from-vercel-ai); it ends with your provider packages swapped for native TanStack AI adapters. The bridge lets you do that last step later, one provider at a time:
|
|
176
223
|
|
|
177
224
|
| Vercel AI SDK | TanStack AI with the bridge |
|
|
178
225
|
|---|---|
|
|
@@ -187,7 +234,7 @@ TanStack's own [migration guide](https://tanstack.com/ai/latest/docs/migration/m
|
|
|
187
234
|
| `useChat` from `@ai-sdk/react` | `useChat` from `@tanstack/ai-react` |
|
|
188
235
|
| `result.toUIMessageStreamResponse()` | `toServerSentEventsResponse(stream)` |
|
|
189
236
|
|
|
190
|
-
|
|
237
|
+
Order: move the server route and client first with the bridge, ship, then replace each bridged provider with its native TanStack AI adapter (see the table at the top), and remove this package when none is left.
|
|
191
238
|
|
|
192
239
|
## What is tested
|
|
193
240
|
|
|
@@ -211,11 +258,13 @@ Everything below ran on 2026-10-23 with `@tanstack/ai` 1.0.0. **No live provider
|
|
|
211
258
|
|
|
212
259
|
## Limits
|
|
213
260
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
The [What you lose versus a native adapter](#what-you-lose-versus-a-native-adapter) list at the top, plus:
|
|
262
|
+
|
|
263
|
+
|
|
217
264
|
- Provider file handles (`{ type: 'file' }` sources) are rejected; send images and documents as data or URLs.
|
|
218
|
-
- Transcription `language` and `
|
|
265
|
+
- Transcription `language`, `prompt` and `responseFormat` are not sent; see the options table.
|
|
266
|
+
- Embeddings cannot be aborted (TanStack AI's `embed()` has no signal).
|
|
267
|
+
- **TanStack AI internals:** the bridge imports `transformMessagesForReplay`, `tanstackMetadata` and `toRunErrorPayload` from `@tanstack/ai/adapter-internals`. That subpath has no stability promise (TanStack AI's maintainers say so), so the peer range is `>=1.0.0 <1.1.0`, the versions tested. Newer TanStack AI releases are flagged at install time until a tested release widens the range.
|
|
219
268
|
- AI SDK reranking models report no token usage, so `rerank()` usage is zero.
|
|
220
269
|
- Video, audio generation and realtime models are not bridged yet.
|
|
221
270
|
|
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,12 @@ 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`. `contentType` is the header value, else the media type of the
|
|
108
|
+
* format, else `application/octet-stream`.
|
|
109
|
+
*/
|
|
102
110
|
export declare class AiSdkSpeechAdapter<TModel extends string = string> extends BaseTTSAdapter<TModel, AiSdkSpeechModelOptions> {
|
|
103
111
|
readonly name: string;
|
|
104
112
|
readonly speechModel: AiSdkSpeechModel;
|
|
@@ -177,7 +185,9 @@ interface AiSdkTranscriptionModelOptions extends AiSdkCallOptions {
|
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
179
187
|
* TanStack AI transcription adapter around an AI SDK transcription model.
|
|
180
|
-
* `language` and `
|
|
188
|
+
* `language`, `prompt` and `responseFormat` have no AI SDK equivalent and are not sent: pass the provider's own
|
|
189
|
+
* options in `modelOptions.providerOptions`. The result always has `text`, plus `segments`, `language` and `duration`
|
|
190
|
+
* when the provider returns them. An `http(s)` URL as `audio` is downloaded with the global `fetch`, not the provider's.
|
|
181
191
|
*/
|
|
182
192
|
export declare class AiSdkTranscriptionAdapter<TModel extends string = string> extends BaseTranscriptionAdapter<TModel, AiSdkTranscriptionModelOptions> {
|
|
183
193
|
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 }
|
|
@@ -300,13 +301,51 @@ const SPECS$2 = [
|
|
|
300
301
|
];
|
|
301
302
|
const CONTENT_TYPES = {
|
|
302
303
|
mp3: "audio/mpeg",
|
|
303
|
-
opus: "audio/
|
|
304
|
+
opus: "audio/ogg",
|
|
305
|
+
ogg: "audio/ogg",
|
|
304
306
|
aac: "audio/aac",
|
|
305
307
|
flac: "audio/flac",
|
|
306
308
|
wav: "audio/wav",
|
|
307
309
|
pcm: "audio/pcm"
|
|
308
310
|
};
|
|
309
|
-
|
|
311
|
+
const FORMATS_BY_TYPE = {
|
|
312
|
+
"audio/mpeg": "mp3",
|
|
313
|
+
"audio/mp3": "mp3",
|
|
314
|
+
"audio/wav": "wav",
|
|
315
|
+
"audio/x-wav": "wav",
|
|
316
|
+
"audio/wave": "wav",
|
|
317
|
+
"audio/opus": "opus",
|
|
318
|
+
"audio/ogg": "ogg",
|
|
319
|
+
"audio/aac": "aac",
|
|
320
|
+
"audio/flac": "flac",
|
|
321
|
+
"audio/x-flac": "flac",
|
|
322
|
+
"audio/pcm": "pcm",
|
|
323
|
+
"audio/l16": "pcm"
|
|
324
|
+
};
|
|
325
|
+
function headerValue(headers, name) {
|
|
326
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
327
|
+
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === name && typeof value === "string") return value;
|
|
328
|
+
}
|
|
329
|
+
function formatFromContentType(type) {
|
|
330
|
+
return type ? FORMATS_BY_TYPE[type.toLowerCase()] : void 0;
|
|
331
|
+
}
|
|
332
|
+
/** Recognises MP3, WAV, Ogg (Opus or other), FLAC and ADTS AAC from their first bytes. */
|
|
333
|
+
function formatFromBytes(bytes) {
|
|
334
|
+
const ascii = (start, text) => [...text].every((char, i) => bytes[start + i] === char.charCodeAt(0));
|
|
335
|
+
if (ascii(0, "RIFF") && ascii(8, "WAVE")) return "wav";
|
|
336
|
+
if (ascii(0, "OggS")) return ascii(27 + (bytes[26] ?? 0), "OpusHead") ? "opus" : "ogg";
|
|
337
|
+
if (ascii(0, "fLaC")) return "flac";
|
|
338
|
+
if (ascii(0, "ID3")) return "mp3";
|
|
339
|
+
const b0 = bytes[0] ?? 0;
|
|
340
|
+
const b1 = bytes[1] ?? 0;
|
|
341
|
+
if (b0 === 255 && (b1 & 224) === 224) return (b1 & 6) === 0 ? "aac" : "mp3";
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* TanStack AI text-to-speech adapter around an AI SDK speech model.
|
|
345
|
+
* `format` in the result is what the provider returned (content-type header or audio bytes),
|
|
346
|
+
* falling back to the requested format, else `unknown`. `contentType` is the header value, else the media type of the
|
|
347
|
+
* format, else `application/octet-stream`.
|
|
348
|
+
*/
|
|
310
349
|
var AiSdkSpeechAdapter = class extends BaseTTSAdapter {
|
|
311
350
|
name;
|
|
312
351
|
speechModel;
|
|
@@ -333,15 +372,16 @@ var AiSdkSpeechAdapter = class extends BaseTTSAdapter {
|
|
|
333
372
|
...modelOptions?.language !== void 0 && { language: modelOptions.language },
|
|
334
373
|
...callExtras(modelOptions, abortSignal)
|
|
335
374
|
});
|
|
375
|
+
const bytes = typeof result.audio === "string" ? base64ToBytes(result.audio) : result.audio;
|
|
336
376
|
const audio = typeof result.audio === "string" ? result.audio : bytesToBase64(result.audio);
|
|
337
|
-
const
|
|
338
|
-
const
|
|
377
|
+
const headerType = headerValue(result.response?.headers, "content-type")?.split(";")[0]?.trim();
|
|
378
|
+
const outputFormat = formatFromContentType(headerType) ?? formatFromBytes(bytes) ?? format ?? "unknown";
|
|
339
379
|
return {
|
|
340
380
|
id: randomId(this.name),
|
|
341
381
|
model: result.response?.modelId ?? model,
|
|
342
382
|
audio,
|
|
343
383
|
format: outputFormat,
|
|
344
|
-
contentType: headerType
|
|
384
|
+
contentType: headerType || CONTENT_TYPES[outputFormat] || "application/octet-stream"
|
|
345
385
|
};
|
|
346
386
|
} catch (error) {
|
|
347
387
|
logger.errors(`${this.name}.generateSpeech fatal`, {
|
|
@@ -1050,7 +1090,9 @@ async function toAudio(audio, fallbackType, signal) {
|
|
|
1050
1090
|
}
|
|
1051
1091
|
/**
|
|
1052
1092
|
* TanStack AI transcription adapter around an AI SDK transcription model.
|
|
1053
|
-
* `language` and `
|
|
1093
|
+
* `language`, `prompt` and `responseFormat` have no AI SDK equivalent and are not sent: pass the provider's own
|
|
1094
|
+
* options in `modelOptions.providerOptions`. The result always has `text`, plus `segments`, `language` and `duration`
|
|
1095
|
+
* when the provider returns them. An `http(s)` URL as `audio` is downloaded with the global `fetch`, not the provider's.
|
|
1054
1096
|
*/
|
|
1055
1097
|
var AiSdkTranscriptionAdapter = class extends BaseTranscriptionAdapter {
|
|
1056
1098
|
name;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amjadiqbal/tanstack-ai-sdk-bridge",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Third-party migration stepping stone: run Vercel AI SDK provider models (@ai-sdk/* and community providers) inside TanStack AI while you move to native TanStack AI adapters. Chat with tools, reasoning and structured output, plus embeddings, transcription, speech, image and rerank. Not affiliated with TanStack.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tanstack",
|
|
7
7
|
"tanstack-ai",
|
|
@@ -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"
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"@ai-sdk/provider": "^4.0.32"
|
|
59
58
|
},
|
|
60
59
|
"peerDependencies": {
|
|
61
|
-
"@tanstack/ai": "
|
|
60
|
+
"@tanstack/ai": ">=1.0.0 <1.1.0"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
63
|
"@ai-sdk/anthropic": "4.0.86",
|