@tonyclaw/llm-inspector 1.14.0 → 1.14.2

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.
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, a as React } from "../_libs/react.mjs";
2
- import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, P as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema, S as StreamingChunkSchema$1 } from "./router-DtleGqN8.mjs";
2
+ import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, c as createPendingProviderTestResults, P as ProviderTestResultsSchema, b as createFailedProviderTestResults, d as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-BoeSXWHG.mjs";
3
3
  import { u as useSWR, a as useSWRConfig } from "../_libs/swr.mjs";
4
4
  import { u as useVirtualizer } from "../_libs/tanstack__react-virtual.mjs";
5
5
  import { J as JSZip } from "../_libs/jszip.mjs";
@@ -10,9 +10,9 @@ import { d as diffLines, a as diffJson } from "../_libs/diff.mjs";
10
10
  import { R as Root, T as Trigger$1, C as Content, a as Close, b as Title, P as Portal$1, O as Overlay } from "../_libs/radix-ui__react-dialog.mjs";
11
11
  import { R as Root2, T as Trigger, I as Icon, V as Value, P as Portal, C as Content2, a as Viewport, b as Item, c as ItemIndicator, d as ItemText, S as ScrollUpButton, e as ScrollDownButton } from "../_libs/radix-ui__react-select.mjs";
12
12
  import "../_libs/modelcontextprotocol__server.mjs";
13
- import { D as Download, L as LayoutGrid, a as List, G as GitCompareArrows, X, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, i as Globe, j as User, F as FileTerminal, k as Radio, l as Rows3, m as Columns2, n as Minus, o as Pencil, E as Equal, p as ExternalLink, q as EyeOff, r as Eye, s as RotateCw, T as Trash2, A as ArrowUp, t as ArrowDown, u as TriangleAlert, v as CircleCheckBig, w as CircleStop, x as CircleQuestionMark, y as Server, z as Gauge, B as Lock, H as Wifi, I as WifiOff, J as ChevronsUp, K as ChevronsDown, N as Brain, O as Terminal } from "../_libs/lucide-react.mjs";
13
+ import { D as Download, L as LayoutGrid, a as List, G as GitCompareArrows, X, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, i as Globe, j as User, F as FileTerminal, k as Radio, l as Rows3, m as Columns2, n as Minus, o as Pencil, E as Equal, p as EyeOff, q as Eye, r as ExternalLink, s as RotateCw, T as Trash2, A as ArrowUp, t as ArrowDown, u as TriangleAlert, v as CircleCheckBig, w as CircleStop, x as CircleQuestionMark, y as Server, z as Gauge, B as Lock, H as Wifi, I as WifiOff, J as ChevronsUp, K as ChevronsDown, N as Brain, O as Terminal } from "../_libs/lucide-react.mjs";
14
14
  import { M as Markdown } from "../_libs/react-markdown.mjs";
15
- import { a as array, b as string, u as union, d as object, l as literal, n as number, c as boolean, r as record, _ as _enum } from "../_libs/zod.mjs";
15
+ import { a as array, b as string, u as union, d as object, l as literal, n as number, c as boolean } from "../_libs/zod.mjs";
16
16
  import { R as Root2$1, L as List$1, T as Trigger$2, C as Content$1 } from "../_libs/radix-ui__react-tabs.mjs";
17
17
  import { S as Slot } from "../_libs/radix-ui__react-slot.mjs";
18
18
  import { P as Provider, R as Root3, T as Trigger$3, a as Portal$2, C as Content2$1, A as Arrow2 } from "../_libs/radix-ui__react-tooltip.mjs";
@@ -276,7 +276,7 @@ async function exportLogsAsZip(logs) {
276
276
  document.body.removeChild(anchor);
277
277
  URL.revokeObjectURL(url);
278
278
  }
279
- const version = "1.14.0";
279
+ const version = "1.14.2";
280
280
  const packageJson = {
281
281
  version
282
282
  };
@@ -2861,13 +2861,13 @@ function SelectScrollDownButton({
2861
2861
  }
2862
2862
  );
2863
2863
  }
2864
- const KNOWN_PROVIDER_DOCS = {
2865
- deepseek: "https://api-docs.deepseek.com/zh-cn/"
2866
- };
2867
2864
  function maskApiKey(apiKey) {
2868
2865
  if (apiKey.length <= 8) return "••••••••";
2869
2866
  return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
2870
2867
  }
2868
+ const KNOWN_PROVIDER_DOCS = {
2869
+ deepseek: "https://api-docs.deepseek.com/zh-cn/"
2870
+ };
2871
2871
  function hasSuccessField(result) {
2872
2872
  return Object.prototype.hasOwnProperty.call(result, "success");
2873
2873
  }
@@ -2984,6 +2984,19 @@ function TestStatus({ result }) {
2984
2984
  ] })
2985
2985
  ] });
2986
2986
  }
