veryfront 0.1.261 → 0.1.263

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  2. package/esm/cli/commands/knowledge/command.js +19 -26
  3. package/esm/cli/templates/manifest.d.ts +470 -470
  4. package/esm/cli/templates/manifest.js +519 -519
  5. package/esm/deno.js +1 -1
  6. package/esm/src/agent/ag-ui-detached-start.d.ts +1 -4
  7. package/esm/src/agent/ag-ui-detached-start.d.ts.map +1 -1
  8. package/esm/src/agent/ag-ui-detached-start.js +4 -67
  9. package/esm/src/agent/ag-ui-handler.d.ts +1 -4
  10. package/esm/src/agent/ag-ui-handler.d.ts.map +1 -1
  11. package/esm/src/agent/ag-ui-handler.js +3 -61
  12. package/esm/src/agent/ag-ui-host-support.d.ts.map +1 -1
  13. package/esm/src/agent/ag-ui-host-support.js +2 -21
  14. package/esm/src/agent/ag-ui-request-shared.d.ts +4 -0
  15. package/esm/src/agent/ag-ui-request-shared.d.ts.map +1 -0
  16. package/esm/src/agent/ag-ui-request-shared.js +47 -0
  17. package/esm/src/agent/ag-ui-run-control.d.ts.map +1 -1
  18. package/esm/src/agent/ag-ui-run-control.js +1 -23
  19. package/esm/src/agent/ag-ui-runtime-handler.d.ts +1 -5
  20. package/esm/src/agent/ag-ui-runtime-handler.d.ts.map +1 -1
  21. package/esm/src/agent/ag-ui-runtime-handler.js +3 -67
  22. package/esm/src/agent/ag-ui-tool-shared.d.ts +15 -0
  23. package/esm/src/agent/ag-ui-tool-shared.d.ts.map +1 -0
  24. package/esm/src/agent/ag-ui-tool-shared.js +47 -0
  25. package/esm/src/agent/conversation-run-event-preparation.d.ts +3 -1
  26. package/esm/src/agent/conversation-run-event-preparation.d.ts.map +1 -1
  27. package/esm/src/agent/conversation-run-event-preparation.js +40 -0
  28. package/esm/src/agent/index.d.ts +3 -1
  29. package/esm/src/agent/index.d.ts.map +1 -1
  30. package/esm/src/agent/index.js +2 -1
  31. package/esm/src/agent/runtime-ag-ui-contract.d.ts.map +1 -1
  32. package/esm/src/agent/runtime-ag-ui-contract.js +2 -21
  33. package/esm/src/chat/chat-ui-message-helpers.d.ts +20 -0
  34. package/esm/src/chat/chat-ui-message-helpers.d.ts.map +1 -0
  35. package/esm/src/chat/chat-ui-message-helpers.js +167 -0
  36. package/esm/src/chat/index.d.ts +2 -0
  37. package/esm/src/chat/index.d.ts.map +1 -1
  38. package/esm/src/chat/index.js +1 -0
  39. package/esm/src/chat/protocol.d.ts +109 -0
  40. package/esm/src/chat/protocol.d.ts.map +1 -1
  41. package/esm/src/provider/runtime-loader/provider-http.d.ts +47 -0
  42. package/esm/src/provider/runtime-loader/provider-http.d.ts.map +1 -0
  43. package/esm/src/provider/runtime-loader/provider-http.js +171 -0
  44. package/esm/src/provider/runtime-loader/provider-records.d.ts +2 -0
  45. package/esm/src/provider/runtime-loader/provider-records.d.ts.map +1 -0
  46. package/esm/src/provider/runtime-loader/provider-records.js +6 -0
  47. package/esm/src/provider/runtime-loader.d.ts +2 -32
  48. package/esm/src/provider/runtime-loader.d.ts.map +1 -1
  49. package/esm/src/provider/runtime-loader.js +3 -176
  50. package/esm/src/routing/api/module-loader/external-import-rewriter.d.ts +27 -0
  51. package/esm/src/routing/api/module-loader/external-import-rewriter.d.ts.map +1 -0
  52. package/esm/src/routing/api/module-loader/external-import-rewriter.js +339 -0
  53. package/esm/src/routing/api/module-loader/loader.d.ts +1 -22
  54. package/esm/src/routing/api/module-loader/loader.d.ts.map +1 -1
  55. package/esm/src/routing/api/module-loader/loader.js +4 -336
  56. package/esm/src/server/dev-ui/manifest.d.ts +17 -17
  57. package/esm/src/server/dev-ui/manifest.js +17 -17
  58. package/esm/src/utils/version-constant.d.ts +1 -1
  59. package/esm/src/utils/version-constant.js +1 -1
  60. package/package.json +1 -1
  61. package/src/cli/commands/knowledge/command.ts +27 -32
  62. package/src/cli/templates/manifest.js +519 -519
  63. package/src/deno.js +1 -1
  64. package/src/src/agent/ag-ui-detached-start.ts +4 -92
  65. package/src/src/agent/ag-ui-handler.ts +3 -81
  66. package/src/src/agent/ag-ui-host-support.ts +5 -28
  67. package/src/src/agent/ag-ui-request-shared.ts +62 -0
  68. package/src/src/agent/ag-ui-run-control.ts +1 -27
  69. package/src/src/agent/ag-ui-runtime-handler.ts +3 -86
  70. package/src/src/agent/ag-ui-tool-shared.ts +77 -0
  71. package/src/src/agent/conversation-run-event-preparation.ts +57 -1
  72. package/src/src/agent/index.ts +19 -0
  73. package/src/src/agent/runtime-ag-ui-contract.ts +5 -28
  74. package/src/src/chat/chat-ui-message-helpers.ts +232 -0
  75. package/src/src/chat/index.ts +19 -0
  76. package/src/src/chat/protocol.ts +148 -0
  77. package/src/src/provider/runtime-loader/provider-http.ts +207 -0
  78. package/src/src/provider/runtime-loader/provider-records.ts +7 -0
  79. package/src/src/provider/runtime-loader.ts +10 -214
  80. package/src/src/routing/api/module-loader/external-import-rewriter.ts +461 -0
  81. package/src/src/routing/api/module-loader/loader.ts +19 -462
  82. package/src/src/server/dev-ui/manifest.js +17 -17
  83. package/src/src/utils/version-constant.ts +1 -1
