@tanstack/ai 0.43.1 → 0.44.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.
Files changed (73) hide show
  1. package/dist/esm/activities/chat/index.js +18 -0
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/messages.js +21 -8
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/embed/adapter.d.ts +69 -0
  6. package/dist/esm/activities/embed/adapter.js +23 -0
  7. package/dist/esm/activities/embed/adapter.js.map +1 -0
  8. package/dist/esm/activities/embed/index.d.ts +117 -0
  9. package/dist/esm/activities/embed/index.js +166 -0
  10. package/dist/esm/activities/embed/index.js.map +1 -0
  11. package/dist/esm/activities/error-payload.d.ts +8 -0
  12. package/dist/esm/activities/error-payload.js +29 -17
  13. package/dist/esm/activities/error-payload.js.map +1 -1
  14. package/dist/esm/activities/generateAudio/index.d.ts +12 -0
  15. package/dist/esm/activities/generateAudio/index.js +19 -6
  16. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  17. package/dist/esm/activities/generateImage/index.d.ts +12 -0
  18. package/dist/esm/activities/generateImage/index.js +21 -7
  19. package/dist/esm/activities/generateImage/index.js.map +1 -1
  20. package/dist/esm/activities/generateSpeech/index.d.ts +17 -1
  21. package/dist/esm/activities/generateSpeech/index.js +19 -6
  22. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  23. package/dist/esm/activities/generateTranscription/index.d.ts +17 -1
  24. package/dist/esm/activities/generateTranscription/index.js +19 -6
  25. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  26. package/dist/esm/activities/generateVideo/index.d.ts +18 -0
  27. package/dist/esm/activities/generateVideo/index.js +54 -15
  28. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  29. package/dist/esm/activities/index.d.ts +8 -2
  30. package/dist/esm/activities/index.js +11 -7
  31. package/dist/esm/activities/middleware/types.d.ts +1 -1
  32. package/dist/esm/activities/rerank/adapter.d.ts +63 -0
  33. package/dist/esm/activities/rerank/adapter.js +23 -0
  34. package/dist/esm/activities/rerank/adapter.js.map +1 -0
  35. package/dist/esm/activities/rerank/index.d.ts +92 -0
  36. package/dist/esm/activities/rerank/index.js +163 -0
  37. package/dist/esm/activities/rerank/index.js.map +1 -0
  38. package/dist/esm/activities/summarize/index.d.ts +17 -1
  39. package/dist/esm/activities/summarize/index.js +19 -5
  40. package/dist/esm/activities/summarize/index.js.map +1 -1
  41. package/dist/esm/index.d.ts +7 -2
  42. package/dist/esm/index.js +5 -1
  43. package/dist/esm/middlewares/otel.js +20 -2
  44. package/dist/esm/middlewares/otel.js.map +1 -1
  45. package/dist/esm/types.d.ts +195 -0
  46. package/dist/esm/utilities/activity-abort.d.ts +53 -0
  47. package/dist/esm/utilities/activity-abort.js +150 -0
  48. package/dist/esm/utilities/activity-abort.js.map +1 -0
  49. package/dist/esm/utilities/embedding-input.d.ts +32 -0
  50. package/dist/esm/utilities/embedding-input.js +61 -0
  51. package/dist/esm/utilities/embedding-input.js.map +1 -0
  52. package/package.json +3 -3
  53. package/skills/ai-core/media-generation/SKILL.md +55 -23
  54. package/src/activities/chat/index.ts +27 -2
  55. package/src/activities/chat/messages.ts +30 -1
  56. package/src/activities/embed/adapter.ts +112 -0
  57. package/src/activities/embed/index.ts +318 -0
  58. package/src/activities/error-payload.ts +41 -9
  59. package/src/activities/generateAudio/index.ts +47 -5
  60. package/src/activities/generateImage/index.ts +48 -5
  61. package/src/activities/generateSpeech/index.ts +52 -9
  62. package/src/activities/generateTranscription/index.ts +52 -9
  63. package/src/activities/generateVideo/index.ts +131 -33
  64. package/src/activities/index.ts +44 -0
  65. package/src/activities/middleware/types.ts +2 -0
  66. package/src/activities/rerank/adapter.ts +90 -0
  67. package/src/activities/rerank/index.ts +302 -0
  68. package/src/activities/summarize/index.ts +59 -19
  69. package/src/index.ts +19 -0
  70. package/src/middlewares/otel.ts +38 -3
  71. package/src/types.ts +219 -0
  72. package/src/utilities/activity-abort.ts +197 -0
  73. package/src/utilities/embedding-input.ts +83 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared abort/timeout composition for media (and summarize) activities.
