@tonyclaw/agent-inspector 2.1.13 → 2.1.15

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 (92) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-BcEd6V-V.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
  4. package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
  6. package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
  8. package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
  9. package/.output/public/assets/index-CI1-G8ua.js +1 -0
  10. package/.output/public/assets/index-DdhFqPsI.css +1 -0
  11. package/.output/public/assets/index-DjKt8XKe.js +14 -0
  12. package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +194 -200
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
  28. package/.output/server/index.mjs +72 -58
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewer.tsx +28 -2
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +18 -13
  34. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  35. package/src/components/providers/ProviderCard.tsx +10 -1
  36. package/src/components/providers/ProviderForm.tsx +157 -41
  37. package/src/components/providers/ProvidersPanel.tsx +5 -1
  38. package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
  39. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
  43. package/src/components/proxy-viewer/LogEntry.tsx +41 -12
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  46. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  47. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  48. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
  50. package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  52. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  53. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  54. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  55. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  56. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  57. package/src/components/proxy-viewer/logFocus.ts +15 -2
  58. package/src/components/proxy-viewer/requestTools.ts +7 -3
  59. package/src/components/proxy-viewer/viewerState.ts +77 -5
  60. package/src/components/ui/json-viewer-bulk.ts +41 -6
  61. package/src/components/ui/json-viewer.tsx +9 -8
  62. package/src/contracts/index.ts +15 -1
  63. package/src/contracts/openai.ts +94 -0
  64. package/src/lib/providerContract.ts +1 -0
  65. package/src/lib/providerModelMetadata.ts +7 -1
  66. package/src/lib/sessionInfoContract.ts +1 -0
  67. package/src/lib/stopReason.ts +59 -15
  68. package/src/lib/upstreamUrl.ts +46 -0
  69. package/src/mcp/previewExtractor.ts +73 -3
  70. package/src/mcp/server.ts +2 -0
  71. package/src/mcp/toolHandlers.ts +2 -0
  72. package/src/proxy/constants.ts +2 -0
  73. package/src/proxy/formats/openai/handler.ts +40 -9
  74. package/src/proxy/formats/openai/index.ts +7 -0
  75. package/src/proxy/formats/openai/schemas.ts +15 -1
  76. package/src/proxy/formats/openai/stream.ts +529 -135
  77. package/src/proxy/formats/registry.ts +9 -1
  78. package/src/proxy/providerImporters.ts +45 -5
  79. package/src/proxy/providers.ts +31 -11
  80. package/src/proxy/schemas.ts +6 -0
  81. package/src/proxy/sessionInfo.ts +5 -1
  82. package/src/proxy/toolSchemaWarnings.ts +25 -3
  83. package/src/proxy/upstream.ts +18 -37
  84. package/src/routes/api/providers.$providerId.ts +1 -0
  85. package/src/routes/api/providers.ts +2 -0
  86. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  87. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  88. package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
  89. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  90. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  91. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  92. 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
  }
@@ -146,11 +146,29 @@ function firstToolLog(logs: CapturedLog[]): CapturedLog | null {
146
146
  return null;
147
147
  }
148
148
 
