@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.
Files changed (81) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-sBA1KAYD.js → ReplayDialog-CU0Tbb2c.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-BNahe83D.js} +1 -1
  6. package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
  8. package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
  9. package/.output/public/assets/index-DWOkqdCa.js +14 -0
  10. package/.output/public/assets/index-D_WfwzUi.js +1 -0
  11. package/.output/public/assets/index-DtLuQrs0.css +1 -0
  12. package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-CSONBwwn.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +20 -20
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-CPAa37n5.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-DsykulzS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-4bdm6Mt2.mjs} +674 -173
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
  28. package/.output/server/index.mjs +76 -62
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewerContainer.tsx +25 -17
  31. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  32. package/src/components/providers/ProviderCard.tsx +10 -1
  33. package/src/components/providers/ProviderForm.tsx +157 -41
  34. package/src/components/providers/ProvidersPanel.tsx +5 -1
  35. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  36. package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
  37. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  38. package/src/components/proxy-viewer/LogEntry.tsx +31 -11
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
  40. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  41. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  42. package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
  43. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  44. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  45. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  46. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  47. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  48. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  49. package/src/components/proxy-viewer/logFocus.ts +15 -2
  50. package/src/components/proxy-viewer/requestTools.ts +7 -3
  51. package/src/components/proxy-viewer/viewerState.ts +72 -3
  52. package/src/components/ui/json-viewer-bulk.ts +41 -6
  53. package/src/components/ui/json-viewer.tsx +9 -8
  54. package/src/contracts/index.ts +15 -1
  55. package/src/contracts/openai.ts +94 -0
  56. package/src/lib/providerContract.ts +1 -0
  57. package/src/lib/providerModelMetadata.ts +7 -1
  58. package/src/lib/stopReason.ts +12 -0
  59. package/src/lib/upstreamUrl.ts +46 -0
  60. package/src/mcp/previewExtractor.ts +73 -3
  61. package/src/mcp/server.ts +2 -0
  62. package/src/mcp/toolHandlers.ts +2 -0
  63. package/src/proxy/constants.ts +2 -0
  64. package/src/proxy/formats/openai/handler.ts +40 -9
  65. package/src/proxy/formats/openai/index.ts +7 -0
  66. package/src/proxy/formats/openai/schemas.ts +15 -1
  67. package/src/proxy/formats/openai/stream.ts +529 -135
  68. package/src/proxy/formats/registry.ts +9 -1
  69. package/src/proxy/providerImporters.ts +45 -5
  70. package/src/proxy/providers.ts +31 -11
  71. package/src/proxy/schemas.ts +6 -0
  72. package/src/proxy/toolSchemaWarnings.ts +25 -3
  73. package/src/proxy/upstream.ts +18 -37
  74. package/src/routes/api/providers.$providerId.ts +1 -0
  75. package/src/routes/api/providers.ts +2 -0
  76. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  77. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  78. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  79. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  80. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  81. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -6,6 +6,7 @@ import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
6
6
  import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
7
7
  import { copyTextToClipboard } from "../../lib/clipboard";
8
8
  import { maskApiKey } from "../../lib/mask";
9
+ import { buildUpstreamUrl } from "../../lib/upstreamUrl";
9
10
  import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
10
11
 
11
12
  const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
@@ -16,17 +17,27 @@ const ZHIPU_CODING_PROVIDER_KEYWORDS = [
16
17
  "z.ai coding",
17
18
  "zai coding",
18
19
  ];
20
+ const ANTHROPIC_MESSAGES_ENDPOINT = "/v1/messages";
21
+ const OPENAI_CHAT_COMPLETIONS_ENDPOINT = "/v1/chat/completions";
22
+ const OPENAI_RESPONSES_ENDPOINT = "/v1/responses";
19
23
 
20
24
  // Known provider presets - maps provider name keywords to their API URLs
21
25
  const KNOWN_PROVIDER_PRESETS: Record<
22
26
  string,