3
+ *
4
+ * Callers pass optional `timeout` and/or `abortSignal` on activity options.
5
+ * Core composes them into one effective signal, races the adapter call so a
6
+ * hung provider still rejects, clears timeout resources on settle, and
7
+ * classifies aborts so lifecycle middleware gets `onAbort` rather than
8
+ * `onError`.
9
+ */
10
+ /**
11
+ * Combine two optional AbortSignals into one that aborts when either does.
12
+ * Returns the other signal directly when one is absent or already aborted.
13
+ * First abort wins and preserves its reason.
14
+ *
15
+ * Manual implementation — `AbortSignal.any` requires Node >= 20.3.
16
+ */
17
+ export declare function combineAbortSignals(a: AbortSignal | undefined, b: AbortSignal | undefined): AbortSignal | undefined;
18
+ /** Normalize an abort reason into an Error the activity can reject with. */
19
+ export declare function toAbortError(reason: unknown): Error;
20
+ export interface ActivityAbortControls {
21
+ /** Effective signal, or `undefined` when neither timeout nor caller signal. */
22
+ signal: AbortSignal | undefined;
23
+ /** Clear the timeout timer if one was set. Idempotent. */
24
+ clear: () => void;
25
+ }
26
+ /**
27
+ * Compose an activity-level timeout with a caller AbortSignal.
28
+ *
29
+ * - No SDK-wide default timeout; omit both for unlimited wait.
30
+ * - First of caller cancellation or timeout wins and keeps its reason.
31
+ * - Call `clear()` when the activity settles (success or failure) so timers
32
+ * do not leak.
33
+ */
34
+ export declare function createActivityAbortControls(options: {
35
+ abortSignal?: AbortSignal;
36
+ timeout?: number;
37
+ }): ActivityAbortControls;
38
+ /**
39
+ * Reject when `signal` aborts, even if the underlying promise ignores it.
40
+ * Ensures activity-level timeouts work for adapters that do not yet forward
41
+ * the signal to the provider SDK.
42
+ *
43
+ * When the signal wins, the adapter promise is observed with an empty handler
44
+ * so a later settle cannot surface as an unhandled rejection.
45
+ */
46
+ export declare function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
47
+ /**
48
+ * Whether a thrown value (and optional effective signal) should route to
49
+ * middleware `onAbort` instead of `onError`.
50
+ */
51
+ export declare function isActivityAbortError(error: unknown, signal?: AbortSignal): boolean;
52
+ /** Best-effort string reason for {@link GenerationAbortInfo}. */
53
+ export declare function abortReasonMessage(error: unknown, signal?: AbortSignal): string | undefined;
@@ -0,0 +1,150 @@
1
+ //#region src/utilities/activity-abort.ts
2
+ /**
3
+ * Shared abort/timeout composition for media (and summarize) activities.
4
+ *
5
+ * Callers pass optional `timeout` and/or `abortSignal` on activity options.
6
+ * Core composes them into one effective signal, races the adapter call so a
7
+ * hung provider still rejects, clears timeout resources on settle, and
8
+ * classifies aborts so lifecycle middleware gets `onAbort` rather than
9
+ * `onError`.
10
+ */
11
+ var ABORT_ERROR_NAMES = /* @__PURE__ */ new Set([
12
+ "AbortError",
13
+ "TimeoutError",
14
+ "APIUserAbortError",
15
+ "RequestAbortedError"
16
+ ]);
17
+ /**
18
+ * Combine two optional AbortSignals into one that aborts when either does.
19
+ * Returns the other signal directly when one is absent or already aborted.
20
+ * First abort wins and preserves its reason.
21
+ *
22
+ * Manual implementation — `AbortSignal.any` requires Node >= 20.3.
23
+ */
24
+ function combineAbortSignals(a, b) {
25
+ if (!a) return b;
26
+ if (!b) return a;
27
+ if (a.aborted) return a;
28
+ if (b.aborted) return b;
29
+ const controller = new AbortController();
30
+ const onAbort = (source) => () => {
31
+ controller.abort(source.reason);
32
+ };
33
+ a.addEventListener("abort", onAbort(a), { once: true });
34
+ b.addEventListener("abort", onAbort(b), { once: true });
35
+ return controller.signal;
36
+ }
37
+ function createTimeoutReason(ms) {
38
+ if (typeof DOMException !== "undefined") return new DOMException(`Activity timed out after ${ms}ms`, "TimeoutError");
39
+ const err = /* @__PURE__ */ new Error(`Activity timed out after ${ms}ms`);
40
+ err.name = "TimeoutError";
41
+ return err;
42
+ }
43
+ /** Normalize an abort reason into an Error the activity can reject with. */
44
+ function toAbortError(reason) {
45
+ if (reason instanceof Error) return reason;
46
+ if (typeof reason === "string" && reason.length > 0) {
47
+ const err = new Error(reason);
48
+ err.name = "AbortError";
49
+ return err;
50
+ }
51
+ const err = /* @__PURE__ */ new Error("The operation was aborted");
52
+ err.name = "AbortError";
53
+ return err;
54
+ }
55
+ /**
56
+ * Compose an activity-level timeout with a caller AbortSignal.
57
+ *
58
+ * - No SDK-wide default timeout; omit both for unlimited wait.
59
+ * - First of caller cancellation or timeout wins and keeps its reason.
60
+ * - Call `clear()` when the activity settles (success or failure) so timers
61
+ * do not leak.
62
+ */
63
+ function createActivityAbortControls(options) {
64
+ let timeoutId;
65
+ let timeoutSignal;
66
+ if (options.timeout !== void 0) {
67
+ if (!Number.isFinite(options.timeout) || options.timeout < 0) throw new Error(`Invalid activity timeout: expected a non-negative finite number, got ${String(options.timeout)}`);
68
+ const controller = new AbortController();
69
+ timeoutSignal = controller.signal;
70
+ const ms = options.timeout;
71
+ timeoutId = setTimeout(() => {
72
+ controller.abort(createTimeoutReason(ms));
73
+ }, ms);
74
+ }
75
+ return {
76
+ signal: combineAbortSignals(options.abortSignal, timeoutSignal),
77
+ clear: () => {
78
+ if (timeoutId !== void 0) {
79
+ clearTimeout(timeoutId);
80
+ timeoutId = void 0;
81
+ }
82
+ }
83
+ };
84
+ }
85
+ /**
86
+ * Reject when `signal` aborts, even if the underlying promise ignores it.
87
+ * Ensures activity-level timeouts work for adapters that do not yet forward
88
+ * the signal to the provider SDK.
89
+ *
90
+ * When the signal wins, the adapter promise is observed with an empty handler
91
+ * so a later settle cannot surface as an unhandled rejection.
92
+ */
93
+ function raceWithAbort(promise, signal) {
94
+ if (!signal) return promise;
95
+ const swallow = () => {
96
+ promise.then(() => void 0, () => void 0);
97
+ };
98
+ if (signal.aborted) {
99
+ swallow();
100
+ return Promise.reject(toAbortError(signal.reason));
101
+ }
102
+ return new Promise((resolve, reject) => {
103
+ let settled = false;
104
+ const onAbort = () => {
105
+ if (settled) return;
106
+ settled = true;
107
+ cleanup();
108
+ swallow();
109
+ reject(toAbortError(signal.reason));
110
+ };
111
+ const cleanup = () => {
112
+ signal.removeEventListener("abort", onAbort);
113
+ };
114
+ signal.addEventListener("abort", onAbort, { once: true });
115
+ promise.then((value) => {
116
+ if (settled) return;
117
+ settled = true;
118
+ cleanup();
119
+ resolve(value);
120
+ }, (error) => {
121
+ if (settled) return;
122
+ settled = true;
123
+ cleanup();
124
+ reject(error);
125
+ });
126
+ });
127
+ }
128
+ /**
129
+ * Whether a thrown value (and optional effective signal) should route to
130
+ * middleware `onAbort` instead of `onError`.
131
+ */
132
+ function isActivityAbortError(error, signal) {
133
+ if (signal?.aborted) return true;
134
+ if (!error || typeof error !== "object") return false;
135
+ const name = error.name;
136
+ return typeof name === "string" && ABORT_ERROR_NAMES.has(name);
137
+ }
138
+ /** Best-effort string reason for {@link GenerationAbortInfo}. */
139
+ function abortReasonMessage(error, signal) {
140
+ if (signal?.reason !== void 0) {
141
+ if (typeof signal.reason === "string") return signal.reason;
142
+ if (signal.reason instanceof Error) return signal.reason.message;
143
+ }
144
+ if (error instanceof Error) return error.message;
145
+ if (typeof error === "string") return error;
146
+ }
147
+ //#endregion
148
+ export { abortReasonMessage, combineAbortSignals, createActivityAbortControls, isActivityAbortError, raceWithAbort, toAbortError };
149
+
150
+ //# sourceMappingURL=activity-abort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-abort.js","names":[],"sources":["../../../src/utilities/activity-abort.ts"],"sourcesContent":["/**\n * Shared abort/timeout composition for media (and summarize) activities.\n *\n * Callers pass optional `timeout` and/or `abortSignal` on activity options.\n * Core composes them into one effective signal, races the adapter call so a\n * hung provider still rejects, clears timeout resources on settle, and\n * classifies aborts so lifecycle middleware gets `onAbort` rather than\n * `onError`.\n */\n\nconst ABORT_ERROR_NAMES = new Set([\n 'AbortError',\n 'TimeoutError',\n 'APIUserAbortError',\n 'RequestAbortedError',\n])\n\n/**\n * Combine two optional AbortSignals into one that aborts when either does.\n * Returns the other signal directly when one is absent or already aborted.\n * First abort wins and preserves its reason.\n *\n * Manual implementation — `AbortSignal.any` requires Node >= 20.3.\n */\nexport function combineAbortSignals(\n a: AbortSignal | undefined,\n b: AbortSignal | undefined,\n): AbortSignal | undefined {\n if (!a) return b\n if (!b) return a\n if (a.aborted) return a\n if (b.aborted) return b\n const controller = new AbortController()\n const onAbort = (source: AbortSignal) => () => {\n controller.abort(source.reason)\n }\n a.addEventListener('abort', onAbort(a), { once: true })\n b.addEventListener('abort', onAbort(b), { once: true })\n return controller.signal\n}\n\nfunction createTimeoutReason(ms: number): Error {\n if (typeof DOMException !== 'undefined') {\n return new DOMException(`Activity timed out after ${ms}ms`, 'TimeoutError')\n }\n const err = new Error(`Activity timed out after ${ms}ms`)\n err.name = 'TimeoutError'\n return err\n}\n\n/** Normalize an abort reason into an Error the activity can reject with. */\nexport function toAbortError(reason: unknown): Error {\n if (reason instanceof Error) return reason\n if (typeof reason === 'string' && reason.length > 0) {\n const err = new Error(reason)\n err.name = 'AbortError'\n return err\n }\n const err = new Error('The operation was aborted')\n err.name = 'AbortError'\n return err\n}\n\nexport interface ActivityAbortControls {\n /** Effective signal, or `undefined` when neither timeout nor caller signal. */\n signal: AbortSignal | undefined\n /** Clear the timeout timer if one was set. Idempotent. */\n clear: () => void\n}\n\n/**\n * Compose an activity-level timeout with a caller AbortSignal.\n *\n * - No SDK-wide default timeout; omit both for unlimited wait.\n * - First of caller cancellation or timeout wins and keeps its reason.\n * - Call `clear()` when the activity settles (success or failure) so timers\n * do not leak.\n */\nexport function createActivityAbortControls(options: {\n abortSignal?: AbortSignal\n timeout?: number\n}): ActivityAbortControls {\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n let timeoutSignal: AbortSignal | undefined\n\n if (options.timeout !== undefined) {\n if (!Number.isFinite(options.timeout) || options.timeout < 0) {\n throw new Error(\n `Invalid activity timeout: expected a non-negative finite number, got ${String(options.timeout)}`,\n )\n }\n const controller = new AbortController()\n timeoutSignal = controller.signal\n const ms = options.timeout\n timeoutId = setTimeout(() => {\n controller.abort(createTimeoutReason(ms))\n }, ms)\n }\n\n const signal = combineAbortSignals(options.abortSignal, timeoutSignal)\n\n return {\n signal,\n clear: () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId)\n timeoutId = undefined\n }\n },\n }\n}\n\n/**\n * Reject when `signal` aborts, even if the underlying promise ignores it.\n * Ensures activity-level timeouts work for adapters that do not yet forward\n * the signal to the provider SDK.\n *\n * When the signal wins, the adapter promise is observed with an empty handler\n * so a later settle cannot surface as an unhandled rejection.\n */\nexport function raceWithAbort<T>(\n promise: Promise<T>,\n signal: AbortSignal | undefined,\n): Promise<T> {\n if (!signal) return promise\n\n const swallow = () => {\n // Observe the adapter promise without acting on its outcome so a late\n // reject after we already aborted cannot become an unhandled rejection.\n promise.then(\n () => undefined,\n () => undefined,\n )\n }\n\n if (signal.aborted) {\n swallow()\n return Promise.reject(toAbortError(signal.reason))\n }\n\n return new Promise<T>((resolve, reject) => {\n let settled = false\n const onAbort = () => {\n if (settled) return\n settled = true\n cleanup()\n swallow()\n reject(toAbortError(signal.reason))\n }\n const cleanup = () => {\n signal.removeEventListener('abort', onAbort)\n }\n signal.addEventListener('abort', onAbort, { once: true })\n promise.then(\n (value) => {\n if (settled) return\n settled = true\n cleanup()\n resolve(value)\n },\n (error: unknown) => {\n if (settled) return\n settled = true\n cleanup()\n reject(error)\n },\n )\n })\n}\n\n/**\n * Whether a thrown value (and optional effective signal) should route to\n * middleware `onAbort` instead of `onError`.\n */\nexport function isActivityAbortError(\n error: unknown,\n signal?: AbortSignal,\n): boolean {\n if (signal?.aborted) return true\n if (!error || typeof error !== 'object') return false\n const name = (error as { name?: unknown }).name\n return typeof name === 'string' && ABORT_ERROR_NAMES.has(name)\n}\n\n/** Best-effort string reason for {@link GenerationAbortInfo}. */\nexport function abortReasonMessage(\n error: unknown,\n signal?: AbortSignal,\n): string | undefined {\n if (signal?.reason !== undefined) {\n if (typeof signal.reason === 'string') return signal.reason\n if (signal.reason instanceof Error) return signal.reason.message\n }\n if (error instanceof Error) return error.message\n if (typeof error === 'string') return error\n return undefined\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { EmbeddingInputItem, ImagePart } from '../types.js';
2
+ /**
3
+ * One embedding input item resolved into its text and image constituents.
4
+ * Produced by {@link resolveEmbeddingInput}; adapters map each entry onto
5
+ * one provider-native input (one vector per entry).
6
+ */
7
+ export interface ResolvedEmbeddingItem {
8
+ /** Text contents of the item, in order (empty for image-only items) */
9
+ texts: Array<string>;
10
+ /** Image parts of the item, in order (empty for text-only items) */
11
+ images: Array<ImagePart>;
12
+ }
13
+ /**
14
+ * Resolve each embedding input item into its text and image constituents,
15
+ * preserving input order (result[i] corresponds to input[i] and to the
16
+ * vector at index i).
17
+ */
18
+ export declare function resolveEmbeddingInput(input: Array<EmbeddingInputItem>): Array<ResolvedEmbeddingItem>;
19
+ /**
20
+ * Extract plain text inputs for a text-only embedding model, throwing a
21
+ * uniform error if any item carries an image. The per-model modality typing
22
+ * rejects these at compile time; this guard covers untyped/dynamic callers.
23
+ */
24
+ export declare function requireTextOnlyEmbeddingInput(input: Array<EmbeddingInputItem>, provider: string, model: string): Array<string>;
25
+ /**
26
+ * Count text-only and image-carrying items for observability events. Never
27
+ * exposes input content.
28
+ */
29
+ export declare function countEmbeddingInputModalities(input: Array<EmbeddingInputItem>): {
30
+ textInputCount: number;
31
+ imageInputCount: number;
32
+ };
@@ -0,0 +1,61 @@
1
+ //#region src/utilities/embedding-input.ts
2
+ function resolveItem(item) {
3
+ if (typeof item === "string") return {
4
+ texts: [item],
5
+ images: []
6
+ };
7
+ if (Array.isArray(item)) {
8
+ const resolved = {
9
+ texts: [],
10
+ images: []
11
+ };
12
+ for (const part of item) if (part.type === "text") resolved.texts.push(part.content);
13
+ else resolved.images.push(part);
14
+ return resolved;
15
+ }
16
+ if (item.type === "text") return {
17
+ texts: [item.content],
18
+ images: []
19
+ };
20
+ return {
21
+ texts: [],
22
+ images: [item]
23
+ };
24
+ }
25
+ /**
26
+ * Resolve each embedding input item into its text and image constituents,
27
+ * preserving input order (result[i] corresponds to input[i] and to the
28
+ * vector at index i).
29
+ */
30
+ function resolveEmbeddingInput(input) {
31
+ return input.map(resolveItem);
32
+ }
33
+ /**
34
+ * Extract plain text inputs for a text-only embedding model, throwing a
35
+ * uniform error if any item carries an image. The per-model modality typing
36
+ * rejects these at compile time; this guard covers untyped/dynamic callers.
37
+ */
38
+ function requireTextOnlyEmbeddingInput(input, provider, model) {
39
+ return resolveEmbeddingInput(input).map((item, index) => {
40
+ if (item.images.length > 0) throw new Error(`${provider} model "${model}" only supports text embedding inputs; input item at index ${index} contains an image part`);
41
+ return item.texts.join("\n");
42
+ });
43
+ }
44
+ /**
45
+ * Count text-only and image-carrying items for observability events. Never
46
+ * exposes input content.
47
+ */
48
+ function countEmbeddingInputModalities(input) {
49
+ let textInputCount = 0;
50
+ let imageInputCount = 0;
51
+ for (const item of resolveEmbeddingInput(input)) if (item.images.length > 0) imageInputCount++;
52
+ else textInputCount++;
53
+ return {
54
+ textInputCount,
55
+ imageInputCount
56
+ };
57
+ }
58
+ //#endregion
59
+ export { countEmbeddingInputModalities, requireTextOnlyEmbeddingInput, resolveEmbeddingInput };
60
+
61
+ //# sourceMappingURL=embedding-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-input.js","names":[],"sources":["../../../src/utilities/embedding-input.ts"],"sourcesContent":["import type { EmbeddingInputItem, ImagePart } from '../types'\n\n/**\n * One embedding input item resolved into its text and image constituents.\n * Produced by {@link resolveEmbeddingInput}; adapters map each entry onto\n * one provider-native input (one vector per entry).\n */\nexport interface ResolvedEmbeddingItem {\n /** Text contents of the item, in order (empty for image-only items) */\n texts: Array<string>\n /** Image parts of the item, in order (empty for text-only items) */\n images: Array<ImagePart>\n}\n\nfunction resolveItem(item: EmbeddingInputItem): ResolvedEmbeddingItem {\n if (typeof item === 'string') {\n return { texts: [item], images: [] }\n }\n // A nested array is a fused item: its parts embed together into one vector.\n if (Array.isArray(item)) {\n const resolved: ResolvedEmbeddingItem = { texts: [], images: [] }\n for (const part of item) {\n if (part.type === 'text') {\n resolved.texts.push(part.content)\n } else {\n resolved.images.push(part)\n }\n }\n return resolved\n }\n if (item.type === 'text') {\n return { texts: [item.content], images: [] }\n }\n return { texts: [], images: [item] }\n}\n\n/**\n * Resolve each embedding input item into its text and image constituents,\n * preserving input order (result[i] corresponds to input[i] and to the\n * vector at index i).\n */\nexport function resolveEmbeddingInput(\n input: Array<EmbeddingInputItem>,\n): Array<ResolvedEmbeddingItem> {\n return input.map(resolveItem)\n}\n\n/**\n * Extract plain text inputs for a text-only embedding model, throwing a\n * uniform error if any item carries an image. The per-model modality typing\n * rejects these at compile time; this guard covers untyped/dynamic callers.\n */\nexport function requireTextOnlyEmbeddingInput(\n input: Array<EmbeddingInputItem>,\n provider: string,\n model: string,\n): Array<string> {\n return resolveEmbeddingInput(input).map((item, index) => {\n if (item.images.length > 0) {\n throw new Error(\n `${provider} model \"${model}\" only supports text embedding inputs; ` +\n `input item at index ${index} contains an image part`,\n )\n }\n return item.texts.join('\\n')\n })\n}\n\n/**\n * Count text-only and image-carrying items for observability events. Never\n * exposes input content.\n */\nexport function countEmbeddingInputModalities(\n input: Array<EmbeddingInputItem>,\n): { textInputCount: number; imageInputCount: number } {\n let textInputCount = 0\n let imageInputCount = 0\n for (const item of resolveEmbeddingInput(input)) {\n if (item.images.length > 0) imageInputCount++\n else textInputCount++\n }\n return { textInputCount, imageInputCount }\n}\n"],"mappings":";AAcA,SAAS,YAAY,MAAiD;CACpE,IAAI,OAAO,SAAS,UAClB,OAAO;EAAE,OAAO,CAAC,IAAI;EAAG,QAAQ,CAAC;CAAE;CAGrC,IAAI,MAAM,QAAQ,IAAI,GAAG;EACvB,MAAM,WAAkC;GAAE,OAAO,CAAC;GAAG,QAAQ,CAAC;EAAE;EAChE,KAAK,MAAM,QAAQ,MACjB,IAAI,KAAK,SAAS,QAChB,SAAS,MAAM,KAAK,KAAK,OAAO;OAEhC,SAAS,OAAO,KAAK,IAAI;EAG7B,OAAO;CACT;CACA,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,OAAO,CAAC,KAAK,OAAO;EAAG,QAAQ,CAAC;CAAE;CAE7C,OAAO;EAAE,OAAO,CAAC;EAAG,QAAQ,CAAC,IAAI;CAAE;AACrC;;;;;;AAOA,SAAgB,sBACd,OAC8B;CAC9B,OAAO,MAAM,IAAI,WAAW;AAC9B;;;;;;AAOA,SAAgB,8BACd,OACA,UACA,OACe;CACf,OAAO,sBAAsB,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,IAAI,KAAK,OAAO,SAAS,GACvB,MAAM,IAAI,MACR,GAAG,SAAS,UAAU,MAAM,6DACH,MAAM,wBACjC;EAEF,OAAO,KAAK,MAAM,KAAK,IAAI;CAC7B,CAAC;AACH;;;;;AAMA,SAAgB,8BACd,OACqD;CACrD,IAAI,iBAAiB;CACrB,IAAI,kBAAkB;CACtB,KAAK,MAAM,QAAQ,sBAAsB,KAAK,GAC5C,IAAI,KAAK,OAAO,SAAS,GAAG;MACvB;CAEP,OAAO;EAAE;EAAgB;CAAgB;AAC3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.43.1",
3
+ "version": "0.44.1",
4
4
  "description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -80,8 +80,8 @@