2987
+ function formatTimeAgo(isoString) {
2988
+ const now = Date.now();
2989
+ const then = new Date(isoString).getTime();
2990
+ const diffMs = now - then;
2991
+ const diffSec = Math.floor(diffMs / 1e3);
2992
+ if (diffSec < 60) return "just now";
2993
+ const diffMin = Math.floor(diffSec / 60);
2994
+ if (diffMin < 60) return `${diffMin}m ago`;
2995
+ const diffHr = Math.floor(diffMin / 60);
2996
+ if (diffHr < 24) return `${diffHr}h ago`;
2997
+ const diffDay = Math.floor(diffHr / 24);
2998
+ return `${diffDay}d ago`;
2999
+ }
2987
3000
  function ProviderCard({
2988
3001
  provider,
2989
3002
  testResults,
@@ -2995,6 +3008,34 @@ function ProviderCard({
2995
3008
  onTest
2996
3009
  }) {
2997
3010
  const [showApiKey, setShowApiKey] = reactExports.useState(false);
3011
+ const [copied, setCopied] = reactExports.useState(false);
3012
+ const [showModelResults, setShowModelResults] = reactExports.useState(false);
3013
+ const hasLoggedRef = reactExports.useRef(null);
3014
+ const lastTestedAtRef = reactExports.useRef(void 0);
3015
+ if (testResults?.testedAt !== void 0 && testResults.testedAt !== lastTestedAtRef.current) {
3016
+ lastTestedAtRef.current = testResults.testedAt;
3017
+ hasLoggedRef.current = null;
3018
+ }
3019
+ const handleToggleModelResults = reactExports.useCallback(() => {
3020
+ setShowModelResults((v) => {
3021
+ const next = !v;
3022
+ if (next && hasLoggedRef.current === null && testResults?.models !== void 0) {
3023
+ hasLoggedRef.current = testResults.testedAt ?? "";
3024
+ void fetch(`/api/providers/${provider.id}/test/log`, {
3025
+ method: "POST",
3026
+ headers: { "Content-Type": "application/json" },
3027
+ body: JSON.stringify(testResults)
3028
+ });
3029
+ }
3030
+ return next;
3031
+ });
3032
+ }, [provider.id, testResults]);
3033
+ function handleCopy() {
3034
+ navigator.clipboard.writeText(provider.apiKey).catch(() => {
3035
+ });
3036
+ setCopied(true);
3037
+ setTimeout(() => setCopied(false), 2e3);
3038
+ }
2998
3039
  const docsUrl = provider.apiDocsUrl ?? Object.entries(KNOWN_PROVIDER_DOCS).find(
2999
3040
  ([keyword]) => provider.name.toLowerCase().includes(keyword)
3000
3041
  )?.[1];
@@ -3004,7 +3045,11 @@ function ProviderCard({
3004
3045
  className: `border rounded-lg p-4 flex flex-col gap-3 bg-card transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`,
3005
3046
  children: [
3006
3047
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
3007
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium truncate", children: provider.model !== void 0 && provider.model !== "" ? `${provider.model} (${provider.name})` : provider.name }) }),
3048
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
3049
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium truncate", children: provider.name }),
3050
+ provider.source === "company" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0", children: "公司" }),
3051
+ provider.source === "personal" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 shrink-0", children: "个人" })
3052
+ ] }),
3008
3053
  docsUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
3009
3054
  "a",
3010
3055
  {
@@ -3020,6 +3065,7 @@ function ProviderCard({
3020
3065
  }
3021
3066
  )
3022
3067
  ] }),
3068
+ provider.models !== void 0 && provider.models.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-wrap gap-1", children: provider.models.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground", children: m }, m)) }),
3023
3069
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3024
3070
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate", children: showApiKey ? provider.apiKey : maskApiKey(provider.apiKey) }),
3025
3071
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -3031,6 +3077,16 @@ function ProviderCard({
3031
3077
  "aria-label": showApiKey ? "Hide API key" : "Show API key",
3032
3078
  children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "size-4" })
3033
3079
  }
3080
+ ),
3081
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3082
+ "button",
3083
+ {
3084
+ type: "button",
3085
+ onClick: handleCopy,
3086
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1",
3087
+ "aria-label": "Copy API key",
3088
+ children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-4 text-green-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-4" })
3089
+ }
3034
3090
  )
3035
3091
  ] }),
3036
3092
  provider.anthropicBaseUrl !== void 0 && provider.anthropicBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
@@ -3049,6 +3105,41 @@ function ProviderCard({
3049
3105
  ] }),
3050
3106
  testResults && /* @__PURE__ */ jsxRuntimeExports.jsx(TestStatus, { result: testResults.openai.nonStreaming })
3051
3107
  ] }),