149
+ function buildDisplayNumberByLogId(logs: readonly CapturedLog[]): Map<number, number> {
150
+ const result = new Map<number, number>();
151
+ for (let index = 0; index < logs.length; index += 1) {
152
+ const log = logs[index];
153
+ if (log === undefined) continue;
154
+ result.set(log.id, index + 1);
155
+ }
156
+ return result;
157
+ }
158
+
159
+ function displayLogNumber(
160
+ logId: number,
161
+ displayNumberByLogId: ReadonlyMap<number, number>,
162
+ ): string {
163
+ return String(displayNumberByLogId.get(logId) ?? logId);
164
+ }
165
+
149
166
  function buildTraceInsights(input: {
150
167
  logs: CapturedLog[];
151
168
  scopeId: string;
152
169
  summary: TraceSummary;
153
170
  candidates: KnowledgeCandidate[];
171
+ displayNumberByLogId: ReadonlyMap<number, number>;
154
172
  }): TraceInsight[] {
155
173
  const insights: TraceInsight[] = [
156
174
  {
@@ -170,7 +188,7 @@ function buildTraceInsights(input: {
170
188
  title: `${String(input.summary.toolCallCount)} tool call${
171
189
  input.summary.toolCallCount === 1 ? "" : "s"
172
190
  }`,
173
- detail: `First tool evidence at #${String(toolLog.id)}`,
191
+ detail: `First tool evidence at #${displayLogNumber(toolLog.id, input.displayNumberByLogId)}`,
174
192
  logId: toolLog.id,
175
193
  });
176
194
  }
@@ -180,7 +198,7 @@ function buildTraceInsights(input: {
180
198
  insights.push({
181
199
  kind: "failure",
182
200
  title: `Failure #${String(failure.responseStatus)}`,
183
- detail: `First failed request is #${String(failure.id)}`,
201
+ detail: `First failed request is #${displayLogNumber(failure.id, input.displayNumberByLogId)}`,
184
202
  logId: failure.id,
185
203
  });
186
204
  }
@@ -190,7 +208,7 @@ function buildTraceInsights(input: {
190
208
  insights.push({
191
209
  kind: "slow",
192
210
  title: `Slowest ${formatElapsed(input.summary.maxElapsedMs)}`,
193
- detail: `Max latency observed at #${String(slowest.id)}`,
211
+ detail: `Max latency observed at #${displayLogNumber(slowest.id, input.displayNumberByLogId)}`,
194
212
  logId: slowest.id,
195
213
  });
196
214
  }
@@ -291,12 +309,16 @@ function previewText(value: string, maxLength: number): string {
291
309
  return `${normalized.slice(0, maxLength - 3)}...`;
292
310
  }
293
311
 
294
- function logRangeLabel(logIds: number[]): string {
312
+ function logRangeLabel(
313
+ logIds: number[],
314
+ displayNumberByLogId: ReadonlyMap<number, number>,
315
+ ): string {
295
316
  const first = logIds[0];
296
317
  const last = logIds[logIds.length - 1];
297
318
  if (first === undefined) return "No evidence logs";
298
- if (last === undefined || first === last) return `#${String(first)}`;
299
- return `#${String(first)}-#${String(last)}`;
319
+ const firstLabel = displayLogNumber(first, displayNumberByLogId);
320
+ if (last === undefined || first === last) return `#${firstLabel}`;
321
+ return `#${firstLabel}-#${displayLogNumber(last, displayNumberByLogId)}`;
300
322
  }
301
323
 
302
324
  function tagsToText(tags: string[]): string {
@@ -316,12 +338,14 @@ function CandidateItem({
316
338
  isUpdating,
317
339
  onPromoteCandidate,
318
340
  onUpdateCandidate,
341
+ displayNumberByLogId,
319
342
  }: {
320
343
  candidate: KnowledgeCandidate;
321
344
  isPromoting: boolean;
322
345
  isUpdating: boolean;
323
346
  onPromoteCandidate: (candidateId: string) => void;
324
347
  onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
348
+ displayNumberByLogId: ReadonlyMap<number, number>;
325
349
  }): JSX.Element {
326
350
  const [editing, setEditing] = useState(false);
327
351
  const [draftType, setDraftType] = useState<KnowledgeCandidate["type"]>(candidate.type);
@@ -484,7 +508,7 @@ function CandidateItem({
484
508
  <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-muted-foreground">
485
509
  <span className="inline-flex items-center gap-1">
486
510
  <FileSearch className="size-3" />
487
- {logRangeLabel(candidate.logIds)}
511
+ {logRangeLabel(candidate.logIds, displayNumberByLogId)}
488
512
  </span>
489
513
  <span className="inline-flex items-center gap-1">
490
514
  <ShieldCheck className="size-3" />
@@ -504,20 +528,24 @@ function CandidateItem({
504
528
  )}
505
529
  </div>
506
530
  <div className="mt-1 flex flex-wrap items-center gap-1.5">
507
- {candidate.logIds.map((logId) => (
508
- <a
509
- key={logId}
510
- href={`#${getLogAnchor(logId)}`}
511
- onClick={(event) => {
512
- event.preventDefault();
513
- jumpToLog(logId);
514
- }}
515
- className="rounded border border-blue-400/25 px-1.5 py-0.5 font-mono text-[10px] text-blue-400 underline-offset-2 transition-colors hover:bg-blue-400/10 hover:text-blue-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
516
- aria-label={`Jump to evidence log ${String(logId)}`}
517
- >
518
- #{logId}
519
- </a>
520
- ))}
531
+ {candidate.logIds.map((logId) => {
532
+ const displayNumber = displayLogNumber(logId, displayNumberByLogId);
533
+ return (
534
+ <a
535
+ key={logId}
536
+ href={`#${getLogAnchor(logId)}`}
537
+ onClick={(event) => {
538
+ event.preventDefault();
539
+ jumpToLog(logId);
540
+ }}
541
+ className="rounded border border-blue-400/25 px-1.5 py-0.5 font-mono text-[10px] text-blue-400 underline-offset-2 transition-colors hover:bg-blue-400/10 hover:text-blue-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
542
+ aria-label={`Jump to evidence log ${displayNumber}`}
543
+ title={`Log ID ${String(logId)}`}
544
+ >
545
+ #{displayNumber}
546
+ </a>
547
+ );
548
+ })}
521
549
  <span className="flex-1" />
522
550
  {canEdit && (
523
551
  <Button
@@ -560,12 +588,14 @@ function CandidateList({
560
588
  updatingCandidateIds,
561
589
  onPromoteCandidate,
562
590
  onUpdateCandidate,
591
+ displayNumberByLogId,
563
592
  }: {
564
593
  candidates: KnowledgeCandidate[];
565
594
  promotingCandidateIds: ReadonlySet<string>;
566
595
  updatingCandidateIds: ReadonlySet<string>;
567
596
  onPromoteCandidate: (candidateId: string) => void;
568
597
  onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
598
+ displayNumberByLogId: ReadonlyMap<number, number>;
569
599
  }): JSX.Element | null {
570
600
  if (candidates.length === 0) return null;
571
601
  return (
@@ -578,6 +608,7 @@ function CandidateList({
578
608
  isUpdating={updatingCandidateIds.has(candidate.id)}
579
609
  onPromoteCandidate={onPromoteCandidate}
580
610
  onUpdateCandidate={onUpdateCandidate}
611
+ displayNumberByLogId={displayNumberByLogId}
581
612
  />
582
613
  ))}
583
614
  </div>
@@ -608,9 +639,10 @@ export function AgentTraceSummary({
608
639
  () => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
609
640
  [candidates.length, logs, slowResponseThresholdSeconds],
610
641
  );
642
+ const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(logs), [logs]);
611
643
  const traceInsights = useMemo(
612
- () => buildTraceInsights({ logs, scopeId, summary, candidates }),
613
- [candidates, logs, scopeId, summary],
644
+ () => buildTraceInsights({ logs, scopeId, summary, candidates, displayNumberByLogId }),
645
+ [candidates, displayNumberByLogId, logs, scopeId, summary],
614
646
  );
615
647
  const showElapsedSummary = showRollupMetrics || summary.maxElapsedMs !== null;
616
648
  const timeRange = useMemo(
@@ -870,6 +902,7 @@ export function AgentTraceSummary({
870
902
  updatingCandidateIds={updatingCandidateIds}
871
903
  onPromoteCandidate={promoteCandidate}
872
904
  onUpdateCandidate={updateCandidate}
905
+ displayNumberByLogId={displayNumberByLogId}
873
906
  />
874
907
  )}
875
908
  </section>
@@ -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
  }