@tonyclaw/llm-inspector 1.14.8 → 1.15.0

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 (30) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-CMuJQyt1.js +105 -0
  3. package/.output/public/assets/index-DciyfYBk.css +1 -0
  4. package/.output/public/assets/{main-CJ4MreBr.js → main-BLYgekFx.js} +1 -1
  5. package/.output/server/_libs/lucide-react.mjs +85 -111
  6. package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
  7. package/.output/server/_ssr/{index-9uTJ4xYR.mjs → index-P66uoVEU.mjs} +677 -304
  8. package/.output/server/_ssr/index.mjs +2 -2
  9. package/.output/server/_ssr/{router-BKnjB_zi.mjs → router-DpLCKk51.mjs} +45 -14
  10. package/.output/server/{_tanstack-start-manifest_v-IsglLVKy.mjs → _tanstack-start-manifest_v-C9Wq6YdJ.mjs} +1 -1
  11. package/.output/server/index.mjs +22 -22
  12. package/package.json +1 -1
  13. package/src/components/ProxyViewer.tsx +99 -180
  14. package/src/components/proxy-viewer/ConversationGroup.tsx +70 -66
  15. package/src/components/proxy-viewer/ConversationHeader.tsx +15 -39
  16. package/src/components/proxy-viewer/LogEntry.tsx +68 -9
  17. package/src/components/proxy-viewer/LogEntryHeader.tsx +62 -75
  18. package/src/components/proxy-viewer/ThreadConnector.tsx +78 -65
  19. package/src/components/proxy-viewer/TurnGroup.tsx +83 -0
  20. package/src/components/ui/crab-variants.tsx +456 -0
  21. package/src/lib/stopReason.ts +7 -6
  22. package/src/proxy/formats/anthropic/handler.ts +2 -5
  23. package/src/proxy/formats/openai/handler.ts +33 -7
  24. package/src/proxy/formats/openai/schemas.ts +1 -0
  25. package/src/proxy/formats/openai/stream.ts +24 -0
  26. package/src/proxy/handler.ts +8 -2
  27. package/src/proxy/schemas.ts +6 -3
  28. package/styles/globals.css +38 -0
  29. package/.output/public/assets/index-CdnotuLh.js +0 -105
  30. package/.output/public/assets/index-vP91146S.css +0 -1