23
- { anthropicBaseUrl?: string; openaiBaseUrl?: string; apiDocsUrl?: string }
27
+ {
28
+ anthropicBaseUrl?: string;
29
+ openaiBaseUrl?: string;
30
+ openaiResponsesBaseUrl?: string;
31
+ apiDocsUrl?: string;
32
+ }
24
33
  > = {
25
34
  deepseek: {
26
35
  openaiBaseUrl: "https://api.deepseek.com",
27
36
  },
28
37
  minimax: {
29
38
  anthropicBaseUrl: "https://api.minimaxi.com/anthropic",
39
+ openaiBaseUrl: "https://api.minimaxi.com",
40
+ openaiResponsesBaseUrl: "https://api.minimaxi.com",
30
41
  apiDocsUrl: "https://platform.minimaxi.com/docs/api-reference/api-overview",
31
42
  },
32
43
  "zhipu coding": {
@@ -122,6 +133,7 @@ type ProviderFormProps = {
122
133
  models: string[];
123
134
  anthropicBaseUrl?: string;
124
135
  openaiBaseUrl?: string;
136
+ openaiResponsesBaseUrl?: string;
125
137
  apiDocsUrl?: string;
126
138
  modelMetadataUrl?: string;
127
139
  modelMetadata?: ProviderModelMetadata[];
@@ -254,6 +266,38 @@ function readPositiveIntegerInput(value: string): number | null {
254
266
  return parsed;
255
267
  }
256
268
 
269
+ function stripEndpointSuffix(value: string | undefined, endpoint: string): string {
270
+ if (value === undefined) return "";
271
+ const trimmed = value.trim();
272
+ const normalized = trimmed.replace(/\/+$/, "");
273
+ return normalized.endsWith(endpoint)
274
+ ? normalized.slice(0, -endpoint.length).replace(/\/+$/, "")
275
+ : trimmed;
276
+ }
277
+
278
+ function previewEndpointUrl(baseUrl: string, endpoint: string): string | null {
279
+ const trimmed = baseUrl.trim();
280
+ return trimmed === "" ? null : buildUpstreamUrl(trimmed, endpoint);
281
+ }
282
+
283
+ function EndpointUrlPreview({
284
+ baseUrl,
285
+ endpoint,
286
+ }: {
287
+ baseUrl: string;
288
+ endpoint: string;
289
+ }): JSX.Element | null {
290
+ const finalUrl = previewEndpointUrl(baseUrl, endpoint);
291
+ if (finalUrl === null) return null;
292
+
293
+ return (
294
+ <div className="rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-xs">
295
+ <div className="text-muted-foreground">Final URL</div>
296
+ <div className="break-all font-mono text-foreground">{finalUrl}</div>
297
+ </div>
298
+ );
299
+ }
300
+
257
301
  export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps): JSX.Element {
258
302
  const [name, setName] = useState(provider?.name ?? "");
259
303
  const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
@@ -270,8 +314,15 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
270
314
  ),
271
315
  );
272
316
  const [activeTab, setActiveTab] = useState<"anthropic" | "openai">("anthropic");
273
- const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(provider?.anthropicBaseUrl ?? "");
274
- const [openaiBaseUrl, setOpenaiBaseUrl] = useState(provider?.openaiBaseUrl ?? "");
317
+ const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(
318
+ stripEndpointSuffix(provider?.anthropicBaseUrl, ANTHROPIC_MESSAGES_ENDPOINT),
319
+ );
320
+ const [openaiBaseUrl, setOpenaiBaseUrl] = useState(
321
+ stripEndpointSuffix(provider?.openaiBaseUrl, OPENAI_CHAT_COMPLETIONS_ENDPOINT),
322
+ );
323
+ const [openaiResponsesBaseUrl, setOpenaiResponsesBaseUrl] = useState(
324
+ stripEndpointSuffix(provider?.openaiResponsesBaseUrl, OPENAI_RESPONSES_ENDPOINT),
325
+ );
275
326
  const [apiDocsUrl, setApiDocsUrl] = useState(provider?.apiDocsUrl ?? "");
276
327
  const [modelMetadataUrl, setModelMetadataUrl] = useState(provider?.modelMetadataUrl ?? "");
277
328
  const [source, setSource] = useState<"company" | "personal" | undefined>(provider?.source);
@@ -298,6 +349,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
298
349
  // Track if URL fields have been manually edited (to avoid overriding user edits)
299
350
  const [manualAnthropicUrlOverride, setManualAnthropicUrlOverride] = useState(false);
300
351
  const [manualOpenaiUrlOverride, setManualOpenaiUrlOverride] = useState(false);
352
+ const [manualOpenaiResponsesUrlOverride, setManualOpenaiResponsesUrlOverride] = useState(false);
301
353
 
302
354
  // Check if MiniMax is detected
303
355
  const isMiniMax = name.toLowerCase().includes("minimax");
@@ -331,13 +383,21 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
331
383
  const nextModels = (provider.models?.length ?? 0) > 0 ? provider.models : [""];
332
384
  setModels(nextModels);
333
385
  setModelMetadataDrafts(createMetadataDrafts(provider, nextModels));
334
- setAnthropicBaseUrl(provider.anthropicBaseUrl ?? "");
335
- setOpenaiBaseUrl(provider.openaiBaseUrl ?? "");
386
+ setAnthropicBaseUrl(
387
+ stripEndpointSuffix(provider.anthropicBaseUrl, ANTHROPIC_MESSAGES_ENDPOINT),
388
+ );
389
+ setOpenaiBaseUrl(
390
+ stripEndpointSuffix(provider.openaiBaseUrl, OPENAI_CHAT_COMPLETIONS_ENDPOINT),
391
+ );
392
+ setOpenaiResponsesBaseUrl(
393
+ stripEndpointSuffix(provider.openaiResponsesBaseUrl, OPENAI_RESPONSES_ENDPOINT),
394
+ );
336
395
  setApiDocsUrl(provider.apiDocsUrl ?? "");
337
396
  setModelMetadataUrl(provider.modelMetadataUrl ?? "");
338
397
  setSource(provider.source);
339
398
  setManualAnthropicUrlOverride(false);
340
399
  setManualOpenaiUrlOverride(false);
400
+ setManualOpenaiResponsesUrlOverride(false);
341
401
  }