80
80
  "@ag-ui/core": "0.1.1-canary.beta.0",
81
81
  "@standard-schema/spec": "^1.1.0",
82
82
  "partial-json": "^0.1.7",
83
- "@tanstack/ai-event-client": "0.7.0",
84
- "@tanstack/ai-utils": "0.4.0"
83
+ "@tanstack/ai-event-client": "^0.8.0",
84
+ "@tanstack/ai-utils": "^0.4.0"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "@opentelemetry/api": ">=1.9.0"
@@ -4,9 +4,10 @@ description: >
4
4
  Image, audio, video, speech (TTS), and transcription generation using
5
5
  activity-specific adapters: generateImage() with openaiImage/geminiImage/byteplusImage,
6
6
  generateAudio() with geminiAudio/falAudio, generateVideo() with async
7
- polling (openaiVideo/geminiVideo/grokVideo/falVideo/byteplusVideo, per-model typed
8
- durations), generateSpeech() with openaiSpeech/byteplusSpeech, generateTranscription()
9
- with openaiTranscription/byteplusTranscription. React hooks: useGenerateImage, useGenerateAudio,
7
+ polling (openaiVideo/geminiVideo/grokVideo/falVideo/byteplusVideo/openRouterVideo,
8
+ per-model typed durations), generateSpeech() with openaiSpeech/byteplusSpeech,
9
+ generateTranscription() with openaiTranscription/byteplusTranscription. React hooks:
10
+ useGenerateImage, useGenerateAudio,
10
11
  useGenerateSpeech, useTranscription, useGenerateVideo.