3108
+ testResults?.testedAt !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
3109
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "size-3" }),
3110
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
3111
+ "Tested ",
3112
+ formatTimeAgo(testResults.testedAt)
3113
+ ] })
3114
+ ] }),
3115
+ testResults?.models !== void 0 && Object.keys(testResults.models).length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t pt-2", children: [
3116
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
3117
+ "button",
3118
+ {
3119
+ type: "button",
3120
+ onClick: handleToggleModelResults,
3121
+ className: "text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1",
3122
+ children: [
3123
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono", children: showModelResults ? "▾" : "▸" }),
3124
+ "Model Test Results (",
3125
+ Object.keys(testResults.models).length,
3126
+ ")"
3127
+ ]
3128
+ }
3129
+ ),
3130
+ showModelResults && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-xs border-collapse", children: [
3131
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-border", children: [
3132
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "text-left py-1 px-2 font-medium text-muted-foreground", children: "Model" }),
3133
+ provider.anthropicBaseUrl !== void 0 && provider.anthropicBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "text-left py-1 px-2 font-medium text-muted-foreground", children: "Anthropic" }),
3134
+ provider.openaiBaseUrl !== void 0 && provider.openaiBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "text-left py-1 px-2 font-medium text-muted-foreground", children: "OpenAI" })
3135
+ ] }) }),
3136
+ /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: Object.entries(testResults.models).map(([modelName, modelResult]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-border/50 last:border-0", children: [
3137
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 px-2 font-medium", children: modelName }),
3138
+ provider.anthropicBaseUrl !== void 0 && provider.anthropicBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 px-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TestStatus, { result: modelResult.anthropic.nonStreaming }) }),
3139
+ provider.openaiBaseUrl !== void 0 && provider.openaiBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 px-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TestStatus, { result: modelResult.openai.nonStreaming }) })
3140
+ ] }, modelName)) })
3141
+ ] }) })
3142
+ ] }),
3052
3143
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-1 border-t", children: [
3053
3144
  onTest !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
3054
3145
  Button,
