@tonyclaw/agent-inspector 2.1.12 → 2.1.14

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/.output/cli.js +0 -8
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
  4. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-Cd1VsfRe.js → ReplayDialog-CU0Tbb2c.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-QxSpbmsj.js → RequestAnatomy-BNahe83D.js} +1 -1
  7. package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
  8. package/.output/public/assets/{StreamingChunkSequence-CwNGqJ5n.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
  9. package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
  10. package/.output/public/assets/index-DWOkqdCa.js +14 -0
  11. package/.output/public/assets/index-D_WfwzUi.js +1 -0
  12. package/.output/public/assets/index-DtLuQrs0.css +1 -0
  13. package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
  14. package/.output/public/assets/{main-B_EcGyf-.js → main-CSONBwwn.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +20 -20
  16. package/.output/server/_libs/react-markdown.mjs +90 -2
  17. package/.output/server/{_sessionId-CmO9read.mjs → _sessionId-CPAa37n5.mjs} +6 -57
  18. package/.output/server/_ssr/{CompareDrawer-D9wf870o.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
  19. package/.output/server/_ssr/{ProxyViewerContainer-BDLJSawP.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
  20. package/.output/server/_ssr/{ReplayDialog-B2dS4kmP.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
  21. package/.output/server/_ssr/{RequestAnatomy-k49Peshi.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
  22. package/.output/server/_ssr/{ResponseView-Bk3B1suu.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
  23. package/.output/server/_ssr/{StreamingChunkSequence-D6MDGCgh.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
  24. package/.output/server/_ssr/{index-CC8KdlTb.mjs → index-DsykulzS.mjs} +6 -57
  25. package/.output/server/_ssr/index.mjs +2 -2
  26. package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
  27. package/.output/server/_ssr/{router-BEmMnVa7.mjs → router-4bdm6Mt2.mjs} +674 -173
  28. package/.output/server/{_tanstack-start-manifest_v-Bf_4hG4w.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
  29. package/.output/server/index.mjs +70 -56
  30. package/package.json +1 -1
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  33. package/src/components/providers/ProviderCard.tsx +10 -1
  34. package/src/components/providers/ProviderForm.tsx +157 -41
  35. package/src/components/providers/ProvidersPanel.tsx +5 -1
  36. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  37. package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
  38. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  39. package/src/components/proxy-viewer/LogEntry.tsx +31 -11
  40. package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
  41. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  42. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  43. package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
  44. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  45. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  46. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  47. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  48. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  49. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  50. package/src/components/proxy-viewer/logFocus.ts +15 -2
  51. package/src/components/proxy-viewer/requestTools.ts +7 -3
  52. package/src/components/proxy-viewer/viewerState.ts +72 -3
  53. package/src/components/ui/json-viewer-bulk.ts +41 -6
  54. package/src/components/ui/json-viewer.tsx +9 -8
  55. package/src/contracts/index.ts +15 -1
  56. package/src/contracts/openai.ts +94 -0
  57. package/src/lib/providerContract.ts +1 -0
  58. package/src/lib/providerModelMetadata.ts +7 -1
  59. package/src/lib/stopReason.ts +12 -0
  60. package/src/lib/upstreamUrl.ts +46 -0
  61. package/src/mcp/previewExtractor.ts +73 -3
  62. package/src/mcp/server.ts +2 -0
  63. package/src/mcp/toolHandlers.ts +2 -0
  64. package/src/proxy/constants.ts +2 -0
  65. package/src/proxy/formats/openai/handler.ts +40 -9
  66. package/src/proxy/formats/openai/index.ts +7 -0
  67. package/src/proxy/formats/openai/schemas.ts +15 -1
  68. package/src/proxy/formats/openai/stream.ts +529 -135
  69. package/src/proxy/formats/registry.ts +9 -1
  70. package/src/proxy/identityProxy.ts +0 -7
  71. package/src/proxy/providerImporters.ts +45 -5
  72. package/src/proxy/providers.ts +31 -11
  73. package/src/proxy/schemas.ts +6 -0
  74. package/src/proxy/toolSchemaWarnings.ts +25 -3
  75. package/src/proxy/upstream.ts +18 -37
  76. package/src/routes/api/providers.$providerId.ts +1 -0
  77. package/src/routes/api/providers.ts +2 -0
  78. package/.output/public/assets/CompareDrawer-XTdSOto9.js +0 -1
  79. package/.output/public/assets/ProxyViewerContainer-Cc944N8i.js +0 -117
  80. package/.output/public/assets/ResponseView-8GivXCzp.js +0 -1
  81. package/.output/public/assets/_sessionId-D89uDTQs.js +0 -1
  82. package/.output/public/assets/index-BsFdgXac.js +0 -1
  83. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -1,16 +1,163 @@
1
1
  import type { CapturedLog, JsonValue } from "../../schemas";
2
2
  import { trustAsJsonValue } from "../jsonSchema";
3
- import { OpenAISSERawChunkSchema } from "./schemas";
3
+ import {
4
+ OpenAISSERawChunkSchema,
5
+ OpenAIResponsesResponseSchema,
6
+ OpenAIResponsesSSEventSchema,
7
+ type OpenAIResponsesResponse,
8
+ } from "./schemas";
4
9
 
5
- /**
6
- * Extract OpenAI streaming response and return normalized JSON.
7
- * Optionally collects SSE chunks into log.streamingChunks for debugging/display.
8
- */
9
- export function extractOpenAIStream(
10
- raw: string,
10
+ type ParsedSseEvent = {
11
+ event: string | null;
12
+ data: string;
13
+ };
14
+
15
+ type CollectedChunk = {
16
+ index: number;
17
+ timestamp: number;
18
+ type: string;
19
+ data: JsonValue;
20
+ };
21
+
22
+ type ResponseFunctionCall = {
23
+ id?: string;
24
+ call_id?: string;
25
+ type: "function_call";
26
+ name?: string;
27
+ arguments: string;
28
+ status?: string;
29
+ };
30
+
31
+ const MAX_CHUNKS = 1000;
32
+
33
+ function readProperty(value: unknown, key: string): unknown {
34
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
35
+ return Object.getOwnPropertyDescriptor(value, key)?.value;
36
+ }
37
+
38
+ function readStringProperty(value: unknown, key: string): string | null {
39
+ const property = readProperty(value, key);
40
+ return typeof property === "string" ? property : null;
41
+ }
42
+
43
+ function readNumberProperty(value: unknown, key: string): number | null {
44
+ const property = readProperty(value, key);
45
+ return typeof property === "number" && Number.isFinite(property) ? property : null;
46
+ }
47
+
48
+ function readObjectProperty(value: unknown, key: string): unknown | null {
49
+ const property = readProperty(value, key);
50
+ return property !== null && typeof property === "object" && !Array.isArray(property)
51
+ ? property
52
+ : null;
53
+ }
54
+
55
+ function readArrayProperty(value: unknown, key: string): unknown[] {
56
+ const property = readProperty(value, key);
57
+ return Array.isArray(property) ? property : [];
58
+ }
59
+
60
+ function parseSseEvents(raw: string): ParsedSseEvent[] {
61
+ const events: ParsedSseEvent[] = [];
62
+ let event: string | null = null;
63
+ let dataLines: string[] = [];
64
+
65
+ const commit = (): void => {
66
+ if (event === null && dataLines.length === 0) return;
67
+ events.push({ event, data: dataLines.join("\n") });
68
+ event = null;
69
+ dataLines = [];
70
+ };
71
+
72
+ for (const rawLine of raw.split("\n")) {
73
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
74
+ if (line === "") {
75
+ commit();
76
+ continue;
77
+ }
78
+ if (line.startsWith(":")) continue;
79
+
80
+ const separatorIndex = line.indexOf(":");
81
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
82
+ const rawValue = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
83
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
84
+
85
+ switch (field) {
86
+ case "event":
87
+ event = value;
88
+ break;
89
+ case "data":
90
+ if (
91
+ event === null &&
92
+ dataLines.length > 0 &&
93
+ (value.startsWith("{") || value === "[DONE]")
94
+ ) {
95
+ commit();
96
+ }
97
+ dataLines.push(value);
98
+ break;
99
+ default:
100
+ break;
101
+ }
102
+ }
103
+
104
+ commit();
105
+ return events;
106
+ }
107
+
108
+ function parseJson(data: string): unknown | null {
109
+ try {
110
+ return JSON.parse(data);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function eventTypeFor(event: ParsedSseEvent, parsed: unknown): string {
117
+ if (event.event !== null && event.event !== "") return event.event;
118
+ const typed = OpenAIResponsesSSEventSchema.safeParse(parsed);
119
+ return typed.success ? typed.data.type : "";
120
+ }
121
+
122
+ function isResponsesEvent(event: ParsedSseEvent): boolean {
123
+ if (event.data === "[DONE]") return false;
124
+ const parsed = parseJson(event.data);
125
+ const type = eventTypeFor(event, parsed);
126
+ return type.startsWith("response.") || type === "error";
127
+ }
128
+
129
+ function collectChunk(
130
+ chunks: CollectedChunk[],
131
+ chunkIndex: number,
132
+ streamStartMs: number,
133
+ type: string,
134
+ parsed: unknown,
135
+ ): void {
136
+ if (chunks.length >= MAX_CHUNKS) return;
137
+ chunks.push({
138
+ index: chunkIndex,
139
+ timestamp: Date.now() - streamStartMs,
140
+ type,
141
+ data: trustAsJsonValue(parsed),
142
+ });
143
+ }
144
+
145
+ function setErrorFromPayload(log: CapturedLog, payload: unknown): void {
146
+ const errorObject = readObjectProperty(payload, "error");
147
+ const message =
148
+ readStringProperty(errorObject, "message") ??
149
+ readStringProperty(payload, "message") ??
150
+ readStringProperty(payload, "error");
151
+ if (message !== null && message.length > 0) {
152
+ log.error = message;
153
+ }
154
+ }
155
+
156
+ function extractChatCompletionsStream(
157
+ events: ParsedSseEvent[],
11
158
  log: CapturedLog,
12
- fallbackModel?: string,
13
- collectChunks: boolean = true,
159
+ fallbackModel: string | undefined,
160
+ collectChunks: boolean,
14
161
  ): string {
15
162
  let id = "";
16
163
  let model = "";
@@ -27,145 +174,89 @@ export function extractOpenAIStream(
27
174
  type: "function";
28
175
  function: { name: string; arguments: string };
29
176
  };
30
- // Map keyed by `index` so delta accumulation is O(1) per delta instead of
31
- // O(N) (the previous Array#find). Map preserves insertion order, so the
32
- // final `[...toolCalls.values()]` keeps tool calls in the order they were
33
- // first seen.
34
177
  const toolCalls: Map<number, ToolCallEntry> = new Map();
35
178
 
36
- // Chunk collection state
37
- const MAX_CHUNKS = 1000;
38
179
  let chunkIndex = 0;
39
180
  let streamStartMs = 0;
40
- const chunks: Array<{ index: number; timestamp: number; type: string; data: JsonValue }> = [];
41
-
42
- for (const line of raw.split("\n")) {
43
- const trimmedLine = line.trim();
44
- if (!trimmedLine.startsWith("data: ")) continue;
45
- const dataStr = trimmedLine.slice(6);
46
- if (dataStr === "[DONE]") break;
47
-
48
- try {
49
- const parsed: unknown = JSON.parse(dataStr);
50
- // Check for error object in SSE stream before trying to parse as chunk
51
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
52
- const errorDesc = Object.getOwnPropertyDescriptor(parsed, "error");
53
- if (
54
- errorDesc !== undefined &&
55
- typeof errorDesc.value === "object" &&
56
- errorDesc.value !== null
57
- ) {
58
- const msgDesc = Object.getOwnPropertyDescriptor(errorDesc.value, "message");
59
- if (msgDesc !== undefined && typeof msgDesc.value === "string") {
60
- log.error = msgDesc.value;
61
- }
62
- continue;
63
- }
64
- }
65
- const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
66
- if (!chunkResult.success) continue;
67
- const chunk = chunkResult.data;
68
-
69
- // Track stream start for timestamps
70
- if (chunkIndex === 0) streamStartMs = Date.now();
71
-
72
- // Collect chunks if enabled
73
- if (collectChunks === true && chunks.length < MAX_CHUNKS) {
74
- // `chunk` has already been validated by OpenAISSERawChunkSchema above;
75
- // skipping the recursive JsonValue walk avoids a second full-tree zod
76
- // parse on every SSE event. See trustAsJsonValue for the rationale.
77
- chunks.push({
78
- index: chunkIndex,
79
- timestamp: Date.now() - streamStartMs,
80
- type: "chat.completion.chunk",
81
- data: trustAsJsonValue(chunk),
82
- });
83
- }
84
- chunkIndex++;
181
+ const chunks: CollectedChunk[] = [];
85
182
 
86
- if (!started) {
87
- id = chunk.id;
88
- model = chunk.model;
89
- started = true;
90
- }
183
+ for (const event of events) {
184
+ if (event.data === "[DONE]") break;
91
185
 
92
- if (!usageCaptured && chunk.usage !== undefined && chunk.usage !== null) {
93
- promptTokens = chunk.usage.prompt_tokens ?? 0;
94
- completionTokens = chunk.usage.completion_tokens ?? 0;
95
- log.inputTokens = promptTokens;
96
- // Extract cached_tokens from raw parsed object (passthrough field in usage)
97
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
98
- const usageDesc = Object.getOwnPropertyDescriptor(parsed, "usage");
99
- if (
100
- usageDesc !== undefined &&
101
- typeof usageDesc.value === "object" &&
102
- usageDesc.value !== null
103
- ) {
104
- const detailsDesc = Object.getOwnPropertyDescriptor(
105
- usageDesc.value,
106
- "prompt_tokens_details",
107
- );
108
- if (
109
- detailsDesc !== undefined &&
110
- typeof detailsDesc.value === "object" &&
111
- detailsDesc.value !== null
112
- ) {
113
- const cacheDesc = Object.getOwnPropertyDescriptor(detailsDesc.value, "cached_tokens");
114
- if (cacheDesc !== undefined && typeof cacheDesc.value === "number") {
115
- log.cacheReadInputTokens = cacheDesc.value;
116
- }
117
- }
118
- }
119
- }
120
- usageCaptured = true;
186
+ const parsed = parseJson(event.data);
187
+ if (parsed === null) continue;
188
+ setErrorFromPayload(log, parsed);
189
+
190
+ const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
191
+ if (!chunkResult.success) continue;
192
+ const chunk = chunkResult.data;
193
+
194
+ if (chunkIndex === 0) streamStartMs = Date.now();
195
+ if (collectChunks) {
196
+ collectChunk(chunks, chunkIndex, streamStartMs, "chat.completion.chunk", chunk);
197
+ }
198
+ chunkIndex++;
199
+
200
+ if (!started) {
201
+ id = chunk.id;
202
+ model = chunk.model;
203
+ started = true;
204
+ }
205
+
206
+ if (!usageCaptured && chunk.usage !== undefined && chunk.usage !== null) {
207
+ promptTokens = chunk.usage.prompt_tokens ?? 0;
208
+ completionTokens = chunk.usage.completion_tokens ?? 0;
209
+ log.inputTokens = promptTokens;
210
+
211
+ const usage = readObjectProperty(parsed, "usage");
212
+ const details = readObjectProperty(usage, "prompt_tokens_details");
213
+ const cachedTokens = readNumberProperty(details, "cached_tokens");
214
+ if (cachedTokens !== null) {
215
+ log.cacheReadInputTokens = cachedTokens;
121
216
  }
217
+ usageCaptured = true;
218
+ }
122
219
 
123
- for (const choice of chunk.choices) {
124
- const delta = choice.delta;
125
- if (delta.content !== undefined && delta.content !== null) {
126
- completionContent += delta.content;
127
- }
128
- if (delta.reasoning_content !== undefined && delta.reasoning_content !== null) {
129
- reasoningContent += delta.reasoning_content;
130
- }
131
- if (delta.thinking !== undefined && delta.thinking !== null) {
132
- reasoningContent += delta.thinking;
133
- }
134
- // MiniMax uses 'think' field for thinking content - check via bracket notation
135
- const thinkValue = delta.think;
136
- if (thinkValue !== undefined && thinkValue !== null) {
137
- reasoningContent += thinkValue;
138
- }
139
- if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
140
- finishReason = choice.finish_reason;
141
- }
142
- // Accumulate tool_calls
143
- if (delta.tool_calls !== undefined && delta.tool_calls !== null) {
144
- for (const tc of delta.tool_calls) {
145
- // Find or create tool call entry by index
146
- let existing = toolCalls.get(tc.index);
147
- if (existing === undefined) {
148
- existing = {
149
- index: tc.index,
150
- type: "function",
151
- function: { name: "", arguments: "" },
152
- };
153
- toolCalls.set(tc.index, existing);
154
- }
155
- if (tc.id !== undefined) existing.id = tc.id;
156
- if (tc.function?.name !== undefined) existing.function.name += tc.function.name;
157
- if (tc.function?.arguments !== undefined)
158
- existing.function.arguments += tc.function.arguments;
220
+ for (const choice of chunk.choices) {
221
+ const delta = choice.delta;
222
+ if (delta.content !== undefined && delta.content !== null) {
223
+ completionContent += delta.content;
224
+ }
225
+ if (delta.reasoning_content !== undefined && delta.reasoning_content !== null) {
226
+ reasoningContent += delta.reasoning_content;
227
+ }
228
+ if (delta.thinking !== undefined && delta.thinking !== null) {
229
+ reasoningContent += delta.thinking;
230
+ }
231
+ const thinkValue = delta.think;
232
+ if (thinkValue !== undefined && thinkValue !== null) {
233
+ reasoningContent += thinkValue;
234
+ }
235
+ if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
236
+ finishReason = choice.finish_reason;
237
+ }
238
+ if (delta.tool_calls !== undefined && delta.tool_calls !== null) {
239
+ for (const tc of delta.tool_calls) {
240
+ let existing = toolCalls.get(tc.index);
241
+ if (existing === undefined) {
242
+ existing = {
243
+ index: tc.index,
244
+ type: "function",
245
+ function: { name: "", arguments: "" },
246
+ };
247
+ toolCalls.set(tc.index, existing);
248
+ }
249
+ if (tc.id !== undefined) existing.id = tc.id;
250
+ if (tc.function?.name !== undefined) existing.function.name += tc.function.name;
251
+ if (tc.function?.arguments !== undefined) {
252
+ existing.function.arguments += tc.function.arguments;
159
253
  }
160
254
  }
161
255
  }
162
- } catch {
163
- // non-JSON SSE line, skip
164
256
  }
165
257
  }
166
258
 
167
- // Store collected chunks on the log
168
- if (collectChunks === true) {
259
+ if (collectChunks) {
169
260
  log.streamingChunks = {
170
261
  chunks,
171
262
  truncated: chunkIndex > MAX_CHUNKS,
@@ -182,7 +273,7 @@ export function extractOpenAIStream(
182
273
  role: "assistant",
183
274
  content: completionContent,
184
275
  };
185
- if (reasoningContent) message.reasoning_content = reasoningContent;
276
+ if (reasoningContent.length > 0) message.reasoning_content = reasoningContent;
186
277
  if (toolCalls.size > 0) message.tool_calls = [...toolCalls.values()];
187
278
 
188
279
  return JSON.stringify({
@@ -204,3 +295,306 @@ export function extractOpenAIStream(
204
295
  },
205
296
  });
206
297
  }
298
+
299
+ function parseResponsesResponse(value: unknown): OpenAIResponsesResponse | null {
300
+ const fromResponseProperty = OpenAIResponsesResponseSchema.safeParse(
301
+ readProperty(value, "response"),
302
+ );
303
+ if (fromResponseProperty.success) return fromResponseProperty.data;
304
+ const direct = OpenAIResponsesResponseSchema.safeParse(value);
305
+ return direct.success ? direct.data : null;
306
+ }
307
+
308
+ function responseTextFromOutput(response: OpenAIResponsesResponse): string {
309
+ if (response.output_text !== undefined && response.output_text.length > 0) {
310
+ return response.output_text;
311
+ }
312
+
313
+ const parts: string[] = [];
314
+ for (const outputItem of response.output ?? []) {
315
+ const content = readArrayProperty(outputItem, "content");
316
+ for (const contentPart of content) {
317
+ const type = readStringProperty(contentPart, "type");
318
+ const text = readStringProperty(contentPart, "text");
319
+ if ((type === "output_text" || type === "text") && text !== null && text.length > 0) {
320
+ parts.push(text);
321
+ }
322
+ }
323
+ }
324
+ return parts.join("\n");
325
+ }
326
+
327
+ function updateUsageFromResponse(
328
+ response: OpenAIResponsesResponse,
329
+ log: CapturedLog,
330
+ ): { inputTokens: number | null; outputTokens: number | null } {
331
+ const usage = response.usage;
332
+ const inputTokens = usage?.input_tokens ?? null;
333
+ const outputTokens = usage?.output_tokens ?? null;
334
+ if (inputTokens !== null) log.inputTokens = inputTokens;
335
+ if (outputTokens !== null) log.outputTokens = outputTokens;
336
+ const cachedTokens = usage?.input_tokens_details?.cached_tokens ?? null;
337
+ if (cachedTokens !== null) log.cacheReadInputTokens = cachedTokens;
338
+ return { inputTokens, outputTokens };
339
+ }
340
+
341
+ function functionCallKey(payload: unknown, fallbackIndex: number): string {
342
+ const itemId = readStringProperty(payload, "item_id");
343
+ if (itemId !== null && itemId.length > 0) return itemId;
344
+ const outputIndex = readNumberProperty(payload, "output_index");
345
+ if (outputIndex !== null) return String(outputIndex);
346
+ return `function-${String(fallbackIndex)}`;
347
+ }
348
+
349
+ function ensureFunctionCall(
350
+ calls: Map<string, ResponseFunctionCall>,
351
+ key: string,
352
+ ): ResponseFunctionCall {
353
+ const existing = calls.get(key);
354
+ if (existing !== undefined) return existing;
355
+ const created: ResponseFunctionCall = {
356
+ type: "function_call",
357
+ arguments: "",
358
+ };
359
+ calls.set(key, created);
360
+ return created;
361
+ }
362
+
363
+ function updateFunctionCallFromItem(
364
+ calls: Map<string, ResponseFunctionCall>,
365
+ key: string,
366
+ item: unknown,
367
+ ): void {
368
+ if (readStringProperty(item, "type") !== "function_call") return;
369
+ const call = ensureFunctionCall(calls, key);
370
+ const id = readStringProperty(item, "id");
371
+ const callId = readStringProperty(item, "call_id");
372
+ const name = readStringProperty(item, "name");
373
+ const args = readStringProperty(item, "arguments");
374
+ const status = readStringProperty(item, "status");
375
+ if (id !== null) call.id = id;
376
+ if (callId !== null) call.call_id = callId;
377
+ if (name !== null) call.name = name;
378
+ if (args !== null) call.arguments = args;
379
+ if (status !== null) call.status = status;
380
+ }
381
+
382
+ function outputFromCollected(
383
+ outputText: string,
384
+ calls: Map<string, ResponseFunctionCall>,
385
+ ): unknown[] {
386
+ const output: unknown[] = [];
387
+ if (outputText.length > 0) {
388
+ output.push({
389
+ type: "message",
390
+ role: "assistant",
391
+ content: [{ type: "output_text", text: outputText, annotations: [] }],
392
+ });
393
+ }
394
+ for (const call of calls.values()) {
395
+ output.push(call);
396
+ }
397
+ return output;
398
+ }
399
+
400
+ function responseUsage(
401
+ inputTokens: number | null,
402
+ outputTokens: number | null,
403
+ outputText: string,
404
+ ): Record<string, unknown> {
405
+ const finalOutputTokens = outputTokens ?? Math.ceil(outputText.length / 4);
406
+ return {
407
+ input_tokens: inputTokens,
408
+ output_tokens: finalOutputTokens,
409
+ total_tokens: inputTokens === null ? finalOutputTokens : inputTokens + finalOutputTokens,
410
+ };
411
+ }
412
+
413
+ function normalizeResponsesResponse(input: {
414
+ completedResponse: OpenAIResponsesResponse | null;
415
+ responseId: string;
416
+ model: string;
417
+ status: string | null;
418
+ outputText: string;
419
+ calls: Map<string, ResponseFunctionCall>;
420
+ inputTokens: number | null;
421
+ outputTokens: number | null;
422
+ }): Record<string, unknown> {
423
+ const completedText =
424
+ input.completedResponse === null ? "" : responseTextFromOutput(input.completedResponse);
425
+ const outputText = completedText.length > 0 ? completedText : input.outputText;
426
+ const completedOutput = input.completedResponse?.output;
427
+ const output =
428
+ completedOutput !== undefined && completedOutput.length > 0
429
+ ? completedOutput
430
+ : outputFromCollected(outputText, input.calls);
431
+ const usage =
432
+ input.completedResponse?.usage ??
433
+ responseUsage(input.inputTokens, input.outputTokens, outputText);
434
+
435
+ return {
436
+ ...(input.completedResponse ?? {}),
437
+ id: input.completedResponse?.id ?? input.responseId,
438
+ object: "response",
439
+ created_at: input.completedResponse?.created_at ?? Math.floor(Date.now() / 1000),
440
+ status: input.completedResponse?.status ?? input.status ?? "completed",
441
+ model: input.completedResponse?.model ?? input.model,
442
+ output,
443
+ output_text: outputText,
444
+ usage,
445
+ };
446
+ }
447
+
448
+ function extractResponsesStream(
449
+ events: ParsedSseEvent[],
450
+ log: CapturedLog,
451
+ fallbackModel: string | undefined,
452
+ collectChunks: boolean,
453
+ ): string {
454
+ let completedResponse: OpenAIResponsesResponse | null = null;
455
+ let responseId = "";
456
+ let model = fallbackModel ?? "";
457
+ let status: string | null = null;
458
+ let outputText = "";
459
+ let inputTokens: number | null = null;
460
+ let outputTokens: number | null = null;
461
+ let chunkIndex = 0;
462
+ let streamStartMs = 0;
463
+ const chunks: CollectedChunk[] = [];
464
+ const calls: Map<string, ResponseFunctionCall> = new Map();
465
+
466
+ for (const event of events) {
467
+ if (event.data === "[DONE]") break;
468
+ const parsed = parseJson(event.data);
469
+ if (parsed === null) continue;
470
+ const type = eventTypeFor(event, parsed);
471
+
472
+ if (chunkIndex === 0) streamStartMs = Date.now();
473
+ if (collectChunks) {
474
+ collectChunk(
475
+ chunks,
476
+ chunkIndex,
477
+ streamStartMs,
478
+ type === "" ? "response.event" : type,
479
+ parsed,
480
+ );
481
+ }
482
+ chunkIndex++;
483
+
484
+ setErrorFromPayload(log, parsed);
485
+ if (type === "error") continue;
486
+
487
+ const response = parseResponsesResponse(parsed);
488
+ if (response !== null) {
489
+ completedResponse = response;
490
+ responseId = response.id;
491
+ model = response.model ?? model;
492
+ status = response.status ?? status;
493
+ const usage = updateUsageFromResponse(response, log);
494
+ inputTokens = usage.inputTokens ?? inputTokens;
495
+ outputTokens = usage.outputTokens ?? outputTokens;
496
+ setErrorFromPayload(log, response);
497
+ }
498
+
499
+ const delta = readStringProperty(parsed, "delta");
500
+ switch (type) {
501
+ case "response.output_text.delta":
502
+ if (delta !== null) outputText += delta;
503
+ break;
504
+ case "response.output_text.done": {
505
+ const text = readStringProperty(parsed, "text");
506
+ if (outputText.length === 0 && text !== null) outputText = text;
507
+ break;
508
+ }
509
+ case "response.function_call_arguments.delta": {
510
+ if (delta !== null) {
511
+ const call = ensureFunctionCall(calls, functionCallKey(parsed, calls.size));
512
+ call.arguments += delta;
513
+ }
514
+ break;
515
+ }
516
+ case "response.function_call_arguments.done": {
517
+ const args = readStringProperty(parsed, "arguments");
518
+ if (args !== null) {
519
+ const call = ensureFunctionCall(calls, functionCallKey(parsed, calls.size));
520
+ call.arguments = args;
521
+ }
522
+ break;
523
+ }
524
+ case "response.output_item.added":
525
+ case "response.output_item.done": {
526
+ const item = readObjectProperty(parsed, "item");
527
+ updateFunctionCallFromItem(calls, functionCallKey(parsed, calls.size), item);
528
+ break;
529
+ }
530
+ case "response.failed":
531
+ case "response.incomplete":
532
+ status = type.slice("response.".length);
533
+ break;
534
+ case "response.completed":
535
+ status = "completed";
536
+ break;
537
+ case "response.created":
538
+ case "response.in_progress":
539
+ case "response.content_part.added":
540
+ case "response.content_part.done":
541
+ case "":
542
+ break;
543
+ default:
544
+ break;
545
+ }
546
+ }
547
+
548
+ if (collectChunks) {
549
+ log.streamingChunks = {
550
+ chunks,
551
+ truncated: chunkIndex > MAX_CHUNKS,
552
+ };
553
+ }
554
+
555
+ const normalized = normalizeResponsesResponse({
556
+ completedResponse,
557
+ responseId,
558
+ model,
559
+ status,
560
+ outputText,
561
+ calls,
562
+ inputTokens,
563
+ outputTokens,
564
+ });
565
+ const normalizedResponse = OpenAIResponsesResponseSchema.safeParse(normalized);
566
+ if (normalizedResponse.success) {
567
+ const finalUsage = updateUsageFromResponse(normalizedResponse.data, log);
568
+ if (finalUsage.outputTokens === null) {
569
+ const finalText = responseTextFromOutput(normalizedResponse.data);
570
+ const estimatedOutputTokens = Math.ceil(finalText.length / 4);
571
+ log.outputTokens = estimatedOutputTokens;
572
+ }
573
+ return JSON.stringify(normalizedResponse.data);
574
+ }
575
+
576
+ if (log.outputTokens === null) {
577
+ const finalText = readStringProperty(normalized, "output_text") ?? outputText;
578
+ const estimatedOutputTokens = Math.ceil(finalText.length / 4);
579
+ log.outputTokens = estimatedOutputTokens;
580
+ }
581
+
582
+ return JSON.stringify(normalized);
583
+ }
584
+
585
+ /**
586
+ * Extract OpenAI streaming response and return normalized JSON.
587
+ * Optionally collects SSE chunks into log.streamingChunks for debugging/display.
588
+ */
589
+ export function extractOpenAIStream(
590
+ raw: string,
591
+ log: CapturedLog,
592
+ fallbackModel?: string,
593
+ collectChunks: boolean = true,
594
+ ): string {
595
+ const events = parseSseEvents(raw);
596
+ if (events.some(isResponsesEvent)) {
597
+ return extractResponsesStream(events, log, fallbackModel, collectChunks);
598
+ }
599
+ return extractChatCompletionsStream(events, log, fallbackModel, collectChunks);
600
+ }