@tonyclaw/agent-inspector 2.0.20 → 2.0.22

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 (52) hide show
  1. package/.output/cli.js +10 -0
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-CUqYtaH3.js → CompareDrawer-NEdpe1wl.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-Dwhi4Q1c.js +115 -0
  5. package/.output/public/assets/{ReplayDialog-CPXNEqVg.js → ReplayDialog-DijzJwaQ.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-qkj8QgkY.js → RequestAnatomy-CYbosGYy.js} +1 -1
  7. package/.output/public/assets/{ResponseView-BPNLZpuw.js → ResponseView-B3dPM63X.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-DGs-YFXI.js → StreamingChunkSequence-DYDx3xRY.js} +1 -1
  9. package/.output/public/assets/_sessionId-b1KDnByv.js +1 -0
  10. package/.output/public/assets/index-780zTVwp.css +1 -0
  11. package/.output/public/assets/index-CUyRKgII.js +1 -0
  12. package/.output/public/assets/{main-j3vS8rmP.js → main-N-sjHJUM.js} +2 -2
  13. package/.output/server/_libs/lucide-react.mjs +27 -27
  14. package/.output/server/{_sessionId-Bc5Nohhe.mjs → _sessionId-BR46y_bO.mjs} +2 -2
  15. package/.output/server/_ssr/{CompareDrawer-D0UewydM.mjs → CompareDrawer-DDxpmdPs.mjs} +3 -3
  16. package/.output/server/_ssr/{ProxyViewerContainer-BEUWt2eX.mjs → ProxyViewerContainer-B1zAefzz.mjs} +304 -55
  17. package/.output/server/_ssr/{ReplayDialog-zaN-xMOw.mjs → ReplayDialog-DleMFvMx.mjs} +4 -4
  18. package/.output/server/_ssr/{RequestAnatomy-BMomYged.mjs → RequestAnatomy-B-P9tC3D.mjs} +3 -3
  19. package/.output/server/_ssr/{ResponseView-DSoHCyAM.mjs → ResponseView-DWfGn0i3.mjs} +3 -3
  20. package/.output/server/_ssr/{StreamingChunkSequence-pt3pZOOL.mjs → StreamingChunkSequence-C8kFF6xJ.mjs} +3 -3
  21. package/.output/server/_ssr/{index-BCTKxcgV.mjs → index-DzENi2RO.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{router-DpaDa5q5.mjs → router-Dy1VEjxy.mjs} +434 -80
  24. package/.output/server/{_tanstack-start-manifest_v-EsubwO0A.mjs → _tanstack-start-manifest_v-CyzuAD8A.mjs} +1 -1
  25. package/.output/server/index.mjs +55 -55
  26. package/package.json +1 -1
  27. package/src/cli/templates/codex-skill-onboard.ts +5 -0
  28. package/src/cli/templates/skill-onboard.ts +5 -0
  29. package/src/components/OnboardingBanner.tsx +4 -0
  30. package/src/components/ProxyViewer.tsx +5 -0
  31. package/src/components/providers/ProviderCard.tsx +178 -38
  32. package/src/components/providers/ProviderForm.tsx +107 -16
  33. package/src/components/providers/ProvidersPanel.tsx +21 -3
  34. package/src/components/providers/SettingsDialog.tsx +76 -2
  35. package/src/lib/providerModelMetadata.ts +67 -3
  36. package/src/lib/providerTestContract.ts +13 -0
  37. package/src/lib/runtimeConfig.ts +8 -0
  38. package/src/lib/useOnboarding.ts +3 -0
  39. package/src/lib/useStripConfig.ts +22 -0
  40. package/src/proxy/config.ts +4 -0
  41. package/src/proxy/formats/index.ts +1 -0
  42. package/src/proxy/formats/openai/index.ts +1 -0
  43. package/src/proxy/formats/openai/zhipuProvider.ts +31 -0
  44. package/src/proxy/providerImporters.ts +47 -0
  45. package/src/proxy/providers.ts +55 -0
  46. package/src/proxy/upstream.ts +25 -0
  47. package/src/routes/api/config.ts +7 -0
  48. package/src/routes/api/providers.$providerId.test.log.ts +28 -12
  49. package/.output/public/assets/ProxyViewerContainer-CX0-R0nn.js +0 -114
  50. package/.output/public/assets/_sessionId-CXtbrFTj.js +0 -1
  51. package/.output/public/assets/index-DrppF281.js +0 -1
  52. package/.output/public/assets/index-DynnYt7S.css +0 -1
@@ -5,23 +5,78 @@ import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
5
5
  import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
6
6
  import { maskApiKey } from "../../lib/mask";
7
7
 
8
+ const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
9
+ const ZHIPU_CODING_PROVIDER_KEYWORDS = [
10
+ "zhipu coding",
11
+ "glm coding",
12
+ "bigmodel coding",
13
+ "z.ai coding",
14
+ "zai coding",
15
+ ];
16
+
8
17
  // Known provider presets - maps provider name keywords to their API URLs
9
18
  const KNOWN_PROVIDER_PRESETS: Record<
10
19
  string,
11
- { format: "anthropic" | "openai"; baseUrl: string; apiDocsUrl?: string }
20
+ { anthropicBaseUrl?: string; openaiBaseUrl?: string; apiDocsUrl?: string }
12
21
  > = {
13
22
  deepseek: {
14
- format: "openai",
15
- baseUrl: "https://api.deepseek.com",
23
+ openaiBaseUrl: "https://api.deepseek.com",
16
24
  },
17
25
  minimax: {
18
- format: "anthropic",
19
- baseUrl: "https://api.minimaxi.com/anthropic",
26
+ anthropicBaseUrl: "https://api.minimaxi.com/anthropic",
20
27
  apiDocsUrl: "https://platform.minimaxi.com/docs/api-reference/api-overview",
21
28
  },
29
+ "zhipu coding": {
30
+ anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic",
31
+ openaiBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
32
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/coding-plan/quick-start",
33
+ },
34
+ "glm coding": {
35
+ anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic",
36
+ openaiBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
37
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/coding-plan/quick-start",
38
+ },
39
+ "bigmodel coding": {
40
+ anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic",
41
+ openaiBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
42
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/coding-plan/quick-start",
43
+ },
44
+ "z.ai coding": {
45
+ anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic",
46
+ openaiBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
47
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/coding-plan/quick-start",
48
+ },
49
+ "zai coding": {
50
+ anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic",
51
+ openaiBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
52
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/coding-plan/quick-start",
53
+ },
54
+ zhipu: {
55
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
56
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
57
+ },
58
+ zhipuai: {
59
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
60
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
61
+ },
62
+ bigmodel: {
63
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
64
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
65
+ },
66
+ "z.ai": {
67
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
68
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
69
+ },
70
+ zai: {
71
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
72
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
73
+ },
74
+ glm: {
75
+ openaiBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
76
+ apiDocsUrl: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction",
77
+ },
22
78
  alibaba: {
23
- format: "openai",
24
- baseUrl: "https://dashscope.aliyuncs.com/compatible-mode",
79
+ openaiBaseUrl: "https://dashscope.aliyuncs.com/compatible-mode",
25
80
  },
26
81
  };
27
82
 
@@ -40,6 +95,22 @@ const MINIMAX_MODELS = [
40
95
  // Alibaba model options
41
96
  const ALIBABA_MODELS = ["glm-5", "glm-5.1", "glm-5.2", "qwen3.6-plus", "qwen3.7-max"];
42
97
 
98
+ // ZhipuAI / Z.AI model options
99
+ const ZHIPU_MODELS = [
100
+ "glm-5.2",
101
+ "glm-5.1",
102
+ "glm-5",
103
+ "glm-5-turbo",
104
+ "glm-4.7",
105
+ "glm-4.7-flashx",
106
+ "glm-4.6",
107
+ "glm-4.5",
108
+ "glm-4.5-air",
109
+ "glm-4.5-x",
110
+ "glm-4.5-airx",
111
+ "glm-4.5-flash",
112
+ ];
113
+
43
114
  type ProviderFormProps = {
44
115
  provider?: ProviderConfig;
45
116
  onSubmit: (data: {
@@ -151,6 +222,16 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
151
222
  const isMiniMax = name.toLowerCase().includes("minimax");
152
223
  // Check if Alibaba is detected
153
224
  const isAlibaba = name.toLowerCase().includes("alibaba");
225
+ const isZhipu = [...ZHIPU_PROVIDER_KEYWORDS, ...ZHIPU_CODING_PROVIDER_KEYWORDS].some((keyword) =>
226
+ name.toLowerCase().includes(keyword),
227
+ );
228
+ const suggestedModels = isMiniMax
229
+ ? MINIMAX_MODELS
230
+ : isZhipu
231
+ ? ZHIPU_MODELS
232
+ : isAlibaba
233
+ ? ALIBABA_MODELS
234
+ : [];
154
235
  const hasContextMetadata =
155
236
  provider?.modelMetadata?.some((metadata) => metadata.contextWindow !== undefined) ?? false;
156
237
 
@@ -182,10 +263,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
182
263
  const lowerName = name.toLowerCase();
183
264
  for (const [keyword, preset] of Object.entries(KNOWN_PROVIDER_PRESETS)) {
184
265
  if (lowerName.includes(keyword)) {
185
- if (preset.format === "anthropic" && !manualAnthropicUrlOverride) {
186
- setAnthropicBaseUrl(preset.baseUrl);
187
- } else if (preset.format === "openai" && !manualOpenaiUrlOverride) {
188
- setOpenaiBaseUrl(preset.baseUrl);
266
+ if (preset.anthropicBaseUrl !== undefined && !manualAnthropicUrlOverride) {
267
+ setAnthropicBaseUrl(preset.anthropicBaseUrl);
268
+ }
269
+ if (preset.openaiBaseUrl !== undefined && !manualOpenaiUrlOverride) {
270
+ setOpenaiBaseUrl(preset.openaiBaseUrl);
189
271
  }
190
272
  if (preset.apiDocsUrl !== undefined && !apiDocsUrl) {
191
273
  setApiDocsUrl(preset.apiDocsUrl);
@@ -198,6 +280,15 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
198
280
  if (keyword === "alibaba" && models.length === 1 && models[0] === "") {
199
281
  setModels([ALIBABA_MODELS[0] ?? ""]);
200
282
  }
283
+ // For ZhipuAI, auto-select the first model if not already set
284
+ if (
285
+ (ZHIPU_PROVIDER_KEYWORDS.includes(keyword) ||
286
+ ZHIPU_CODING_PROVIDER_KEYWORDS.includes(keyword)) &&
287
+ models.length === 1 &&
288
+ models[0] === ""
289
+ ) {
290
+ setModels([ZHIPU_MODELS[0] ?? ""]);
291
+ }
201
292
  break;
202
293
  }
203
294
  }
@@ -396,8 +487,8 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
396
487
  )}
397
488
  {modelMetadataUrl.trim() === "" && (
398
489
  <div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
399
- Built-in limits cover DeepSeek v4 pro/flash and MiniMax M3/M2.7/M2.5/M2.1. Use a
400
- registry URL for private or custom model limits.
490
+ Built-in limits cover DeepSeek v4 pro/flash, MiniMax M3/M2.7/M2.5/M2.1, and GLM coding
491
+ models. Use a registry URL for private or custom model limits.
401
492
  </div>
402
493
  )}
403
494
  {provider !== undefined && !hasContextMetadata && modelMetadataUrl.trim() === "" && (
@@ -472,11 +563,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
472
563
  value={m}
473
564
  onChange={(e) => updateModel(i, e.target.value)}
474
565
  placeholder={
475
- isMiniMax || isAlibaba ? "Type or select a model..." : "Model name"
566
+ suggestedModels.length > 0 ? "Type or select a model..." : "Model name"
476
567
  }
477
568
  className="w-full rounded-md border border-input bg-background px-4 py-3 pr-8 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"
478
569
  />
479
- {(isMiniMax || isAlibaba) && (
570
+ {suggestedModels.length > 0 && (
480
571
  <>
481
572
  <button
482
573
  type="button"
@@ -488,7 +579,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
488
579
  </button>
489
580
  {openModelDropdown === i && (
490
581
  <div className="absolute left-0 right-0 top-full mt-1 z-50 bg-popover border border-border rounded-md shadow-md max-h-48 overflow-y-auto">
491
- {(isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((opt) => (
582
+ {suggestedModels.map((opt) => (
492
583
  <button
493
584
  key={opt}
494
585
  type="button"
@@ -19,6 +19,7 @@ import {
19
19
  ProviderTestResultsSchema,
20
20
  type ProviderTestResults as TestResults,
21
21
  } from "../../lib/providerTestContract";
22
+ import { useStripConfig } from "../../lib/useStripConfig";
22
23
 
23
24
  export type { ProviderTestResults as TestResults } from "../../lib/providerTestContract";
24
25
 
@@ -41,7 +42,6 @@ const ModelMetadataRefreshResponseSchema = z.object({
41
42
  sourceUrl: z.string(),
42
43
  });
43
44
 
44
- const TEST_TIMEOUT_SECONDS = 30;
45
45
  const NETWORK_ERROR_MESSAGE = "Network error: could not reach the server. Is the proxy running?";
46
46
 
47
47
  type ProviderFormData = {
@@ -70,6 +70,15 @@ function createProviderPayload(data: ProviderFormData) {
70
70
  };
71
71
  }
72
72
 
73
+ function getProviderTestTimeoutSeconds(
74
+ provider: ProviderConfig | undefined,
75
+ timeoutSecondsPerModel: number,
76
+ ): number {
77
+ const modelCount =
78
+ provider?.models?.map((model) => model.trim()).filter((model) => model !== "").length ?? 0;
79
+ return Math.max(1, modelCount) * timeoutSecondsPerModel;
80
+ }
81
+
73
82
  async function persistProviderTestLog(providerId: string, results: TestResults): Promise<void> {
74
83
  if (results.models === undefined) {
75
84
  return;
@@ -126,6 +135,7 @@ export function ProvidersPanel({
126
135
  const [refreshingMetadataProviders, setRefreshingMetadataProviders] = useState<Set<string>>(
127
136
  new Set(),
128
137
  );
138
+ const { providerTestTimeoutSeconds } = useStripConfig();
129
139
  const listScrollRef = useRef<HTMLDivElement>(null);
130
140
  const highlightTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
131
141
 
@@ -239,9 +249,11 @@ export function ProvidersPanel({
239
249
 
240
250
  // Create abort controller for this test request
241
251
  const controller = new AbortController();
252
+ const provider = providers.find((candidate) => candidate.id === providerId);
253
+ const timeoutSeconds = getProviderTestTimeoutSeconds(provider, providerTestTimeoutSeconds);
242
254
 
243
255
  // Start countdown
244
- let remaining = TEST_TIMEOUT_SECONDS;
256
+ let remaining = timeoutSeconds;
245
257
  setTestingTimeLeft(providerId, remaining);
246
258
  const intervalId = setInterval(() => {
247
259
  remaining--;
@@ -289,7 +301,13 @@ export function ProvidersPanel({
289
301
  updateTestingState(providerId, false);
290
302
  }
291
303
  },
292
- [setTestingTimeLeft, updateTestResults, updateTestingState],
304
+ [
305
+ providerTestTimeoutSeconds,
306
+ providers,
307
+ setTestingTimeLeft,
308
+ updateTestResults,
309
+ updateTestingState,
310
+ ],
293
311
  );
294
312
 
295
313
  const refreshProviderMetadata = useCallback(
@@ -8,6 +8,7 @@ import { ProvidersPanel } from "./ProvidersPanel";
8
8
  import { useProviders } from "../../lib/useProviders";
9
9
  import { useStripConfig } from "../../lib/useStripConfig";
10
10
  import {
11
+ MAX_PROVIDER_TEST_TIMEOUT_SECONDS,
11
12
  MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
12
13
  TimeDisplayFormatSchema,
13
14
  type TimeDisplayFormat,
@@ -140,6 +141,7 @@ function StorageSettingsTab(): JSX.Element {
140
141
  const [stats, setStats] = useState<LogStorageStats | null>(null);
141
142
  const [error, setError] = useState<string | null>(null);
142
143
  const [loading, setLoading] = useState(false);
144
+ const [copiedId, setCopiedId] = useState<string | null>(null);
143
145
 
144
146
  const refresh = useCallback(async () => {
145
147
  setLoading(true);
@@ -168,6 +170,13 @@ function StorageSettingsTab(): JSX.Element {
168
170
  void refresh();
169
171
  }, [refresh]);
170
172
 
173
+ const handleCopy = useCallback((id: string, value: string) => {
174
+ void window.navigator.clipboard.writeText(value).then(() => {
175
+ setCopiedId(id);
176
+ setTimeout(() => setCopiedId(null), 1600);
177
+ });
178
+ }, []);
179
+
171
180
  return (
172
181
  <div className="space-y-4">
173
182
  <div className="flex items-center justify-between gap-3">
@@ -184,6 +193,20 @@ function StorageSettingsTab(): JSX.Element {
184
193
 
185
194
  {stats !== null && (
186
195
  <div className="grid gap-2 text-xs">
196
+ <CopyableSetupValue
197
+ id="log-dir"
198
+ label="Log storage path"
199
+ value={stats.logDir}
200
+ copiedId={copiedId}
201
+ onCopy={handleCopy}
202
+ />
203
+ <CopyableSetupValue
204
+ id="chunk-dir"
205
+ label="Streaming chunks storage path"
206
+ value={stats.chunkDir}
207
+ copiedId={copiedId}
208
+ onCopy={handleCopy}
209
+ />
187
210
  <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
188
211
  <div className="font-medium">In-memory logs</div>
189
212
  <div className="mt-1 font-mono text-muted-foreground">
@@ -192,14 +215,12 @@ function StorageSettingsTab(): JSX.Element {
192
215
  </div>
193
216
  <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
194
217
  <div className="font-medium">Log files</div>
195
- <div className="mt-1 break-all font-mono text-muted-foreground">{stats.logDir}</div>
196
218
  <div className="mt-1 font-mono text-muted-foreground">
197
219
  {stats.logFileCount.toLocaleString()} files / {formatBytes(stats.logBytes)}
198
220
  </div>
199
221
  </div>
200
222
  <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
201
223
  <div className="font-medium">Streaming chunks</div>
202
- <div className="mt-1 break-all font-mono text-muted-foreground">{stats.chunkDir}</div>
203
224
  <div className="mt-1 font-mono text-muted-foreground">
204
225
  {stats.chunkFileCount.toLocaleString()} files / {formatBytes(stats.chunkBytes)}
205
226
  </div>
@@ -314,6 +335,13 @@ function OnboardingSettingsTab(): JSX.Element {
314
335
  <Check className="size-3.5 text-emerald-500" />
315
336
  <span>Captured sessions can produce reviewable memory candidates.</span>
316
337
  </div>
338
+ <div className="flex items-start gap-2">
339
+ <Check className="mt-0.5 size-3.5 text-emerald-500" />
340
+ <span>
341
+ If your AI tool runs in another container or host, replace localhost with an address
342
+ reachable from that tool and verify the network/firewall path.
343
+ </span>
344
+ </div>
317
345
  </div>
318
346
  </div>
319
347
  );
@@ -324,10 +352,12 @@ function ProxySettingsTab(): JSX.Element {
324
352
  strip,
325
353
  captureMode,
326
354
  slowResponseThresholdSeconds,
355
+ providerTestTimeoutSeconds,
327
356
  timeDisplayFormat,
328
357
  isLoading,
329
358
  setStrip,
330
359
  setSlowResponseThresholdSeconds,
360
+ setProviderTestTimeoutSeconds,
331
361
  setTimeDisplayFormat,
332
362
  } = useStripConfig();
333
363
  const [error, setError] = useState<string | null>(null);
@@ -364,6 +394,21 @@ function ProxySettingsTab(): JSX.Element {
364
394
  [setSlowResponseThresholdSeconds],
365
395
  );
366
396
 
397
+ const handleProviderTestTimeoutChange = useCallback(
398
+ async (next: number) => {
399
+ setError(null);
400
+ setPending(true);
401
+ try {
402
+ await setProviderTestTimeoutSeconds(next);
403
+ } catch (err) {
404
+ setError(err instanceof Error ? err.message : String(err));
405
+ } finally {
406
+ setPending(false);
407
+ }
408
+ },
409
+ [setProviderTestTimeoutSeconds],
410
+ );
411
+
367
412
  const handleTimeDisplayFormatChange = useCallback(
368
413
  async (next: TimeDisplayFormat) => {
369
414
  setError(null);
@@ -449,6 +494,35 @@ function ProxySettingsTab(): JSX.Element {
449
494
  </div>
450
495
  </div>
451
496
 
497
+ <div className="space-y-1">
498
+ <label htmlFor="provider-test-timeout" className="text-sm font-semibold">
499
+ Provider test timeout
500
+ </label>
501
+ <p className="text-xs text-muted-foreground">
502
+ Provider Test waits this many seconds per configured model before aborting slow probes.
503
+ The full test window scales with model count.
504
+ </p>
505
+ <div className="flex items-center gap-2">
506
+ <input
507
+ id="provider-test-timeout"
508
+ type="number"
509
+ min={1}
510
+ max={MAX_PROVIDER_TEST_TIMEOUT_SECONDS}
511
+ step={1}
512
+ value={providerTestTimeoutSeconds}
513
+ disabled={isLoading || pending}
514
+ onChange={(e) => {
515
+ const next = Number(e.currentTarget.value);
516
+ if (!Number.isInteger(next)) return;
517
+ if (next < 1 || next > MAX_PROVIDER_TEST_TIMEOUT_SECONDS) return;
518
+ void handleProviderTestTimeoutChange(next);
519
+ }}
520
+ className="h-8 w-24 rounded-md border border-input bg-transparent px-2 text-sm font-mono disabled:cursor-not-allowed disabled:opacity-50"
521
+ />
522
+ <span className="text-xs text-muted-foreground">seconds / model</span>
523
+ </div>
524
+ </div>
525
+
452
526
  <div className="space-y-1">
453
527
  <label htmlFor="time-display-format" className="text-sm font-semibold">
454
528
  Time display
@@ -47,6 +47,10 @@ export const HF_MINIMAX_M3_URL = "https://huggingface.co/MiniMaxAI/MiniMax-M3";
47
47
  export const HF_MINIMAX_M2_7_URL = "https://huggingface.co/MiniMaxAI/MiniMax-M2.7";
48
48
  export const HF_MINIMAX_M2_5_URL = "https://huggingface.co/MiniMaxAI/MiniMax-M2.5";
49
49
  export const HF_MINIMAX_M2_1_URL = "https://huggingface.co/MiniMaxAI/MiniMax-M2.1";
50
+ export const ZHIPU_GLM_4_5_URL = "https://docs.bigmodel.cn/cn/guide/models/text/glm-4.5";
51
+ export const ZHIPU_GLM_4_6_URL = "https://docs.bigmodel.cn/cn/guide/models/text/glm-4.6";
52
+ export const ZHIPU_GLM_4_7_URL = "https://docs.bigmodel.cn/cn/guide/models/text/glm-4.7";
53
+ export const ZHIPU_GLM_5_TURBO_URL = "https://docs.bigmodel.cn/cn/guide/models/text/glm-5-turbo";
50
54
  export const HF_GLM_5_URL = "https://huggingface.co/zai-org/GLM-5";
51
55
  export const HF_GLM_5_1_URL = "https://huggingface.co/zai-org/GLM-5.1";
52
56
  export const HF_GLM_5_2_URL = "https://huggingface.co/zai-org/GLM-5.2";
@@ -124,10 +128,63 @@ export const BUILTIN_PROVIDER_MODEL_REGISTRY: ProviderModelRegistry = {
124
128
  ],
125
129
  },
126
130
  {
127
- name: "GLM",
128
- aliases: ["glm", "zai", "zhipu", "bigmodel"],
129
- baseUrls: [],
131
+ name: "ZhipuAI",
132
+ aliases: ["glm", "zai", "zhipu", "zhipuai", "bigmodel"],
133
+ baseUrls: [
134
+ "https://open.bigmodel.cn/api/paas/v4",
135
+ "https://open.bigmodel.cn/api/coding/paas/v4",
136
+ "https://open.bigmodel.cn/api/anthropic",
137
+ ],
130
138
  models: [
139
+ {
140
+ id: "glm-4.5",
141
+ aliases: ["GLM-4.5"],
142
+ contextWindow: 131_072,
143
+ outputLimit: 98_304,
144
+ sourceUrl: ZHIPU_GLM_4_5_URL,
145
+ },
146
+ {
147
+ id: "glm-4.5-air",
148
+ aliases: ["GLM-4.5-Air"],
149
+ contextWindow: 131_072,
150
+ outputLimit: 98_304,
151
+ sourceUrl: ZHIPU_GLM_4_5_URL,
152
+ },
153
+ {
154
+ id: "glm-4.5-x",
155
+ aliases: ["GLM-4.5-X"],
156
+ contextWindow: 131_072,
157
+ outputLimit: 98_304,
158
+ sourceUrl: ZHIPU_GLM_4_5_URL,
159
+ },
160
+ {
161
+ id: "glm-4.5-airx",
162
+ aliases: ["GLM-4.5-AirX"],
163
+ contextWindow: 131_072,
164
+ outputLimit: 98_304,
165
+ sourceUrl: ZHIPU_GLM_4_5_URL,
166
+ },
167
+ {
168
+ id: "glm-4.5-flash",
169
+ aliases: ["GLM-4.5-Flash"],
170
+ contextWindow: 131_072,
171
+ outputLimit: 98_304,
172
+ sourceUrl: ZHIPU_GLM_4_5_URL,
173
+ },
174
+ {
175
+ id: "glm-4.6",
176
+ aliases: ["GLM-4.6"],
177
+ contextWindow: 202_752,
178
+ outputLimit: 65_536,
179
+ sourceUrl: ZHIPU_GLM_4_6_URL,
180
+ },
181
+ {
182
+ id: "glm-4.7",
183
+ aliases: ["GLM-4.7"],
184
+ contextWindow: 202_752,
185
+ outputLimit: 131_072,
186
+ sourceUrl: ZHIPU_GLM_4_7_URL,
187
+ },
131
188
  {
132
189
  id: "glm-5",
133
190
  aliases: ["GLM-5"],
@@ -146,6 +203,13 @@ export const BUILTIN_PROVIDER_MODEL_REGISTRY: ProviderModelRegistry = {
146
203
  contextWindow: 1_048_576,
147
204
  sourceUrl: HF_GLM_5_2_URL,
148
205
  },
206
+ {
207
+ id: "glm-5-turbo",
208
+ aliases: ["GLM-5-Turbo"],
209
+ contextWindow: 202_752,
210
+ outputLimit: 131_072,
211
+ sourceUrl: ZHIPU_GLM_5_TURBO_URL,
212
+ },
149
213
  ],
150
214
  },
151
215
  ],
@@ -25,6 +25,17 @@ const ProviderTestContentSchema = z.object({
25
25
  thinking: z.string().optional(),
26
26
  });
27
27
 
28
+ const ProviderTestDebugSchema = z.object({
29
+ requestUrl: z.string().optional(),
30
+ requestMethod: z.string().optional(),
31
+ requestHeaders: z.record(z.string(), z.string()).optional(),
32
+ requestBody: z.string().optional(),
33
+ responseStatus: z.number().optional(),
34
+ responseStatusText: z.string().optional(),
35
+ responseHeaders: z.record(z.string(), z.string()).optional(),
36
+ responseBody: z.string().optional(),
37
+ });
38
+
28
39
  export const ProviderTestResultSchema = z.object({
29
40
  success: z.boolean(),
30
41
  error: ProviderTestErrorSchema.optional(),
@@ -44,6 +55,7 @@ export const ProviderTestResultSchema = z.object({
44
55
  })
45
56
  .optional(),
46
57
  requestHeaders: z.record(z.string(), z.string()).optional(),
58
+ debug: ProviderTestDebugSchema.optional(),
47
59
  });
48
60
 
49
61
  export const ProviderTestStateSchema = z.union([
@@ -70,6 +82,7 @@ export const ProviderTestResultsSchema = z.object({
70
82
  });
71
83
 
72
84
  export type ProviderTestErrorType = z.infer<typeof ProviderTestErrorTypeSchema>;
85
+ export type ProviderTestDebug = z.infer<typeof ProviderTestDebugSchema>;
73
86
  export type ProviderTestResult = z.infer<typeof ProviderTestResultSchema>;
74
87
  export type ProviderTestState = z.infer<typeof ProviderTestStateSchema>;
75
88
  export type ProviderTestResults = z.infer<typeof ProviderTestResultsSchema>;
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
 
3
3
  export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
4
4
  export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
5
+ export const DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS = 60;
6
+ export const MAX_PROVIDER_TEST_TIMEOUT_SECONDS = 600;
5
7
  export const DEFAULT_TIME_DISPLAY_FORMAT = "time";
6
8
  export const DEFAULT_CAPTURE_MODE = "simple";
7
9
 
@@ -28,6 +30,12 @@ export const RuntimeConfigSchema = z.object({
28
30
  .min(0)
29
31
  .max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
30
32
  .default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
33
+ providerTestTimeoutSeconds: z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(MAX_PROVIDER_TEST_TIMEOUT_SECONDS)
38
+ .default(DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS),
31
39
  timeDisplayFormat: TimeDisplayFormatSchema.default(DEFAULT_TIME_DISPLAY_FORMAT),
32
40
  captureMode: CaptureModeSchema.default(DEFAULT_CAPTURE_MODE),
33
41
  });
@@ -2,6 +2,7 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
2
  import { fetchJson } from "./apiClient";
3
3
  import {
4
4
  DEFAULT_CAPTURE_MODE,
5
+ DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
5
6
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
6
7
  DEFAULT_TIME_DISPLAY_FORMAT,
7
8
  RuntimeConfigSchema,
@@ -73,6 +74,8 @@ export function useOnboarding(): UseOnboarding {
73
74
  hasSeenOnboarding: true,
74
75
  slowResponseThresholdSeconds:
75
76
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
77
+ providerTestTimeoutSeconds:
78
+ response.data?.providerTestTimeoutSeconds ?? DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
76
79
  timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
77
80
  captureMode,
78
81
  },
@@ -2,6 +2,7 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
2
  import { fetchJson } from "./apiClient";
3
3
  import {
4
4
  DEFAULT_CAPTURE_MODE,
5
+ DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
5
6
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
6
7
  DEFAULT_TIME_DISPLAY_FORMAT,
7
8
  type CaptureMode,
@@ -43,11 +44,13 @@ export type UseStripConfig = {
43
44
  strip: boolean;
44
45
  captureMode: CaptureMode;
45
46
  slowResponseThresholdSeconds: number;
47
+ providerTestTimeoutSeconds: number;
46
48
  timeDisplayFormat: TimeDisplayFormat;
47
49
  isLoading: boolean;
48
50
  isError: boolean;
49
51
  setStrip: (next: boolean) => Promise<void>;
50
52
  setSlowResponseThresholdSeconds: (next: number) => Promise<void>;
53
+ setProviderTestTimeoutSeconds: (next: number) => Promise<void>;
51
54
  setTimeDisplayFormat: (next: TimeDisplayFormat) => Promise<void>;
52
55
  };
53
56
 
@@ -56,6 +59,7 @@ export type UseStripConfig = {
56
59
  *
57
60
  * - `strip` is the live value from the server (or the optimistic value mid-PATCH).
58
61
  * - `slowResponseThresholdSeconds` controls the UI's slow-response indicator.
62
+ * - `providerTestTimeoutSeconds` controls each per-model Provider Test probe.
59
63
  * - `setStrip(next)` PATCHes the server with optimistic update; on failure
60
64
  * it throws and SWR rolls back the optimistic value.
61
65
  */
@@ -74,6 +78,8 @@ export function useStripConfig(): UseStripConfig {
74
78
  const captureMode = response.data?.captureMode ?? DEFAULT_CAPTURE_MODE;
75
79
  const slowResponseThresholdSeconds =
76
80
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
81
+ const providerTestTimeoutSeconds =
82
+ response.data?.providerTestTimeoutSeconds ?? DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS;
77
83
  const timeDisplayFormat = response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT;
78
84
 
79
85
  const optimisticConfig = (patch: RuntimeConfigPatch): RuntimeConfig => ({
@@ -81,6 +87,8 @@ export function useStripConfig(): UseStripConfig {
81
87
  hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
82
88
  slowResponseThresholdSeconds:
83
89
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
90
+ providerTestTimeoutSeconds:
91
+ response.data?.providerTestTimeoutSeconds ?? DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
84
92
  timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
85
93
  captureMode,
86
94
  ...patch,
@@ -118,15 +126,29 @@ export function useStripConfig(): UseStripConfig {
118
126
  });
119
127
  };
120
128
 
129
+ const setProviderTestTimeoutSeconds = async (next: number): Promise<void> => {
130
+ await globalMutate(
131
+ STRIP_CONFIG_SWR_KEY,
132
+ setRuntimeConfig({ providerTestTimeoutSeconds: next }),
133
+ {
134
+ optimisticData: optimisticConfig({ providerTestTimeoutSeconds: next }),
135
+ rollbackOnError: true,
136
+ revalidate: false,
137
+ },
138
+ );
139
+ };
140
+
121
141
  return {
122
142
  strip,
123
143
  captureMode,
124
144
  slowResponseThresholdSeconds,
145
+ providerTestTimeoutSeconds,
125
146
  timeDisplayFormat,
126
147
  isLoading: response.isLoading,
127
148
  isError: response.error !== undefined,
128
149
  setStrip,
129
150
  setSlowResponseThresholdSeconds,
151
+ setProviderTestTimeoutSeconds,
130
152
  setTimeDisplayFormat,
131
153
  };
132
154
  }
@@ -13,6 +13,7 @@ import { getDataDir } from "./dataDir";
13
13
  import {
14
14
  CaptureModeSchema,
15
15
  DEFAULT_CAPTURE_MODE,
16
+ DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
16
17
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
17
18
  DEFAULT_TIME_DISPLAY_FORMAT,
18
19
  RuntimeConfigSchema,
@@ -98,6 +99,7 @@ function resolveInitialConfig(): RuntimeConfig {
98
99
  stripClaudeCodeBillingHeader: true,
99
100
  hasSeenOnboarding: false,
100
101
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
102
+ providerTestTimeoutSeconds: DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
101
103
  timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
102
104
  captureMode,
103
105
  };
@@ -108,6 +110,7 @@ function resolveInitialConfig(): RuntimeConfig {
108
110
  stripClaudeCodeBillingHeader: false,
109
111
  hasSeenOnboarding: false,
110
112
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
113
+ providerTestTimeoutSeconds: DEFAULT_PROVIDER_TEST_TIMEOUT_SECONDS,
111
114
  timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
112
115
  captureMode,
113
116
  };
@@ -200,6 +203,7 @@ function toPersistedConfig(value: RuntimeConfig): Record<string, unknown> {
200
203
  stripClaudeCodeBillingHeader: value.stripClaudeCodeBillingHeader,
201
204
  hasSeenOnboarding: value.hasSeenOnboarding,
202
205
  slowResponseThresholdSeconds: value.slowResponseThresholdSeconds,
206
+ providerTestTimeoutSeconds: value.providerTestTimeoutSeconds,
203
207
  timeDisplayFormat: value.timeDisplayFormat,
204
208
  };
205
209
  }