@@ -3124,42 +3215,59 @@ const ALIBABA_MODELS = ["glm-5", "glm-5.1", "qwen3.6-plus", "qwen3.7-max"];
3124
3215
  function ProviderForm({ provider, onSubmit, onCancel }) {
3125
3216
  const [name, setName] = reactExports.useState(provider?.name ?? "Provider Name");
3126
3217
  const [apiKey, setApiKey] = reactExports.useState(provider?.apiKey ?? "");
3127
- const [model, setModel] = reactExports.useState(provider?.model ?? "");
3128
- const [format, setFormat] = reactExports.useState(provider?.format ?? "anthropic");
3129
- const [baseUrl, setBaseUrl] = reactExports.useState(provider?.baseUrl ?? "");
3218
+ const [showApiKey, setShowApiKey] = reactExports.useState(false);
3219
+ const [copied, setCopied] = reactExports.useState(false);
3220
+ const initialModels = provider?.models;
3221
+ const [models, setModels] = reactExports.useState(
3222
+ initialModels !== void 0 && initialModels.length > 0 ? initialModels : [""]
3223
+ );
3224
+ const [activeTab, setActiveTab] = reactExports.useState("anthropic");
3225
+ const [anthropicBaseUrl, setAnthropicBaseUrl] = reactExports.useState(provider?.anthropicBaseUrl ?? "");
3226
+ const [openaiBaseUrl, setOpenaiBaseUrl] = reactExports.useState(provider?.openaiBaseUrl ?? "");
3130
3227
  const [apiDocsUrl, setApiDocsUrl] = reactExports.useState(provider?.apiDocsUrl ?? "");
3228
+ const [source, setSource] = reactExports.useState(provider?.source);
3131
3229
  const [errors, setErrors] = reactExports.useState({});
3132
3230
  const [isSubmitting, setIsSubmitting] = reactExports.useState(false);
3133
- const [manualBaseUrlOverride, setManualBaseUrlOverride] = reactExports.useState(false);
3231
+ const [manualAnthropicUrlOverride, setManualAnthropicUrlOverride] = reactExports.useState(false);
3232
+ const [manualOpenaiUrlOverride, setManualOpenaiUrlOverride] = reactExports.useState(false);
3134
3233
  const isMiniMax = name.toLowerCase().includes("minimax");
3135
3234
  const isAlibaba = name.toLowerCase().includes("alibaba");
3235
+ function handleCopy() {
3236
+ navigator.clipboard.writeText(apiKey).catch(() => {
3237
+ });
3238
+ setCopied(true);
3239
+ setTimeout(() => setCopied(false), 2e3);
3240
+ }
3136
3241
  reactExports.useEffect(() => {
3137
3242
  if (provider) {
3138
3243
  setName(provider.name);
3139
3244
  setApiKey(provider.apiKey);
3140
- setModel(provider.model ?? "");
3141
- setFormat(provider.format ?? "anthropic");
3142
- setBaseUrl(provider.baseUrl ?? "");
3245
+ setModels((provider.models?.length ?? 0) > 0 ? provider.models : [""]);
3246
+ setAnthropicBaseUrl(provider.anthropicBaseUrl ?? "");
3247
+ setOpenaiBaseUrl(provider.openaiBaseUrl ?? "");
3143
3248
  setApiDocsUrl(provider.apiDocsUrl ?? "");
3144
- setManualBaseUrlOverride(false);
3249
+ setSource(provider.source);
3250
+ setManualAnthropicUrlOverride(false);
3251
+ setManualOpenaiUrlOverride(false);
3145
3252
  }
3146
3253
  }, [provider]);
3147
3254
  reactExports.useEffect(() => {
3148
3255
  const lowerName = name.toLowerCase();
3149
3256
  for (const [keyword, preset] of Object.entries(KNOWN_PROVIDER_PRESETS)) {
3150
3257
  if (lowerName.includes(keyword)) {
3151
- if (!manualBaseUrlOverride) {
3152
- setFormat(preset.format);
3153
- setBaseUrl(preset.baseUrl);
3258
+ if (preset.format === "anthropic" && !manualAnthropicUrlOverride) {
3259
+ setAnthropicBaseUrl(preset.baseUrl);
3260
+ } else if (preset.format === "openai" && !manualOpenaiUrlOverride) {
3261
+ setOpenaiBaseUrl(preset.baseUrl);
3154
3262
  }
3155
3263
  if (preset.apiDocsUrl !== void 0 && !apiDocsUrl) {
3156
3264
  setApiDocsUrl(preset.apiDocsUrl);
3157
3265
  }
3158
- if (keyword === "minimax" && !model) {
3159
- setModel(MINIMAX_MODELS[0] ?? "");
3266
+ if (keyword === "minimax" && models.length === 1 && models[0] === "") {
3267
+ setModels([MINIMAX_MODELS[0] ?? ""]);
3160
3268
  }
3161
- if (keyword === "alibaba" && !model) {
3162
- setModel(ALIBABA_MODELS[0] ?? "");
3269
+ if (keyword === "alibaba" && models.length === 1 && models[0] === "") {
3270
+ setModels([ALIBABA_MODELS[0] ?? ""]);
3163
3271
  }
3164
3272
  break;
3165
3273
  }
@@ -3173,13 +3281,17 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3173
3281
  if (!apiKey.trim()) {
3174
3282
  newErrors.apiKey = "API key is required";
3175
3283
  }
3176
- if (!model.trim()) {
3177
- newErrors.model = "Model is required";
3284
+ if (models.length === 0 || models.every((m) => !m.trim())) {
3285
+ newErrors.models = "At least one model is required";
3286
+ }
3287
+ if (anthropicBaseUrl.trim() && !isValidUrl(anthropicBaseUrl.trim())) {
3288
+ newErrors.anthropicBaseUrl = "Invalid URL format";
3178
3289
  }
3179
- if (!baseUrl.trim()) {
3180
- newErrors.baseUrl = "Base URL is required";
3181
- } else if (!isValidUrl(baseUrl.trim())) {
3182
- newErrors.baseUrl = "Invalid URL format";
3290
+ if (openaiBaseUrl.trim() && !isValidUrl(openaiBaseUrl.trim())) {
3291
+ newErrors.openaiBaseUrl = "Invalid URL format";
3292
+ }
3293
+ if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim()) {
3294
+ newErrors.format = "At least one format URL (Anthropic or OpenAI) is required";
3183
3295
  }
3184
3296
  setErrors(newErrors);
3185
3297
  return Object.keys(newErrors).length === 0;
@@ -3200,10 +3312,11 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3200
3312
  onSubmit({
3201
3313
  name: name.trim(),
3202
3314
  apiKey: apiKey.trim(),
3203
- model: model.trim() || void 0,
3204
- format,
3205
- baseUrl: baseUrl.trim() || void 0,
3206
- apiDocsUrl: apiDocsUrl.trim() || void 0
3315
+ models: models.map((m) => m.trim()).filter((m) => m !== ""),
3316
+ anthropicBaseUrl: anthropicBaseUrl.trim() || void 0,
3317
+ openaiBaseUrl: openaiBaseUrl.trim() || void 0,
3318
+ apiDocsUrl: apiDocsUrl.trim() || void 0,
3319
+ source
3207
3320
  });
3208
3321
  } finally {
3209
3322
  setIsSubmitting(false);
@@ -3228,92 +3341,194 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3228
3341
  ),
3229
3342
  errors.name !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.name })
3230
3343
  ] }),
3344
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3345
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-source", className: "text-sm font-medium", children: "Token Source" }),
3346
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
3347
+ "select",
3348
+ {
3349
+ id: "provider-source",
3350
+ value: source ?? "",
3351
+ onChange: (e) => setSource(
3352
+ e.target.value === "company" || e.target.value === "personal" ? e.target.value : void 0
3353
+ ),
3354
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
3355
+ children: [
3356
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "—" }),
3357
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "personal", children: "个人 (Personal)" }),
3358
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "company", children: "公司 (Company)" })
3359
+ ]
3360
+ }
3361
+ ),
3362
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Label whether this API key is company-provided or personal." })
3363
+ ] }),
3231
3364
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3232
3365
  /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-apikey", className: "text-sm font-medium", children: [
