@tanstack/ai 0.45.1 → 0.47.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.d.ts +36 -11
- package/dist/esm/activities/chat/index.js +462 -66
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +1 -0
- package/dist/esm/activities/chat/messages.js +12 -7
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.d.ts +7 -2
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -3
- package/dist/esm/activities/chat/middleware/compose.js +55 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.d.ts +6 -3
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/generic-interrupts.d.ts +13 -0
- package/dist/esm/activities/chat/middleware/generic-interrupts.js +8 -0
- package/dist/esm/activities/chat/middleware/generic-interrupts.js.map +1 -0
- package/dist/esm/activities/chat/middleware/index.d.ts +4 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +54 -3
- package/dist/esm/activities/chat/middleware/types.js +16 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -0
- package/dist/esm/activities/chat/stream/processor.js +18 -5
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/unique-tool-names.d.ts +20 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js +57 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js.map +1 -0
- package/dist/esm/adapter-internals.d.ts +7 -0
- package/dist/esm/adapter-internals.js +5 -1
- package/dist/esm/client.d.ts +4 -0
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
- package/dist/esm/generic-interrupt-continuation.js +80 -0
- package/dist/esm/generic-interrupt-continuation.js.map +1 -0
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/interrupt-definition.d.ts +113 -0
- package/dist/esm/interrupt-definition.js +169 -0
- package/dist/esm/interrupt-definition.js.map +1 -0
- package/dist/esm/interrupt-resume.d.ts +3 -0
- package/dist/esm/interrupt-resume.js +77 -16
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/interrupts.d.ts +12 -3
- package/dist/esm/interrupts.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.d.ts +2 -2
- package/dist/esm/middlewares/usage-attributes.js +9 -2
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/stream-to-response.d.ts +26 -0
- package/dist/esm/stream-to-response.js +1 -1
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.d.ts +123 -0
- package/dist/esm/stream-to-websocket.js +249 -0
- package/dist/esm/stream-to-websocket.js.map +1 -0
- package/dist/esm/types.d.ts +19 -10
- package/dist/esm/utilities/chat-params.js +10 -1
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/package.json +2 -2
- package/skills/ai-core/media-generation/SKILL.md +13 -9
- package/skills/ai-core/middleware/SKILL.md +53 -44
- package/skills/ai-core/structured-outputs/SKILL.md +59 -55
- package/skills/ai-core/tool-calling/SKILL.md +54 -1
- package/src/activities/chat/index.ts +1076 -194
- package/src/activities/chat/messages.ts +11 -3
- package/src/activities/chat/middleware/builder.ts +29 -4
- package/src/activities/chat/middleware/compose.ts +95 -5
- package/src/activities/chat/middleware/define.ts +13 -3
- package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
- package/src/activities/chat/middleware/index.ts +15 -0
- package/src/activities/chat/middleware/types.ts +127 -2
- package/src/activities/chat/stream/processor.ts +21 -0
- package/src/activities/chat/tools/unique-tool-names.ts +73 -0
- package/src/adapter-internals.ts +24 -0
- package/src/client.ts +20 -0
- package/src/generic-interrupt-continuation.ts +162 -0
- package/src/index.ts +51 -0
- package/src/interrupt-definition.ts +581 -0
- package/src/interrupt-resume.ts +156 -25
- package/src/interrupts.ts +13 -3
- package/src/middlewares/usage-attributes.ts +12 -2
- package/src/stream-to-response.ts +2 -2
- package/src/stream-to-websocket.ts +418 -0
- package/src/types.ts +21 -8
- package/src/utilities/chat-params.ts +16 -3
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolveDebugOption } from "./logger/resolve.js";
|
|
2
|
+
import { durableStreamSource, runErrorChunk } from "./stream-to-response.js";
|
|
3
|
+
import { chatParamsFromRequestBody } from "./utilities/chat-params.js";
|
|
4
|
+
//#region src/stream-to-websocket.ts
|
|
5
|
+
/**
|
|
6
|
+
* Encode one server→client frame. Durable frames carry the opaque offset in an
|
|
7
|
+
* `{ id, chunk }` envelope (identical to the NDJSON wire); non-durable frames
|
|
8
|
+
* are the bare chunk. Unambiguous because a bare chunk always has a top-level
|
|
9
|
+
* `type` and the envelope never does.
|
|
10
|
+
*/
|
|
11
|
+
function encodeWsFrame(chunk, id) {
|
|
12
|
+
return JSON.stringify(id === void 0 ? chunk : {
|
|
13
|
+
id,
|
|
14
|
+
chunk
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Decode one client→server frame. An `{ type: 'abort', runId }` object is a
|
|
19
|
+
* control frame; anything else is treated as a `RunAgentInput` and validated
|
|
20
|
+
* downstream by `chatParamsFromRequestBody`.
|
|
21
|
+
*/
|
|
22
|
+
function decodeWsFrame(data) {
|
|
23
|
+
const parsed = JSON.parse(data);
|
|
24
|
+
if (typeof parsed === "object" && parsed !== null && parsed.type === "abort" && typeof parsed.runId === "string") return {
|
|
25
|
+
kind: "abort",
|
|
26
|
+
runId: parsed.runId
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
kind: "run",
|
|
30
|
+
input: parsed
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the synthetic per-turn request. A conversation-scoped socket multiplexes
|
|
35
|
+
* many runs; each turn's durability adapter must key on the frame's `runId`,
|
|
36
|
+
* which we carry in the URL query (`memoryStream`/`durableStream` already read
|
|
37
|
+
* `?runId` / `?offset` there). Headers are copied from the handshake so
|
|
38
|
+
* auth/cookies survive. A handshake carrying `?offset` is a resume and never
|
|
39
|
+
* reaches a fresh turn (`resumeWebSocketStream` serves it), so the offset is
|
|
40
|
+
* scrubbed here — otherwise a mis-routed resume handshake would make the turn's
|
|
41
|
+
* durability adapter silently take the replay branch instead of running onRun.
|
|
42
|
+
*/
|
|
43
|
+
function buildTurnRequest(handshake, runId) {
|
|
44
|
+
const url = new URL(handshake.url);
|
|
45
|
+
url.searchParams.set("runId", runId);
|
|
46
|
+
url.searchParams.delete("offset");
|
|
47
|
+
return new Request(url, { headers: handshake.headers });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run a full-duplex, conversation-scoped chat over an already-accepted server
|
|
51
|
+
* socket. Each inbound RunAgentInput frame starts one chat() turn (via onRun)
|
|
52
|
+
* whose chunks are pumped back as frames; the socket stays open across turns
|
|
53
|
+
* (pending client-tool resubmit, next user message) until the client closes it
|
|
54
|
+
* or the idle timeout fires. An abort control frame aborts only its turn.
|
|
55
|
+
*/
|
|
56
|
+
function toWebSocketStream(socket, request, init) {
|
|
57
|
+
const logger = resolveDebugOption(init.debug);
|
|
58
|
+
const activeTurns = /* @__PURE__ */ new Map();
|
|
59
|
+
const earlyAborts = /* @__PURE__ */ new Set();
|
|
60
|
+
const heartbeatMs = init.heartbeatMs ?? 3e4;
|
|
61
|
+
const idleTimeoutMs = init.idleTimeoutMs ?? 3e5;
|
|
62
|
+
let lastActivity = Date.now();
|
|
63
|
+
let closed = false;
|
|
64
|
+
const heartbeat = setInterval(() => {
|
|
65
|
+
try {
|
|
66
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
67
|
+
} catch {}
|
|
68
|
+
}, heartbeatMs);
|
|
69
|
+
const idle = setInterval(() => {
|
|
70
|
+
if (activeTurns.size === 0 && Date.now() - lastActivity > idleTimeoutMs) socket.close(1e3, "idle");
|
|
71
|
+
}, Math.min(idleTimeoutMs, 3e4));
|
|
72
|
+
function teardown() {
|
|
73
|
+
closed = true;
|
|
74
|
+
for (const controller of activeTurns.values()) controller.abort();
|
|
75
|
+
activeTurns.clear();
|
|
76
|
+
clearInterval(heartbeat);
|
|
77
|
+
clearInterval(idle);
|
|
78
|
+
}
|
|
79
|
+
socket.addEventListener("close", teardown);
|
|
80
|
+
socket.addEventListener("error", () => {
|
|
81
|
+
logger.errors("WebSocket errored; aborting its turns");
|
|
82
|
+
teardown();
|
|
83
|
+
try {
|
|
84
|
+
socket.close(1011, "socket error");
|
|
85
|
+
} catch {}
|
|
86
|
+
});
|
|
87
|
+
socket.addEventListener("message", (event) => {
|
|
88
|
+
if (typeof event.data !== "string") return;
|
|
89
|
+
lastActivity = Date.now();
|
|
90
|
+
let frame;
|
|
91
|
+
try {
|
|
92
|
+
frame = decodeWsFrame(event.data);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.errors("Failed to decode inbound WS frame; dropping it", { error });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (frame.kind === "abort") {
|
|
98
|
+
const turn = activeTurns.get(frame.runId);
|
|
99
|
+
if (turn) turn.abort();
|
|
100
|
+
else earlyAborts.add(frame.runId);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
handleInbound(frame.input);
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* Surface a turn failure to the client as a live `RUN_ERROR` frame. The
|
|
107
|
+
* socket is conversation-scoped and stays open, so without this frame the
|
|
108
|
+
* client would see neither a terminal chunk nor a close — a permanent hang.
|
|
109
|
+
* Mirrors the HTTP transports, which synthesize the live `RUN_ERROR` when
|
|
110
|
+
* the producer rethrows (see `durableStreamSource`'s terminal contract).
|
|
111
|
+
*/
|
|
112
|
+
function sendRunError(error) {
|
|
113
|
+
try {
|
|
114
|
+
socket.send(encodeWsFrame(runErrorChunk(error), void 0));
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
async function handleInbound(input) {
|
|
118
|
+
let params;
|
|
119
|
+
try {
|
|
120
|
+
params = await chatParamsFromRequestBody(input);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.errors("Invalid inbound WS run frame; dropping it", { error });
|
|
123
|
+
sendRunError(error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (closed) return;
|
|
127
|
+
const turnAbort = new AbortController();
|
|
128
|
+
activeTurns.get(params.runId)?.abort();
|
|
129
|
+
activeTurns.set(params.runId, turnAbort);
|
|
130
|
+
if (earlyAborts.delete(params.runId)) turnAbort.abort();
|
|
131
|
+
const ctx = {
|
|
132
|
+
messages: params.messages,
|
|
133
|
+
threadId: params.threadId,
|
|
134
|
+
runId: params.runId,
|
|
135
|
+
forwardedProps: params.forwardedProps,
|
|
136
|
+
request: buildTurnRequest(request, params.runId),
|
|
137
|
+
signal: turnAbort.signal
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
if (init.durability) {
|
|
141
|
+
const adapter = init.durability(ctx);
|
|
142
|
+
const { source, getId } = durableStreamSource(init.onRun(ctx), adapter, {
|
|
143
|
+
abortController: turnAbort,
|
|
144
|
+
...init.batch === void 0 ? {} : { batch: init.batch },
|
|
145
|
+
logger
|
|
146
|
+
});
|
|
147
|
+
for await (const chunk of source) socket.send(encodeWsFrame(chunk, getId(chunk)));
|
|
148
|
+
} else for await (const chunk of init.onRun(ctx)) socket.send(encodeWsFrame(chunk, void 0));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (!turnAbort.signal.aborted) {
|
|
151
|
+
logger.errors("WS turn failed", { error });
|
|
152
|
+
sendRunError(error);
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
if (activeTurns.get(params.runId) === turnAbort) activeTurns.delete(params.runId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A resume is served entirely from the durability log, so there is no
|
|
161
|
+
* producer to iterate. This empty source satisfies `durableStreamSource`'s
|
|
162
|
+
* signature; on a resume it replays from the log and never touches this.
|
|
163
|
+
* Mirrors the private helper of the same name in `stream-to-response.ts`.
|
|
164
|
+
*/
|
|
165
|
+
function emptyDurableSource() {
|
|
166
|
+
return (async function* () {})();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Read-only replay of a run's durability log over a socket (mirrors
|
|
170
|
+
* `resumeServerSentEventsResponse`). The adapter captures the offset from the
|
|
171
|
+
* request (`?offset`/`Last-Event-ID`); no model runs. Closes 1008 when there
|
|
172
|
+
* is nothing to resume.
|
|
173
|
+
*/
|
|
174
|
+
function resumeWebSocketStream(socket, options) {
|
|
175
|
+
const logger = resolveDebugOption(options.debug);
|
|
176
|
+
if (options.adapter.resumeFrom() === null) {
|
|
177
|
+
socket.close(1008, "no resume offset");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const abortController = new AbortController();
|
|
181
|
+
socket.addEventListener("close", () => abortController.abort());
|
|
182
|
+
socket.addEventListener("error", () => abortController.abort());
|
|
183
|
+
const { source, getId } = durableStreamSource(emptyDurableSource(), options.adapter, {
|
|
184
|
+
abortController,
|
|
185
|
+
...options.batch === void 0 ? {} : { batch: options.batch },
|
|
186
|
+
logger
|
|
187
|
+
});
|
|
188
|
+
(async () => {
|
|
189
|
+
for await (const chunk of source) socket.send(encodeWsFrame(chunk, getId(chunk)));
|
|
190
|
+
try {
|
|
191
|
+
socket.close(1e3);
|
|
192
|
+
} catch {}
|
|
193
|
+
})().catch((error) => {
|
|
194
|
+
logger.errors("resume websocket replay failed", { error });
|
|
195
|
+
try {
|
|
196
|
+
socket.close(1011, "resume failed");
|
|
197
|
+
} catch {}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function upgradeOrThrow(helper) {
|
|
201
|
+
const Pair = globalThis.WebSocketPair;
|
|
202
|
+
if (!Pair) throw new Error(`${helper} requires a runtime with WebSocketPair (Cloudflare Workers/Durable Objects). On other runtimes upgrade the socket yourself and call ${helper.replace("Response", "Stream")}.`);
|
|
203
|
+
const pair = new Pair();
|
|
204
|
+
const server = pair[1];
|
|
205
|
+
server.accept?.();
|
|
206
|
+
return {
|
|
207
|
+
client: pair[0],
|
|
208
|
+
server
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function upgradeResponse(client) {
|
|
212
|
+
return new Response(null, {
|
|
213
|
+
status: 101,
|
|
214
|
+
webSocket: client
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Cloudflare wrapper (Workers/Durable Objects): creates a `WebSocketPair`,
|
|
219
|
+
* accepts the server socket, delegates to {@link toWebSocketStream}, and
|
|
220
|
+
* returns the 101 upgrade `Response` carrying the client socket. Throws when
|
|
221
|
+
* the runtime has no `WebSocketPair` (Node, Deno, Bun) — upgrade the socket
|
|
222
|
+
* yourself and call {@link toWebSocketStream} directly there.
|
|
223
|
+
*/
|
|
224
|
+
function toWebSocketResponse(request, init) {
|
|
225
|
+
const { client, server } = upgradeOrThrow("toWebSocketResponse");
|
|
226
|
+
toWebSocketStream(server, request, init);
|
|
227
|
+
return upgradeResponse(client);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Cloudflare wrapper (Workers/Durable Objects): creates a `WebSocketPair`,
|
|
231
|
+
* accepts the server socket, delegates to {@link resumeWebSocketStream}, and
|
|
232
|
+
* returns the 101 upgrade `Response` carrying the client socket. Throws when
|
|
233
|
+
* the runtime has no `WebSocketPair` (Node, Deno, Bun) — upgrade the socket
|
|
234
|
+
* yourself and call {@link resumeWebSocketStream} directly there.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* resumeWebSocketResponse({ adapter: memoryStream(request) })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
function resumeWebSocketResponse(options) {
|
|
242
|
+
const { client, server } = upgradeOrThrow("resumeWebSocketResponse");
|
|
243
|
+
resumeWebSocketStream(server, options);
|
|
244
|
+
return upgradeResponse(client);
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
export { buildTurnRequest, decodeWsFrame, encodeWsFrame, resumeWebSocketResponse, resumeWebSocketStream, toWebSocketResponse, toWebSocketStream };
|
|
248
|
+
|
|
249
|
+
//# sourceMappingURL=stream-to-websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-to-websocket.js","names":[],"sources":["../../src/stream-to-websocket.ts"],"sourcesContent":["import { chatParamsFromRequestBody } from './utilities/chat-params'\nimport { durableStreamSource, runErrorChunk } from './stream-to-response'\nimport { resolveDebugOption } from './logger/resolve'\nimport type { StreamDurability } from './stream-durability'\nimport type { DebugOption } from './logger/types'\nimport type { ModelMessage, StreamChunk, UIMessage } from './types'\n\n/**\n * The minimal WHATWG WebSocket surface the core needs. Cloudflare\n * `WebSocketPair` server sockets, Deno's upgraded sockets, and `ws` (Node)\n * sockets already satisfy it; Bun's `ServerWebSocket` (handler-object API)\n * gets a ~10-line adapter at the call site.\n */\nexport interface WebSocketLike {\n send: (data: string) => void\n close: (code?: number, reason?: string) => void\n addEventListener: {\n (type: 'message', handler: (ev: { data: unknown }) => void): void\n (type: 'close' | 'error', handler: () => void): void\n }\n}\n\n/** One inbound WS text frame, after JSON parse + shape discrimination. */\nexport type InboundFrame =\n | { kind: 'run'; input: unknown }\n | { kind: 'abort'; runId: string }\n\n/**\n * Encode one server→client frame. Durable frames carry the opaque offset in an\n * `{ id, chunk }` envelope (identical to the NDJSON wire); non-durable frames\n * are the bare chunk. Unambiguous because a bare chunk always has a top-level\n * `type` and the envelope never does.\n */\nexport function encodeWsFrame(\n chunk: StreamChunk,\n id: string | undefined,\n): string {\n return JSON.stringify(id === undefined ? chunk : { id, chunk })\n}\n\n/**\n * Decode one client→server frame. An `{ type: 'abort', runId }` object is a\n * control frame; anything else is treated as a `RunAgentInput` and validated\n * downstream by `chatParamsFromRequestBody`.\n */\nexport function decodeWsFrame(data: string): InboundFrame {\n const parsed: unknown = JSON.parse(data)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n (parsed as { type?: unknown }).type === 'abort' &&\n typeof (parsed as { runId?: unknown }).runId === 'string'\n ) {\n return { kind: 'abort', runId: (parsed as { runId: string }).runId }\n }\n return { kind: 'run', input: parsed }\n}\n\n/** Per-turn context for one inbound `run` frame on a conversation-scoped socket. */\nexport interface WsRunContext {\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n forwardedProps?: Record<string, unknown>\n /** Synthetic per-turn request carrying `?runId=` so durability keys correctly. */\n request: Request\n /** Aborts on socket close or an `abort` control frame for this run. */\n signal: AbortSignal\n}\n\n/**\n * Build the synthetic per-turn request. A conversation-scoped socket multiplexes\n * many runs; each turn's durability adapter must key on the frame's `runId`,\n * which we carry in the URL query (`memoryStream`/`durableStream` already read\n * `?runId` / `?offset` there). Headers are copied from the handshake so\n * auth/cookies survive. A handshake carrying `?offset` is a resume and never\n * reaches a fresh turn (`resumeWebSocketStream` serves it), so the offset is\n * scrubbed here — otherwise a mis-routed resume handshake would make the turn's\n * durability adapter silently take the replay branch instead of running onRun.\n */\nexport function buildTurnRequest(handshake: Request, runId: string): Request {\n const url = new URL(handshake.url)\n url.searchParams.set('runId', runId)\n url.searchParams.delete('offset')\n return new Request(url, { headers: handshake.headers })\n}\n\nexport interface WebSocketStreamInit<TOffset extends string = string> {\n /** Build a fresh chat() stream for each inbound RunAgentInput frame. */\n onRun: (ctx: WsRunContext) => AsyncIterable<StreamChunk>\n /** Per-TURN durability factory, keyed by the frame's runId via ctx.request. */\n durability?: (ctx: WsRunContext) => StreamDurability<TOffset>\n /** Chunks buffered per durability append (default 32). */\n batch?: number\n /** Heartbeat ping interval in ms (default 30_000). */\n heartbeatMs?: number\n /**\n * Close after this many ms without any inbound frame (default 300_000).\n * Never fires while a turn is still streaming, so a long single generation\n * (agentic loop, >5-min turn) is safe.\n */\n idleTimeoutMs?: number\n debug?: DebugOption\n}\n\n/**\n * Run a full-duplex, conversation-scoped chat over an already-accepted server\n * socket. Each inbound RunAgentInput frame starts one chat() turn (via onRun)\n * whose chunks are pumped back as frames; the socket stays open across turns\n * (pending client-tool resubmit, next user message) until the client closes it\n * or the idle timeout fires. An abort control frame aborts only its turn.\n */\nexport function toWebSocketStream<TOffset extends string = string>(\n socket: WebSocketLike,\n request: Request,\n init: WebSocketStreamInit<TOffset>,\n): void {\n const logger = resolveDebugOption(init.debug)\n const activeTurns = new Map<string, AbortController>()\n // Abort frames that raced ahead of their run's registration: `handleInbound`\n // awaits body validation before it registers into `activeTurns`, so an abort\n // arriving inside that window would otherwise be silently discarded.\n const earlyAborts = new Set<string>()\n const heartbeatMs = init.heartbeatMs ?? 30_000\n const idleTimeoutMs = init.idleTimeoutMs ?? 300_000\n let lastActivity = Date.now()\n let closed = false\n\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ type: 'ping' }))\n } catch {\n // Socket is CLOSING/CLOSED between ticks — teardown below clears this\n // interval; swallow so the timer callback doesn't throw uncaught in the\n // meantime.\n }\n }, heartbeatMs)\n const idle = setInterval(\n () => {\n // Never idle-reap while a turn is in flight: a long single onRun\n // iteration (agentic loop / >5-min generation) sends no INBOUND\n // frames, so idle would otherwise fire and kill live work.\n if (activeTurns.size === 0 && Date.now() - lastActivity > idleTimeoutMs) {\n socket.close(1000, 'idle')\n }\n },\n Math.min(idleTimeoutMs, 30_000),\n )\n\n function teardown(): void {\n closed = true\n for (const controller of activeTurns.values()) controller.abort()\n activeTurns.clear()\n clearInterval(heartbeat)\n clearInterval(idle)\n }\n\n socket.addEventListener('close', teardown)\n // Without this, an errored socket whose `close` never follows would leak\n // both intervals and never abort its turns — and on `ws` (an EventEmitter)\n // an `error` event with no listener is thrown as an uncaught exception.\n socket.addEventListener('error', () => {\n logger.errors('WebSocket errored; aborting its turns')\n teardown()\n try {\n socket.close(1011, 'socket error')\n } catch {\n // socket already closing/closed — nothing to do\n }\n })\n\n socket.addEventListener('message', (event: { data: unknown }) => {\n if (typeof event.data !== 'string') return\n lastActivity = Date.now()\n\n // Inbound frames are client-controlled: a malformed frame (bad JSON, or\n // valid JSON that isn't an AG-UI RunAgentInput/abort shape) must be\n // dropped, not crash the socket or leak an unhandled rejection.\n let frame: InboundFrame\n try {\n frame = decodeWsFrame(event.data)\n } catch (error) {\n logger.errors('Failed to decode inbound WS frame; dropping it', {\n error,\n })\n return\n }\n\n if (frame.kind === 'abort') {\n const turn = activeTurns.get(frame.runId)\n if (turn) turn.abort()\n else earlyAborts.add(frame.runId)\n return\n }\n\n void handleInbound(frame.input)\n })\n\n /**\n * Surface a turn failure to the client as a live `RUN_ERROR` frame. The\n * socket is conversation-scoped and stays open, so without this frame the\n * client would see neither a terminal chunk nor a close — a permanent hang.\n * Mirrors the HTTP transports, which synthesize the live `RUN_ERROR` when\n * the producer rethrows (see `durableStreamSource`'s terminal contract).\n */\n function sendRunError(error: unknown): void {\n try {\n socket.send(encodeWsFrame(runErrorChunk(error), undefined))\n } catch {\n // Socket is CLOSING/CLOSED — the client sees onclose instead.\n }\n }\n\n async function handleInbound(input: unknown): Promise<void> {\n let params: Awaited<ReturnType<typeof chatParamsFromRequestBody>>\n try {\n params = await chatParamsFromRequestBody(input)\n } catch (error) {\n logger.errors('Invalid inbound WS run frame; dropping it', { error })\n sendRunError(error)\n return\n }\n // The socket may have closed (or errored) during the await above — the\n // teardown that drains `activeTurns` already ran, so registering now\n // would start a turn nothing can ever abort.\n if (closed) return\n const turnAbort = new AbortController()\n // A second inbound frame with the same runId (client resubmit) must\n // abort the earlier turn. Otherwise the old controller is overwritten\n // and close/abort frames can no longer reach it.\n activeTurns.get(params.runId)?.abort()\n activeTurns.set(params.runId, turnAbort)\n if (earlyAborts.delete(params.runId)) turnAbort.abort()\n const ctx: WsRunContext = {\n messages: params.messages,\n threadId: params.threadId,\n runId: params.runId,\n forwardedProps: params.forwardedProps,\n request: buildTurnRequest(request, params.runId),\n signal: turnAbort.signal,\n }\n try {\n if (init.durability) {\n const adapter = init.durability(ctx)\n const { source, getId } = durableStreamSource(\n init.onRun(ctx),\n adapter,\n {\n abortController: turnAbort,\n ...(init.batch === undefined ? {} : { batch: init.batch }),\n logger,\n },\n )\n for await (const chunk of source) {\n socket.send(encodeWsFrame(chunk, getId(chunk)))\n }\n } else {\n for await (const chunk of init.onRun(ctx)) {\n socket.send(encodeWsFrame(chunk, undefined))\n }\n }\n } catch (error) {\n // An aborted turn (socket close, abort frame, same-runId resubmit) is\n // expected teardown, not a turn failure — nothing to report.\n if (!turnAbort.signal.aborted) {\n logger.errors('WS turn failed', { error })\n sendRunError(error)\n }\n } finally {\n // Only delete if this turn still owns the entry: a duplicate in-flight\n // runId (e.g. a client resubmitting before the first turn finished)\n // would otherwise let the OLDER turn's cleanup delete the NEWER turn's\n // still-active controller (TOCTOU).\n if (activeTurns.get(params.runId) === turnAbort) {\n activeTurns.delete(params.runId)\n }\n }\n }\n}\n\n/**\n * A resume is served entirely from the durability log, so there is no\n * producer to iterate. This empty source satisfies `durableStreamSource`'s\n * signature; on a resume it replays from the log and never touches this.\n * Mirrors the private helper of the same name in `stream-to-response.ts`.\n */\nfunction emptyDurableSource(): AsyncIterable<StreamChunk> {\n return (async function* () {})()\n}\n\n/**\n * Read-only replay of a run's durability log over a socket (mirrors\n * `resumeServerSentEventsResponse`). The adapter captures the offset from the\n * request (`?offset`/`Last-Event-ID`); no model runs. Closes 1008 when there\n * is nothing to resume.\n */\nexport function resumeWebSocketStream<TOffset extends string = string>(\n socket: WebSocketLike,\n options: {\n adapter: StreamDurability<TOffset>\n batch?: number\n debug?: DebugOption\n },\n): void {\n const logger = resolveDebugOption(options.debug)\n if (options.adapter.resumeFrom() === null) {\n socket.close(1008, 'no resume offset')\n return\n }\n const abortController = new AbortController()\n socket.addEventListener('close', () => abortController.abort())\n // An `error` with no listener is an uncaught exception on `ws`; abort the\n // replay so the pump below stops instead of writing to a dead socket.\n socket.addEventListener('error', () => abortController.abort())\n const { source, getId } = durableStreamSource(\n emptyDurableSource(),\n options.adapter,\n {\n abortController,\n ...(options.batch === undefined ? {} : { batch: options.batch }),\n logger,\n },\n )\n void (async () => {\n for await (const chunk of source) {\n socket.send(encodeWsFrame(chunk, getId(chunk)))\n }\n // Source exhausted = the durability log is complete/terminal; nothing more\n // will arrive on this read-only socket. Close so the client's reconnect\n // loop sees onclose and terminates (bounded) instead of awaiting a chunk\n // that never comes. Safe across durability models: a live decoupled\n // producer (e.g. durableStream) keeps `read` parked until the terminal,\n // so the source doesn't exhaust until the run truly ends; a completed\n // in-process log closes immediately.\n try {\n socket.close(1000)\n } catch {\n // socket already closing/closed — nothing to do\n }\n })().catch((error: unknown) => {\n logger.errors('resume websocket replay failed', { error })\n try {\n socket.close(1011, 'resume failed')\n } catch {\n // socket already closing/closed — nothing to do\n }\n })\n}\n\ninterface WebSocketPairCtor {\n new (): { 0: unknown; 1: WebSocketLike & { accept?: () => void } }\n}\n\nfunction upgradeOrThrow(helper: string): {\n client: unknown\n server: WebSocketLike\n} {\n const Pair = (globalThis as { WebSocketPair?: WebSocketPairCtor })\n .WebSocketPair\n if (!Pair) {\n throw new Error(\n `${helper} requires a runtime with WebSocketPair (Cloudflare Workers/Durable Objects). ` +\n `On other runtimes upgrade the socket yourself and call ${helper.replace('Response', 'Stream')}.`,\n )\n }\n const pair = new Pair()\n const server = pair[1]\n server.accept?.()\n return { client: pair[0], server }\n}\n\nfunction upgradeResponse(client: unknown): Response {\n return new Response(null, {\n status: 101,\n // Cloudflare-specific field; typed loosely to avoid a DOM lib dependency.\n webSocket: client,\n } as ResponseInit & { webSocket: unknown })\n}\n\n/**\n * Cloudflare wrapper (Workers/Durable Objects): creates a `WebSocketPair`,\n * accepts the server socket, delegates to {@link toWebSocketStream}, and\n * returns the 101 upgrade `Response` carrying the client socket. Throws when\n * the runtime has no `WebSocketPair` (Node, Deno, Bun) — upgrade the socket\n * yourself and call {@link toWebSocketStream} directly there.\n */\nexport function toWebSocketResponse<TOffset extends string = string>(\n request: Request,\n init: WebSocketStreamInit<TOffset>,\n): Response {\n const { client, server } = upgradeOrThrow('toWebSocketResponse')\n toWebSocketStream(server, request, init)\n return upgradeResponse(client)\n}\n\n/**\n * Cloudflare wrapper (Workers/Durable Objects): creates a `WebSocketPair`,\n * accepts the server socket, delegates to {@link resumeWebSocketStream}, and\n * returns the 101 upgrade `Response` carrying the client socket. Throws when\n * the runtime has no `WebSocketPair` (Node, Deno, Bun) — upgrade the socket\n * yourself and call {@link resumeWebSocketStream} directly there.\n *\n * @example\n * ```ts\n * resumeWebSocketResponse({ adapter: memoryStream(request) })\n * ```\n */\nexport function resumeWebSocketResponse<\n TOffset extends string = string,\n>(options: {\n adapter: StreamDurability<TOffset>\n batch?: number\n debug?: DebugOption\n}): Response {\n const { client, server } = upgradeOrThrow('resumeWebSocketResponse')\n resumeWebSocketStream(server, options)\n return upgradeResponse(client)\n}\n"],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { SystemPrompt } from './system-prompts.js';
|
|
|
4
4
|
import { CapabilityContext } from './activities/chat/middleware/capabilities.js';
|
|
5
5
|
import { InterruptSubmissionError } from './interrupts.js';
|
|
6
6
|
import { ProviderTool } from './tools/provider-tool.js';
|
|
7
|
-
import { CompletionTokensDetails, PromptTokensDetails, ProviderUsageDetails, TokenUsage, UsageCostBreakdown } from '@tanstack/ai-event-client';
|
|
7
|
+
import { BilledUsage, BillingUnit, CompletionTokensDetails, PromptTokensDetails, ProviderUsageDetails, TokenUsage, UsageCostBreakdown } from '@tanstack/ai-event-client';
|
|
8
8
|
import { BaseEvent as AGUIBaseEvent, CustomEvent as AGUICustomEvent, Interrupt as AGUIInterrupt, MessagesSnapshotEvent as AGUIMessagesSnapshotEvent, ReasoningEncryptedValueEvent as AGUIReasoningEncryptedValueEvent, ReasoningEndEvent as AGUIReasoningEndEvent, ReasoningMessageContentEvent as AGUIReasoningMessageContentEvent, ReasoningMessageEndEvent as AGUIReasoningMessageEndEvent, ReasoningMessageStartEvent as AGUIReasoningMessageStartEvent, ReasoningStartEvent as AGUIReasoningStartEvent, ResumeEntry as AGUIResumeEntry, RunErrorEvent as AGUIRunErrorEvent, RunFinishedEvent as AGUIRunFinishedEvent, RunFinishedOutcome as AGUIRunFinishedOutcome, RunStartedEvent as AGUIRunStartedEvent, StateDeltaEvent as AGUIStateDeltaEvent, StateSnapshotEvent as AGUIStateSnapshotEvent, StepFinishedEvent as AGUIStepFinishedEvent, StepStartedEvent as AGUIStepStartedEvent, TextMessageContentEvent as AGUITextMessageContentEvent, TextMessageEndEvent as AGUITextMessageEndEvent, TextMessageStartEvent as AGUITextMessageStartEvent, ToolCallArgsEvent as AGUIToolCallArgsEvent, ToolCallEndEvent as AGUIToolCallEndEvent, ToolCallResultEvent as AGUIToolCallResultEvent, ToolCallStartEvent as AGUIToolCallStartEvent, EventType } from '@ag-ui/core';
|
|
9
9
|
export type { ProviderTool } from './tools/provider-tool.js';
|
|
10
10
|
/**
|
|
@@ -250,6 +250,12 @@ export interface ModelMessage<TContent extends string | null | Array<ContentPart
|
|
|
250
250
|
content: string;
|
|
251
251
|
signature?: string;
|
|
252
252
|
}>;
|
|
253
|
+
/**
|
|
254
|
+
* Completed structured output represented by this assistant message.
|
|
255
|
+
* `content` remains the provider-facing JSON text; this field preserves the
|
|
256
|
+
* typed UI part across persistence and message conversion.
|
|
257
|
+
*/
|
|
258
|
+
structuredOutput?: StructuredOutputPart;
|
|
253
259
|
/**
|
|
254
260
|
* Optional stable message id. Providers ignore it; it exists so a persisted
|
|
255
261
|
* transcript can retain the streaming `messageId` and survive the
|
|
@@ -830,8 +836,7 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
|
|
|
830
836
|
state?: unknown;
|
|
831
837
|
/**
|
|
832
838
|
* AG-UI interrupt resume responses supplied by the client on a follow-up run.
|
|
833
|
-
*
|
|
834
|
-
* upstream-native interrupts.
|
|
839
|
+
* A first-party generic item carries the original request in `metadata`.
|
|
835
840
|
*/
|
|
836
841
|
resume?: Array<RunAgentResumeItem>;
|
|
837
842
|
/**
|
|
@@ -891,7 +896,7 @@ export interface RunStartedEvent extends AGUIRunStartedEvent {
|
|
|
891
896
|
/** Model identifier for multi-model support */
|
|
892
897
|
model?: string;
|
|
893
898
|
}
|
|
894
|
-
export type { CompletionTokensDetails, PromptTokensDetails, ProviderUsageDetails, TokenUsage, UsageCostBreakdown, };
|
|
899
|
+
export type { BilledUsage, BillingUnit, CompletionTokensDetails, PromptTokensDetails, ProviderUsageDetails, TokenUsage, UsageCostBreakdown, };
|
|
895
900
|
/**
|
|
896
901
|
* @deprecated Renamed to {@link TokenUsage}. Kept as an alias for backward
|
|
897
902
|
* compatibility with `@tanstack/ai@0.23` and earlier; will be removed in a
|
|
@@ -900,7 +905,10 @@ export type { CompletionTokensDetails, PromptTokensDetails, ProviderUsageDetails
|
|
|
900
905
|
export type UsageTotals = TokenUsage;
|
|
901
906
|
export type Interrupt = AGUIInterrupt;
|
|
902
907
|
export type RunFinishedOutcome = AGUIRunFinishedOutcome;
|
|
903
|
-
export type RunAgentResumeItem = AGUIResumeEntry
|
|
908
|
+
export type RunAgentResumeItem = AGUIResumeEntry & {
|
|
909
|
+
/** AG-UI resume metadata. First-party generic requests ride here. */
|
|
910
|
+
metadata?: Record<string, unknown>;
|
|
911
|
+
};
|
|
904
912
|
/**
|
|
905
913
|
* Emitted when a run completes successfully.
|
|
906
914
|
*
|
|
@@ -1734,9 +1742,10 @@ export interface RerankResult<TDocument = string> {
|
|
|
1734
1742
|
rerankedDocuments: Array<TDocument>;
|
|
1735
1743
|
/**
|
|
1736
1744
|
* Usage for the request. Rerank typically bills in provider-defined "search
|
|
1737
|
-
* units" (`usage.
|
|
1738
|
-
* OpenRouter) may also report `totalTokens` and `cost
|
|
1739
|
-
* search units and leaves the token counts at 0.
|
|
1745
|
+
* units" (`usage.billed = { quantity, unit: 'units' }`) rather than tokens.
|
|
1746
|
+
* Some providers (e.g. OpenRouter) may also report `totalTokens` and `cost`.
|
|
1747
|
+
* Cohere reports only search units and leaves the token counts at 0.
|
|
1748
|
+
* The deprecated `unitsBilled` field is still populated for compatibility.
|
|
1740
1749
|
*/
|
|
1741
1750
|
usage: TokenUsage;
|
|
1742
1751
|
}
|
|
@@ -2054,8 +2063,8 @@ export interface VideoUrlResult {
|
|
|
2054
2063
|
expiresAt?: Date;
|
|
2055
2064
|
/**
|
|
2056
2065
|
* Usage information for the completed generation, when the adapter can report
|
|
2057
|
-
* it. For usage-based providers (e.g. fal) this carries `
|
|
2058
|
-
*
|
|
2066
|
+
* it. For usage-based providers (e.g. fal) this carries `billed` — the real
|
|
2067
|
+
* billed quantity paired with its unit — so consumers can compute exact cost.
|
|
2059
2068
|
*/
|
|
2060
2069
|
usage?: TokenUsage;
|
|
2061
2070
|
/** Persisted artifact references for generated assets, when available */
|
|
@@ -8,7 +8,8 @@ var KNOWN_PART_TYPES = /* @__PURE__ */ new Set([
|
|
|
8
8
|
"document",
|
|
9
9
|
"tool-call",
|
|
10
10
|
"tool-result",
|
|
11
|
-
"thinking"
|
|
11
|
+
"thinking",
|
|
12
|
+
"structured-output"
|
|
12
13
|
]);
|
|
13
14
|
function isValidParts(value) {
|
|
14
15
|
if (!Array.isArray(value)) return false;
|
|
@@ -16,6 +17,10 @@ function isValidParts(value) {
|
|
|
16
17
|
if (!p || typeof p !== "object") return false;
|
|
17
18
|
const type = p.type;
|
|
18
19
|
if (typeof type !== "string" || !KNOWN_PART_TYPES.has(type)) return false;
|
|
20
|
+
if (type === "structured-output") {
|
|
21
|
+
const raw = p.raw;
|
|
22
|
+
if (raw !== void 0 && typeof raw !== "string") return false;
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
return true;
|
|
21
26
|
}
|
|
@@ -121,6 +126,10 @@ function validateResumeEntry(value, index) {
|
|
|
121
126
|
status
|
|
122
127
|
};
|
|
123
128
|
if (value.payload !== void 0) entry.payload = value.payload;
|
|
129
|
+
if (value.metadata !== void 0) {
|
|
130
|
+
if (!isRecord(value.metadata)) invalidBody(`${at}.metadata must be an object`);
|
|
131
|
+
entry.metadata = value.metadata;
|
|
132
|
+
}
|
|
124
133
|
return entry;
|
|
125
134
|
}
|
|
126
135
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-params.js","names":[],"sources":["../../../src/utilities/chat-params.ts"],"sourcesContent":["import { AGUIError } from '@ag-ui/core'\nimport type {\n Context as AGUIContext,\n Message as AGUIMessage,\n ResumeEntry as AGUIResumeEntry,\n Role as AGUIRole,\n} from '@ag-ui/core'\nimport type {\n AnyTool,\n JSONSchema,\n ModelMessage,\n RunAgentResumeItem,\n UIMessage,\n} from '../types'\n\nconst KNOWN_PART_TYPES = new Set([\n 'text',\n 'image',\n 'audio',\n 'video',\n 'document',\n 'tool-call',\n 'tool-result',\n 'thinking',\n])\n\nfunction isValidParts(value: unknown): value is Array<{ type: string }> {\n if (!Array.isArray(value)) return false\n for (const p of value) {\n if (!p || typeof p !== 'object') return false\n const type = (p as { type?: unknown }).type\n if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false\n }\n return true\n}\n\n/**\n * Keyed by `AGUIRole` so a role added upstream fails to compile here until it\n * is handled, rather than silently falling through as an unknown role.\n */\nconst AGUI_ROLES: Record<AGUIRole, true> = {\n developer: true,\n system: true,\n assistant: true,\n user: true,\n tool: true,\n activity: true,\n reasoning: true,\n}\n\nfunction isAGUIRole(value: unknown): value is AGUIRole {\n return typeof value === 'string' && value in AGUI_ROLES\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reject the request body, pointing at the migration guide. Mirrors the\n * message the previous `RunAgentInputSchema.safeParse` failure produced.\n */\nfunction invalidBody(reason: string): never {\n throw new AGUIError(\n `Request body is not a valid AG-UI RunAgentInput. ` +\n `If you're upgrading from a previous @tanstack/ai-client release, ` +\n `see docs/migration/ag-ui-compliance.md. ` +\n `Validation errors: ${reason}`,\n )\n}\n\nfunction requireString(value: unknown, at: string): string {\n if (typeof value !== 'string') invalidBody(`${at} must be a string`)\n return value\n}\n\nfunction requireArray(value: unknown, at: string): Array<unknown> {\n if (!Array.isArray(value)) invalidBody(`${at} must be an array`)\n return value\n}\n\n/**\n * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream\n * `MessageSchema` discriminated union does. The record view is retained on the\n * asserted type so callers can still inspect non-AG-UI extras like `parts`.\n */\nfunction assertAGUIMessage(\n value: Record<string, unknown>,\n at: string,\n): asserts value is Record<string, unknown> & AGUIMessage {\n requireString(value.id, `${at}.id`)\n\n const role = value.role\n if (!isAGUIRole(role)) {\n invalidBody(\n `${at}.role must be one of ${Object.keys(AGUI_ROLES).join(' | ')}`,\n )\n }\n\n switch (role) {\n case 'assistant':\n // Both optional: a tool-calling turn carries no text content.\n if (value.content !== undefined) {\n requireString(value.content, `${at}.content`)\n }\n if (value.toolCalls !== undefined) {\n requireArray(value.toolCalls, `${at}.toolCalls`)\n }\n break\n case 'user':\n if (typeof value.content !== 'string' && !Array.isArray(value.content)) {\n invalidBody(\n `${at}.content must be a string or an array of content parts`,\n )\n }\n break\n case 'tool':\n requireString(value.content, `${at}.content`)\n requireString(value.toolCallId, `${at}.toolCallId`)\n break\n case 'activity':\n requireString(value.activityType, `${at}.activityType`)\n if (!isRecord(value.content)) {\n invalidBody(`${at}.content must be an object`)\n }\n break\n case 'developer':\n case 'system':\n case 'reasoning':\n requireString(value.content, `${at}.content`)\n break\n }\n}\n\nfunction validateMessage(value: unknown, index: number): AGUIMessage {\n const at = `messages[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n assertAGUIMessage(value, at)\n\n // `parts` is TanStack's canonical extra, carried through so the UIMessage\n // path inside `chat()` can use it. Keep it only when it holds recognized\n // part types — the previous schema-based path dropped `parts` during parse\n // and re-attached it from the raw body behind this same check.\n if ('parts' in value && !isValidParts(value.parts)) {\n const withoutParts = { ...value }\n Reflect.deleteProperty(withoutParts, 'parts')\n return withoutParts\n }\n return value\n}\n\nfunction validateTool(\n value: unknown,\n index: number,\n): { name: string; description: string; parameters: JSONSchema } {\n const at = `tools[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n name: requireString(value.name, `${at}.name`),\n description: requireString(value.description, `${at}.description`),\n // Upstream `ToolSchema` types this as optional `any`; it reaches the\n // provider as a raw JSON Schema either way.\n parameters: value.parameters as JSONSchema,\n }\n}\n\nfunction validateContext(value: unknown, index: number): AGUIContext {\n const at = `context[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n description: requireString(value.description, `${at}.description`),\n value: requireString(value.value, `${at}.value`),\n }\n}\n\nfunction validateResumeEntry(value: unknown, index: number): AGUIResumeEntry {\n const at = `resume[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n const status = value.status\n if (status !== 'resolved' && status !== 'cancelled') {\n invalidBody(`${at}.status must be \"resolved\" or \"cancelled\"`)\n }\n const entry: AGUIResumeEntry = {\n interruptId: requireString(value.interruptId, `${at}.interruptId`),\n status,\n }\n // Omit the key entirely when absent, matching the optional-field shape the\n // schema produced.\n if (value.payload !== undefined) entry.payload = value.payload\n return entry\n}\n\n/**\n * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.\n *\n * Returns a spread-friendly object whose `messages` field is suitable for\n * passing directly to `chat({ messages })`. The existing\n * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and\n * reasoning/activity/developer-role normalization internally.\n *\n * Validated structurally against the AG-UI `RunAgentInput` contract without a\n * schema library, so this package pulls in no validation runtime of its own.\n *\n * @throws An error with a migration-pointing message when the body does\n * not conform to AG-UI `RunAgentInput`. Surface this as a\n * 400 Bad Request to the client.\n */\nexport async function chatParamsFromRequestBody(body: unknown): Promise<{\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n parentRunId?: string\n tools: Array<{ name: string; description: string; parameters: JSONSchema }>\n forwardedProps: Record<string, unknown>\n state: unknown\n resume?: Array<RunAgentResumeItem>\n /**\n * @deprecated Use `aguiContext` instead. This alias will be removed in a\n * future release.\n */\n context: Array<AGUIContext>\n aguiContext: Array<AGUIContext>\n}> {\n if (!isRecord(body)) invalidBody('body must be a JSON object')\n\n const threadId = requireString(body.threadId, 'threadId')\n const runId = requireString(body.runId, 'runId')\n const parentRunId =\n body.parentRunId === undefined\n ? undefined\n : requireString(body.parentRunId, 'parentRunId')\n\n const messages = requireArray(body.messages, 'messages').map(validateMessage)\n const tools = requireArray(body.tools, 'tools').map(validateTool)\n const aguiContext = requireArray(body.context, 'context').map(validateContext)\n const resume =\n body.resume === undefined\n ? undefined\n : requireArray(body.resume, 'resume').map(validateResumeEntry)\n\n if (body.forwardedProps !== undefined && !isRecord(body.forwardedProps)) {\n invalidBody('forwardedProps must be an object')\n }\n\n return {\n // Unknown top-level fields (e.g. a legacy `cursor`) are dropped by\n // construction: only the fields below are copied onto the result.\n messages: messages as Array<UIMessage | ModelMessage>,\n threadId,\n runId,\n parentRunId,\n tools,\n forwardedProps: (body.forwardedProps ?? {}) as Record<string, unknown>,\n state: body.state,\n resume: resume as Array<RunAgentResumeItem> | undefined,\n context: aguiContext,\n aguiContext,\n }\n}\n\n/**\n * Read an HTTP `Request`, parse its JSON body, and validate it as an\n * AG-UI `RunAgentInput` — collapsing the standard `req.json()` +\n * `chatParamsFromRequestBody(...)` pair into a single call.\n *\n * On a malformed body or invalid AG-UI shape, this **throws a\n * `Response`** with status 400 and a migration-pointing message in the\n * body. Frameworks that natively handle thrown `Response` objects\n * (TanStack Start, SolidStart, Remix, React Router 7) will return the\n * 400 to the client automatically, so the handler reduces to:\n *\n * ```ts\n * export async function POST(req: Request) {\n * const params = await chatParamsFromRequest(req)\n * // ...use params\n * }\n * ```\n *\n * In frameworks that do not auto-handle thrown `Response` objects\n * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call\n * with try/catch and return the caught Response yourself, or use\n * `chatParamsFromRequestBody` directly with your own JSON-parsing.\n *\n * @throws {Response} 400 on malformed JSON or invalid AG-UI shape.\n */\nexport async function chatParamsFromRequest(\n req: Request,\n): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {\n let body: unknown\n try {\n body = await req.json()\n } catch (cause) {\n // Preserve the underlying error on the thrown Response for\n // server-side observability without leaking it to the client.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n try {\n return await chatParamsFromRequestBody(body)\n } catch (cause) {\n // Generic public message — avoid echoing Zod paths (which can contain\n // user payload fragments) or internal validator strings to the client.\n // The original AGUIError is attached as `cause` so server logs can\n // surface it without exposing it to remote callers.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n}\n\n/**\n * Client-declared tool stub (no execute). `name` is `string`, so arrays that\n * include these stubs intentionally widen `TypedStreamChunk` tool-name\n * discrimination — pass server tools alone when you need a closed name union.\n */\nexport type ClientToolDeclaration = {\n name: string\n description: string\n inputSchema: JSONSchema\n}\n\nexport type MergedAgentTools<TServerTools extends ReadonlyArray<AnyTool>> =\n ReadonlyArray<TServerTools[number] | ClientToolDeclaration>\n\n/**\n * Merge a server-side tool array with the AG-UI client-declared tools\n * received in the request body.\n *\n * Rules:\n * - Server tools win on name collision. The client's declaration is\n * ignored if the server already has a tool with that name. The client's\n * UI-side handler still fires when the streamed tool-result event comes\n * through (see `chat-client.ts` `onToolCall`), giving the\n * \"after server execution the client also handles\" semantic for free.\n * - Client-only tools (name not in `serverTools`) become no-execute\n * entries: the runtime's existing `ClientToolRequest` path handles\n * them — server emits a tool-call request, client executes via its\n * registered handler, client posts back the result.\n *\n * Typing:\n * - Empty `clientTools` preserves the server tuple (closed name union).\n * - Non-empty `clientTools` returns a widened array that honestly includes\n * client stubs, so `TypedStreamChunk` does not claim a closed server-only\n * name union.\n *\n * @param serverTools - The server's tool array (e.g. from\n * `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.\n * @param clientTools - The `tools` array received from\n * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.\n * @returns A merged array suitable for `chat({ tools })`.\n */\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(serverTools: TServerTools, clientTools: readonly []): TServerTools\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): MergedAgentTools<TServerTools>\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): TServerTools | MergedAgentTools<TServerTools> {\n if (clientTools.length === 0) {\n return serverTools\n }\n const seen = new Set(serverTools.map((t) => t.name))\n const merged: Array<TServerTools[number] | ClientToolDeclaration> = [\n ...serverTools,\n ]\n for (const ct of clientTools) {\n if (seen.has(ct.name)) {\n // Server wins on name collision.\n continue\n }\n seen.add(ct.name)\n merged.push({\n name: ct.name,\n description: ct.description,\n inputSchema: ct.parameters,\n // No `execute` — runtime treats this as a client-side tool and\n // emits ClientToolRequest events.\n })\n }\n return merged\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-params.js","names":[],"sources":["../../../src/utilities/chat-params.ts"],"sourcesContent":["import { AGUIError } from '@ag-ui/core'\nimport type {\n Context as AGUIContext,\n Message as AGUIMessage,\n Role as AGUIRole,\n} from '@ag-ui/core'\nimport type {\n AnyTool,\n JSONSchema,\n ModelMessage,\n RunAgentResumeItem,\n UIMessage,\n} from '../types'\n\nconst KNOWN_PART_TYPES = new Set([\n 'text',\n 'image',\n 'audio',\n 'video',\n 'document',\n 'tool-call',\n 'tool-result',\n 'thinking',\n 'structured-output',\n])\n\nfunction isValidParts(value: unknown): value is Array<{ type: string }> {\n if (!Array.isArray(value)) return false\n for (const p of value) {\n if (!p || typeof p !== 'object') return false\n const type = (p as { type?: unknown }).type\n if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false\n if (type === 'structured-output') {\n const raw = (p as { raw?: unknown }).raw\n if (raw !== undefined && typeof raw !== 'string') return false\n }\n }\n return true\n}\n\n/**\n * Keyed by `AGUIRole` so a role added upstream fails to compile here until it\n * is handled, rather than silently falling through as an unknown role.\n */\nconst AGUI_ROLES: Record<AGUIRole, true> = {\n developer: true,\n system: true,\n assistant: true,\n user: true,\n tool: true,\n activity: true,\n reasoning: true,\n}\n\nfunction isAGUIRole(value: unknown): value is AGUIRole {\n return typeof value === 'string' && value in AGUI_ROLES\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reject the request body, pointing at the migration guide. Mirrors the\n * message the previous `RunAgentInputSchema.safeParse` failure produced.\n */\nfunction invalidBody(reason: string): never {\n throw new AGUIError(\n `Request body is not a valid AG-UI RunAgentInput. ` +\n `If you're upgrading from a previous @tanstack/ai-client release, ` +\n `see docs/migration/ag-ui-compliance.md. ` +\n `Validation errors: ${reason}`,\n )\n}\n\nfunction requireString(value: unknown, at: string): string {\n if (typeof value !== 'string') invalidBody(`${at} must be a string`)\n return value\n}\n\nfunction requireArray(value: unknown, at: string): Array<unknown> {\n if (!Array.isArray(value)) invalidBody(`${at} must be an array`)\n return value\n}\n\n/**\n * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream\n * `MessageSchema` discriminated union does. The record view is retained on the\n * asserted type so callers can still inspect non-AG-UI extras like `parts`.\n */\nfunction assertAGUIMessage(\n value: Record<string, unknown>,\n at: string,\n): asserts value is Record<string, unknown> & AGUIMessage {\n requireString(value.id, `${at}.id`)\n\n const role = value.role\n if (!isAGUIRole(role)) {\n invalidBody(\n `${at}.role must be one of ${Object.keys(AGUI_ROLES).join(' | ')}`,\n )\n }\n\n switch (role) {\n case 'assistant':\n // Both optional: a tool-calling turn carries no text content.\n if (value.content !== undefined) {\n requireString(value.content, `${at}.content`)\n }\n if (value.toolCalls !== undefined) {\n requireArray(value.toolCalls, `${at}.toolCalls`)\n }\n break\n case 'user':\n if (typeof value.content !== 'string' && !Array.isArray(value.content)) {\n invalidBody(\n `${at}.content must be a string or an array of content parts`,\n )\n }\n break\n case 'tool':\n requireString(value.content, `${at}.content`)\n requireString(value.toolCallId, `${at}.toolCallId`)\n break\n case 'activity':\n requireString(value.activityType, `${at}.activityType`)\n if (!isRecord(value.content)) {\n invalidBody(`${at}.content must be an object`)\n }\n break\n case 'developer':\n case 'system':\n case 'reasoning':\n requireString(value.content, `${at}.content`)\n break\n }\n}\n\nfunction validateMessage(value: unknown, index: number): AGUIMessage {\n const at = `messages[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n assertAGUIMessage(value, at)\n\n // `parts` is TanStack's canonical extra, carried through so the UIMessage\n // path inside `chat()` can use it. Keep it only when it holds recognized\n // part types — the previous schema-based path dropped `parts` during parse\n // and re-attached it from the raw body behind this same check.\n if ('parts' in value && !isValidParts(value.parts)) {\n const withoutParts = { ...value }\n Reflect.deleteProperty(withoutParts, 'parts')\n return withoutParts\n }\n return value\n}\n\nfunction validateTool(\n value: unknown,\n index: number,\n): { name: string; description: string; parameters: JSONSchema } {\n const at = `tools[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n name: requireString(value.name, `${at}.name`),\n description: requireString(value.description, `${at}.description`),\n // Upstream `ToolSchema` types this as optional `any`; it reaches the\n // provider as a raw JSON Schema either way.\n parameters: value.parameters as JSONSchema,\n }\n}\n\nfunction validateContext(value: unknown, index: number): AGUIContext {\n const at = `context[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n description: requireString(value.description, `${at}.description`),\n value: requireString(value.value, `${at}.value`),\n }\n}\n\nfunction validateResumeEntry(\n value: unknown,\n index: number,\n): RunAgentResumeItem {\n const at = `resume[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n const status = value.status\n if (status !== 'resolved' && status !== 'cancelled') {\n invalidBody(`${at}.status must be \"resolved\" or \"cancelled\"`)\n }\n const entry: RunAgentResumeItem = {\n interruptId: requireString(value.interruptId, `${at}.interruptId`),\n status,\n }\n // Omit the key entirely when absent, matching the optional-field shape the\n // schema produced.\n if (value.payload !== undefined) entry.payload = value.payload\n if (value.metadata !== undefined) {\n if (!isRecord(value.metadata)) {\n invalidBody(`${at}.metadata must be an object`)\n }\n entry.metadata = value.metadata\n }\n return entry\n}\n\n/**\n * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.\n *\n * Returns a spread-friendly object whose `messages` field is suitable for\n * passing directly to `chat({ messages })`. The existing\n * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and\n * reasoning/activity/developer-role normalization internally.\n *\n * Validated structurally against the AG-UI `RunAgentInput` contract without a\n * schema library, so this package pulls in no validation runtime of its own.\n *\n * @throws An error with a migration-pointing message when the body does\n * not conform to AG-UI `RunAgentInput`. Surface this as a\n * 400 Bad Request to the client.\n */\nexport async function chatParamsFromRequestBody(body: unknown): Promise<{\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n parentRunId?: string\n tools: Array<{ name: string; description: string; parameters: JSONSchema }>\n forwardedProps: Record<string, unknown>\n state: unknown\n resume?: Array<RunAgentResumeItem>\n /**\n * @deprecated Use `aguiContext` instead. This alias will be removed in a\n * future release.\n */\n context: Array<AGUIContext>\n aguiContext: Array<AGUIContext>\n}> {\n if (!isRecord(body)) invalidBody('body must be a JSON object')\n\n const threadId = requireString(body.threadId, 'threadId')\n const runId = requireString(body.runId, 'runId')\n const parentRunId =\n body.parentRunId === undefined\n ? undefined\n : requireString(body.parentRunId, 'parentRunId')\n\n const messages = requireArray(body.messages, 'messages').map(validateMessage)\n const tools = requireArray(body.tools, 'tools').map(validateTool)\n const aguiContext = requireArray(body.context, 'context').map(validateContext)\n const resume =\n body.resume === undefined\n ? undefined\n : requireArray(body.resume, 'resume').map(validateResumeEntry)\n\n if (body.forwardedProps !== undefined && !isRecord(body.forwardedProps)) {\n invalidBody('forwardedProps must be an object')\n }\n\n return {\n // Unknown top-level fields (e.g. a legacy `cursor`) are dropped by\n // construction: only the fields below are copied onto the result.\n messages: messages as Array<UIMessage | ModelMessage>,\n threadId,\n runId,\n parentRunId,\n tools,\n forwardedProps: (body.forwardedProps ?? {}) as Record<string, unknown>,\n state: body.state,\n resume: resume as Array<RunAgentResumeItem> | undefined,\n context: aguiContext,\n aguiContext,\n }\n}\n\n/**\n * Read an HTTP `Request`, parse its JSON body, and validate it as an\n * AG-UI `RunAgentInput` — collapsing the standard `req.json()` +\n * `chatParamsFromRequestBody(...)` pair into a single call.\n *\n * On a malformed body or invalid AG-UI shape, this **throws a\n * `Response`** with status 400 and a migration-pointing message in the\n * body. Frameworks that natively handle thrown `Response` objects\n * (TanStack Start, SolidStart, Remix, React Router 7) will return the\n * 400 to the client automatically, so the handler reduces to:\n *\n * ```ts\n * export async function POST(req: Request) {\n * const params = await chatParamsFromRequest(req)\n * // ...use params\n * }\n * ```\n *\n * In frameworks that do not auto-handle thrown `Response` objects\n * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call\n * with try/catch and return the caught Response yourself, or use\n * `chatParamsFromRequestBody` directly with your own JSON-parsing.\n *\n * @throws {Response} 400 on malformed JSON or invalid AG-UI shape.\n */\nexport async function chatParamsFromRequest(\n req: Request,\n): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {\n let body: unknown\n try {\n body = await req.json()\n } catch (cause) {\n // Preserve the underlying error on the thrown Response for\n // server-side observability without leaking it to the client.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n try {\n return await chatParamsFromRequestBody(body)\n } catch (cause) {\n // Generic public message — avoid echoing Zod paths (which can contain\n // user payload fragments) or internal validator strings to the client.\n // The original AGUIError is attached as `cause` so server logs can\n // surface it without exposing it to remote callers.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n}\n\n/**\n * Client-declared tool stub (no execute). `name` is `string`, so arrays that\n * include these stubs intentionally widen `TypedStreamChunk` tool-name\n * discrimination — pass server tools alone when you need a closed name union.\n */\nexport type ClientToolDeclaration = {\n name: string\n description: string\n inputSchema: JSONSchema\n}\n\nexport type MergedAgentTools<TServerTools extends ReadonlyArray<AnyTool>> =\n ReadonlyArray<TServerTools[number] | ClientToolDeclaration>\n\n/**\n * Merge a server-side tool array with the AG-UI client-declared tools\n * received in the request body.\n *\n * Rules:\n * - Server tools win on name collision. The client's declaration is\n * ignored if the server already has a tool with that name. The client's\n * UI-side handler still fires when the streamed tool-result event comes\n * through (see `chat-client.ts` `onToolCall`), giving the\n * \"after server execution the client also handles\" semantic for free.\n * - Client-only tools (name not in `serverTools`) become no-execute\n * entries: the runtime's existing `ClientToolRequest` path handles\n * them — server emits a tool-call request, client executes via its\n * registered handler, client posts back the result.\n *\n * Typing:\n * - Empty `clientTools` preserves the server tuple (closed name union).\n * - Non-empty `clientTools` returns a widened array that honestly includes\n * client stubs, so `TypedStreamChunk` does not claim a closed server-only\n * name union.\n *\n * @param serverTools - The server's tool array (e.g. from\n * `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.\n * @param clientTools - The `tools` array received from\n * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.\n * @returns A merged array suitable for `chat({ tools })`.\n */\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(serverTools: TServerTools, clientTools: readonly []): TServerTools\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): MergedAgentTools<TServerTools>\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): TServerTools | MergedAgentTools<TServerTools> {\n if (clientTools.length === 0) {\n return serverTools\n }\n const seen = new Set(serverTools.map((t) => t.name))\n const merged: Array<TServerTools[number] | ClientToolDeclaration> = [\n ...serverTools,\n ]\n for (const ct of clientTools) {\n if (seen.has(ct.name)) {\n // Server wins on name collision.\n continue\n }\n seen.add(ct.name)\n merged.push({\n name: ct.name,\n description: ct.description,\n inputSchema: ct.parameters,\n // No `execute` — runtime treats this as a client-side tool and\n // emits ClientToolRequest events.\n })\n }\n return merged\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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,7 +80,7 @@
|
|
|
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.
|
|
83
|
+
"@tanstack/ai-event-client": "^0.9.0",
|
|
84
84
|
"@tanstack/ai-utils": "^0.4.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
@@ -561,11 +561,14 @@ durations 4/8/12s, single `input_reference` image prompt part), `grokVideo(...)`
|
|
|
561
561
|
outputs inherit the source clip's properties, so `size`/`aspect_ratio`/`resolution`
|
|
562
562
|
throw in both modes and `duration` throws in edit mode — pass none of them there;
|
|
563
563
|
generation uses the aspect-ratio size template like `'16:9_720p'` (1080p is 1.5-only),
|
|
564
|
-
integer durations 1-15s, reports `usage.
|
|
564
|
+
integer durations 1-15s, reports `usage.billed` seconds ({ quantity, unit: 'seconds' }) and exact `usage.cost`), `byteplusVideo(...)` (Seedance —
|
|
565
565
|
aspect-ratio size template like `'16:9_720p'`, durations 4-15s on the 2.0 family,
|
|
566
566
|
4-12s on 1.5-pro, 2-12s on the 1.0-pro models; reads `ARK_API_KEY`),
|
|
567
567
|
`openRouterVideo(...)` (OpenRouter's dedicated `POST /api/v1/videos` gateway),
|
|
568
|
-
and `falVideo(...)` (hosted models
|
|
568
|
+
and `falVideo(...)` (hosted models; `duration` typed from `@fal-ai/client`'s
|
|
569
|
+
`EndpointTypeMap` — `'5' | '10'` on Kling 2.6, `'3'`…`'15'` on Kling 3,
|
|
570
|
+
`'4s' | '6s' | '8s'` on Veo 3.1, `'5s' | '9s'` on Luma; `availableDurations()` /
|
|
571
|
+
`snapDuration()` on the curated set; see cost tracking below).
|
|
569
572
|
|
|
570
573
|
> **Seedance option applicability is per model and enforced server-side** —
|
|
571
574
|
> Ark returns a 400 for an inapplicable field rather than ignoring it.
|
|
@@ -620,10 +623,11 @@ const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
|
|
|
620
623
|
|
|
621
624
|
fal bills media generation by usage-based units, not tokens. Every fal media
|
|
622
625
|
adapter (`falImage`, `falAudio`, `falSpeech`, `falTranscription`, `falVideo`)
|
|
623
|
-
surfaces the real billed quantity on the result as `usage.
|
|
624
|
-
from fal's `x-fal-billable-units` response
|
|
625
|
-
needed. It rides on the canonical `TokenUsage`
|
|
626
|
-
media), mirroring how duration-billed
|
|
626
|
+
surfaces the real billed quantity on the result as `usage.billed`
|
|
627
|
+
({ quantity, unit: 'units' }), read from fal's `x-fal-billable-units` response
|
|
628
|
+
header — no `fetch` interceptor needed. It rides on the canonical `TokenUsage`
|
|
629
|
+
shape (token fields are `0` for media), mirroring how duration-billed
|
|
630
|
+
transcription reports { quantity, unit: 'seconds' }.
|
|
627
631
|
|
|
628
632
|
```typescript
|
|
629
633
|
import { generateImage } from '@tanstack/ai'
|
|
@@ -634,10 +638,10 @@ const result = await generateImage({
|
|
|
634
638
|
prompt: 'a serene mountain lake',
|
|
635
639
|
})
|
|
636
640
|
|
|
637
|
-
// usage.
|
|
641
|
+
// usage.billed.quantity is the priced quantity. Multiply by the endpoint unit
|
|
638
642
|
// price (GET https://api.fal.ai/v1/models/pricing?endpoint_id=…) for exact cost.
|
|
639
|
-
if (result.usage?.
|
|
640
|
-
const cost = result.usage.
|
|
643
|
+
if (result.usage?.billed) {
|
|
644
|
+
const cost = result.usage.billed.quantity * unitPrice
|
|
641
645
|
}
|
|
642
646
|
```
|
|
643
647
|
|