@@ -273,8 +273,10 @@ export {
273
273
  normalizeEncodedConversationRunEvents,
274
274
  } from "./conversation-run-events.js";
275
275
  export {
276
+ prepareConversationRunChunkEvents,
276
277
  prepareConversationRunExternalEvents,
277
278
  prepareConversationRunStreamEvents,
279
+ toConversationRunStreamEvent,
278
280
  } from "./conversation-run-event-preparation.js";
279
281
  export {
280
282
  type ConversationRunMirror,
@@ -379,6 +381,23 @@ export {
379
381
  streamDataStreamEvents,
380
382
  stripLeadingEmptyObjectPlaceholder,
381
383
  } from "./data-stream.js";
384
+ export type {
385
+ ChatMessageMetadata,
386
+ ChatMessageMetadataUsage,
387
+ ChatUiMessageChunk,
388
+ ChildRunAudit,
389
+ ChildRunAuditToolCall,
390
+ ChildRunAuditToolResult,
391
+ } from "../chat/protocol.js";
392
+ export {
393
+ buildChatStreamChunkMessageMetadata,
394
+ type BuildChatStreamChunkMessageMetadataInput,
395
+ dedupeChatUiMessageChunks,
396
+ extractChatMessageMetadata,
397
+ normalizeChatMessageMetadata,
398
+ normalizeChatUiMessageChunk,
399
+ normalizeChatUiMessageStream,
400
+ } from "../chat/chat-ui-message-helpers.js";
382
401
  export {
383
402
  expandAllowedRemoteToolNames,
384
403
  getProviderNativeToolNames,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { parseAgUiJsonRequestOrError } from "./ag-ui-request-shared.js";
2
3
 
3
4
  const AGENT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
4
5
  const MAX_TOOL_PARAMETERS_BYTES = 16_384;
@@ -172,32 +173,8 @@ export async function parseAgUiRuntimeRequest(request: Request): Promise<AgUiRun
172
173
  export async function parseAgUiRuntimeRequestOrError(
173
174
  request: Request,
174
175
  ): Promise<AgUiRuntimeRequest | Response> {
175
- try {
176
- return await parseAgUiRuntimeRequest(request);
177
- } catch (error) {
178
- if (error instanceof z.ZodError) {
179
- return Response.json(
180
- {
181
- error: "Invalid AG-UI runtime request",
182
- details: error.issues.map((issue) => ({
183
- path: issue.path,
184
- message: issue.message,
185
- })),
186
- },
187
- { status: 400 },
188
- );
189
- }
190
-
191
- if (error instanceof SyntaxError || error instanceof TypeError) {
192
- return Response.json(
193
- {
194
- error: "Invalid AG-UI runtime request",
195
- details: [{ path: [], message: "Malformed JSON request body" }],
196
- },
197
- { status: 400 },
198
- );
199
- }
200
-
201
- throw error;
202
- }
176
+ return await parseAgUiJsonRequestOrError(
177
+ () => parseAgUiRuntimeRequest(request),
178
+ "Invalid AG-UI runtime request",
179
+ );
203
180
  }
@@ -0,0 +1,232 @@
1
+ import type { ChatMessageMetadata, ChatUiMessageChunk } from "./protocol.js";
2
+
3
+ type StreamChunkMetadataPart = {
4
+ type: string;
5
+ totalUsage?: unknown;
6
+ };
7
+
8
+ export interface BuildChatStreamChunkMessageMetadataInput {
9
+ agentId: string;
10
+ modelId: string;
11
+ runId?: string;
12
+ streamingMessageId?: string;
13
+ part: StreamChunkMetadataPart;
14
+ }
15
+
16
+ type ReplayState = {
17
+ content: string;
18
+ replayOffset: number | null;
19
+ started: boolean;
20
+ ended: boolean;
21
+ };
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function normalizeUsageMetadata(value: unknown): ChatMessageMetadata["usage"] | undefined {
28
+ if (!isRecord(value)) {
29
+ return undefined;
30
+ }
31
+
32
+ const usage = {
33
+ ...(typeof value.inputTokens === "number" ? { inputTokens: value.inputTokens } : {}),
34
+ ...(typeof value.outputTokens === "number" ? { outputTokens: value.outputTokens } : {}),
35
+ ...(typeof value.reasoningTokens === "number"
36
+ ? { reasoningTokens: value.reasoningTokens }
37
+ : {}),
38
+ ...(typeof value.cachedInputTokens === "number"
39
+ ? { cachedInputTokens: value.cachedInputTokens }
40
+ : {}),
41
+ };
42
+
43
+ return Object.keys(usage).length > 0 ? usage : undefined;
44
+ }
45
+
46
+ function splitReplayDelta(
47
+ existing: string,
48
+ replayOffset: number,
49
+ delta: string,
50
+ ): { emit: string; nextReplayOffset: number | null } {
51
+ const remaining = existing.slice(replayOffset);
52
+
53
+ if (!remaining) {
54
+ return { emit: delta, nextReplayOffset: null };
55
+ }
56
+
57
+ if (delta === remaining.slice(0, delta.length)) {
58
+ return { emit: "", nextReplayOffset: replayOffset + delta.length };
59
+ }
60
+
61
+ if (delta.startsWith(remaining)) {
62
+ return { emit: delta.slice(remaining.length), nextReplayOffset: null };
63
+ }
64
+
65
+ if (remaining.startsWith(delta)) {
66
+ return { emit: "", nextReplayOffset: replayOffset + delta.length };
67
+ }
68
+
69
+ return { emit: delta, nextReplayOffset: null };
70
+ }
71
+
72
+ function getReplayState(stateMap: Map<string, ReplayState>, id: string): ReplayState {
73
+ const existing = stateMap.get(id);
74
+ if (existing) {
75
+ return existing;
76
+ }
77
+
78
+ const created: ReplayState = {
79
+ content: "",
80
+ replayOffset: null,
81
+ started: false,
82
+ ended: false,
83
+ };
84
+ stateMap.set(id, created);
85
+ return created;
86
+ }
87
+
88
+ export function normalizeChatMessageMetadata(value: unknown): ChatMessageMetadata {
89
+ if (!isRecord(value)) {
90
+ return {};
91
+ }
92
+
93
+ const usage = normalizeUsageMetadata(value.usage);
94
+
95
+ return {
96
+ ...(typeof value.createdAt === "string" ? { createdAt: value.createdAt } : {}),
97
+ ...(typeof value.isStopped === "boolean" ? { isStopped: value.isStopped } : {}),
98
+ ...(typeof value.isCompleted === "boolean" ? { isCompleted: value.isCompleted } : {}),
99
+ ...(typeof value.completedAt === "string" ? { completedAt: value.completedAt } : {}),
100
+ ...(typeof value.agentId === "string" ? { agentId: value.agentId } : {}),
101
+ ...(typeof value.agentName === "string" ? { agentName: value.agentName } : {}),
102
+ ...(typeof value.conversationId === "string" ? { conversationId: value.conversationId } : {}),
103
+ ...(typeof value.modelId === "string" ? { modelId: value.modelId } : {}),
104
+ ...(typeof value.runId === "string" ? { runId: value.runId } : {}),
105
+ ...(typeof value.streamingMessageId === "string"
106
+ ? { streamingMessageId: value.streamingMessageId }
107
+ : {}),
108
+ ...(usage ? { usage } : {}),
109
+ };
110
+ }
111
+
112
+ export function extractChatMessageMetadata(value: unknown): ChatMessageMetadata | undefined {
113
+ const normalized = normalizeChatMessageMetadata(value);
114
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
115
+ }
116
+
117
+ export function buildChatStreamChunkMessageMetadata(
118
+ input: BuildChatStreamChunkMessageMetadataInput,
119
+ ): ChatMessageMetadata {
120
+ const baseMetadata: ChatMessageMetadata = {
121
+ agentId: input.agentId,
122
+ modelId: input.modelId,
123
+ ...(input.runId ? { runId: input.runId } : {}),
124
+ ...(input.streamingMessageId ? { streamingMessageId: input.streamingMessageId } : {}),
125
+ };
126
+
127
+ if (input.part.type !== "finish" || !input.part.totalUsage) {
128
+ return baseMetadata;
129
+ }
130
+
131
+ const usage = normalizeUsageMetadata(input.part.totalUsage);
132
+ return usage ? { ...baseMetadata, usage } : baseMetadata;
133
+ }
134
+
135
+ export function normalizeChatUiMessageChunk(
136
+ chunk: ChatUiMessageChunk<unknown>,
137
+ ): ChatUiMessageChunk<ChatMessageMetadata> {
138
+ switch (chunk.type) {
139
+ case "start":
140
+ return {
141
+ type: "start",
142
+ ...(chunk.messageId ? { messageId: chunk.messageId } : {}),
143
+ ...(chunk.messageMetadata !== undefined
144
+ ? { messageMetadata: normalizeChatMessageMetadata(chunk.messageMetadata) }
145
+ : {}),
146
+ };
147
+ case "message-metadata":
148
+ return {
149
+ type: "message-metadata",
150
+ messageMetadata: normalizeChatMessageMetadata(chunk.messageMetadata),
151
+ };
152
+ case "finish":
153
+ return {
154
+ type: "finish",
155
+ ...(chunk.finishReason ? { finishReason: chunk.finishReason } : {}),
156
+ ...(chunk.messageMetadata !== undefined
157
+ ? { messageMetadata: normalizeChatMessageMetadata(chunk.messageMetadata) }
158
+ : {}),
159
+ };
160
+ default:
161
+ return chunk;
162
+ }
163
+ }
164
+
165
+ export async function* dedupeChatUiMessageChunks<TMessageMetadata>(
166
+ stream: AsyncIterable<ChatUiMessageChunk<TMessageMetadata>>,
167
+ ): AsyncIterable<ChatUiMessageChunk<TMessageMetadata>> {
168
+ const textStates = new Map<string, ReplayState>();
169
+ const reasoningStates = new Map<string, ReplayState>();
170
+
171
+ for await (const chunk of stream) {
172
+ if (chunk.type === "text-start" || chunk.type === "reasoning-start") {
173
+ const stateMap = chunk.type === "text-start" ? textStates : reasoningStates;
174
+ const state = getReplayState(stateMap, chunk.id);
175
+
176
+ if (state.started) {
177
+ state.replayOffset = 0;
178
+ state.ended = false;
179
+ continue;
180
+ }
181
+
182
+ state.started = true;
183
+ state.ended = false;
184
+ yield chunk;
185
+ continue;
186
+ }
187
+
188
+ if (chunk.type === "text-delta" || chunk.type === "reasoning-delta") {
189
+ const stateMap = chunk.type === "text-delta" ? textStates : reasoningStates;
190
+ const state = getReplayState(stateMap, chunk.id);
191
+ const { emit, nextReplayOffset } = state.replayOffset === null
192
+ ? { emit: chunk.delta, nextReplayOffset: null as number | null }
193
+ : splitReplayDelta(state.content, state.replayOffset, chunk.delta);
194
+
195
+ state.replayOffset = nextReplayOffset;
196
+ if (!emit) {
197
+ continue;
198
+ }
199
+
200
+ state.content += emit;
201
+ yield {
202
+ ...chunk,
203
+ delta: emit,
204
+ };
205
+ continue;
206
+ }
207
+
208
+ if (chunk.type === "text-end" || chunk.type === "reasoning-end") {
209
+ const stateMap = chunk.type === "text-end" ? textStates : reasoningStates;
210
+ const state = stateMap.get(chunk.id);
211
+
212
+ if (!state || state.ended) {
213
+ continue;
214
+ }
215
+
216
+ state.replayOffset = null;
217
+ state.ended = true;
218
+ yield chunk;
219
+ continue;
220
+ }
221
+
222
+ yield chunk;
223
+ }
224
+ }
225
+
226
+ export async function* normalizeChatUiMessageStream(
227
+ stream: AsyncIterable<ChatUiMessageChunk<unknown>>,
228
+ ): AsyncIterable<ChatUiMessageChunk<ChatMessageMetadata>> {
229
+ for await (const chunk of dedupeChatUiMessageChunks(stream)) {
230
+ yield normalizeChatUiMessageChunk(chunk);
231
+ }
232
+ }
@@ -212,12 +212,31 @@ export {
212
212
  type UseChatResult,
213
213
  } from "../agent/react/use-chat/index.js";
214
214
 
215
+ export type {
216
+ ChatMessageMetadata,
217
+ ChatMessageMetadataUsage,
218
+ ChatUiMessageChunk,
219
+ ChildRunAudit,
220
+ ChildRunAuditToolCall,
221
+ ChildRunAuditToolResult,
222
+ } from "./protocol.js";
223
+
215
224
  export {
216
225
  useAgent,
217
226
  type UseAgentOptions,
218
227
  type UseAgentResult,
219
228
  } from "../agent/react/use-agent.js";
220
229
 
230
+ export {
231
+ buildChatStreamChunkMessageMetadata,
232
+ type BuildChatStreamChunkMessageMetadataInput,
233
+ dedupeChatUiMessageChunks,
234
+ extractChatMessageMetadata,
235
+ normalizeChatMessageMetadata,
236
+ normalizeChatUiMessageChunk,
237
+ normalizeChatUiMessageStream,
238
+ } from "./chat-ui-message-helpers.js";
239
+
221
240
  export {
222
241
  useCompletion,
223
242
  type UseCompletionOptions,
@@ -77,6 +77,52 @@ export interface ChatMessage {
77
77
  createdAt?: Date | string;
78
78
  }
79
79
 
80
+ export interface ChatMessageMetadataUsage {
81
+ inputTokens?: number;
82
+ outputTokens?: number;
83
+ reasoningTokens?: number;
84
+ cachedInputTokens?: number;
85
+ }
86
+
87
+ export interface ChildRunAuditToolCall {
88
+ toolName: string;
89
+ toolCallId: string;
90
+ input?: unknown;
91
+ }
92
+
93
+ export interface ChildRunAuditToolResult {
94
+ toolName: string;
95
+ toolCallId: string;
96
+ input: unknown;
97
+ output: unknown;
98
+ }
99
+
100
+ export interface ChildRunAudit {
101
+ status: "completed" | "failed" | "cancelled" | "stopped";
102
+ description?: string;
103
+ steps?: number;
104
+ durationMs?: number;
105
+ toolCalls?: ChildRunAuditToolCall[];
106
+ toolResults?: ChildRunAuditToolResult[];
107
+ terminalErrorCode?: string | null;
108
+ terminalErrorMessage?: string | null;
109
+ }
110
+
111
+ export interface ChatMessageMetadata {
112
+ createdAt?: string;
113
+ isStopped?: boolean;
114
+ isCompleted?: boolean;
115
+ completedAt?: string;
116
+ agentId?: string;
117
+ agentName?: string;
118
+ conversationId?: string;
119
+ modelId?: string;
120
+ runId?: string;
121
+ streamingMessageId?: string;
122
+ childRunAudit?: ChildRunAudit;
123
+ usage?: ChatMessageMetadataUsage;
124
+ }
125
+
80
126
  export type ChatFinishReason =
81
127
  | "stop"
82
128
  | "length"
@@ -208,3 +254,105 @@ export type ChatStreamEvent =
208
254
  type: "error";
209
255
  errorText: string;
210
256
  };
257
+
258
+ type MessageLifecycleChunk<TMessageMetadata> =
259
+ | {
260
+ type: "start";
261
+ messageId?: string;
262
+ messageMetadata?: TMessageMetadata;
263
+ }
264
+ | {
265
+ type: "finish";
266
+ finishReason?: string;
267
+ messageMetadata?: TMessageMetadata;
268
+ }
269
+ | {
270
+ type: "message-metadata";
271
+ messageMetadata: TMessageMetadata;
272
+ };
273
+
274
+ type IdChunk<TType extends string> = {
275
+ type: TType;
276
+ id: string;
277
+ };
278
+
279
+ type IdDeltaChunk<TType extends string> = IdChunk<TType> & {
280
+ delta: string;
281
+ };
282
+
283
+ type ToolCallChunk<TType extends string> = {
284
+ type: TType;
285
+ toolCallId: string;
286
+ };
287
+
288
+ type NamedToolCallChunk<TType extends string> = ToolCallChunk<TType> & {
289
+ toolName: string;
290
+ };
291
+
292
+ type ToolInputChunk<TType extends string> = NamedToolCallChunk<TType> & {
293
+ input: unknown;
294
+ };
295
+
296
+ type ToolErrorChunk<TType extends string> = ToolCallChunk<TType> & {
297
+ errorText: string;
298
+ };
299
+
300
+ export type ChatUiMessageChunk<TMessageMetadata = ChatMessageMetadata> =
301
+ | MessageLifecycleChunk<TMessageMetadata>
302
+ | {
303
+ type: "start-step";
304
+ }
305
+ | {
306
+ type: "finish-step";
307
+ }
308
+ | {
309
+ type: "abort";
310
+ }
311
+ | IdChunk<"reasoning-start">
312
+ | IdDeltaChunk<"reasoning-delta">
313
+ | IdChunk<"reasoning-end">
314
+ | IdChunk<"text-start">
315
+ | IdDeltaChunk<"text-delta">
316
+ | IdChunk<"text-end">
317
+ | {
318
+ type: "source-url";
319
+ sourceId: string;
320
+ url: string;
321
+ title?: string;
322
+ }
323
+ | {
324
+ type: "source-document";
325
+ sourceId: string;
326
+ mediaType: string;
327
+ title: string;
328
+ filename?: string;
329
+ }
330
+ | {
331
+ type: "file";
332
+ mediaType: string;
333
+ url: string;
334
+ }
335
+ | NamedToolCallChunk<"tool-input-start">
336
+ | (ToolCallChunk<"tool-input-delta"> & {
337
+ inputTextDelta: string;
338
+ })
339
+ | ToolInputChunk<"tool-input-available">
340
+ | (ToolInputChunk<"tool-input-error"> & {
341
+ errorText: string;
342
+ })
343
+ | (ToolCallChunk<"tool-output-available"> & {
344
+ output: unknown;
345
+ })
346
+ | ToolErrorChunk<"tool-output-error">
347
+ | ToolCallChunk<"tool-output-denied">
348
+ | (ToolCallChunk<"tool-approval-request"> & {
349
+ approvalId: string;
350
+ })
351
+ | {
352
+ type: "error";
353
+ errorText: string;
354
+ }
355
+ | {
356
+ type: `data-${string}`;
357
+ data: unknown;
358
+ };
@@ -0,0 +1,207 @@
1
+ import { readRecord } from "./provider-records.js";
2
+
3
+ export type ProviderKind = "anthropic" | "openai" | "google";
4
+
5
+ /**
6
+ * Base class for typed provider errors. The `retryable` flag is the
7
+ * primary signal for callers (or a retry wrapper) to decide whether to
8
+ * re-issue the request. `retryAfterMs` is set when the provider gave an
9
+ * explicit delay hint (Retry-After header, Retry-Info trailer).
10
+ */
11
+ export class ProviderError extends Error {
12
+ readonly provider: ProviderKind;
13
+ readonly status: number;
14
+ readonly retryable: boolean;
15
+ readonly retryAfterMs?: number;
16
+
17
+ constructor(options: {
18
+ provider: ProviderKind;
19
+ status: number;
20
+ message: string;
21
+ retryable: boolean;
22
+ retryAfterMs?: number;
23
+ }) {
24
+ super(options.message);
25
+ this.name = new.target.name;
26
+ this.provider = options.provider;
27
+ this.status = options.status;
28
+ this.retryable = options.retryable;
29
+ if (options.retryAfterMs !== undefined) {
30
+ this.retryAfterMs = options.retryAfterMs;
31
+ }
32
+ }
33
+ }
34
+
35
+ /** Provider reports it is overloaded (Anthropic 529, OpenAI/Google 503). */
36
+ export class ProviderOverloadedError extends ProviderError {}
37
+
38
+ /** Provider is rate limiting this API key (OpenAI/Google 429 with Retry-After). */
39
+ export class ProviderRateLimitError extends ProviderError {}
40
+
41
+ /** Provider account quota is exhausted — non-retryable. */
42
+ export class ProviderQuotaError extends ProviderError {}
43
+
44
+ /** Non-retryable 4xx/5xx that doesn't fit another bucket. */
45
+ export class ProviderRequestError extends ProviderError {}
46
+
47
+ function parseRetryAfterMs(header: string | null): number | undefined {
48
+ if (!header) return undefined;
49
+ const asNumber = Number(header);
50
+ if (Number.isFinite(asNumber) && asNumber >= 0) {
51
+ return Math.round(asNumber * 1000);
52
+ }
53
+ // HTTP-date form (rare in practice for LLM providers).
54
+ const parsed = Date.parse(header);
55
+ if (!Number.isNaN(parsed)) {
56
+ return Math.max(0, parsed - Date.now());
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ /**
62
+ * Inspect a non-2xx response and build the most specific ProviderError
63
+ * subclass we can. Reads the response body as text (it's already dead
64
+ * on the wire by this point). Body classification handles the cases
65
+ * where HTTP status alone is ambiguous — notably OpenAI
66
+ * `insufficient_quota` vs `rate_limit_exceeded` both arriving as 429.
67
+ */
68
+ async function buildProviderError(
69
+ provider: ProviderKind,
70
+ response: Response,
71
+ ): Promise<ProviderError> {
72
+ const rawBody = await response.text();
73
+ const message = rawBody.trim() || `${response.status} ${response.statusText}`.trim();
74
+ const status = response.status;
75
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
76
+
77
+ const parsedBody = (() => {
78
+ try {
79
+ return JSON.parse(rawBody) as Record<string, unknown>;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ })();
84
+ const errorRecord = readRecord(parsedBody?.error);
85
+ const errorCode = typeof errorRecord?.code === "string"
86
+ ? errorRecord.code
87
+ : typeof errorRecord?.type === "string"
88
+ ? errorRecord.type
89
+ : typeof errorRecord?.status === "string"
90
+ ? errorRecord.status
91
+ : undefined;
92
+
93
+ // Anthropic 529 = overloaded. Anthropic surfaces this with
94
+ // { error: { type: "overloaded_error" } } in the body.
95
+ if (provider === "anthropic" && status === 529) {
96
+ return new ProviderOverloadedError({
97
+ provider,
98
+ status,
99
+ message,
100
+ retryable: true,
101
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
102
+ });
103
+ }
104
+
105
+ // OpenAI / Google 503 = overloaded.
106
+ if ((provider === "openai" || provider === "google") && status === 503) {
107
+ return new ProviderOverloadedError({
108
+ provider,
109
+ status,
110
+ message,
111
+ retryable: true,
112
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
113
+ });
114
+ }
115
+
116
+ // OpenAI 429 splits based on the error code in the body:
117
+ // - insufficient_quota → hard quota, non-retryable
118
+ // - rate_limit_exceeded / tokens_per_min_exceeded → retry with Retry-After
119
+ if (provider === "openai" && status === 429) {
120
+ if (errorCode === "insufficient_quota") {
121
+ return new ProviderQuotaError({
122
+ provider,
123
+ status,
124
+ message,
125
+ retryable: false,
126
+ });
127
+ }
128
+ return new ProviderRateLimitError({
129
+ provider,
130
+ status,
131
+ message,
132
+ retryable: true,
133
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
134
+ });
135
+ }
136
+
137
+ // Google 429 RESOURCE_EXHAUSTED is almost always the daily free-tier
138
+ // quota — surface as a hard quota error so callers don't hot-loop on
139
+ // retries that can't possibly succeed until midnight UTC.
140
+ if (provider === "google" && status === 429) {
141
+ if (errorCode === "RESOURCE_EXHAUSTED") {
142
+ return new ProviderQuotaError({
143
+ provider,
144
+ status,
145
+ message,
146
+ retryable: false,
147
+ });
148
+ }
149
+ return new ProviderRateLimitError({
150
+ provider,
151
+ status,
152
+ message,
153
+ retryable: true,
154
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
155
+ });
156
+ }
157
+
158
+ return new ProviderRequestError({
159
+ provider,
160
+ status,
161
+ message,
162
+ retryable: false,
163
+ });
164
+ }
165
+
166
+ export async function requestJson(options: {
167
+ url: string;
168
+ fetchImpl: typeof globalThis.fetch;
169
+ init: RequestInit;
170
+ providerLabel: string;
171
+ providerKind: ProviderKind;
172
+ }): Promise<unknown> {
173
+ const response = await options.fetchImpl(options.url, options.init);
174
+ if (!response.ok) {
175
+ const err = await buildProviderError(options.providerKind, response);
176
+ err.message = `${options.providerLabel} request failed: ${err.message}`;
177
+ throw err;
178
+ }
179
+
180
+ return response.json();
181
+ }
182
+
183
+ export async function requestStream(options: {
184
+ url: string;
185
+ fetchImpl: typeof globalThis.fetch;
186
+ init: RequestInit;
187
+ providerLabel: string;
188
+ providerKind: ProviderKind;
189
+ }): Promise<ReadableStream<Uint8Array>> {
190
+ const response = await options.fetchImpl(options.url, options.init);
191
+ if (!response.ok) {
192
+ const err = await buildProviderError(options.providerKind, response);
193
+ err.message = `${options.providerLabel} request failed: ${err.message}`;
194
+ throw err;
195
+ }
196
+
197
+ if (!response.body) {
198
+ throw new ProviderRequestError({
199
+ provider: options.providerKind,
200
+ status: response.status,
201
+ message: `${options.providerLabel} request failed: stream body missing`,
202
+ retryable: false,
203
+ });
204
+ }
205
+
206
+ return response.body;
207
+ }