3233
3366
  "API Key ",
3234
3367
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3235
3368
  ] }),
3236
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3237
- "input",
3238
- {
3239
- id: "provider-apikey",
3240
- type: "password",
3241
- value: apiKey,
3242
- onChange: (e) => setApiKey(e.target.value),
3243
- placeholder: "sk-ant-...",
3244
- 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"
3245
- }
3246
- ),
3369
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3370
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3371
+ "input",
3372
+ {
3373
+ id: "provider-apikey",
3374
+ type: "text",
3375
+ value: showApiKey || apiKey.length === 0 ? apiKey : maskApiKey(apiKey),
3376
+ onChange: (e) => setApiKey(e.target.value),
3377
+ onFocus: () => {
3378
+ if (!showApiKey && apiKey.length > 0) setShowApiKey(true);
3379
+ },
3380
+ placeholder: "sk-ant-...",
3381
+ readOnly: !showApiKey && apiKey.length > 0,
3382
+ className: "flex-1 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"
3383
+ }
3384
+ ),
3385
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3386
+ "button",
3387
+ {
3388
+ type: "button",
3389
+ onClick: () => setShowApiKey((s) => !s),
3390
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1 shrink-0",
3391
+ "aria-label": showApiKey ? "Hide API key" : "Show API key",
3392
+ children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "size-4" })
3393
+ }
3394
+ ),
3395
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3396
+ "button",
3397
+ {
3398
+ type: "button",
3399
+ onClick: handleCopy,
3400
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1 shrink-0",
3401
+ "aria-label": "Copy API key",
3402
+ children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-4 text-green-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-4" })
3403
+ }
3404
+ )
3405
+ ] }),
3247
3406
  errors.apiKey !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.apiKey })
3248
3407
  ] }),
3249
3408
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3250
- /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-model", className: "text-sm font-medium", children: [
3251
- "Model ",
3409
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-sm font-medium", children: [
3410
+ "Models ",
3252
3411
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3253
3412
  ] }),
3254
- isMiniMax || isAlibaba ? /* @__PURE__ */ jsxRuntimeExports.jsx(
3255
- "select",
3256
- {
3257
- id: "provider-model",
3258
- value: model,
3259
- onChange: (e) => setModel(e.target.value),
3260
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
3261
- children: (isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, children: m }, m))
3262
- }
3263
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
3264
- "input",
3413
+ (isMiniMax || isAlibaba) && /* @__PURE__ */ jsxRuntimeExports.jsx("datalist", { id: "model-suggestions", children: (isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: opt }, opt)) }),
3414
+ models.map((m, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3415
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3416
+ "input",
3417
+ {
3418
+ type: "text",
3419
+ value: m,
3420
+ onChange: (e) => {
3421
+ setModels((prev) => {
3422
+ const next = [...prev];
3423
+ next[i] = e.target.value;
3424
+ return next;
3425
+ });
3426
+ },
3427
+ placeholder: isMiniMax || isAlibaba ? "Type or select a model..." : "Model name",
3428
+ list: isMiniMax || isAlibaba ? "model-suggestions" : void 0,
3429
+ className: "flex-1 rounded-md border border-input bg-background px-4 py-3 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"
3430
+ }
3431
+ ),
3432
+ models.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx(
3433
+ "button",
3434
+ {
3435
+ type: "button",
3436
+ onClick: () => setModels((prev) => prev.filter((_, idx) => idx !== i)),
3437
+ className: "text-muted-foreground hover:text-destructive transition-colors p-1 shrink-0",
3438
+ "aria-label": "Remove model",
3439
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
3440
+ "svg",
3441
+ {
3442
+ xmlns: "http://www.w3.org/2000/svg",
3443
+ width: "16",
3444
+ height: "16",
3445
+ viewBox: "0 0 24 24",
3446
+ fill: "none",
3447
+ stroke: "currentColor",
3448
+ strokeWidth: "2",
3449
+ strokeLinecap: "round",
3450
+ strokeLinejoin: "round",
3451
+ children: [
3452
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M3 6h18" }),
3453
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
3454
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
3455
+ ]
3456
+ }
3457
+ )
3458
+ }
3459
+ )
3460
+ ] }, i)),
3461
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3462
+ "button",
3265
3463
  {
3266
- id: "provider-model",
3267
- type: "text",
3268
- value: model,
3269
- onChange: (e) => setModel(e.target.value),
3270
- placeholder: "",
3271
- 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"
3464
+ type: "button",
3465
+ onClick: () => setModels((prev) => [...prev, ""]),
3466
+ className: "text-xs text-primary hover:underline flex items-center gap-1",
3467
+ children: "+ Add Model"
3272
3468
  }
