@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.
- package/dist/esm/activities/chat/index.js +18 -0
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +21 -8
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/embed/adapter.d.ts +69 -0
- package/dist/esm/activities/embed/adapter.js +23 -0
- package/dist/esm/activities/embed/adapter.js.map +1 -0
- package/dist/esm/activities/embed/index.d.ts +117 -0
- package/dist/esm/activities/embed/index.js +166 -0
- package/dist/esm/activities/embed/index.js.map +1 -0
- package/dist/esm/activities/error-payload.d.ts +8 -0
- package/dist/esm/activities/error-payload.js +29 -17
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +12 -0
- package/dist/esm/activities/generateAudio/index.js +19 -6
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +12 -0
- package/dist/esm/activities/generateImage/index.js +21 -7
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +17 -1
- package/dist/esm/activities/generateSpeech/index.js +19 -6
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +17 -1
- package/dist/esm/activities/generateTranscription/index.js +19 -6
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +18 -0
- package/dist/esm/activities/generateVideo/index.js +54 -15
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +8 -2
- package/dist/esm/activities/index.js +11 -7
- package/dist/esm/activities/middleware/types.d.ts +1 -1
- package/dist/esm/activities/rerank/adapter.d.ts +63 -0
- package/dist/esm/activities/rerank/adapter.js +23 -0
- package/dist/esm/activities/rerank/adapter.js.map +1 -0
- package/dist/esm/activities/rerank/index.d.ts +92 -0
- package/dist/esm/activities/rerank/index.js +163 -0
- package/dist/esm/activities/rerank/index.js.map +1 -0
- package/dist/esm/activities/summarize/index.d.ts +17 -1
- package/dist/esm/activities/summarize/index.js +19 -5
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/index.d.ts +7 -2
- package/dist/esm/index.js +5 -1
- package/dist/esm/middlewares/otel.js +20 -2
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +195 -0
- package/dist/esm/utilities/activity-abort.d.ts +53 -0
- package/dist/esm/utilities/activity-abort.js +150 -0
- package/dist/esm/utilities/activity-abort.js.map +1 -0
- package/dist/esm/utilities/embedding-input.d.ts +32 -0
- package/dist/esm/utilities/embedding-input.js +61 -0
- package/dist/esm/utilities/embedding-input.js.map +1 -0
- package/package.json +3 -3
- package/skills/ai-core/media-generation/SKILL.md +55 -23
- package/src/activities/chat/index.ts +27 -2
- package/src/activities/chat/messages.ts +30 -1
- package/src/activities/embed/adapter.ts +112 -0
- package/src/activities/embed/index.ts +318 -0
- package/src/activities/error-payload.ts +41 -9
- package/src/activities/generateAudio/index.ts +47 -5
- package/src/activities/generateImage/index.ts +48 -5
- package/src/activities/generateSpeech/index.ts +52 -9
- package/src/activities/generateTranscription/index.ts +52 -9
- package/src/activities/generateVideo/index.ts +131 -33
- package/src/activities/index.ts +44 -0
- package/src/activities/middleware/types.ts +2 -0
- package/src/activities/rerank/adapter.ts +90 -0
- package/src/activities/rerank/index.ts +302 -0
- package/src/activities/summarize/index.ts +59 -19
- package/src/index.ts +19 -0
- package/src/middlewares/otel.ts +38 -3
- package/src/types.ts +219 -0
- package/src/utilities/activity-abort.ts +197 -0
- 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":";;;;;;;;;;AAUA,IAAM,oCAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;AACF,CAAC;;;;;;;;AASD,SAAgB,oBACd,GACA,GACyB;CACzB,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,EAAE,SAAS,OAAO;CACtB,IAAI,EAAE,SAAS,OAAO;CACtB,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,WAAW,iBAA8B;EAC7C,WAAW,MAAM,OAAO,MAAM;CAChC;CACA,EAAE,iBAAiB,SAAS,QAAQ,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC;CACtD,EAAE,iBAAiB,SAAS,QAAQ,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC;CACtD,OAAO,WAAW;AACpB;AAEA,SAAS,oBAAoB,IAAmB;CAC9C,IAAI,OAAO,iBAAiB,aAC1B,OAAO,IAAI,aAAa,4BAA4B,GAAG,KAAK,cAAc;CAE5E,MAAM,sBAAM,IAAI,MAAM,4BAA4B,GAAG,GAAG;CACxD,IAAI,OAAO;CACX,OAAO;AACT;;AAGA,SAAgB,aAAa,QAAwB;CACnD,IAAI,kBAAkB,OAAO,OAAO;CACpC,IAAI,OAAO,WAAW,YAAY,OAAO,SAAS,GAAG;EACnD,MAAM,MAAM,IAAI,MAAM,MAAM;EAC5B,IAAI,OAAO;EACX,OAAO;CACT;CACA,MAAM,sBAAM,IAAI,MAAM,2BAA2B;CACjD,IAAI,OAAO;CACX,OAAO;AACT;;;;;;;;;AAiBA,SAAgB,4BAA4B,SAGlB;CACxB,IAAI;CACJ,IAAI;CAEJ,IAAI,QAAQ,YAAY,KAAA,GAAW;EACjC,IAAI,CAAC,OAAO,SAAS,QAAQ,OAAO,KAAK,QAAQ,UAAU,GACzD,MAAM,IAAI,MACR,wEAAwE,OAAO,QAAQ,OAAO,GAChG;EAEF,MAAM,aAAa,IAAI,gBAAgB;EACvC,gBAAgB,WAAW;EAC3B,MAAM,KAAK,QAAQ;EACnB,YAAY,iBAAiB;GAC3B,WAAW,MAAM,oBAAoB,EAAE,CAAC;EAC1C,GAAG,EAAE;CACP;CAIA,OAAO;EACL,QAHa,oBAAoB,QAAQ,aAAa,aAGtD;EACA,aAAa;GACX,IAAI,cAAc,KAAA,GAAW;IAC3B,aAAa,SAAS;IACtB,YAAY,KAAA;GACd;EACF;CACF;AACF;;;;;;;;;AAUA,SAAgB,cACd,SACA,QACY;CACZ,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,gBAAgB;EAGpB,QAAQ,WACA,KAAA,SACA,KAAA,CACR;CACF;CAEA,IAAI,OAAO,SAAS;EAClB,QAAQ;EACR,OAAO,QAAQ,OAAO,aAAa,OAAO,MAAM,CAAC;CACnD;CAEA,OAAO,IAAI,SAAY,SAAS,WAAW;EACzC,IAAI,UAAU;EACd,MAAM,gBAAgB;GACpB,IAAI,SAAS;GACb,UAAU;GACV,QAAQ;GACR,QAAQ;GACR,OAAO,aAAa,OAAO,MAAM,CAAC;EACpC;EACA,MAAM,gBAAgB;GACpB,OAAO,oBAAoB,SAAS,OAAO;EAC7C;EACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACxD,QAAQ,MACL,UAAU;GACT,IAAI,SAAS;GACb,UAAU;GACV,QAAQ;GACR,QAAQ,KAAK;EACf,IACC,UAAmB;GAClB,IAAI,SAAS;GACb,UAAU;GACV,QAAQ;GACR,OAAO,KAAK;EACd,CACF;CACF,CAAC;AACH;;;;;AAMA,SAAgB,qBACd,OACA,QACS;CACT,IAAI,QAAQ,SAAS,OAAO;CAC5B,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,MAAM,OAAQ,MAA6B;CAC3C,OAAO,OAAO,SAAS,YAAY,kBAAkB,IAAI,IAAI;AAC/D;;AAGA,SAAgB,mBACd,OACA,QACoB;CACpB,IAAI,QAAQ,WAAW,KAAA,GAAW;EAChC,IAAI,OAAO,OAAO,WAAW,UAAU,OAAO,OAAO;EACrD,IAAI,OAAO,kBAAkB,OAAO,OAAO,OAAO,OAAO;CAC3D;CACA,IAAI,iBAAiB,OAAO,OAAO,MAAM;CACzC,IAAI,OAAO,UAAU,UAAU,OAAO;AAExC"}
|
|
@@ -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.
|
|
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.
|
|
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,
|
|
8
|
-
durations), generateSpeech() with openaiSpeech/byteplusSpeech,
|
|
9
|
-
with openaiTranscription/byteplusTranscription. React hooks:
|
|
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
|
|
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
|
|
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`
|
|
292
|
-
| `'end_frame'` | fal `end_image_url` (or e.g. `tail_image_url` / `last_frame_url`); Veo `lastFrame`
|
|
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. |
|
|
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. |
|
|
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`),
|
|
545
|
-
`
|
|
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,
|
|
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
|
}),
|