342
402
  }, [provider]);
343
403
 
@@ -352,6 +412,9 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
352
412
  if (preset.openaiBaseUrl !== undefined && !manualOpenaiUrlOverride) {
353
413
  setOpenaiBaseUrl(preset.openaiBaseUrl);
354
414
  }
415
+ if (preset.openaiResponsesBaseUrl !== undefined && !manualOpenaiResponsesUrlOverride) {
416
+ setOpenaiResponsesBaseUrl(preset.openaiResponsesBaseUrl);
417
+ }
355
418
  if (preset.apiDocsUrl !== undefined && !apiDocsUrl) {
356
419
  setApiDocsUrl(preset.apiDocsUrl);
357
420
  }
@@ -394,6 +457,9 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
394
457
  if (openaiBaseUrl.trim() && !isValidUrl(openaiBaseUrl.trim())) {
395
458
  newErrors.openaiBaseUrl = "Invalid URL format";
396
459
  }
460
+ if (openaiResponsesBaseUrl.trim() && !isValidUrl(openaiResponsesBaseUrl.trim())) {
461
+ newErrors.openaiResponsesBaseUrl = "Invalid URL format";
462
+ }
397
463
  if (modelMetadataUrl.trim() && !isValidUrl(modelMetadataUrl.trim())) {
398
464
  newErrors.modelMetadataUrl = "Invalid URL format";
399
465
  }
@@ -412,7 +478,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
412
478
  "Output limit should not exceed context window";
413
479
  }
414
480
  });
415
- if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim()) {
481
+ if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim() && !openaiResponsesBaseUrl.trim()) {
416
482
  newErrors.format = "At least one format URL (Anthropic or OpenAI) is required";
417
483
  }
418
484
  setErrors(newErrors);
@@ -546,6 +612,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
546
612
  models: models.map((m) => m.trim()).filter((m) => m !== ""),
547
613
  anthropicBaseUrl: anthropicBaseUrl.trim() || undefined,
548
614
  openaiBaseUrl: openaiBaseUrl.trim() || undefined,
615
+ openaiResponsesBaseUrl: openaiResponsesBaseUrl.trim() || undefined,
549
616
  apiDocsUrl: apiDocsUrl.trim() || undefined,