3273
3469
  ),
3274
- errors.model !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.model })
3470
+ errors.models !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.models })
3275
3471
  ] }),
3276
3472
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3277
- /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-format", className: "text-sm font-medium", children: [
3278
- "API Format ",
3279
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3473
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 border-b border-border", children: [
3474
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3475
+ "button",
3476
+ {
3477
+ type: "button",
3478
+ onClick: () => setActiveTab("anthropic"),
3479
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === "anthropic" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3480
+ children: "Anthropic Format"
3481
+ }
3482
+ ),
3483
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3484
+ "button",
3485
+ {
3486
+ type: "button",
3487
+ onClick: () => setActiveTab("openai"),
3488
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === "openai" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3489
+ children: "OpenAI Format"
3490
+ }
3491
+ )
3280
3492
  ] }),
3281
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3282
- "select",
3493
+ errors.format !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.format })
3494
+ ] }),
3495
+ activeTab === "anthropic" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3496
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-anthropic-base-url", className: "text-sm font-medium", children: "Anthropic Base URL" }),
3497
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3498
+ "input",
3283
3499
  {
3284
- id: "provider-format",
3285
- value: format,
3286
- onChange: (e) => setFormat(e.target.value === "anthropic" ? "anthropic" : "openai"),
3287
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
3288
- children: [
3289
- /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "anthropic", children: "Anthropic (/v1/messages)" }),
3290
- /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "openai", children: "OpenAI (/v1/chat/completions)" })
3291
- ]
3500
+ id: "provider-anthropic-base-url",
3501
+ type: "text",
3502
+ value: anthropicBaseUrl,
3503
+ onChange: (e) => {
3504
+ setManualAnthropicUrlOverride(true);
3505
+ setAnthropicBaseUrl(e.target.value);
3506
+ },
3507
+ placeholder: "https://api.anthropic.com",
3508
+ 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"
3292
3509
  }
3293
3510
  ),
3294
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: format === "anthropic" ? "Use Anthropic format for /v1/messages endpoint" : "Use OpenAI format for /v1/chat/completions endpoint" })
3511
+ errors.anthropicBaseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.anthropicBaseUrl }),
3512
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Anthropic-compatible endpoint URL. Leave empty if this provider does not support Anthropic format." })
3295
3513
  ] }),
3296
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3297
- /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-base-url", className: "text-sm font-medium", children: [
3298
- "Base URL ",
3299
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3300
- ] }),
3514
+ activeTab === "openai" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3515
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-openai-base-url", className: "text-sm font-medium", children: "OpenAI Base URL" }),
3301
3516
  /* @__PURE__ */ jsxRuntimeExports.jsx(
3302
3517
  "input",
3303
3518
  {
3304
- id: "provider-base-url",
3519
+ id: "provider-openai-base-url",
3305
3520
  type: "text",
3306
- value: baseUrl,
3521
+ value: openaiBaseUrl,
3307
3522
  onChange: (e) => {
3308
- setManualBaseUrlOverride(true);
3309
- setBaseUrl(e.target.value);
3523
+ setManualOpenaiUrlOverride(true);
3524
+ setOpenaiBaseUrl(e.target.value);
3310
3525
  },
3311
- placeholder: format === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1",
3526
+ placeholder: "https://api.openai.com/v1",
3312
3527
  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"
3313
3528
  }
3314
3529
  ),
3315
- errors.baseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.baseUrl }),
3316
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Base URL for the provider API." })
3530
+ errors.openaiBaseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.openaiBaseUrl }),
3531
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "OpenAI-compatible endpoint URL. Leave empty if this provider does not support OpenAI format." })
3317
3532
  ] }),
3318
3533
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3319
3534
  /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-api-docs-url", className: "text-sm font-medium", children: "API Docs URL" }),
@@ -3330,89 +3545,12 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3330
3545
  ),
3331
3546
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Optional API documentation URL. If not set, uses known provider docs." })
3332
3547
  ] }),
3333
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
3548
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "sticky bottom-0 bg-card border-t flex gap-2 justify-end pt-2 pb-2", children: [
3334
3549
  /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: isSubmitting, children: "Cancel" }),
3335
3550
  /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Saving..." : provider ? "Update Provider" : "Add Provider" })
3336
3551
  ] })
3337
3552
  ] });
3338
3553
  }
