@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
@@ -46,6 +46,32 @@ type SummaryAccumulator = {
46
46
  peak: SessionContextSnapshot;
47
47
  };
48
48
 
49
+ type SnapshotCacheEntry = {
50
+ id: number;
51
+ rawRequestBody: string | null;
52
+ inputTokens: number | null;
53
+ model: string | null;
54
+ path: string;
55
+ apiFormat: CapturedLog["apiFormat"];
56
+ timestamp: string;
57
+ providersSignature: string;
58
+ snapshot: SessionContextSnapshot | null;
59
+ };
60
+
61
+ const snapshotCache = new WeakMap<CapturedLog, SnapshotCacheEntry>();
62
+
63
+ function providerSignature(providers: readonly ProviderConfig[]): string {
64
+ return providers
65
+ .map((provider) => {
66
+ const metadata =
67
+ provider.modelMetadata
68
+ ?.map((entry) => `${entry.model}:${String(entry.contextWindow ?? "")}`)
69
+ .join(",") ?? "";
70
+ return `${provider.id}:${provider.updatedAt}:${provider.models.join(",")}:${metadata}`;
71
+ })
72
+ .join("|");
73
+ }
74
+
49
75
  function parseRequestBody(rawBody: string | null): unknown | null {
50
76
  if (rawBody === null) return null;
51
77
  try {
@@ -104,6 +130,41 @@ function snapshotFromLog(
104
130
  };
105
131
  }
106
132
 
133
+ function cachedSnapshotFromLog(
134
+ log: CapturedLog,
135
+ providers: readonly ProviderConfig[],
136
+ providersSignature: string,
137
+ ): SessionContextSnapshot | null {
138
+ const cached = snapshotCache.get(log);
139
+ if (
140
+ cached !== undefined &&
141
+ cached.id === log.id &&
142
+ cached.rawRequestBody === log.rawRequestBody &&
143
+ cached.inputTokens === log.inputTokens &&
144
+ cached.model === log.model &&
145
+ cached.path === log.path &&
146
+ cached.apiFormat === log.apiFormat &&
147
+ cached.timestamp === log.timestamp &&
148
+ cached.providersSignature === providersSignature
149
+ ) {
150
+ return cached.snapshot;
151
+ }
152
+
153
+ const snapshot = snapshotFromLog(log, providers);
154
+ snapshotCache.set(log, {
155
+ id: log.id,
156
+ rawRequestBody: log.rawRequestBody,
157
+ inputTokens: log.inputTokens,
158
+ model: log.model,
159
+ path: log.path,
160
+ apiFormat: log.apiFormat,
161
+ timestamp: log.timestamp,
162
+ providersSignature,
163
+ snapshot,
164
+ });
165
+ return snapshot;
166
+ }
167
+
107
168
  function isHigherPeak(candidate: SessionContextSnapshot, current: SessionContextSnapshot): boolean {
108
169
  if (candidate.usagePercent !== null && current.usagePercent !== null) {
109
170
  return candidate.usagePercent > current.usagePercent;
@@ -149,15 +210,26 @@ function trendPercent(
149
210
  return (latest.usagePercent - first.usagePercent) * 100;
150
211
  }
151
212
 
213
+ function logsInAscendingIdOrder(logs: readonly CapturedLog[]): readonly CapturedLog[] {
214
+ for (let index = 1; index < logs.length; index++) {
215
+ const previous = logs[index - 1];
216
+ const current = logs[index];
217
+ if (previous === undefined || current === undefined) continue;
218
+ if (previous.id > current.id) return [...logs].sort((left, right) => left.id - right.id);
219
+ }
220
+ return logs;
221
+ }
222
+
152
223
  export function buildSessionContextSummary(
153
224
  logs: readonly CapturedLog[],
154
225
  providers: readonly ProviderConfig[] = [],
155
226
  ): SessionContextSummary {
156
- const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
227
+ const sortedLogs = logsInAscendingIdOrder(logs);
228
+ const providersSignature = providerSignature(providers);
157
229
  const byModel = new Map<string, SummaryAccumulator>();
158
230
 
159
231
  for (const log of sortedLogs) {
160
- const snapshot = snapshotFromLog(log, providers);
232
+ const snapshot = cachedSnapshotFromLog(log, providers, providersSignature);
161
233
  if (snapshot === null) continue;
162
234
 
163
235
  const existing = byModel.get(snapshot.model);
@@ -1,12 +1,12 @@
1
1
  import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
2
- import { type JSX, memo, useState } from "react";
2
+ import { type JSX, memo, Suspense, useState } from "react";
3
3
  import type { ResponseContentBlockType } from "../../../../contracts/anthropic";
4
4
  import { AnswerMarkdown } from "../../AnswerMarkdown";
5
5
  import { Badge } from "../../../ui/badge";
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
7
- import { JsonViewer } from "../../../ui/json-viewer";
8
7
  import { safeJsonValue } from "../../../ui/json-viewer-bulk";
9
8
  import { ScrollArea } from "../../../ui/scroll-area";
9
+ import { LazyJsonViewer } from "../../lazy";
10
10
  import { extractThinkingFromContent } from "./thinkingExtract";
11
11
 
12
12
  function assertNever(_value: never): JSX.Element {
@@ -121,11 +121,17 @@ export const ToolUseBlock = memo(function ToolUseBlock({
121
121
  )}
122
122
  </CollapsibleTrigger>
123
123
  <CollapsibleContent>
124
- <div className="px-3 pb-2">
125
- <ScrollArea className="max-h-[60vh]">
126
- <JsonViewer data={safeJsonValue(input)} defaultExpandDepth={0} />
127
- </ScrollArea>
128
- </div>
124
+ {open && (
125
+ <div className="px-3 pb-2">
126
+ <ScrollArea className="max-h-[60vh]">
127
+ <Suspense
128
+ fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
129
+ >
130
+ <LazyJsonViewer data={safeJsonValue(input)} defaultExpandDepth={0} />
131
+ </Suspense>
132
+ </ScrollArea>
133
+ </div>
134
+ )}
129
135
  </CollapsibleContent>
130
136
  </div>
131
137
  </Collapsible>
@@ -1,12 +1,12 @@
1
1
  import type { JSX } from "react";
2
2
  import type { ApiFormat } from "../../../contracts";
3
3
  import type { AnthropicResponse } from "../../../contracts/anthropic";
4
- import type { OpenAIResponse } from "../../../contracts/openai";
4
+ import type { OpenAIResponse, OpenAIResponsesResponse } from "../../../contracts/openai";
5
5
  import { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
6
- import { OpenAIResponseView } from "./openai/ResponseView";
6
+ import { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
7
7
 
8
8
  export { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
9
- export { OpenAIResponseView } from "./openai/ResponseView";
9
+ export { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
10
10
  export { ResponseContentBlockRenderer } from "./anthropic/ContentBlocks";
11
11
 
12
12
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -17,6 +17,10 @@ function isOpenAIResponse(response: unknown): response is OpenAIResponse {
17
17
  return isRecord(response) && response.object === "chat.completion";
18
18
  }
19
19
 
20
+ function isOpenAIResponsesResponse(response: unknown): response is OpenAIResponsesResponse {
21
+ return isRecord(response) && response.object === "response";
22
+ }
23
+
20
24
  function isAnthropicResponse(response: unknown): response is AnthropicResponse {
21
25
  return isRecord(response) && response.type === "message" && Array.isArray(response.content);
22
26
  }
@@ -28,6 +32,9 @@ export function formatViewFor(
28
32
  if (apiFormat === "openai" && isOpenAIResponse(response)) {
29
33
  return <OpenAIResponseView response={response} />;
30
34
  }
35
+ if (apiFormat === "openai" && isOpenAIResponsesResponse(response)) {
36
+ return <OpenAIResponsesResponseView response={response} />;
37
+ }
31
38
  if (apiFormat === "anthropic" && isAnthropicResponse(response)) {
32
39
  return <StructuredResponseViewAnthropic response={response} />;
33
40
  }
@@ -1,10 +1,14 @@
1
1
  import { StopCircle, Terminal, Zap } from "lucide-react";
2
- import { memo, useState, type JSX } from "react";
3
- import type { OpenAIResponse, OpenAIToolCall } from "../../../../contracts/openai";
2
+ import { memo, Suspense, useState, type JSX } from "react";
3
+ import type {
4
+ OpenAIResponse,
5
+ OpenAIResponsesResponse,
6
+ OpenAIToolCall,
7
+ } from "../../../../contracts/openai";
8
+ import { safeGetOwnProperty } from "../../../../lib/objectUtils";
4
9
  import { formatTokens } from "../../../../lib/utils";
5
10
  import { AnswerMarkdown } from "../../AnswerMarkdown";
6
11
  import { Badge } from "../../../ui/badge";
7
- import { JsonViewer } from "../../../ui/json-viewer";
8
12
  import { safeJsonValue } from "../../../ui/json-viewer-bulk";
9
13
  import { ScrollArea } from "../../../ui/scroll-area";
10
14
  import { Separator } from "../../../ui/separator";
@@ -12,6 +16,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui
12
16
  import { ChevronDown, ChevronRight } from "lucide-react";
13
17
  import { ThinkingBlock } from "../anthropic/ContentBlocks";
14
18
  import { extractThinkingFromContent } from "../anthropic/thinkingExtract";
19
+ import { LazyJsonViewer } from "../../lazy";
15
20
 
16
21
  // Re-export for use in other components
17
22
  export { extractThinkingFromContent } from "../anthropic/thinkingExtract";
@@ -34,7 +39,7 @@ function parseToolArguments(raw: string | undefined): unknown {
34
39
  function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
35
40
  const [open, setOpen] = useState(false);
36
41
  const name = call.function.name ?? "(unnamed tool)";
37
- const parsed = parseToolArguments(call.function.arguments);
42
+ const parsed = open ? parseToolArguments(call.function.arguments) : {};
38
43
 
39
44
  return (
40
45
  <Collapsible open={open} onOpenChange={setOpen}>
@@ -57,19 +62,128 @@ function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
57
62
  )}
58
63
  </CollapsibleTrigger>
59
64
  <CollapsibleContent>
60
- <div className="px-3 pb-2">
61
- <ScrollArea className="max-h-[60vh]">
62
- {parsed === null ? (
63
- // JSON.parse failed show the raw string so the user can
64
- // still see what the model tried to call.
65
- <pre className="font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90">
66
- {call.function.arguments}
67
- </pre>
68
- ) : (
69
- <JsonViewer data={safeJsonValue(parsed)} defaultExpandDepth={0} />
70
- )}
71
- </ScrollArea>
72
- </div>
65
+ {open && (
66
+ <div className="px-3 pb-2">
67
+ <ScrollArea className="max-h-[60vh]">
68
+ {parsed === null ? (
69
+ // JSON.parse failed show the raw string so the user can
70
+ // still see what the model tried to call.
71
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90">
72
+ {call.function.arguments}
73
+ </pre>
74
+ ) : (
75
+ <Suspense
76
+ fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
77
+ >
78
+ <LazyJsonViewer data={safeJsonValue(parsed)} defaultExpandDepth={0} />
79
+ </Suspense>
80
+ )}
81
+ </ScrollArea>
82
+ </div>
83
+ )}
84
+ </CollapsibleContent>
85
+ </div>
86
+ </Collapsible>
87
+ );
88
+ }
89
+
90
+ type ResponsesFunctionCall = {
91
+ id: string | null;
92
+ callId: string | null;
93
+ name: string;
94
+ argumentsText: string;
95
+ status: string | null;
96
+ };
97
+
98
+ function extractResponsesOutputText(response: OpenAIResponsesResponse): string {
99
+ if (response.output_text !== undefined && response.output_text.length > 0) {
100
+ return response.output_text;
101
+ }
102
+ const parts: string[] = [];
103
+ for (const outputItem of response.output ?? []) {
104
+ if (safeGetOwnProperty(outputItem, "type") !== "message") continue;
105
+ const content = safeGetOwnProperty(outputItem, "content");
106
+ if (!Array.isArray(content)) continue;
107
+ for (const contentPart of content) {
108
+ const type = safeGetOwnProperty(contentPart, "type");
109
+ const text = safeGetOwnProperty(contentPart, "text");
110
+ if ((type === "output_text" || type === "text") && typeof text === "string") {
111
+ parts.push(text);
112
+ }
113
+ }
114
+ }
115
+ return parts.join("\n");
116
+ }
117
+
118
+ function extractResponsesFunctionCalls(response: OpenAIResponsesResponse): ResponsesFunctionCall[] {
119
+ const calls: ResponsesFunctionCall[] = [];
120
+ for (const outputItem of response.output ?? []) {
121
+ if (safeGetOwnProperty(outputItem, "type") !== "function_call") continue;
122
+ const name = safeGetOwnProperty(outputItem, "name");
123
+ if (typeof name !== "string" || name.length === 0) continue;
124
+ const id = safeGetOwnProperty(outputItem, "id");
125
+ const callId = safeGetOwnProperty(outputItem, "call_id");
126
+ const args = safeGetOwnProperty(outputItem, "arguments");
127
+ const status = safeGetOwnProperty(outputItem, "status");
128
+ calls.push({
129
+ id: typeof id === "string" && id.length > 0 ? id : null,
130
+ callId: typeof callId === "string" && callId.length > 0 ? callId : null,
131
+ name,
132
+ argumentsText: typeof args === "string" ? args : "",
133
+ status: typeof status === "string" && status.length > 0 ? status : null,
134
+ });
135
+ }
136
+ return calls;
137
+ }
138
+
139
+ function ResponsesFunctionCallBlock({ call }: { call: ResponsesFunctionCall }): JSX.Element {
140
+ const [open, setOpen] = useState(false);
141
+ const parsed = open ? parseToolArguments(call.argumentsText) : {};
142
+ const identity = call.callId ?? call.id;
143
+
144
+ return (
145
+ <Collapsible open={open} onOpenChange={setOpen}>
146
+ <div className="border-l-2 border-sky-400/25 my-1">
147
+ <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
148
+ <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
149
+ <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
150
+ {call.name}
151
+ </Badge>
152
+ {identity !== null && (
153
+ <span className="text-[10px] font-mono text-muted-foreground/60 truncate">
154
+ {identity}
155
+ </span>
156
+ )}
157
+ {call.status !== null && (
158
+ <Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4 font-mono">
159
+ {call.status}
160
+ </Badge>
161
+ )}
162
+ <span className="flex-1" />
163
+ {open ? (
164
+ <ChevronDown className="size-3 text-muted-foreground" />
165
+ ) : (
166
+ <ChevronRight className="size-3 text-muted-foreground" />
167
+ )}
168
+ </CollapsibleTrigger>
169
+ <CollapsibleContent>
170
+ {open && (
171
+ <div className="px-3 pb-2">
172
+ <ScrollArea className="max-h-[60vh]">
173
+ {parsed === null ? (
174
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90">
175
+ {call.argumentsText}
176
+ </pre>
177
+ ) : (
178
+ <Suspense
179
+ fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
180
+ >
181
+ <LazyJsonViewer data={safeJsonValue(parsed)} defaultExpandDepth={0} />
182
+ </Suspense>
183
+ )}
184
+ </ScrollArea>
185
+ </div>
186
+ )}
73
187
  </CollapsibleContent>
74
188
  </div>
75
189
  </Collapsible>
@@ -164,3 +278,57 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
164
278
  </div>
165
279
  );
166
280
  });
281
+
282
+ export const OpenAIResponsesResponseView = memo(function OpenAIResponsesResponseView({
283
+ response,
284
+ }: {
285
+ response: OpenAIResponsesResponse;
286
+ }): JSX.Element {
287
+ const text = extractResponsesOutputText(response);
288
+ const calls = extractResponsesFunctionCalls(response);
289
+ const status = response.status ?? null;
290
+ const inputTokens = response.usage?.input_tokens ?? 0;
291
+ const outputTokens = response.usage?.output_tokens ?? 0;
292
+
293
+ return (
294
+ <div className="space-y-3">
295
+ <div className="flex items-center gap-2 flex-wrap">
296
+ {response.model !== undefined && response.model.length > 0 && (
297
+ <Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
298
+ {response.model}
299
+ </Badge>
300
+ )}
301
+
302
+ {status !== null && (
303
+ <Badge
304
+ variant="outline"
305
+ className="text-[10px] px-1.5 py-0 h-5 font-mono flex items-center gap-1"
306
+ >
307
+ <StopCircle className="size-2.5" />
308
+ {status}
309
+ </Badge>
310
+ )}
311
+
312
+ <span className="flex items-center gap-1 text-muted-foreground text-xs">
313
+ <Zap className="size-3" />
314
+ <span className="font-mono tabular-nums">
315
+ {formatTokens(inputTokens)} in / {formatTokens(outputTokens)} out
316
+ </span>
317
+ </span>
318
+ </div>
319
+
320
+ <Separator className="opacity-50" />
321
+
322
+ <div className="space-y-2">
323
+ {text.length > 0 && <AnswerMarkdown text={text} />}
324
+ {calls.map((call, i) => (
325
+ // biome-ignore lint/suspicious/noArrayIndexKey: response output order is stable
326
+ <ResponsesFunctionCallBlock key={call.callId ?? call.id ?? `call-${i}`} call={call} />
327
+ ))}
328
+ {text.length === 0 && calls.length === 0 && (
329
+ <p className="text-xs text-muted-foreground italic">Empty response content</p>
330
+ )}
331
+ </div>
332
+ </div>
333
+ );
334
+ });
@@ -1,4 +1,10 @@
1
- import { OpenAIRequestSchema, parseOpenAIResponse } from "../../../contracts/openai";
1
+ import {
2
+ OpenAIRequestSchema,
3
+ OpenAIResponsesRequestSchema,
4
+ parseOpenAIResponsesResponse,
5
+ parseOpenAIResponse,
6
+ } from "../../../contracts/openai";
7
+ import { safeGetOwnProperty } from "../../../lib/objectUtils";
2
8
  import type { AnatomySegment } from "../anatomy/types";
3
9
  import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
4
10
  import type { LogFormatAdapter } from "./types";
@@ -16,9 +22,12 @@ function contentToText(content: unknown): string {
16
22
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
17
23
  const b = block as Record<string, unknown>;
18
24
  const type = b.type;
19
- if (type === "text" && typeof b.text === "string") {
25
+ if (
26
+ (type === "text" || type === "input_text" || type === "output_text") &&
27
+ typeof b.text === "string"
28
+ ) {
20
29
  parts.push(b.text);
21
- } else if (type === "image_url") {
30
+ } else if (type === "image_url" || type === "input_image") {
22
31
  parts.push("[image]");
23
32
  }
24
33
  }
@@ -60,19 +69,61 @@ function toolsToText(tools: unknown): string {
60
69
  const parts: string[] = [];
61
70
  for (const tool of tools) {
62
71
  if (tool === null || typeof tool !== "object") continue;
63
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
64
- const t = tool as {
65
- function?: { name?: unknown; description?: unknown; parameters?: unknown };
66
- };
67
- const fn = t.function;
68
- if (fn === undefined) continue;
69
- if (typeof fn.name === "string") parts.push(fn.name);
70
- if (typeof fn.description === "string") parts.push(fn.description);
71
- if (fn.parameters !== undefined) parts.push(JSON.stringify(fn.parameters));
72
+ const fn = safeGetOwnProperty(tool, "function");
73
+ const name = safeGetOwnProperty(fn, "name") ?? safeGetOwnProperty(tool, "name");
74
+ const description =
75
+ safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
76
+ const parameters =
77
+ safeGetOwnProperty(fn, "parameters") ?? safeGetOwnProperty(tool, "parameters");
78
+ if (typeof name === "string") parts.push(name);
79
+ if (typeof description === "string") parts.push(description);
80
+ if (parameters !== undefined) parts.push(JSON.stringify(parameters));
72
81
  }
73
82
  return parts.join("\n");
74
83
  }
75
84
 
85
+ function countResponsesInput(input: unknown): number | null {
86
+ if (typeof input === "string") return input.length > 0 ? 1 : 0;
87
+ if (Array.isArray(input)) return input.length;
88
+ if (input === undefined || input === null) return null;
89
+ return 1;
90
+ }
91
+
92
+ function responsesInputItemToText(input: unknown): string {
93
+ if (typeof input === "string") return input;
94
+ const directText = safeGetOwnProperty(input, "text");
95
+ if (typeof directText === "string") return directText;
96
+ const contentText = contentToText(safeGetOwnProperty(input, "content"));
97
+ if (contentText.length > 0) return contentText;
98
+ const outputText = contentToText(safeGetOwnProperty(input, "output"));
99
+ if (outputText.length > 0) return outputText;
100
+ const argumentsText = safeGetOwnProperty(input, "arguments");
101
+ return typeof argumentsText === "string" ? argumentsText : "";
102
+ }
103
+
104
+ function responsesInputRole(input: unknown): AnatomySegment["role"] {
105
+ const role = safeGetOwnProperty(input, "role");
106
+ if (role === "system" || role === "user" || role === "assistant" || role === "tool") {
107
+ return role;
108
+ }
109
+ const type = safeGetOwnProperty(input, "type");
110
+ if (type === "function_call_output") return "tool";
111
+ if (type === "message") return "user";
112
+ return "user";
113
+ }
114
+
115
+ function responsesToolNames(response: unknown): string[] {
116
+ const output = safeGetOwnProperty(response, "output");
117
+ if (!Array.isArray(output)) return [];
118
+ const names: string[] = [];
119
+ for (const item of output) {
120
+ if (safeGetOwnProperty(item, "type") !== "function_call") continue;
121
+ const name = safeGetOwnProperty(item, "name");
122
+ if (typeof name === "string" && name.length > 0) names.push(name);
123
+ }
124
+ return names;
125
+ }
126
+
76
127
  function segment(
77
128
  role: AnatomySegment["role"],
78
129
  label: string,
@@ -96,7 +147,19 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
96
147
  if (rawBody === null) return emptyRequestAnalysis(rawBody);
97
148
  try {
98
149
  const result = OpenAIRequestSchema.safeParse(JSON.parse(rawBody));
99
- if (!result.success) return emptyRequestAnalysis(rawBody);
150
+ if (!result.success) {
151
+ const responsesResult = OpenAIResponsesRequestSchema.safeParse(JSON.parse(rawBody));
152
+ if (!responsesResult.success) return emptyRequestAnalysis(rawBody);
153
+ return {
154
+ parsed: responsesResult.data,
155
+ comparisonValue: responsesResult.data,
156
+ messageCount: countResponsesInput(responsesResult.data.input),
157
+ toolCount:
158
+ responsesResult.data.tools !== undefined && responsesResult.data.tools.length > 0
159
+ ? responsesResult.data.tools.length
160
+ : null,
161
+ };
162
+ }
100
163
  return {
101
164
  parsed: result.data,
102
165
  comparisonValue: result.data,
@@ -114,7 +177,15 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
114
177
  analyzeResponse(responseText) {
115
178
  if (responseText === null) return EMPTY_RESPONSE_ANALYSIS;
116
179
  const parsed = parseOpenAIResponse(responseText);
117
- if (parsed === null) return EMPTY_RESPONSE_ANALYSIS;
180
+ if (parsed === null) {
181
+ const responsesParsed = parseOpenAIResponsesResponse(responseText);
182
+ if (responsesParsed === null) return EMPTY_RESPONSE_ANALYSIS;
183
+ const toolNames = responsesToolNames(responsesParsed);
184
+ return {
185
+ parsed: responsesParsed,
186
+ toolNames: toolNames.length > 0 ? toolNames : null,
187
+ };
188
+ }
118
189
  const toolNames =
119
190
  parsed.choices[0]?.message?.tool_calls
120
191
  ?.map((toolCall) => toolCall.function?.name ?? "")
@@ -132,9 +203,18 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
132
203
  // render even when the body shape is slightly off-schema. We only
133
204
  // need the top-level `messages` and `tools` arrays.
134
205
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
135
- const body = parsed as { messages?: unknown; tools?: unknown };
206
+ const body = parsed as {
207
+ messages?: unknown;
208
+ input?: unknown;
209
+ instructions?: unknown;
210
+ tools?: unknown;
211
+ };
136
212
  const segments: AnatomySegment[] = [];
137
213
 
214
+ if (typeof body.instructions === "string" && body.instructions.length > 0) {
215
+ segments.push(segment("system", "instructions", body.instructions, "/instructions"));
216
+ }
217
+
138
218
  // Promote a leading role: "system" message into a "system" segment.
139
219
  // Spec: derive system role from a leading role: "system" message, then
140
220
  // a segment per remaining message. We still keep the system message in
@@ -157,6 +237,18 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
157
237
  });
158
238
  }
159
239
 
240
+ if (!Array.isArray(body.messages)) {
241
+ if (typeof body.input === "string" && body.input.length > 0) {
242
+ segments.push(segment("user", "input", body.input, "/input"));
243
+ } else if (Array.isArray(body.input)) {
244
+ body.input.forEach((item, index) => {
245
+ const role = responsesInputRole(item);
246
+ const text = responsesInputItemToText(item);
247
+ segments.push(segment(role, `[${index}] ${role}`, text, `/input/${index}`));
248
+ });
249
+ }
250
+ }
251
+
160
252
  if (Array.isArray(body.tools) && body.tools.length > 0) {
161
253
  const text = toolsToText(body.tools);
162
254
  segments.push(segment("tools", "tools", text, "/tools"));
@@ -4,9 +4,12 @@ export const LOG_FOCUS_REQUEST_EVENT = "agent-inspector:focus-log";
4
4
 
5
5
  export type LogFocusTab = "anatomy" | "request";
6
6
 
7
+ export type LogFocusSource = "virtualizer";
8
+
7
9
  export type LogFocusRequest = {
8
10
  logId: number;
9
11
  tab: LogFocusTab;
12
+ source?: LogFocusSource;
10
13
  };
11
14
 
12
15
  export function dispatchLogFocusRequest(request: LogFocusRequest): void {
@@ -24,9 +27,19 @@ export function readLogFocusRequest(event: Event): LogFocusRequest | null {
24
27
  switch (tab) {
25
28
  case "anatomy":
26
29
  case "request":
27
- return { logId, tab };
30
+ break;
31
+ case undefined:
32
+ break;
33
+ default:
34
+ return null;
35
+ }
36
+
37
+ const source = safeGetOwnProperty(detail, "source");
38
+ switch (source) {
39
+ case "virtualizer":
40
+ return { logId, tab: tab ?? "request", source };
28
41
  case undefined:
29
- return { logId, tab: "request" };
42
+ return { logId, tab: tab ?? "request" };
30
43
  default:
31
44
  return null;
32
45
  }
@@ -144,10 +144,14 @@ function parseAnthropicTool(tool: unknown): RequestToolDefinition | null {
144
144
 
145
145
  function parseOpenAITool(tool: unknown): RequestToolDefinition | null {
146
146
  const fn = safeGetOwnProperty(tool, "function");
147
- const name = safeGetOwnProperty(fn, "name");
147
+ const toolType = safeGetOwnProperty(tool, "type");
148
+ if (fn === undefined && toolType !== "function") return null;
149
+ const name = safeGetOwnProperty(fn, "name") ?? safeGetOwnProperty(tool, "name");
148
150
  if (typeof name !== "string" || name.length === 0) return null;
149
- const description = safeGetOwnProperty(fn, "description");
150
- const schema = safeGetOwnProperty(fn, "parameters") ?? null;
151
+ const description =
152
+ safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
153
+ const schema =
154
+ safeGetOwnProperty(fn, "parameters") ?? safeGetOwnProperty(tool, "parameters") ?? null;
151
155
  const normalizedDescription = typeof description === "string" ? description : null;
152
156
  return {
153
157
  name,