550
617
  modelMetadataUrl: modelMetadataUrl.trim() || undefined,
551
618
  modelMetadata: buildModelMetadata(),
@@ -878,50 +945,99 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
878
945
  <label htmlFor="provider-anthropic-base-url" className="text-sm font-medium">
879
946
  Anthropic Base URL
880
947
  </label>
881
- <input
882
- id="provider-anthropic-base-url"
883
- type="text"
884
- value={anthropicBaseUrl}
885
- onChange={(e) => {
886
- setManualAnthropicUrlOverride(true);
887
- setAnthropicBaseUrl(e.target.value);
888
- }}
889
- placeholder="https://api.anthropic.com"
890
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
891
- />
948
+ <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
949
+ <input
950
+ id="provider-anthropic-base-url"
951
+ type="text"
952
+ value={anthropicBaseUrl}
953
+ onChange={(e) => {
954
+ setManualAnthropicUrlOverride(true);
955
+ setAnthropicBaseUrl(e.target.value);
956
+ }}
957
+ placeholder="https://api.anthropic.com"
958
+ className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
959
+ />
960
+ <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
961
+ {ANTHROPIC_MESSAGES_ENDPOINT}
962
+ </span>
963
+ </div>
892
964
  {errors.anthropicBaseUrl !== undefined && (
893
965
  <p className="text-xs text-destructive">{errors.anthropicBaseUrl}</p>
894
966
  )}
967
+ <EndpointUrlPreview baseUrl={anthropicBaseUrl} endpoint={ANTHROPIC_MESSAGES_ENDPOINT} />
895
968
  <p className="text-xs text-muted-foreground">
896
- Anthropic-compatible endpoint URL. Leave empty if this provider does not support
897
- Anthropic format.
969
+ Anthropic-compatible base URL. Leave empty if this provider does not support Anthropic
970
+ format.
898
971
  </p>
899
972
  </div>
900
973
  )}
901
974
 
902
975
  {activeTab === "openai" && (
903
- <div className="space-y-2">
904
- <label htmlFor="provider-openai-base-url" className="text-sm font-medium">
905
- OpenAI Base URL
906
- </label>
907
- <input
908
- id="provider-openai-base-url"
909
- type="text"
910
- value={openaiBaseUrl}
911
- onChange={(e) => {
912
- setManualOpenaiUrlOverride(true);
913
- setOpenaiBaseUrl(e.target.value);
914
- }}
915
- placeholder="https://api.openai.com/v1"
916
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
917
- />
918
- {errors.openaiBaseUrl !== undefined && (
919
- <p className="text-xs text-destructive">{errors.openaiBaseUrl}</p>
920
- )}
921
- <p className="text-xs text-muted-foreground">
922
- OpenAI-compatible endpoint URL. Leave empty if this provider does not support OpenAI
923
- format.
924
- </p>
976
+ <div className="space-y-4">
977
+ <div className="space-y-2">
978
+ <label htmlFor="provider-openai-base-url" className="text-sm font-medium">
979
+ OpenAI Chat Base URL
980
+ </label>
981
+ <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
982
+ <input
983
+ id="provider-openai-base-url"
984
+ type="text"
985
+ value={openaiBaseUrl}
986
+ onChange={(e) => {
987
+ setManualOpenaiUrlOverride(true);
988
+ setOpenaiBaseUrl(e.target.value);
989
+ }}
990
+ placeholder="https://api.openai.com"
991
+ className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
992
+ />
993
+ <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
994
+ {OPENAI_CHAT_COMPLETIONS_ENDPOINT}
995
+ </span>
996
+ </div>
997
+ {errors.openaiBaseUrl !== undefined && (
998
+ <p className="text-xs text-destructive">{errors.openaiBaseUrl}</p>
999
+ )}
1000
+ <EndpointUrlPreview
1001
+ baseUrl={openaiBaseUrl}
1002
+ endpoint={OPENAI_CHAT_COMPLETIONS_ENDPOINT}
1003
+ />
1004
+ <p className="text-xs text-muted-foreground">
1005
+ OpenAI Chat Completions-compatible base URL. Leave empty if this provider only
1006
+ supports the Responses API.
1007
+ </p>
1008
+ </div>
1009
+ <div className="space-y-2">
1010
+ <label htmlFor="provider-openai-responses-base-url" className="text-sm font-medium">
1011
+ OpenAI Responses Base URL
1012
+ </label>
1013
+ <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1014
+ <input
1015
+ id="provider-openai-responses-base-url"
1016
+ type="text"
1017
+ value={openaiResponsesBaseUrl}
1018
+ onChange={(e) => {
1019
+ setManualOpenaiResponsesUrlOverride(true);
1020
+ setOpenaiResponsesBaseUrl(e.target.value);
1021
+ }}
1022
+ placeholder="https://api.openai.com"
1023
+ className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1024
+ />
1025
+ <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
1026
+ {OPENAI_RESPONSES_ENDPOINT}
1027
+ </span>
1028
+ </div>
1029
+ {errors.openaiResponsesBaseUrl !== undefined && (
1030
+ <p className="text-xs text-destructive">{errors.openaiResponsesBaseUrl}</p>
1031
+ )}
1032
+ <EndpointUrlPreview
1033
+ baseUrl={openaiResponsesBaseUrl}
1034
+ endpoint={OPENAI_RESPONSES_ENDPOINT}
1035
+ />
1036
+ <p className="text-xs text-muted-foreground">
1037
+ Optional Responses-compatible base URL. Set only when the provider supports the
1038
+ Responses API.
1039
+ </p>
1040
+ </div>
925
1041
  </div>
926
1042
  )}