11
12
  TanStack Start server function integration with toServerSentEventsResponse.
12
13
  type: sub-skill
@@ -251,7 +252,8 @@ await generateImage({
251
252
  ],
252
253
  })
253
254
 
254
- // Image-to-video (OpenAI Sora: single input_reference; fal: image_url + optional end_image_url)
255
+ // Image-to-video (OpenAI Sora: single input_reference; fal: image_url + optional
256
+ // end_image_url; OpenRouter: frame_images + input_references)
255
257
  import { generateVideo } from '@tanstack/ai'
256
258
  import { falVideo } from '@tanstack/ai-fal'
257
259
 
@@ -282,25 +284,25 @@ with `allowUrlFetch: true` on the adapter config
282
284
 
283
285
  **Role hints** (`metadata.role`):
284
286
 
285
- | Role | Maps to |
286
- | --------------- | ----------------------------------------------------------------------------------------------------- |
287
- | `'reference'` | fal `reference_image_urls`; Gemini multimodal part; positional otherwise |
288
- | `'character'` | Same as `'reference'`; Veo `referenceImages` slot (planned — no Veo adapter yet) |
289
- | `'mask'` | OpenAI `mask` (gpt-image-2, gpt-image-1, dall-e-2); fal `mask_url` |
290
- | `'control'` | fal `control_image_url` (ControlNet / depth / pose) |
291
- | `'start_frame'` | fal `start_image_url` (or the endpoint's field, e.g. `image_url` on Kling i2v); Veo `image` (planned) |
292
- | `'end_frame'` | fal `end_image_url` (or e.g. `tail_image_url` / `last_frame_url`); Veo `lastFrame` (planned) |
287
+ | Role | Maps to |
288
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
289
+ | `'reference'` | fal `reference_image_urls`; OpenRouter video `input_references[]`; Gemini multimodal part; positional otherwise |
290
+ | `'character'` | Same as `'reference'`; Veo `referenceImages`; OpenRouter `input_references[]` |
291
+ | `'mask'` | OpenAI `mask` (gpt-image-2, gpt-image-1, dall-e-2); fal `mask_url` |
292
+ | `'control'` | fal `control_image_url` (ControlNet / depth / pose) |
293
+ | `'start_frame'` | fal `start_image_url` (or the endpoint's field, e.g. `image_url` on Kling i2v); OpenRouter `frame_images[]` `first_frame`; Veo `image` |
294
+ | `'end_frame'` | fal `end_image_url` (or e.g. `tail_image_url` / `last_frame_url`); OpenRouter `frame_images[]` `last_frame`; Veo `lastFrame` |
293
295
 
294
296
  **Provider support matrix:**
295
297
 
296
- | Provider | `generateImage` image parts | `generateVideo` image parts |
297
- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
298
- | OpenAI | gpt-image-2 / gpt-image-1 / -mini → `images.edit()` (up to 16). dall-e-2 → edit (1). dall-e-3 throws. | Sora-2 / -pro → `input_reference` (single). Throws if >1. |
299
- | Gemini | Native (gemini-\*-flash-image, "nano-banana") → multimodal `contents`. Imagen throws. | No native Veo adapter yet — deferred to a follow-up. |
300
- | fal | Per-endpoint field names from a generated map (`pnpm generate:fal-image-fields`). Defaults: 1 input → `image_url`; >1 → `image_urls`; roles → `mask_url` / `control_image_url` / `reference_image_urls`. | Per-endpoint map (e.g. Kling i2v start frame → `image_url`). Defaults: 1 input → `image_url`; `start_frame`/`end_frame` → `start_image_url`/`end_image_url`; `reference` → `reference_image_urls`. |
301
- | Grok | grok-imagine models → `/v1/images/edits` JSON endpoint (≤3 sources, addressed by xAI in request order; prompt sent verbatim; mask/control throw). grok-2-image-1212 throws. | n/a |
302
- | OpenRouter | Prompt parts map 1:1 onto multimodal `text` / `image_url` content parts, preserving interleaved order. | n/a |
303
- | Anthropic | n/a (no image generation API). | n/a |
298
+ | Provider | `generateImage` image parts | `generateVideo` image parts |
299
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
300
+ | OpenAI | gpt-image-2 / gpt-image-1 / -mini → `images.edit()` (up to 16). dall-e-2 → edit (1). dall-e-3 throws. | Sora-2 / -pro → `input_reference` (single). Throws if >1. |
301
+ | Gemini | Native (gemini-\*-flash-image, "nano-banana") → multimodal `contents`. Imagen throws. | Veo → first un-roled / `'start_frame'` image is the input image; `'end_frame'` → `lastFrame`; `'reference'` / `'character'` → `referenceImages`. Omni Flash sends image/video parts as interaction content blocks (no role routing). |
302
+ | fal | Per-endpoint field names from a generated map (`pnpm generate:fal-image-fields`). Defaults: 1 input → `image_url`; >1 → `image_urls`; roles → `mask_url` / `control_image_url` / `reference_image_urls`. | Per-endpoint map (e.g. Kling i2v start frame → `image_url`). Defaults: 1 input → `image_url`; `start_frame`/`end_frame` → `start_image_url`/`end_image_url`; `reference` → `reference_image_urls`. |
303
+ | Grok | grok-imagine models → `/v1/images/edits` JSON endpoint (≤3 sources, addressed by xAI in request order; prompt sent verbatim; mask/control throw). grok-2-image-1212 throws. | n/a |
304
+ | OpenRouter | Prompt parts map 1:1 onto multimodal `text` / `image_url` content parts, preserving interleaved order. | Dedicated async API (`openRouterVideo`): `start_frame`/`end_frame` → `frame_images[]` (`first_frame`/`last_frame`); `reference`/`character` → `input_references[]`; an unroled image defaults to the start frame. Frame roles validated against the model's `supported_frame_images` metadata. |
305
+ | Anthropic | n/a (no image generation API). | n/a |
304
306
 
305
307
  Video and audio prompt parts follow the same `metadata.role` convention
306
308
  for video-to-video and lipsync flows on fal; other providers throw when
@@ -445,7 +447,13 @@ const { generate, result, isLoading } = useTranscription({
445
447
  ### 5. Video Generation (Experimental -- async polling)
446
448
 
447
449
  Video generation uses a jobs/polling architecture. The server creates a job,
448
- polls for status, and streams updates to the client.
450
+ polls for status, and streams updates to the client. Adapters: `openaiVideo`
451
+ (Sora), `geminiVideo` (Veo / Omni Flash), `grokVideo`, `byteplusVideo`
452
+ (Seedance), `falVideo` (Kling, MiniMax, Hunyuan, …), and `openRouterVideo`
453
+ (OpenRouter's dedicated `POST /api/v1/videos` gateway — Seedance, Veo, Wan,
454
+ Kling, Sora 2 Pro and others through one API key; `getVideoJobStatus()`
455
+ returns the video as a `data:` URL since OpenRouter's download URLs require
456
+ the API key, and surfaces the gateway-reported cost as `usage.cost`).
449
457
 
450
458
  ```typescript
451
459
  import {
@@ -541,8 +549,9 @@ image-to-video only — needs an `image` prompt part as the starting frame, text
541
549
  aspect-ratio size template like `'16:9_720p'`, integer durations 1-15s, reports
542
550
  `usage.unitsBilled` seconds and exact `usage.cost`), `byteplusVideo(...)` (Seedance —
543
551
  aspect-ratio size template like `'16:9_720p'`, durations 4-15s on the 2.0 family,
544
- 4-12s on 1.5-pro, 2-12s on the 1.0-pro models; reads `ARK_API_KEY`), and
545
- `falVideo(...)` (hosted models, see cost tracking below).
552
+ 4-12s on 1.5-pro, 2-12s on the 1.0-pro models; reads `ARK_API_KEY`),
553
+ `openRouterVideo(...)` (OpenRouter's dedicated `POST /api/v1/videos` gateway),
554
+ and `falVideo(...)` (hosted models, see cost tracking below).
546
555
 
547
556
  > **Seedance option applicability is per model and enforced server-side** —
548
557
  > Ark returns a 400 for an inapplicable field rather than ignoring it.
@@ -554,6 +563,29 @@ aspect-ratio size template like `'16:9_720p'`, durations 4-15s on the 2.0 family
554
563
  > days). Seedance is also reachable via `falVideo` — `byteplusVideo` is the
555
564
  > direct-to-BytePlus path.
556
565
 
566
+ OpenRouter (`@tanstack/ai-openrouter`, `openRouterVideo`) runs the dedicated
567
+ async video API (`POST /api/v1/videos`) and shares the same typed-duration
568
+ contract — `duration`, `size`, and provider options are narrowed per model
569
+ from OpenRouter's published metadata, with the same `availableDurations()` /
570
+ `snapDuration()` helpers:
571
+
572
+ ```typescript
573
+ import { openRouterVideo } from '@tanstack/ai-openrouter'
574
+
575
+ const adapter = openRouterVideo('bytedance/seedance-2.0')
576
+ adapter.availableDurations()
577
+ // { kind: 'discrete', values: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }
578
+ adapter.snapDuration(7.4) // 7
579
+
580
+ const sliderSeconds = 7 // raw seconds from a UI control
581
+ const { jobId } = await generateVideo({
582
+ adapter,
583
+ prompt: 'A timelapse of clouds',
584
+ duration: adapter.snapDuration(sliderSeconds),
585
+ })
586
+ // Completed url is a data: URL; usage.cost carries the real billed cost.
587
+ ```
588
+
557
589
  Client hook with job tracking:
558
590
 
559
591
  ```tsx
@@ -729,6 +729,8 @@ class TextEngine<
729
729
  private streamStartTime = 0
730
730
  private totalChunkCount = 0
731
731
  private currentMessageId: string | null = null
732
+ private currentMessageCreatedAt: Date | null = null
733
+ private streamIdentityCaptured = false
732
734
  private accumulatedContent = ''
733
735
  private accumulatedThinking: Array<{ content: string; signature?: string }> =
734
736
  []
@@ -1268,6 +1270,8 @@ class TextEngine<
1268
1270
 
1269
1271
  private async beginIteration(): Promise<void> {
1270
1272
  this.currentMessageId = this.createId('msg')
1273
+ this.currentMessageCreatedAt = new Date()
1274
+ this.streamIdentityCaptured = false
1271
1275
  this.accumulatedContent = ''
1272
1276
  this.accumulatedThinking = []
1273
1277
  this.currentThinkingContent = ''
@@ -1455,6 +1459,11 @@ class TextEngine<
1455
1459
  // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
1456
1460
  switch (chunk.type) {
1457
1461
  // AG-UI Events
1462
+ case 'TEXT_MESSAGE_START':
1463
+ if (typeof chunk.messageId === 'string' && chunk.messageId !== '') {
1464
+ this.captureStreamMessageIdentity(chunk.messageId)
1465
+ }
1466
+ break
1458
1467
  case 'TEXT_MESSAGE_CONTENT':
1459
1468
  this.handleTextMessageContentEvent(chunk)
1460
1469
  break
@@ -1493,8 +1502,7 @@ class TextEngine<
1493
1502
  break
1494
1503
 
1495
1504
  default:
1496
- // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END,
1497
- // STATE_SNAPSHOT, STATE_DELTA, CUSTOM
1505
+ // RUN_STARTED, TEXT_MESSAGE_END, STATE_SNAPSHOT, STATE_DELTA, CUSTOM
1498
1506
  // - no special handling needed in chat activity
1499
1507
  break
1500
1508
  }
@@ -1513,7 +1521,22 @@ class TextEngine<
1513
1521
  this.middlewareCtx.accumulatedContent = this.accumulatedContent
1514
1522
  }
1515
1523
 
1524
+ private captureStreamMessageIdentity(messageId: string): void {
1525
+ this.currentMessageId = messageId
1526
+ this.middlewareCtx.currentMessageId = messageId
1527
+ if (!this.streamIdentityCaptured) {
1528
+ this.currentMessageCreatedAt = new Date()
1529
+ this.streamIdentityCaptured = true
1530
+ }
1531
+ }
1532
+
1516
1533
  private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
1534
+ if (
1535
+ typeof chunk.parentMessageId === 'string' &&
1536
+ chunk.parentMessageId !== ''
1537
+ ) {
1538
+ this.captureStreamMessageIdentity(chunk.parentMessageId)
1539
+ }
1517
1540
  this.toolCallManager.addToolCallStartEvent(chunk)
1518
1541
  }
1519
1542
 
@@ -1954,6 +1977,8 @@ class TextEngine<
1954
1977
  role: 'assistant',
1955
1978
  content: this.accumulatedContent || null,
1956
1979
  toolCalls,
1980
+ id: this.currentMessageId ?? undefined,
1981
+ createdAt: this.currentMessageCreatedAt ?? undefined,
1957
1982
  ...(this.accumulatedThinking.length > 0 && {
1958
1983
  thinking: this.accumulatedThinking,
1959
1984
  }),