3339
- const ProviderTestErrorTypeSchema = _enum([
3340
- "timeout",
3341
- "network_unreachable",
3342
- "connection_refused",
3343
- "auth_failed",
3344
- "rate_limited",
3345
- "server_error",
3346
- "invalid_response",
3347
- "unknown"
3348
- ]);
3349
- const ProviderTestErrorSchema = object({
3350
- message: string(),
3351
- type: ProviderTestErrorTypeSchema,
3352
- details: string().optional(),
3353
- hint: string().optional()
3354
- });
3355
- const ProviderTestContentSchema = object({
3356
- type: _enum(["text", "thinking"]),
3357
- text: string().optional(),
3358
- thinking: string().optional()
3359
- });
3360
- const ProviderTestResultSchema = object({
3361
- success: boolean(),
3362
- error: ProviderTestErrorSchema.optional(),
3363
- model: string().optional(),
3364
- inputTokens: number().optional(),
3365
- outputTokens: number().optional(),
3366
- cacheCreationInputTokens: number().optional(),
3367
- cacheReadInputTokens: number().optional(),
3368
- latencyMs: number().optional(),
3369
- content: array(ProviderTestContentSchema).optional(),
3370
- rawResponse: string().optional(),
3371
- streaming: boolean().optional(),
3372
- streamingChunks: object({
3373
- chunks: array(StreamingChunkSchema$1),
3374
- truncated: boolean().default(false)
3375
- }).optional(),
3376
- requestHeaders: record(string(), string()).optional()
3377
- });
3378
- const ProviderTestStateSchema = union([
3379
- ProviderTestResultSchema,
3380
- object({ notConfigured: literal(true) }),
3381
- object({ testing: literal(true) })
3382
- ]);
3383
- const ProviderFormatTestResultsSchema = object({
3384
- nonStreaming: ProviderTestStateSchema,
3385
- streaming: ProviderTestStateSchema
3386
- });
3387
- const ProviderTestResultsSchema = object({
3388
- anthropic: ProviderFormatTestResultsSchema,
3389
- openai: ProviderFormatTestResultsSchema
3390
- });
3391
- function createPendingProviderTestResults() {
3392
- return {
3393
- anthropic: {
3394
- nonStreaming: { testing: true },
3395
- streaming: { testing: true }
3396
- },
3397
- openai: {
3398
- nonStreaming: { testing: true },
3399
- streaming: { testing: true }
3400
- }
3401
- };
3402
- }
3403
- function createFailedProviderTestResults(message, type) {
3404
- const createFormatResult = () => ({
3405
- nonStreaming: {
3406
- success: false,
3407
- error: { message, type }
3408
- },
3409
- streaming: { notConfigured: true }
3410
- });
3411
- return {
3412
- anthropic: createFormatResult(),
3413
- openai: createFormatResult()
3414
- };
3415
- }
3416
3554
  const ConfigPathsResponseSchema = object({
3417
3555
  providerConfig: string()
3418
3556
  });
@@ -3428,10 +3566,11 @@ function createProviderPayload(data) {
3428
3566
  return {
3429
3567
  name: data.name,
3430
3568
  apiKey: data.apiKey,
3431
- model: data.model,
3432
- format: data.format,
3433
- anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : void 0,
3434
- openaiBaseUrl: data.format === "openai" ? data.baseUrl : void 0
3569
+ models: data.models,
3570
+ anthropicBaseUrl: (data.anthropicBaseUrl?.length ?? 0) > 0 ? data.anthropicBaseUrl : void 0,
3571
+ openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : void 0,
3572
+ apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : void 0,
3573
+ source: data.source
3435
3574
  };
3436
3575
  }