927
1043
 
@@ -51,6 +51,7 @@ type ProviderFormData = {
51
51
  models: string[];
52
52
  anthropicBaseUrl?: string;
53
53
  openaiBaseUrl?: string;
54
+ openaiResponsesBaseUrl?: string;
54
55
  apiDocsUrl?: string;
55
56
  modelMetadataUrl?: string;
56
57
  modelMetadata?: ProviderModelMetadata[];
@@ -64,6 +65,8 @@ function createProviderPayload(data: ProviderFormData) {
64
65
  models: data.models,
65
66
  anthropicBaseUrl: (data.anthropicBaseUrl?.length ?? 0) > 0 ? data.anthropicBaseUrl : undefined,
66
67
  openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : undefined,
68
+ openaiResponsesBaseUrl:
69
+ (data.openaiResponsesBaseUrl?.length ?? 0) > 0 ? data.openaiResponsesBaseUrl : undefined,
67
70
  apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : undefined,
68
71
  modelMetadataUrl: (data.modelMetadataUrl?.length ?? 0) > 0 ? data.modelMetadataUrl : undefined,
69
72
  modelMetadata: data.modelMetadata,
@@ -407,7 +410,8 @@ export function ProvidersPanel({
407
410
  (data.apiKey ?? "") !== (editingProvider.apiKey ?? "") ||
408
411
  JSON.stringify(data.models) !== JSON.stringify(editingProvider.models) ||
409
412
  (data.anthropicBaseUrl ?? "") !== (editingProvider.anthropicBaseUrl ?? "") ||
410
- (data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "");
413
+ (data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "") ||
414
+ (data.openaiResponsesBaseUrl ?? "") !== (editingProvider.openaiResponsesBaseUrl ?? "");
411
415
  if (criticalFieldsChanged) {
412
416
  await runTest(updated.id);
413
417
  }
@@ -1,5 +1,6 @@
1
- import type { JSX } from "react";
2
- import ReactMarkdown from "react-markdown";
1
+ import { lazy, Suspense, type JSX } from "react";
2
+
3
+ const LazyReactMarkdown = lazy(() => import("react-markdown"));
3
4
 
4
5
  const ANSWER_MARKDOWN_CLASS =
5
6
  "prose prose-sm dark:prose-invert max-w-none text-[13px] leading-[1.65] " +
@@ -10,7 +11,11 @@ const ANSWER_MARKDOWN_CLASS =
10
11
  export function AnswerMarkdown({ text }: { text: string }): JSX.Element {
11
12
  return (
12
13
  <div className={ANSWER_MARKDOWN_CLASS}>
13
- <ReactMarkdown>{text}</ReactMarkdown>
14
+ <Suspense
15
+ fallback={<span className="text-xs text-muted-foreground">Loading Markdown...</span>}
16
+ >
17
+ <LazyReactMarkdown>{text}</LazyReactMarkdown>
18
+ </Suspense>
14
19
  </div>
15
20
  );
16
21
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { JSX } from "react";
3
3
  import {
4
4
  Check,
@@ -23,9 +23,9 @@ import {
23
23
  previewNode,
24
24
  } from "./requestDiff";
25
25
  import { getConversationId } from "./ConversationHeader";
26
- import { JsonViewerFromString } from "../ui/json-viewer";
27
26
  import { Badge } from "../ui/badge";
28
27
  import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
28
+ import { LazyJsonViewerFromString } from "./lazy";
29
29
 
30
30
  export type CompareDrawerProps = {
31
31
  /** Log selected first (shown on the left). */
@@ -65,6 +65,20 @@ function nodeToJsonValue(node: JsonNode): unknown {
65
65
  }
66
66
  }
67
67
 
68
+ function LazyJsonStringView({
69
+ text,
70
+ defaultExpandDepth,
71
+ }: {
72
+ text: string;
73
+ defaultExpandDepth: number;
74
+ }): JSX.Element {
75
+ return (
76
+ <Suspense fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}>
77
+ <LazyJsonViewerFromString text={text} defaultExpandDepth={defaultExpandDepth} />
78
+ </Suspense>
79
+ );
80
+ }
81
+
68
82
  /** The parent path of a JSON path string. E.g. `messages[3].content` →
69
83
  * `messages[3]`, `messages[3]` → `messages`, `messages` → `""`. */
70
84
  function parentPath(path: string): string {
@@ -222,7 +236,7 @@ function EqualRunRow({
222
236
  {ops.map((op) => (
223
237
  <div key={op.path} className="border border-border/50 rounded p-2 bg-muted/20">
224
238
  <div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
225
- <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
239
+ <LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
226
240
  </div>
227
241
  ))}
228
242
  </div>
@@ -393,7 +407,7 @@ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.E
393
407
  if (op.kind === "added" || op.kind === "removed") {
394
408
  return (
395
409
  <div className="pl-5 mt-2 border border-border/50 rounded p-2 bg-muted/20">
396
- <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
410
+ <LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
397
411
  </div>
398
412
  );
399
413
  }
@@ -411,11 +425,11 @@ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.E
411
425
  <div className="pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
412
426
  <div className="border border-rose-500/30 rounded p-2 bg-rose-500/5">
413
427
  <div className="text-[10px] uppercase tracking-wider text-rose-500 mb-1">Old</div>
414
- <JsonViewerFromString text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
428
+ <LazyJsonStringView text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
415
429
  </div>
416
430
  <div className="border border-emerald-500/30 rounded p-2 bg-emerald-500/5">
417
431
  <div className="text-[10px] uppercase tracking-wider text-emerald-500 mb-1">New</div>
418
- <JsonViewerFromString text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
432
+ <LazyJsonStringView text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
419
433
  </div>
420
434
  </div>
421
435
  );
@@ -4,7 +4,7 @@ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
4
4
  import type { CapturedLog } from "../../contracts";
5
5
  import { ConversationHeader, type ConversationGroupData } from "./ConversationHeader";
6
6
  import { detectProvider } from "../providers/ProviderLogo";
7
- import { TurnGroup } from "./TurnGroup";
7
+ import { TurnGroupList } from "./TurnGroupList";
8
8
  import { AgentTraceSummary } from "./AgentTraceSummary";
9
9
  import type { CacheTrendEntry } from "./cacheTrend";
10
10
  import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
@@ -180,20 +180,16 @@ export const ConversationGroup = memo(function ({
180
180
  showRollupMetrics={showTraceRollupMetrics}
181
181
  timeDisplayFormat={timeDisplayFormat}
182
182
  />
183
- {turnGroups.map((tg) => (
184
- <TurnGroup
185
- key={tg.turnIndex}
186
- entries={tg.entries}
187
- viewMode={viewMode}
188
- strip={strip}
189
- slowResponseThresholdSeconds={slowResponseThresholdSeconds}
190
- timeDisplayFormat={timeDisplayFormat}
191
- cacheTrends={cacheTrends}
192
- onCompareWithPrevious={onCompareWithPrevious}
193
- comparisonPredecessors={comparisonPredecessors}
194
- turnIndex={tg.turnIndex}
195
- />
196
- ))}
183
+ <TurnGroupList
184
+ turnGroups={turnGroups}
185
+ viewMode={viewMode}
186
+ strip={strip}
187
+ slowResponseThresholdSeconds={slowResponseThresholdSeconds}
188
+ timeDisplayFormat={timeDisplayFormat}
189
+ cacheTrends={cacheTrends}
190
+ onCompareWithPrevious={onCompareWithPrevious}
191
+ comparisonPredecessors={comparisonPredecessors}
192
+ />
197
193
  </div>
198
194
  )}
199
195
  </div>
@@ -75,7 +75,7 @@ const BODY_CHUNK_BYTES = 256 * 1024;
75
75
  const CHUNKED_BODY_THRESHOLD_BYTES = 1024 * 1024;
76
76
 
77
77
  const TAB_TRIGGER_CLASS =
78
- "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none hover:bg-cyan-400/[0.04] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-transparent data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
78
+ "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none transition-colors hover:bg-cyan-400/[0.05] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-cyan-300/[0.04] data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
79
79
 
80
80
  function createEmptyBodyPreviewState(totalBytes: number | null = null): BodyPreviewState {
81
81
  return {
@@ -140,7 +140,7 @@ function ExpandedPanel({
140
140
  return (
141
141
  <div
142
142
  className={cn(
143
- "inspector-scrollbar mx-3 mb-3 max-h-[72vh] overflow-auto rounded-md border border-white/10 bg-[#06080b] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
143
+ "inspector-scrollbar mx-3 mb-3 max-h-[72vh] overflow-auto rounded-md border border-cyan-300/10 bg-[#05070a] p-3 shadow-[0_16px_45px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.05)]",
144
144
  className,
145
145
  )}
146
146
  >
@@ -384,6 +384,7 @@ export const LogEntry = memo(function ({
384
384
  );
385
385
  const requestJsonRef = useRef<HTMLDivElement | null>(null);
386
386
  const logIdRef = useRef(log.id);
387
+ const bodyHydrationInFlightRef = useRef(false);
387
388
  const displayLog = hydratedLog ?? log;
388
389
  const useChunkedBody = hydratedLog === null && shouldUseChunkedBodyLoading(log);
389
390
  const requestPreviewBody = bodyPreviewTextForView(useChunkedBody, requestPreview);
@@ -421,10 +422,10 @@ export const LogEntry = memo(function ({
421
422
  requestPreviewShouldParse ? displayedRequestBody : null,
422
423
  );
423
424
  const rawRequestExpansion = useJsonBulkExpansion(
424
- requestPreviewShouldParse ? rawRequestBodyForView : null,
425
+ activeTab === "raw-request" && requestPreviewShouldParse ? rawRequestBodyForView : null,
425
426
  );
426
427
  const responseExpansion = useJsonBulkExpansion(
427
- responsePreviewShouldParse ? responseTextForView : null,
428
+ activeTab === "raw" && responsePreviewShouldParse ? responseTextForView : null,
428
429
  );
429
430
 
430
431
  // Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
@@ -634,6 +635,7 @@ export const LogEntry = memo(function ({
634
635
 
635
636
  useEffect(() => {
636
637
  logIdRef.current = log.id;
638
+ bodyHydrationInFlightRef.current = false;
637
639
  setHydratedLog(null);
638
640
  setBodyHydrationState("idle");
639
641
  setRequestPreview(createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null));
@@ -643,9 +645,10 @@ export const LogEntry = memo(function ({
643
645
  useEffect(() => {
644
646
  if (!expanded) return;
645
647
  if (!shouldHydrateLogBody(log)) return;
646
- if (hydratedLog !== null || bodyHydrationState !== "idle") return;
648
+ if (hydratedLog !== null || bodyHydrationInFlightRef.current) return;
647
649
 
648
650
  let cancelled = false;
651
+ bodyHydrationInFlightRef.current = true;
649
652
  setBodyHydrationState("loading");
650
653
  void fetchJsonWithTimeout(
651
654
  `/api/logs/${String(log.id)}`,
@@ -654,19 +657,21 @@ export const LogEntry = memo(function ({
654
657
  )
655
658
  .then((fullLog) => {
656
659
  if (cancelled) return;
660
+ bodyHydrationInFlightRef.current = false;
657
661
  setHydratedLog(fullLog);
658
662
  setBodyHydrationState("idle");
659
663
  })
660
664
  .catch(() => {
661
665
  if (cancelled) return;
666
+ bodyHydrationInFlightRef.current = false;
662
667
  setBodyHydrationState("failed");
663
668
  });
664
669
 
665
670
  return () => {
666
671
  cancelled = true;
672
+ bodyHydrationInFlightRef.current = false;
667
673
  };
668
674
  }, [
669
- bodyHydrationState,
670
675
  expanded,
671
676
  hydratedLog,
672
677
  log.bodyContentMode,
@@ -710,12 +715,23 @@ export const LogEntry = memo(function ({
710
715
  <TooltipProvider>
711
716
  <div
712
717
  className={cn(
713
- "mb-2 overflow-hidden rounded-md border bg-[#090a0d] transition-[background-color,border-color,box-shadow]",
718
+ "relative mb-2 overflow-hidden rounded-lg border bg-[#07090d] transition-[background-color,border-color,box-shadow]",
714
719
  expanded
715
- ? "border-cyan-300/20 shadow-[0_18px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.05)]"
716
- : "border-border/70 hover:border-border",
720
+ ? "border-cyan-300/25 shadow-[0_24px_80px_rgba(0,0,0,0.34),0_0_0_1px_rgba(103,232,249,0.04),inset_0_1px_0_rgba(255,255,255,0.06)]"
721
+ : "border-white/10 shadow-[0_6px_24px_rgba(0,0,0,0.16)] hover:border-cyan-300/15",
717
722
  )}
718
723
  >
724
+ <div
725
+ className={cn(
726
+ "pointer-events-none absolute inset-y-3 left-1 w-px rounded-full",
727
+ expanded ? "bg-cyan-300/45 shadow-[0_0_20px_rgba(103,232,249,0.28)]" : "bg-white/10",
728
+ )}
729
+ aria-hidden="true"
730
+ />
731
+ <div
732
+ className="pointer-events-none absolute inset-x-3 top-0 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent"
733
+ aria-hidden="true"
734
+ />
719
735
  <LogEntryHeader
720
736
  log={displayLog}
721
737
  messageCount={requestAnalysis.messageCount}
@@ -738,14 +754,18 @@ export const LogEntry = memo(function ({
738
754
 
739
755
  {expanded && (
740
756
  <div
741
- className="border-t border-white/10 bg-[#050607]/80 pt-2"
757
+ className="relative border-t border-cyan-300/10 bg-[linear-gradient(180deg,#06090d_0%,#040506_100%)] pt-2"
742
758
  onClick={(e) => e.stopPropagation()}
743
759
  onKeyDown={(e) => e.stopPropagation()}
744
760
  >
761
+ <div
762
+ className="pointer-events-none absolute inset-x-0 top-0 h-12 bg-gradient-to-b from-cyan-300/[0.035] to-transparent"
763
+ aria-hidden="true"
764
+ />
745
765
  <Tabs value={activeTab} onValueChange={setActiveTab} className="gap-2">
746
766
  <TabsList
747
767
  variant="line"
748
- className="inspector-scrollbar mx-3 h-auto w-[calc(100%-1.5rem)] justify-start overflow-x-auto rounded-none border-b border-white/10 bg-transparent p-0"
768
+ className="inspector-scrollbar relative mx-3 h-auto w-[calc(100%-1.5rem)] justify-start overflow-x-auto rounded-none border-b border-cyan-300/10 bg-black/10 p-0"
749
769
  >
750
770
  {viewMode === "full" && (
751
771
  <TabsTrigger className={TAB_TRIGGER_CLASS} value="raw-headers">