@@ -0,0 +1,456 @@
1
+ import type { JSX } from "react";
2
+ import { cn } from "../../lib/utils";
3
+
4
+ type CrabVariantProps = { className?: string; style?: React.CSSProperties };
5
+
6
+ function SvgShell({
7
+ className,
8
+ style,
9
+ d,
10
+ eyeStalks,
11
+ eyes,
12
+ legs,
13
+ extras,
14
+ }: CrabVariantProps & {
15
+ d: string;
16
+ eyeStalks: JSX.Element;
17
+ eyes: JSX.Element;
18
+ legs: JSX.Element;
19
+ extras?: JSX.Element;
20
+ }): JSX.Element {
21
+ return (
22
+ <svg
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ strokeWidth="1.5"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ aria-hidden="true"
30
+ style={style}
31
+ className={cn("inline-block size-5", className)}
32
+ >
33
+ <path d={d} />
34
+ {eyeStalks}
35
+ {eyes}
36
+ {legs}
37
+ {extras}
38
+ </svg>
39
+ );
40
+ }
41
+
42
+ // Shared legs
43
+ const StdLegs = (
44
+ <>
45
+ <line x1="6.5" y1="16" x2="4.5" y2="19.5" />
46
+ <line x1="9" y1="17.5" x2="8" y2="20.5" />
47
+ <line x1="15" y1="17.5" x2="16" y2="20.5" />
48
+ <line x1="17.5" y1="16" x2="19.5" y2="19.5" />
49
+ </>
50
+ );
51
+
52
+ const ShortLegs = (
53
+ <>
54
+ <line x1="7" y1="16" x2="5.5" y2="18.5" />
55
+ <line x1="9.5" y1="17" x2="8.5" y2="19.5" />
56
+ <line x1="14.5" y1="17" x2="15.5" y2="19.5" />
57
+ <line x1="17" y1="16" x2="18.5" y2="18.5" />
58
+ </>
59
+ );
60
+
61
+ // ── 1. Classic ──
62
+ function Crab1({ className }: CrabVariantProps): JSX.Element {
63
+ return (
64
+ <SvgShell
65
+ className={className}
66
+ d="M5 13 C5 9 8 7 12 7 C16 7 19 9 19 13 C19 16 16 18 12 18 C8 18 5 16 5 13 Z"
67
+ eyeStalks={
68
+ <>
69
+ <line x1="10" y1="7" x2="9.5" y2="5" />
70
+ <line x1="14" y1="7" x2="14.5" y2="5" />
71
+ </>
72
+ }
73
+ eyes={
74
+ <>
75
+ <circle cx="9.5" cy="4.5" r="0.9" fill="currentColor" stroke="none" />
76
+ <circle cx="14.5" cy="4.5" r="0.9" fill="currentColor" stroke="none" />
77
+ </>
78
+ }
79
+ legs={StdLegs}
80
+ extras={
81
+ <>
82
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
83
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
84
+ </>
85
+ }
86
+ />
87
+ );
88
+ }
89
+
90
+ // ── 2. Round (chubby) ──
91
+ function Crab2({ className }: CrabVariantProps): JSX.Element {
92
+ return (
93
+ <SvgShell
94
+ className={className}
95
+ d="M4 13 C4 7 7.5 5.5 12 5.5 C16.5 5.5 20 7 20 13 C20 17.5 16.5 19 12 19 C7.5 19 4 17.5 4 13 Z"
96
+ eyeStalks={
97
+ <>
98
+ <line x1="9.5" y1="6.5" x2="9.5" y2="4.5" />
99
+ <line x1="14.5" y1="6.5" x2="14.5" y2="4.5" />
100
+ </>
101
+ }
102
+ eyes={
103
+ <>
104
+ <circle cx="9.5" cy="4" r="1.1" fill="currentColor" stroke="none" />
105
+ <circle cx="14.5" cy="4" r="1.1" fill="currentColor" stroke="none" />
106
+ </>
107
+ }
108
+ legs={ShortLegs}
109
+ extras={
110
+ <>
111
+ <path d="M5 11 C4 9.5 2.5 10 3 12 C3.5 13.5 4.5 13 5 12" />
112
+ <path d="M19 11 C20 9.5 21.5 10 21 12 C20.5 13.5 19.5 13 19 12" />
113
+ </>
114
+ }
115
+ />
116
+ );
117
+ }
118
+
119
+ // ── 3. Wide (pancake) ──
120
+ function Crab3({ className }: CrabVariantProps): JSX.Element {
121
+ return (
122
+ <SvgShell
123
+ className={className}
124
+ d="M3 13.5 C3 8 7 6 12 6 C17 6 21 8 21 13.5 C21 17 17 19 12 19 C7 19 3 17 3 13.5 Z"
125
+ eyeStalks={
126
+ <>
127
+ <line x1="7" y1="7" x2="6.5" y2="4.5" />
128
+ <line x1="17" y1="7" x2="17.5" y2="4.5" />
129
+ </>
130
+ }
131
+ eyes={
132
+ <>
133
+ <circle cx="6.5" cy="4" r="0.8" fill="currentColor" stroke="none" />
134
+ <circle cx="17.5" cy="4" r="0.8" fill="currentColor" stroke="none" />
135
+ </>
136
+ }
137
+ legs={StdLegs}
138
+ extras={
139
+ <>
140
+ <path d="M4 12 C2.5 10.5 1 11 1.5 13 C2 14.5 3.5 14 4.5 13" />
141
+ <path d="M20 12 C21.5 10.5 23 11 22.5 13 C22 14.5 20.5 14 19.5 13" />
142
+ </>
143
+ }
144
+ />
145
+ );
146
+ }
147
+
148
+ // ── 4. Tall (stretch) ──
149
+ function Crab4({ className }: CrabVariantProps): JSX.Element {
150
+ return (
151
+ <SvgShell
152
+ className={className}
153
+ d="M6 14 C6 8 9 5.5 12 5.5 C15 5.5 18 8 18 14 C18 18 15 20 12 20 C9 20 6 18 6 14 Z"
154
+ eyeStalks={
155
+ <>
156
+ <line x1="10" y1="6.5" x2="10" y2="3.5" />
157
+ <line x1="14" y1="6.5" x2="14" y2="3.5" />
158
+ </>
159
+ }
160
+ eyes={
161
+ <>
162
+ <circle cx="10" cy="3" r="0.7" fill="currentColor" stroke="none" />
163
+ <circle cx="14" cy="3" r="0.7" fill="currentColor" stroke="none" />
164
+ </>
165
+ }
166
+ legs={
167
+ <>
168
+ <line x1="7" y1="17" x2="5" y2="20.5" />
169
+ <line x1="9.5" y1="18.5" x2="8.5" y2="21.5" />
170
+ <line x1="14.5" y1="18.5" x2="15.5" y2="21.5" />
171
+ <line x1="17" y1="17" x2="19" y2="20.5" />
172
+ </>
173
+ }
174
+ extras={
175
+ <>
176
+ <path d="M6.5 11 C5 8.5 3 9 3.5 11.5 C4 13.5 5 13 6 12" />
177
+ <path d="M17.5 11 C19 8.5 21 9 20.5 11.5 C20 13.5 19 13 18 12" />
178
+ </>
179
+ }
180
+ />
181
+ );
182
+ }
183
+
184
+ // ── 5. Spiky ──
185
+ function Crab5({ className }: CrabVariantProps): JSX.Element {
186
+ return (
187
+ <SvgShell
188
+ className={className}
189
+ d="M5 13 C5 9 8 7 12 7 L13 4 L14 7 C16 7 19 9 19 13 C19 16 16 18 13 18 L12 21 L11 18 C8 18 5 16 5 13 Z"
190
+ eyeStalks={
191
+ <>
192
+ <line x1="10" y1="7" x2="9.5" y2="5" />
193
+ <line x1="14" y1="7" x2="14.5" y2="5" />
194
+ </>
195
+ }
196
+ eyes={
197
+ <>
198
+ <circle cx="9.5" cy="4.5" r="0.8" fill="currentColor" stroke="none" />
199
+ <circle cx="14.5" cy="4.5" r="0.8" fill="currentColor" stroke="none" />
200
+ </>
201
+ }
202
+ legs={StdLegs}
203
+ extras={
204
+ <>
205
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
206
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
207
+ </>
208
+ }
209
+ />
210
+ );
211
+ }
212
+
213
+ // ── 6. Wink ──
214
+ function Crab6({ className }: CrabVariantProps): JSX.Element {
215
+ return (
216
+ <SvgShell
217
+ className={className}
218
+ d="M5 13 C5 9 8 7 12 7 C16 7 19 9 19 13 C19 16 16 18 12 18 C8 18 5 16 5 13 Z"
219
+ eyeStalks={
220
+ <>
221
+ <line x1="10" y1="7" x2="9.5" y2="5" />
222
+ <line x1="14" y1="7" x2="14.5" y2="5" />
223
+ </>
224
+ }
225
+ eyes={
226
+ <>
227
+ <circle cx="9.5" cy="4.5" r="0.9" fill="currentColor" stroke="none" />
228
+ <line x1="13.5" y1="4" x2="15.5" y2="5" />
229
+ </>
230
+ }
231
+ legs={StdLegs}
232
+ extras={
233
+ <>
234
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
235
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
236
+ </>
237
+ }
238
+ />
239
+ );
240
+ }
241
+
242
+ // ── 7. Sleepy ──
243
+ function Crab7({ className }: CrabVariantProps): JSX.Element {
244
+ return (
245
+ <SvgShell
246
+ className={className}
247
+ d="M5.5 13.5 C5.5 10 8.5 8 12 8 C15.5 8 18.5 10 18.5 13.5 C18.5 16 15.5 17.5 12 17.5 C8.5 17.5 5.5 16 5.5 13.5 Z"
248
+ eyeStalks={
249
+ <>
250
+ <line x1="10" y1="8" x2="10" y2="6" />
251
+ <line x1="14" y1="8" x2="14" y2="6" />
252
+ </>
253
+ }
254
+ eyes={
255
+ <>
256
+ <line x1="9" y1="6" x2="11" y2="6.5" />
257
+ <line x1="13" y1="6" x2="15" y2="6.5" />
258
+ </>
259
+ }
260
+ legs={ShortLegs}
261
+ extras={
262
+ <>
263
+ <path d="M6 11.5 C4.5 10.5 3 11 3.5 12.5 C4 13.5 5 13 5.5 12.5" />
264
+ <path d="M18 11.5 C19.5 10.5 21 11 20.5 12.5 C20 13.5 19 13 18.5 12.5" />
265
+ </>
266
+ }
267
+ />
268
+ );
269
+ }
270
+
271
+ // ── 8. Grumpy (angry brows) ──
272
+ function Crab8({ className }: CrabVariantProps): JSX.Element {
273
+ return (
274
+ <SvgShell
275
+ className={className}
276
+ d="M5 13 C5 9 8 7 12 7 C16 7 19 9 19 13 C19 16 16 18 12 18 C8 18 5 16 5 13 Z"
277
+ eyeStalks={
278
+ <>
279
+ <line x1="10" y1="7" x2="9.5" y2="5" />
280
+ <line x1="14" y1="7" x2="14.5" y2="5" />
281
+ </>
282
+ }
283
+ eyes={
284
+ <>
285
+ <circle cx="9.5" cy="4.5" r="0.7" fill="currentColor" stroke="none" />
286
+ <circle cx="14.5" cy="4.5" r="0.7" fill="currentColor" stroke="none" />
287
+ </>
288
+ }
289
+ legs={StdLegs}
290
+ extras={
291
+ <>
292
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
293
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
294
+ {/* Angry brows */}
295
+ <line x1="8.5" y1="4" x2="10.5" y2="4.5" />
296
+ <line x1="15.5" y1="4" x2="13.5" y2="4.5" />
297
+ {/* Frown */}
298
+ <path d="M9 15.5 C10 14.5 14 14.5 15 15.5" />
299
+ </>
300
+ }
301
+ />
302
+ );
303
+ }
304
+
305
+ // ── 9. Surprised ──
306
+ function Crab9({ className }: CrabVariantProps): JSX.Element {
307
+ return (
308
+ <SvgShell
309
+ className={className}
310
+ d="M5 13 C5 9 8 7 12 7 C16 7 19 9 19 13 C19 16 16 18 12 18 C8 18 5 16 5 13 Z"
311
+ eyeStalks={
312
+ <>
313
+ <line x1="10" y1="7" x2="9.5" y2="5" />
314
+ <line x1="14" y1="7" x2="14.5" y2="5" />
315
+ </>
316
+ }
317
+ eyes={
318
+ <>
319
+ <circle cx="9.5" cy="4.5" r="1.2" fill="none" stroke="currentColor" />
320
+ <circle cx="14.5" cy="4.5" r="1.2" fill="none" stroke="currentColor" />
321
+ </>
322
+ }
323
+ legs={StdLegs}
324
+ extras={
325
+ <>
326
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
327
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
328
+ {/* Open mouth */}
329
+ <ellipse cx="12" cy="15" rx="2" ry="1.5" fill="currentColor" stroke="none" />
330
+ </>
331
+ }
332
+ />
333
+ );
334
+ }
335
+
336
+ // ── 10. Happy (big smile) ──
337
+ function Crab10({ className }: CrabVariantProps): JSX.Element {
338
+ return (
339
+ <SvgShell
340
+ className={className}
341
+ d="M4.5 13 C4.5 8 7.5 6 12 6 C16.5 6 19.5 8 19.5 13 C19.5 17 16.5 18.5 12 18.5 C7.5 18.5 4.5 17 4.5 13 Z"
342
+ eyeStalks={
343
+ <>
344
+ <line x1="9.5" y1="7" x2="9" y2="5" />
345
+ <line x1="14.5" y1="7" x2="15" y2="5" />
346
+ </>
347
+ }
348
+ eyes={
349
+ <>
350
+ <circle cx="9" cy="4.5" r="1" fill="currentColor" stroke="none" />
351
+ <circle cx="15" cy="4.5" r="1" fill="currentColor" stroke="none" />
352
+ </>
353
+ }
354
+ legs={StdLegs}
355
+ extras={
356
+ <>
357
+ <path d="M5 11 C3.5 9 2 9.5 2.5 12 C3 14 4 13 5 12" />
358
+ <path d="M19 11 C20.5 9 22 9.5 21.5 12 C21 14 20 13 19 12" />
359
+ {/* Smile */}
360
+ <path d="M9 14.5 C10 16 14 16 15 14.5" />
361
+ {/* Rosy cheeks */}
362
+ <circle cx="7" cy="13" r="1" fill="currentColor" stroke="none" opacity="0.3" />
363
+ <circle cx="17" cy="13" r="1" fill="currentColor" stroke="none" opacity="0.3" />
364
+ </>
365
+ }
366
+ />
367
+ );
368
+ }
369
+
370
+ // ── 11. Cool (sunglasses) ──
371
+ function Crab11({ className }: CrabVariantProps): JSX.Element {
372
+ return (
373
+ <SvgShell
374
+ className={className}
375
+ d="M5 13 C5 9 8 7 12 7 C16 7 19 9 19 13 C19 16 16 18 12 18 C8 18 5 16 5 13 Z"
376
+ eyeStalks={
377
+ <>
378
+ <line x1="10" y1="7" x2="9.5" y2="5.5" />
379
+ <line x1="14" y1="7" x2="14.5" y2="5.5" />
380
+ </>
381
+ }
382
+ eyes={
383
+ <>
384
+ {/* Sunglasses bar */}
385
+ <line x1="7.5" y1="5.5" x2="16.5" y2="5.5" />
386
+ <rect x="7" y="5" width="4" height="2" rx="0.5" fill="currentColor" stroke="none" />
387
+ <rect x="13" y="5" width="4" height="2" rx="0.5" fill="currentColor" stroke="none" />
388
+ </>
389
+ }
390
+ legs={StdLegs}
391
+ extras={
392
+ <>
393
+ <path d="M5 11 C3.5 9.5 1.5 10 2 12.5 C2.5 14 4 13.5 5 12.5" />
394
+ <path d="M19 11 C20.5 9.5 22.5 10 22 12.5 C21.5 14 20 13.5 19 12.5" />
395
+ {/* Cool smile */}
396
+ <path d="M9.5 15 C10.5 16 13.5 16 14.5 15" />
397
+ </>
398
+ }
399
+ />
400
+ );
401
+ }
402
+
403
+ // ── 12. Baby (tiny, cute) ──
404
+ function Crab12({ className }: CrabVariantProps): JSX.Element {
405
+ return (
406
+ <SvgShell
407
+ className={className}
408
+ d="M7 13.5 C7 10.5 9.5 9 12 9 C14.5 9 17 10.5 17 13.5 C17 16 14.5 17.5 12 17.5 C9.5 17.5 7 16 7 13.5 Z"
409
+ eyeStalks={
410
+ <>
411
+ <line x1="10.5" y1="9.5" x2="10.5" y2="8" />
412
+ <line x1="13.5" y1="9.5" x2="13.5" y2="8" />
413
+ </>
414
+ }
415
+ eyes={
416
+ <>
417
+ <circle cx="10.5" cy="7.5" r="0.8" fill="currentColor" stroke="none" />
418
+ <circle cx="13.5" cy="7.5" r="0.8" fill="currentColor" stroke="none" />
419
+ </>
420
+ }
421
+ legs={
422
+ <>
423
+ <line x1="8" y1="16" x2="6.5" y2="18" />
424
+ <line x1="10" y1="17" x2="9.5" y2="19" />
425
+ <line x1="14" y1="17" x2="14.5" y2="19" />
426
+ <line x1="16" y1="16" x2="17.5" y2="18" />
427
+ </>
428
+ }
429
+ extras={
430
+ <>
431
+ <path d="M7.5 12 C6.5 11 5.5 11.5 6 13 C6.5 14 7 13.5 7.5 13" />
432
+ <path d="M16.5 12 C17.5 11 18.5 11.5 18 13 C17.5 14 17 13.5 16.5 13" />
433
+ </>
434
+ }
435
+ />
436
+ );
437
+ }
438
+
439
+ export const crabVariants: ((props: CrabVariantProps) => JSX.Element)[] = [
440
+ Crab1,
441
+ Crab2,
442
+ Crab3,
443
+ Crab4,
444
+ Crab5,
445
+ Crab6,
446
+ Crab7,
447
+ Crab8,
448
+ Crab9,
449
+ Crab10,
450
+ Crab11,
451
+ Crab12,
452
+ ];
453
+
454
+ export function getCrabVariant(index: number): (props: CrabVariantProps) => JSX.Element {
455
+ return crabVariants[Math.abs(index) % crabVariants.length] ?? Crab1;
456
+ }
@@ -8,8 +8,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
8
8
 
