@tonyclaw/llm-inspector 1.14.8 → 1.14.9
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/index-Dv-dj1xH.js +105 -0
- package/.output/public/assets/index-bqeypwJB.css +1 -0
- package/.output/public/assets/{main-CJ4MreBr.js → main-C8OUJKbz.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +3 -3
- package/.output/server/_ssr/{index-9uTJ4xYR.mjs → index-_9xcAkkw.mjs} +193 -103
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BKnjB_zi.mjs → router-CmanwZJc.mjs} +45 -14
- package/.output/server/{_tanstack-start-manifest_v-IsglLVKy.mjs → _tanstack-start-manifest_v-BVIiyDeJ.mjs} +1 -1
- package/.output/server/index.mjs +24 -24
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +24 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +42 -19
- package/src/components/proxy-viewer/ConversationHeader.tsx +15 -0
- package/src/components/proxy-viewer/LogEntry.tsx +68 -9
- package/src/components/proxy-viewer/LogEntryHeader.tsx +59 -72
- package/src/components/proxy-viewer/ThreadConnector.tsx +36 -47
- package/src/proxy/formats/anthropic/handler.ts +2 -5
- package/src/proxy/formats/openai/handler.ts +33 -7
- package/src/proxy/formats/openai/schemas.ts +1 -0
- package/src/proxy/formats/openai/stream.ts +24 -0
- package/src/proxy/handler.ts +8 -2
- package/src/proxy/schemas.ts +6 -3
- package/.output/public/assets/index-CdnotuLh.js +0 -105
- package/.output/public/assets/index-vP91146S.css +0 -1
|
@@ -3,61 +3,39 @@ import { cn } from "../../lib/utils";
|
|
|
3
3
|
import type { StopReason } from "../../lib/stopReason";
|
|
4
4
|
|
|
5
5
|
export type ThreadConnectorProps = {
|
|
6
|
-
/** The stop reason extracted from this log entry's response. */
|
|
7
6
|
stopReason: StopReason;
|
|
8
|
-
/** True when the response is still in-flight (responseStatus === null). */
|
|
9
7
|
isPending: boolean;
|
|
10
|
-
/** True when this is the first entry in the group. */
|
|
11
8
|
isFirst: boolean;
|
|
12
|
-
/** True when this is the last entry in the group. */
|
|
13
9
|
isLast: boolean;
|
|
10
|
+
/** True when this entry starts a new turn (first overall, or after end_turn/stop). */
|
|
11
|
+
isTurnStart: boolean;
|
|
14
12
|
};
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - Line continues through intermediate entries
|
|
21
|
-
* - Line ends at a turn-boundary dot/cap when stop_reason is end_turn/stop
|
|
22
|
-
* - Dimmed/dashed line for pending (in-flight) entries
|
|
23
|
-
* - A solid dot marks turn boundaries
|
|
15
|
+
* Vertical timeline connector for thread view. Uses flexbox layout (no
|
|
16
|
+
* absolute positioning) so the connector naturally tracks its sibling
|
|
17
|
+
* LogEntry height — no scroll jitter.
|
|
24
18
|
*/
|
|
25
19
|
export function ThreadConnector({
|
|
26
20
|
stopReason,
|
|
27
21
|
isPending,
|
|
28
22
|
isFirst,
|
|
29
|
-
isLast,
|
|
23
|
+
isLast: _isLast,
|
|
24
|
+
isTurnStart,
|
|
30
25
|
}: ThreadConnectorProps): JSX.Element {
|
|
31
|
-
// Turn boundary: "end_turn" for Anthropic, "stop" for OpenAI
|
|
32
26
|
const isBoundary = stopReason === "end_turn" || stopReason === "stop";
|
|
33
|
-
// Tool use: the turn continues
|
|
34
27
|
const isToolUse = stopReason === "tool_use";
|
|
35
28
|
|
|
36
|
-
// Compute classes for each segment of the connector:
|
|
37
|
-
// - top half line (from previous entry down to this entry's top)
|
|
38
|
-
// - center dot/circle (this entry's response marker)
|
|
39
|
-
// - bottom half line (from this entry's bottom down to the next entry)
|
|
40
|
-
|
|
41
|
-
const lineClass = cn(
|
|
42
|
-
"w-0.5 bg-muted-foreground/30",
|
|
43
|
-
isPending && "border-dashed bg-transparent border-l-2 border-muted-foreground/20",
|
|
44
|
-
);
|
|
45
|
-
|
|
46
29
|
return (
|
|
47
|
-
<div className="flex
|
|
48
|
-
{/* Top line
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
className={cn("w-0.5 grow", "bg-muted-foreground/30", "h-4")}
|
|
53
|
-
style={{ minHeight: "0.5rem" }}
|
|
54
|
-
/>
|
|
55
|
-
)}
|
|
56
|
-
{isFirst && <div className="h-4" />}
|
|
30
|
+
<div className="flex flex-col items-center w-6 shrink-0">
|
|
31
|
+
{/* Top: incoming line from previous entry, or empty spacer for first.
|
|
32
|
+
Fixed height so the marker stays near the LogEntry header row. */}
|
|
33
|
+
<div className="flex justify-center h-4">
|
|
34
|
+
{!isFirst && <div className="w-0.5 bg-muted-foreground/30" />}
|
|
57
35
|
</div>
|
|
58
36
|
|
|
59
|
-
{/* Center marker */}
|
|
60
|
-
<div className="
|
|
37
|
+
{/* Center marker — aligned with the LogEntry header row */}
|
|
38
|
+
<div className="flex items-center justify-center py-0.5">
|
|
61
39
|
{isBoundary ? (
|
|
62
40
|
<div
|
|
63
41
|
className={cn(
|
|
@@ -69,8 +47,13 @@ export function ThreadConnector({
|
|
|
69
47
|
/>
|
|
70
48
|
) : isToolUse ? (
|
|
71
49
|
<div
|
|
72
|
-
className=
|
|
73
|
-
|
|
50
|
+
className={cn(
|
|
51
|
+
"size-2 rounded-full",
|
|
52
|
+
isTurnStart
|
|
53
|
+
? "bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
|
54
|
+
: "bg-muted-foreground/25",
|
|
55
|
+
)}
|
|
56
|
+
title={isTurnStart ? "Tool Use — start of turn" : "Tool Use — turn continues"}
|
|
74
57
|
/>
|
|
75
58
|
) : isPending ? (
|
|
76
59
|
<div
|
|
@@ -78,26 +61,32 @@ export function ThreadConnector({
|
|
|
78
61
|
title="Response pending"
|
|
79
62
|
/>
|
|
80
63
|
) : (
|
|
81
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"size-1.5 rounded-full",
|
|
67
|
+
isTurnStart
|
|
68
|
+
? "bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
|
69
|
+
: "bg-muted-foreground/30",
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
82
72
|
)}
|
|
83
73
|
</div>
|
|
84
74
|
|
|
85
|
-
{/* Bottom line
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
75
|
+
{/* Bottom: outgoing line to next entry, or short terminator at boundaries.
|
|
76
|
+
flex-1 fills the remaining height of the LogEntry card. */}
|
|
77
|
+
<div className="flex-1 flex justify-center min-h-1">
|
|
78
|
+
{isBoundary ? (
|
|
79
|
+
<div className="w-0.5 bg-muted-foreground/10 h-4" />
|
|
80
|
+
) : (
|
|
91
81
|
<div
|
|
92
82
|
className={cn(
|
|
93
|
-
"w-0.5
|
|
83
|
+
"w-0.5 h-full",
|
|
94
84
|
isPending
|
|
95
85
|
? "border-dashed bg-transparent border-l-2 border-muted-foreground/20"
|
|
96
86
|
: "bg-muted-foreground/30",
|
|
97
87
|
)}
|
|
98
88
|
/>
|
|
99
89
|
)}
|
|
100
|
-
{isBoundary && <div className="w-0.5 h-4 bg-muted-foreground/10" />}
|
|
101
90
|
</div>
|
|
102
91
|
</div>
|
|
103
92
|
);
|
|
@@ -60,11 +60,8 @@ export const AnthropicFormatHandler: FormatHandler = {
|
|
|
60
60
|
const json: unknown = JSON.parse(rawBody);
|
|
61
61
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
62
62
|
const keys = Object.keys(json);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
63
|
+
// Anthropic puts `system` as a top-level key alongside `model` and `messages`
|
|
64
|
+
return keys.includes("model") && keys.includes("messages") && keys.includes("system");
|
|
68
65
|
}
|
|
69
66
|
return false;
|
|
70
67
|
} catch {
|
|
@@ -25,11 +25,41 @@ export const OpenAIFormatHandler: FormatHandler = {
|
|
|
25
25
|
extractTokens(responseBody: string): TokenUsage {
|
|
26
26
|
const parsed = parseOpenAIResponse(responseBody);
|
|
27
27
|
if (parsed) {
|
|
28
|
+
// OpenAI puts cached_tokens in usage.prompt_tokens_details (passthrough field)
|
|
29
|
+
let cacheReadInputTokens: number | null = null;
|
|
30
|
+
try {
|
|
31
|
+
const raw: unknown = JSON.parse(responseBody);
|
|
32
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
33
|
+
const usageDesc = Object.getOwnPropertyDescriptor(raw, "usage");
|
|
34
|
+
if (
|
|
35
|
+
usageDesc !== undefined &&
|
|
36
|
+
typeof usageDesc.value === "object" &&
|
|
37
|
+
usageDesc.value !== null
|
|
38
|
+
) {
|
|
39
|
+
const detailsDesc = Object.getOwnPropertyDescriptor(
|
|
40
|
+
usageDesc.value,
|
|
41
|
+
"prompt_tokens_details",
|
|
42
|
+
);
|
|
43
|
+
if (
|
|
44
|
+
detailsDesc !== undefined &&
|
|
45
|
+
typeof detailsDesc.value === "object" &&
|
|
46
|
+
detailsDesc.value !== null
|
|
47
|
+
) {
|
|
48
|
+
const cacheDesc = Object.getOwnPropertyDescriptor(detailsDesc.value, "cached_tokens");
|
|
49
|
+
if (cacheDesc !== undefined && typeof cacheDesc.value === "number") {
|
|
50
|
+
cacheReadInputTokens = cacheDesc.value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore parse errors
|
|
57
|
+
}
|
|
28
58
|
return {
|
|
29
59
|
inputTokens: parsed.usage.prompt_tokens ?? null,
|
|
30
60
|
outputTokens: parsed.usage.completion_tokens ?? null,
|
|
31
61
|
cacheCreationInputTokens: null,
|
|
32
|
-
cacheReadInputTokens
|
|
62
|
+
cacheReadInputTokens,
|
|
33
63
|
};
|
|
34
64
|
}
|
|
35
65
|
return {
|
|
@@ -55,12 +85,8 @@ export const OpenAIFormatHandler: FormatHandler = {
|
|
|
55
85
|
const json: unknown = JSON.parse(rawBody);
|
|
56
86
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
57
87
|
const keys = Object.keys(json);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!keys.includes("system") && !keys.includes("tools")) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
88
|
+
// OpenAI has `model` and `messages` at the top level, but NOT `system`
|
|
89
|
+
return keys.includes("model") && keys.includes("messages") && !keys.includes("system");
|
|
64
90
|
}
|
|
65
91
|
return false;
|
|
66
92
|
} catch {
|
|
@@ -73,6 +73,7 @@ export const OpenAIRequestSchema = z.object({
|
|
|
73
73
|
tools: z.array(OpenAIToolDefinition).optional(),
|
|
74
74
|
tool_choice: z
|
|
75
75
|
.union([
|
|
76
|
+
z.enum(["auto", "none", "required"]),
|
|
76
77
|
z.object({ type: z.literal("auto") }),
|
|
77
78
|
z.object({ type: z.literal("none") }),
|
|
78
79
|
z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) }),
|
|
@@ -93,6 +93,30 @@ export function extractOpenAIStream(
|
|
|
93
93
|
promptTokens = chunk.usage.prompt_tokens ?? 0;
|
|
94
94
|
completionTokens = chunk.usage.completion_tokens ?? 0;
|
|
95
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
|
+
}
|
|
96
120
|
usageCaptured = true;
|
|
97
121
|
}
|
|
98
122
|
|
package/src/proxy/handler.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { extractRequestMetadata } from "./schemas";
|
|
|
5
5
|
import { registry } from "./formats";
|
|
6
6
|
import { findProviderByModel } from "./providers";
|
|
7
7
|
import { getClientInfo } from "./socketTracker";
|
|
8
|
-
import { formatForPath, type FormatHandler } from "./formats";
|
|
8
|
+
import { formatForPath, formatRegistry, type FormatHandler } from "./formats";
|
|
9
9
|
import {
|
|
10
10
|
PROXY_IDENTITY,
|
|
11
11
|
PRESERVE_HEADERS,
|
|
@@ -308,6 +308,12 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
308
308
|
upstreamHeaders.forEach((value, key) => {
|
|
309
309
|
upstreamHeadersObj[key.toLowerCase()] = value;
|
|
310
310
|
});
|
|
311
|
+
// Detect the true format from the request body for accurate UI display.
|
|
312
|
+
// The path-based format (formatHandler.format) drives routing, but the body
|
|
313
|
+
// structure determines whether it's actually Anthropic or OpenAI.
|
|
314
|
+
const bodyFormat = formatRegistry.detectFormat(requestBody);
|
|
315
|
+
const displayApiFormat = bodyFormat !== "unknown" ? bodyFormat : formatHandler.format;
|
|
316
|
+
|
|
311
317
|
const log = await createLog(
|
|
312
318
|
req.method,
|
|
313
319
|
parsed.apiPath,
|
|
@@ -316,7 +322,7 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
316
322
|
clientInfo,
|
|
317
323
|
rawHeaders,
|
|
318
324
|
upstreamHeadersObj,
|
|
319
|
-
|
|
325
|
+
displayApiFormat,
|
|
320
326
|
model,
|
|
321
327
|
sessionId,
|
|
322
328
|
preAcquiredId,
|
package/src/proxy/schemas.ts
CHANGED
|
@@ -111,9 +111,12 @@ function detectFormat(rawBody: string | null): RequestFormat {
|
|
|
111
111
|
try {
|
|
112
112
|
const json: unknown = JSON.parse(rawBody);
|
|
113
113
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
const hasModel = safeGetProperty(json, "model") !== undefined;
|
|
115
|
+
const hasMessages = safeGetProperty(json, "messages") !== undefined;
|
|
116
|
+
if (hasModel && hasMessages) {
|
|
117
|
+
// Anthropic requests put `system` as a top-level key; OpenAI does not.
|
|
118
|
+
// Both formats can have `tools`, so we check `system` as the discriminator.
|
|
119
|
+
if (safeGetProperty(json, "system") !== undefined) {
|
|
117
120
|
return "anthropic";
|
|
118
121
|
}
|
|
119
122
|
const msgVal = safeGetProperty(json, "messages");
|