@tonyclaw/agent-inspector 2.1.13 → 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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
- package/.output/public/assets/{ReplayDialog-sBA1KAYD.js → ReplayDialog-CU0Tbb2c.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-BNahe83D.js} +1 -1
- package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
- package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
- package/.output/public/assets/index-DWOkqdCa.js +14 -0
- package/.output/public/assets/index-D_WfwzUi.js +1 -0
- package/.output/public/assets/index-DtLuQrs0.css +1 -0
- package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-CSONBwwn.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +20 -20
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-CPAa37n5.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-DsykulzS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-4bdm6Mt2.mjs} +674 -173
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
- package/.output/server/index.mjs +76 -62
- package/package.json +1 -1
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/providers/ImportWizardDialog.tsx +7 -1
- package/src/components/providers/ProviderCard.tsx +10 -1
- package/src/components/providers/ProviderForm.tsx +157 -41
- package/src/components/providers/ProvidersPanel.tsx +5 -1
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/LogEntry.tsx +31 -11
- package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
- package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
- package/src/components/proxy-viewer/formats/index.tsx +10 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
- package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
- package/src/components/proxy-viewer/logFocus.ts +15 -2
- package/src/components/proxy-viewer/requestTools.ts +7 -3
- package/src/components/proxy-viewer/viewerState.ts +72 -3
- package/src/components/ui/json-viewer-bulk.ts +41 -6
- package/src/components/ui/json-viewer.tsx +9 -8
- package/src/contracts/index.ts +15 -1
- package/src/contracts/openai.ts +94 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerModelMetadata.ts +7 -1
- package/src/lib/stopReason.ts +12 -0
- package/src/lib/upstreamUrl.ts +46 -0
- package/src/mcp/previewExtractor.ts +73 -3
- package/src/mcp/server.ts +2 -0
- package/src/mcp/toolHandlers.ts +2 -0
- package/src/proxy/constants.ts +2 -0
- package/src/proxy/formats/openai/handler.ts +40 -9
- package/src/proxy/formats/openai/index.ts +7 -0
- package/src/proxy/formats/openai/schemas.ts +15 -1
- package/src/proxy/formats/openai/stream.ts +529 -135
- package/src/proxy/formats/registry.ts +9 -1
- package/src/proxy/providerImporters.ts +45 -5
- package/src/proxy/providers.ts +31 -11
- package/src/proxy/schemas.ts +6 -0
- package/src/proxy/toolSchemaWarnings.ts +25 -3
- package/src/proxy/upstream.ts +18 -37
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
- package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
- package/.output/public/assets/index-B-QQLbpz.js +0 -1
- 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 =
|
|
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 =
|
|
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
|
-
|
|
125
|
-
<
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
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 {
|
|
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
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (typeof
|
|
70
|
-
if (typeof
|
|
71
|
-
if (
|
|
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)
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
150
|
-
|
|
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,
|