3437
3576
  function ProvidersPanel({
@@ -3456,9 +3595,11 @@ function ProvidersPanel({
3456
3595
  const [configPath, setConfigPath] = reactExports.useState(null);
3457
3596
  const [configPathCopied, setConfigPathCopied] = reactExports.useState(false);
3458
3597
  const [highlightedProviderId, setHighlightedProviderId] = reactExports.useState(null);
3598
+ const [sourceFilter, setSourceFilter] = reactExports.useState("all");
3459
3599
  const listScrollRef = reactExports.useRef(null);
3460
3600
  const highlightTimeoutRef = reactExports.useRef(null);
3461
3601
  const providers = externalProviders ?? [];
3602
+ const filteredProviders = sourceFilter === "all" ? providers : providers.filter((p) => p.source === sourceFilter);
3462
3603
  const testResults = externalTestResults ?? internalTestResults;
3463
3604
  const testingProviders = externalTestingProviders ?? internalTestingProviders;
3464
3605
  const testingTimeLeft = externalTestingTimeLeft ?? internalTestingTimeLeft;
@@ -3586,51 +3727,50 @@ function ProvidersPanel({
3586
3727
  );
3587
3728
  function handleAddProvider(data) {
3588
3729
  void (async () => {
3589
- let res;
3590
3730
  try {
3591
- res = await fetch("/api/providers", {
3731
+ const res = await fetch("/api/providers", {
3592
3732
  method: "POST",
3593
3733
  headers: { "Content-Type": "application/json" },
3594
3734
  body: JSON.stringify(createProviderPayload(data))
3595
3735
  });
3736
+ if (!res.ok) {
3737
+ setError(await readApiError(res, "Failed to add provider"));
3738
+ return;
3739
+ }
3740
+ const newProvider = await parseJsonResponse(res, ProviderConfigSchema);
3741
+ setShowForm(false);
3742
+ triggerHighlight(newProvider.id);
3743
+ refreshProviders();
3744
+ await runTest(newProvider.id);
3596
3745
  } catch {
3597
3746
  setError(NETWORK_ERROR_MESSAGE);
3598
- return;
3599
- }
3600
- if (!res.ok) {
3601
- setError(await readApiError(res, "Failed to add provider"));
3602
- return;
3603
3747
  }
3604
- const newProvider = await parseJsonResponse(res, ProviderConfigSchema);
3605
- setShowForm(false);
3606
- triggerHighlight(newProvider.id);
3607
- refreshProviders();
3608
- await runTest(newProvider.id);
3609
3748
  })();
3610
3749
  }
3611
3750
  function handleUpdateProvider(data) {
3612
3751
  if (!editingProvider) return;
3613
3752
  void (async () => {
3614
- let res;
3615
3753
  try {
3616
- res = await fetch(`/api/providers/${editingProvider.id}`, {
3754
+ const res = await fetch(`/api/providers/${editingProvider.id}`, {
3617
3755
  method: "PUT",
3618
3756
  headers: { "Content-Type": "application/json" },
3619
3757
  body: JSON.stringify(createProviderPayload(data))
3620
3758
  });
3759
+ if (!res.ok) {
3760
+ setError(await readApiError(res, "Failed to update provider"));
3761
+ return;
3762
+ }
3763
+ const updated = await parseJsonResponse(res, ProviderConfigSchema);
3764
+ setEditingProvider(void 0);
3765
+ triggerHighlight(updated.id);
3766
+ refreshProviders();
3767
+ const criticalFieldsChanged = (data.apiKey ?? "") !== (editingProvider.apiKey ?? "") || JSON.stringify(data.models) !== JSON.stringify(editingProvider.models) || (data.anthropicBaseUrl ?? "") !== (editingProvider.anthropicBaseUrl ?? "") || (data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "");
3768
+ if (criticalFieldsChanged) {
3769
+ await runTest(updated.id);
3770
+ }
3621
3771
  } catch {
3622
3772
  setError(NETWORK_ERROR_MESSAGE);
3623
- return;
3624
- }
3625
- if (!res.ok) {
3626
- setError(await readApiError(res, "Failed to update provider"));
3627
- return;
3628
3773
  }
3629
- const updated = await parseJsonResponse(res, ProviderConfigSchema);
3630
- setEditingProvider(void 0);
3631
- triggerHighlight(updated.id);
3632
- refreshProviders();
3633
- await runTest(updated.id);
3634
3774
  })();
3635
3775
  }
3636
3776
  function handleDeleteProvider(providerId) {
@@ -3799,22 +3939,34 @@ function ProvidersPanel({
3799
3939
  /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "size-4" }),
3800
3940
  "Add Your First Provider"
3801
3941
  ] })
3802
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: listScrollRef, className: "space-y-3", children: providers.map((provider) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3803
- ProviderCard,
3804
- {
3805
- provider,
3806
- testResults: testResults[provider.id],
3807
- isTesting: testingProviders.has(provider.id),
3808
- testingTimeLeft: testingTimeLeft[provider.id],
3809
- isHighlighted: provider.id === highlightedProviderId,
3810
- onEdit: (p) => setEditingProvider(p),
3811
- onDelete: handleDeleteProvider,
3812
- onTest: (id) => {
3813
- void runTest(id);
3814
- }
3815
- },
3816
- provider.id
3817
- )) })
3942
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
3943
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-1 border-b border-border", children: ["all", "personal", "company"].map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3944
+ "button",
3945
+ {
3946
+ type: "button",
3947
+ onClick: () => setSourceFilter(tab),
3948
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${sourceFilter === tab ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3949
+ children: tab === "all" ? "All" : tab === "personal" ? "Personal" : "Company"
3950
+ },
3951
+ tab
3952
+ )) }),
3953
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: listScrollRef, className: "space-y-3", children: filteredProviders.map((provider) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3954
+ ProviderCard,
3955
+ {
3956
+ provider,
3957
+ testResults: testResults[provider.id],
3958
+ isTesting: testingProviders.has(provider.id),
3959
+ testingTimeLeft: testingTimeLeft[provider.id],
3960
+ isHighlighted: provider.id === highlightedProviderId,
3961
+ onEdit: (p) => setEditingProvider(p),
3962
+ onDelete: handleDeleteProvider,
3963
+ onTest: (id) => {
3964
+ void runTest(id);
3965
+ }
3966
+ },
3967
+ provider.id
3968
+ )) })
3969
+ ] })
3818
3970
  ] });
3819
3971
  }
3820
3972
  async function fetcher(url) {