9
9
  /**
10
10
  * Extracts the stop/finish reason from a captured log's response text.
11
- * Returns the raw stop_reason value for Anthropic, the finish_reason for
12
- * OpenAI, or null if the response is pending, malformed, or unrecognized.
11
+ * Detects the response body shape (Anthropic or OpenAI) independently of
12
+ * `log.apiFormat`, since the body shape may not match the declared format
13
+ * (e.g. an Anthropic-compatible endpoint returning Anthropic-shaped JSON
14
+ * while the request was classified as OpenAI).
13
15
  */
14
16
  export function extractStopReason(log: CapturedLog): StopReason {
15
17
  if (log.responseText === null) return null;
@@ -22,17 +24,16 @@ export function extractStopReason(log: CapturedLog): StopReason {
22
24
  }
23
25
  if (!isRecord(json)) return null;
24
26
 
25
- // Anthropic: { stop_reason: "end_turn" | "tool_use" | ... }
26
- if (log.apiFormat === "anthropic" && typeof json.stop_reason === "string") {
27
+ // Anthropic shape: { stop_reason: "end_turn" | "tool_use" | ... }
28
+ if (typeof json.stop_reason === "string") {
27
29
  if (json.stop_reason === "end_turn" || json.stop_reason === "tool_use") {
28
30
  return json.stop_reason;
29
31
  }
30
32
  return null;
31
33
  }
32
34
 
33
- // OpenAI: { choices: [{ finish_reason: "stop" | ... }] }
35
+ // OpenAI shape: { choices: [{ finish_reason: "stop" | ... }] }
34
36
  if (
35
- log.apiFormat === "openai" &&
36
37
  Array.isArray(json.choices) &&
37
38
  json.choices.length > 0 &&
38
39
  isRecord(json.choices[0]) &&
@@ -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
- if (keys.includes("model") && keys.includes("messages")) {
64
- if (keys.includes("system") || keys.includes("tools")) {
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: null,
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
- if (keys.includes("model") && keys.includes("messages")) {
59
- // OpenAI doesn't use "system" or "tools" keys at root level
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
 
@@ -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
- formatHandler.format,
325
+ displayApiFormat,
320
326
  model,
321
327
  sessionId,
322
328
  preAcquiredId,
@@ -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 keys = Object.keys(json);
115
- if (keys.includes("model") && keys.includes("messages")) {
116
- if (keys.includes("system") || keys.includes("tools")) {
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");