dsh-keys-manager 0.2.2 → 0.2.4

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.
package/lib/client.js CHANGED
@@ -44,9 +44,20 @@ var zh = {
44
44
  title: "\u5BC6\u94A5\u7BA1\u7406",
45
45
  subtitle: "\u4E3A\u540C\u4E00\u4E2A Provider \u914D\u7F6E\u591A\u4E2A API Key\u3002\u5355\u4F1A\u8BDD\u56FA\u5B9A\u4F7F\u7528\u4E00\u4E2A Key\uFF0C\u65B0\u4F1A\u8BDD\u8F6E\u6362\uFF1B\u5931\u8D25\u540E\u81EA\u52A8\u51B7\u5374\u5E76\u6062\u590D\u3002",
46
46
  search: "\u641C\u7D22 Provider\uFF08\u540D\u79F0\u3001ID \u6216\u73AF\u5883\u53D8\u91CF\uFF09",
47
+ filterAll: "\u5168\u90E8",
48
+ filterBuiltin: "\u5185\u7F6E",
49
+ filterPlugin: "\u63D2\u4EF6",
50
+ filterOauth: "\u652F\u6301 OAuth",
51
+ filterTransport: "\u4F20\u8F93",
52
+ filterGroup: "\u7B5B\u9009",
53
+ // Unattributed requests (I-000120, D8): the compass's fixed copy, shared
54
+ // with the filter plan's dictionary task — one pair, never a second.
55
+ unattributed: "{n} \u65E0\u5F52\u5C5E",
56
+ unattributedHint: "\u8BE5\u8DEF\u7531\u5728\u5F52\u5C5E\u8BFB\u53D6\u5C1A\u672A\u843D\u5730\uFF08\u6216\u5DF2\u5931\u8D25\uFF09\u671F\u95F4\u5E94\u7B54\u7684\u6D41\u5C1D\u8BD5\u6570\uFF1B\u5931\u8D25\u8F6C\u79FB\u91CD\u8BD5\u4F1A\u5404\u8BA1\u4E00\u6B21\uFF0C\u6545\u5355\u4E2A\u8BF7\u6C42\u53EF\u80FD\u88AB\u8BA1\u591A\u6B21\u3002\u8BFB\u53D6\u5B8C\u6210\u5F52\u5C5E\u540E\u6E05\u96F6\u3002",
47
57
  managed: "\u5DF2\u63A5\u7BA1",
48
58
  available: "\u53EF\u6DFB\u52A0 Providers",
49
59
  managedEmpty: "\u8FD8\u6CA1\u6709\u63A5\u7BA1\u7684 Provider\u3002\u4ECE\u4E0B\u65B9\u9009\u62E9\u4E00\u4E2A\u5F00\u59CB\u3002",
60
+ managedFilteredEmpty: "\u5F53\u524D\u7B5B\u9009\u4E0B\u6CA1\u6709\u5DF2\u63A5\u7BA1\u7684 Provider\u3002",
50
61
  availableEmpty: "\u6CA1\u6709\u5339\u914D\u7684 Provider\u3002",
51
62
  keys: "\u5BC6\u94A5",
52
63
  healthy: "\u53EF\u7528",
@@ -194,7 +205,8 @@ var zh = {
194
205
  deleteAccountBodyNoKeys: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u8D26\u6237\u4FDD\u5B58\u7684 OAuth \u6388\u6743\u3002\u8BE5\u8DEF\u7531\u6CA1\u6709 API-key \u6C60\uFF0C\u5220\u9664\u540E\u5C06\u505C\u6B62\u670D\u52A1\uFF0C\u76F4\u5230\u91CD\u65B0\u767B\u5F55\u8D26\u6237\u3002",
195
206
  // Transport accounts (llm-omp-transports/<route>): managed beside the
196
207
  // pi-ai pool, so the copy never promises a key-pool fallback.
197
- transportAccounts: "\u4F20\u8F93\u8D26\u6237",
208
+ transportEmpty: "\u8FD8\u6CA1\u6709\u4F20\u8F93\u8D26\u6237\u3002",
209
+ transportProviderHint: "\u8BE5\u8DEF\u7531\u7531\u4F20\u8F93\u63D2\u4EF6\u63D0\u4F9B\uFF0C\u51ED\u8BC1\u5C31\u5728\u672C\u5361\u7247\u4E2D\u7BA1\u7406\u3002",
198
210
  transportTag: "\u4F20\u8F93",
199
211
  transportSignIn: "\u767B\u5F55",
200
212
  transportExpires: "\u6709\u6548\u671F\u81F3 {time}",
@@ -203,16 +215,32 @@ var zh = {
203
215
  transportStatusNeedsReauth: "\u9700\u91CD\u65B0\u767B\u5F55",
204
216
  transportStatusSignedOut: "\u672A\u767B\u5F55",
205
217
  transportRemoveTitle: "\u79FB\u9664 {name} \u4F20\u8F93\u8D26\u6237\uFF1F",
206
- transportRemoveBody: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u4F20\u8F93\u8DEF\u7531\u4FDD\u5B58\u7684\u6388\u6743\uFF0C\u5E76\u53D6\u6D88\u4ECD\u5728\u8FDB\u884C\u7684\u767B\u5F55\uFF1B\u8DEF\u7531\u81EA\u8EAB\u4E0D\u4F1A\u91CD\u5EFA\u5B83\u3002\u8981\u6062\u590D\u670D\u52A1\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002"
218
+ transportRemoveBody: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u4F20\u8F93\u8DEF\u7531\u4FDD\u5B58\u7684\u6388\u6743\uFF0C\u5E76\u53D6\u6D88\u4ECD\u5728\u8FDB\u884C\u7684\u767B\u5F55\uFF1B\u8DEF\u7531\u81EA\u8EAB\u4E0D\u4F1A\u91CD\u5EFA\u5B83\u3002\u8981\u6062\u590D\u670D\u52A1\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002",
219
+ // The route's model allow-list, edited in the transport card.
220
+ transportModels: "\u6A21\u578B",
221
+ transportModelsAll: "\u5168\u90E8\u6A21\u578B",
222
+ transportModelsSelected: "\u5DF2\u9009 {n}/{total}",
223
+ transportModelsLoading: "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026",
224
+ transportModelsClear: "\u6E05\u9664\u9009\u62E9",
225
+ transportModelsHint: "\u4E0D\u52FE\u9009\u4EFB\u4F55\u6A21\u578B\u8868\u793A\u4F7F\u7528\u5168\u90E8\u6A21\u578B\uFF1B\u4FDD\u5B58\u540E\u8BE5\u8DEF\u7531\u7684\u6A21\u578B\u5217\u8868\u968F\u4E4B\u6536\u7A84\u3002"
207
226
  };
208
227
  var en = {
209
228
  nav: "Keys Manager",
210
229
  title: "Keys Manager",
211
230
  subtitle: "Give one provider several API keys. A session keeps one key; new sessions rotate; failures cool down and recover automatically.",
212
231
  search: "Search providers by name, id, or environment variable",
232
+ filterAll: "All",
233
+ filterBuiltin: "Built-in",
234
+ filterPlugin: "Plugin",
235
+ filterOauth: "OAuth-capable",
236
+ filterTransport: "Transport",
237
+ filterGroup: "Filters",
238
+ unattributed: "{n} unattributed",
239
+ unattributedHint: "Stream attempts this route served with no account owner because its ownership read had not landed (or had failed). A failover retry is a new attempt, so one request can be counted more than once. The count clears when a read attributes the route.",
213
240
  managed: "Managed",
214
241
  available: "Available providers",
215
242
  managedEmpty: "No managed providers yet. Adopt one below to start.",
243
+ managedFilteredEmpty: "No managed providers match the current filter.",
216
244
  availableEmpty: "No matching providers.",
217
245
  keys: "keys",
218
246
  healthy: "healthy",
@@ -359,7 +387,8 @@ var en = {
359
387
  deleteAccountBody: "This removes the account\u2019s stored OAuth grant from the credential store. If it is the active account, the provider route stops serving it and falls back to its API-key pool; the provider stays managed.",
360
388
  deleteAccountBodyNoKeys: "This removes the account\u2019s stored OAuth grant from the credential store. This route has no API-key pool, so it stops serving requests until an account is signed in again.",
361
389
  // Transport accounts — same values as locale/en.json.
362
- transportAccounts: "Transport accounts",
390
+ transportEmpty: "No transport accounts yet.",
391
+ transportProviderHint: "Served by a transport plugin: its credential is managed inside this card.",
363
392
  transportTag: "Transport",
364
393
  transportSignIn: "Sign in",
365
394
  transportExpires: "Expires {time}",
@@ -368,7 +397,14 @@ var en = {
368
397
  transportStatusNeedsReauth: "Sign in again",
369
398
  transportStatusSignedOut: "Not signed in",
370
399
  transportRemoveTitle: "Remove the {name} transport account?",
371
- transportRemoveBody: "This removes the grant stored for this transport route from the credential store and cancels a sign-in still in progress; the route does not recreate it. Sign in again to restore service."
400
+ transportRemoveBody: "This removes the grant stored for this transport route from the credential store and cancels a sign-in still in progress; the route does not recreate it. Sign in again to restore service.",
401
+ // The route's model allow-list, edited in the transport card.
402
+ transportModels: "Models",
403
+ transportModelsAll: "All models",
404
+ transportModelsSelected: "{n} of {total} selected",
405
+ transportModelsLoading: "Loading models\u2026",
406
+ transportModelsClear: "Clear selection",
407
+ transportModelsHint: "Leave every model unchecked to use the full catalog; saving narrows the list this route advertises."
372
408
  };
373
409
  var LOCALE_DICTS = { zh, en };
374
410
 
@@ -386,6 +422,13 @@ var CSS = `
386
422
  .km-stat{display:flex;flex-direction:column;gap:2px;padding:11px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
387
423
  .km-stat strong{font-size:19px;line-height:1.15;font-weight:650;font-variant-numeric:tabular-nums}
388
424
  .km-stat span{color:var(--dsw-alias-label-secondary);font-size:12px}
425
+ .km-filters{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
426
+ .km-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
427
+ .km-chip:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
428
+ .km-chip:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-state-business-primary) 20%,transparent)}
429
+ .km-chip-on{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary)}
430
+ .km-chip-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
431
+ .km-chip-on .km-chip-count{color:inherit}
389
432
  .km-search{position:relative;display:flex;align-items:center;margin:0 0 20px}
390
433
  .km-search-icon{position:absolute;left:12px;color:var(--dsw-alias-label-secondary);pointer-events:none}
391
434
  .km-search-input{width:100%;box-sizing:border-box;padding:10px 38px 10px 36px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;outline:none}
@@ -428,6 +471,19 @@ var CSS = `
428
471
  .km-account-row{position:relative;display:grid;grid-template-columns:minmax(0,1.35fr) minmax(150px,.8fr) minmax(150px,.9fr) 32px;gap:10px;align-items:center;padding:10px 11px 10px 14px;border-top:1px solid var(--dsw-alias-border-l1)}
429
472
  .km-account-row:first-child{border-top:0}
430
473
  .km-account-row::before{content:'';position:absolute;left:0;top:12px;bottom:12px;width:2px;border-radius:1px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
474
+ .km-transport-row{grid-template-columns:minmax(0,1.35fr) minmax(150px,.8fr) minmax(150px,.9fr) auto}
475
+ .km-transport-body{display:flex;flex-direction:column;gap:10px}
476
+ .km-models{border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
477
+ .km-models-head{display:flex;align-items:center;gap:7px;width:100%;padding:9px 11px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;font-weight:550;text-align:left;cursor:pointer}
478
+ .km-models-head:hover{background:var(--dsw-alias-bg-layer-2)}
479
+ .km-models-title{font-weight:600}
480
+ .km-models-body{display:flex;flex-direction:column;gap:9px;padding:9px 11px 11px;border-top:1px solid var(--dsw-alias-border-l1)}
481
+ .km-models-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:3px 10px;max-height:220px;overflow:auto}
482
+ .km-model-option{display:flex;align-items:center;gap:7px;min-width:0;font-size:12px;color:var(--dsw-alias-label-primary);cursor:pointer}
483
+ .km-model-option input{flex:0 0 auto;margin:0;accent-color:var(--dsw-alias-state-business-primary)}
484
+ .km-model-option span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
485
+ .km-models-actions{display:flex;justify-content:flex-end;gap:8px}
486
+ .km-models-failure{display:flex;flex-direction:column;gap:8px;align-items:flex-start}
431
487
  .km-account-glyph{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;flex:0 0 22px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 14%,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-business-primary)}
432
488
  .km-key-identity{min-width:0;display:flex;flex-direction:column;gap:2px}
433
489
  .km-key-alias-line{display:flex;align-items:center;gap:6px;min-width:0}
@@ -437,7 +493,7 @@ var CSS = `
437
493
  .km-key-last{display:flex;flex-direction:column;gap:2px;min-width:0}
438
494
  .km-key-time{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
439
495
  .km-key-time.km-warn{color:var(--dsw-alias-state-warn-primary)}
440
- .km-key-actions{position:relative;display:flex;justify-content:flex-end}
496
+ .km-key-actions{position:relative;display:flex;justify-content:flex-end;gap:6px}
441
497
  .km-status{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);font-size:11px;white-space:nowrap;color:var(--dsw-alias-label-secondary)}
442
498
  .km-status-success{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 26%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent)}
443
499
  .km-status-warn{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent)}
@@ -553,6 +609,8 @@ select.km-input{appearance:auto}
553
609
  .km-key-row,.km-account-row{grid-template-columns:minmax(0,1fr) 32px;gap:6px}
554
610
  .km-key-status,.km-key-last{grid-column:1;padding-left:0}
555
611
  .km-key-actions{grid-column:2;grid-row:1}
612
+ .km-transport-row{grid-template-columns:minmax(0,1fr) auto}
613
+ .km-transport-row .km-key-actions{justify-content:flex-end;flex-wrap:wrap}
556
614
  .km-settings-grid{grid-template-columns:1fr}
557
615
  .km-form-grid{grid-template-columns:1fr}
558
616
  .km-provider-side .km-status{display:none}
@@ -614,6 +672,37 @@ function wrapComponent(Component, runtime) {
614
672
  };
615
673
  }
616
674
 
675
+ // src/client/format.ts
676
+ function cooldownMinutes(ms) {
677
+ return ms / 6e4;
678
+ }
679
+ function cooldownMs(minutes) {
680
+ return Math.max(0.5, minutes) * 6e4;
681
+ }
682
+ function graceMinutes(ms) {
683
+ return ms / 6e4;
684
+ }
685
+ function graceMs(minutes) {
686
+ return Math.max(0, minutes) * 6e4;
687
+ }
688
+ function shapeTransportModels(value) {
689
+ const record = value !== null && typeof value === "object" ? value : {};
690
+ const catalog = [];
691
+ if (Array.isArray(record.catalog)) {
692
+ for (const row of record.catalog) {
693
+ if (row === null || typeof row !== "object") continue;
694
+ const { id, name } = row;
695
+ if (typeof id === "string" && id.length > 0 && typeof name === "string") catalog.push({ id, name });
696
+ }
697
+ }
698
+ const models = Array.isArray(record.models) ? record.models.filter((id) => typeof id === "string") : [];
699
+ return { catalog, models };
700
+ }
701
+ function modelPickerRows(view) {
702
+ const selected = new Set(view.models);
703
+ return view.catalog.map((row) => ({ id: row.id, name: row.name, selected: selected.has(row.id) }));
704
+ }
705
+
617
706
  // src/client/store.ts
618
707
  function messageOf(error) {
619
708
  if (error instanceof Error && error.message.length > 0) return error.message;
@@ -713,6 +802,9 @@ function createStore(ctx) {
713
802
  const attemptOutcome = (value) => {
714
803
  return { ok: true, value: value ?? null };
715
804
  };
805
+ const modelsOutcome = (value) => {
806
+ return { ok: true, value: shapeTransportModels(value) };
807
+ };
716
808
  const testOutcome = (value) => {
717
809
  const result = value ?? {};
718
810
  return {
@@ -801,6 +893,14 @@ function createStore(ctx) {
801
893
  removeTransportAccount(route) {
802
894
  return send("removeTransportAccount", { provider: route }, stateOutcome);
803
895
  },
896
+ async transportModels(route) {
897
+ const result = await rpcRead("transportModels", { provider: route });
898
+ if (!result.ok) return { ok: false, error: result.error.message };
899
+ return { ok: true, value: shapeTransportModels(result.value) };
900
+ },
901
+ saveTransportModels(route, models) {
902
+ return send("saveTransportModels", { provider: route, models }, modelsOutcome);
903
+ },
804
904
  tick() {
805
905
  update({ now: Date.now() });
806
906
  if (snapshot.status === "ready" && snapshot.busy === null) void this.load({ keepError: true });
@@ -814,22 +914,6 @@ function createStore(ctx) {
814
914
 
815
915
  // src/client/ui.ts
816
916
  var import_react2 = __toESM(require("react"), 1);
817
-
818
- // src/client/format.ts
819
- function cooldownMinutes(ms) {
820
- return ms / 6e4;
821
- }
822
- function cooldownMs(minutes) {
823
- return Math.max(0.5, minutes) * 6e4;
824
- }
825
- function graceMinutes(ms) {
826
- return ms / 6e4;
827
- }
828
- function graceMs(minutes) {
829
- return Math.max(0, minutes) * 6e4;
830
- }
831
-
832
- // src/client/ui.ts
833
917
  var h = import_react2.default.createElement;
834
918
  function cx(...values) {
835
919
  return values.filter(Boolean).join(" ");
@@ -873,6 +957,37 @@ function providerInitial(provider) {
873
957
  function oauthCapable(provider) {
874
958
  return provider.nativeAuth?.oauth === true && provider.accountSupported !== false;
875
959
  }
960
+ function originOf(provider) {
961
+ return provider.origin ?? (provider.extra ? "plugin" : "dsh");
962
+ }
963
+ var FILTER_CHIPS = [
964
+ { value: "all", label: "filterAll" },
965
+ { value: "builtin", label: "filterBuiltin" },
966
+ { value: "plugin", label: "filterPlugin" },
967
+ { value: "oauth", label: "filterOauth" },
968
+ { value: "transport", label: "filterTransport" }
969
+ ];
970
+ function matchesFilter(provider, filter) {
971
+ switch (filter) {
972
+ case "transport":
973
+ return provider.transport === true;
974
+ case "builtin":
975
+ return provider.transport !== true && originOf(provider) === "dsh";
976
+ case "plugin":
977
+ return provider.transport !== true && originOf(provider) === "plugin";
978
+ case "oauth":
979
+ return provider.transport !== true && oauthCapable(provider);
980
+ default:
981
+ return true;
982
+ }
983
+ }
984
+ function orphanTransportAccounts(providers, transportAccounts) {
985
+ return transportAccounts.filter((account) => !providers.some((provider) => provider.transport === true && provider.provider === account.id));
986
+ }
987
+ function managedEntryCount(providers, transportAccounts) {
988
+ const cards = providers.filter((provider) => provider.managed || provider.transport === true).length;
989
+ return cards + orphanTransportAccounts(providers, transportAccounts).length;
990
+ }
876
991
  function accountIdentityOf(key, provider) {
877
992
  const oauth = key.oauth;
878
993
  if (oauth !== null && oauth !== void 0) {
@@ -1648,7 +1763,118 @@ function TransportAccountRow({ account }) {
1648
1763
  }) : null
1649
1764
  );
1650
1765
  }
1651
- function ProviderCard({ provider, expanded, onToggle, now }) {
1766
+ function TransportModels({ route }) {
1767
+ const runtime = useRuntime();
1768
+ const t = useT();
1769
+ const [open, setOpen] = import_react2.default.useState(false);
1770
+ const [view, setView] = import_react2.default.useState(null);
1771
+ const [draft, setDraft] = import_react2.default.useState(/* @__PURE__ */ new Set());
1772
+ const [busy, setBusy] = import_react2.default.useState(false);
1773
+ const [error, setError] = import_react2.default.useState(null);
1774
+ const loading = import_react2.default.useRef(false);
1775
+ async function load() {
1776
+ if (loading.current) return;
1777
+ loading.current = true;
1778
+ setError(null);
1779
+ const result = await runtime.store.transportModels(route);
1780
+ loading.current = false;
1781
+ if (!result.ok) {
1782
+ setError(result.error);
1783
+ return;
1784
+ }
1785
+ setView(result.value);
1786
+ setDraft(new Set(result.value.models));
1787
+ }
1788
+ function toggle() {
1789
+ const next = !open;
1790
+ setOpen(next);
1791
+ if (next && view === null) void load();
1792
+ }
1793
+ function toggleModel(id) {
1794
+ setDraft((current) => {
1795
+ const next = new Set(current);
1796
+ if (next.has(id)) next.delete(id);
1797
+ else next.add(id);
1798
+ return next;
1799
+ });
1800
+ }
1801
+ async function save() {
1802
+ setBusy(true);
1803
+ setError(null);
1804
+ const result = await runtime.store.saveTransportModels(route, [...draft]);
1805
+ setBusy(false);
1806
+ if (!result.ok) {
1807
+ setError(result.error);
1808
+ return;
1809
+ }
1810
+ setView(result.value);
1811
+ setDraft(new Set(result.value.models));
1812
+ }
1813
+ const rows = view === null ? [] : modelPickerRows(view);
1814
+ const errorLine = error === null ? null : h("div", { className: "km-inline-error", role: "alert" }, error);
1815
+ return h(
1816
+ "div",
1817
+ { className: "km-models" },
1818
+ h(
1819
+ "button",
1820
+ {
1821
+ type: "button",
1822
+ className: "km-models-head",
1823
+ onClick: toggle,
1824
+ "aria-expanded": open
1825
+ },
1826
+ h(Icon, { name: "chevron", size: 14, className: cx("km-chevron", open && "km-chevron-open") }),
1827
+ h("span", { className: "km-models-title" }, t("transportModels")),
1828
+ view === null ? null : h(
1829
+ "span",
1830
+ { className: "km-muted" },
1831
+ draft.size === 0 ? t("transportModelsAll") : t("transportModelsSelected", { n: draft.size, total: view.catalog.length })
1832
+ )
1833
+ ),
1834
+ open ? h(
1835
+ "div",
1836
+ { className: "km-models-body" },
1837
+ // A failed load leaves no view: showing the loading line next to the
1838
+ // error would claim a request that already finished, and Save must stay
1839
+ // off without a view so a failed read cannot clear the saved selection.
1840
+ // A failed save keeps the loaded view and reports above the grid.
1841
+ view === null ? null : errorLine,
1842
+ view === null ? error !== null ? h(
1843
+ "div",
1844
+ { className: "km-models-failure" },
1845
+ errorLine,
1846
+ h(Button, { size: "small", variant: "primary", onClick: () => void load() }, t("reload"))
1847
+ ) : h("div", { className: "km-muted" }, t("transportModelsLoading")) : h("div", { className: "km-models-grid" }, rows.map((row) => h(
1848
+ "label",
1849
+ { key: row.id, className: "km-model-option" },
1850
+ h("input", {
1851
+ type: "checkbox",
1852
+ checked: draft.has(row.id),
1853
+ onChange: () => toggleModel(row.id),
1854
+ disabled: busy
1855
+ }),
1856
+ h("span", null, row.name)
1857
+ ))),
1858
+ h(
1859
+ "div",
1860
+ { className: "km-models-actions" },
1861
+ h(
1862
+ Button,
1863
+ { size: "small", variant: "primary", onClick: () => void save(), disabled: busy || view === null },
1864
+ busy ? t("saving") : t("save")
1865
+ ),
1866
+ h(Button, {
1867
+ size: "small",
1868
+ variant: "ghost",
1869
+ onClick: () => setDraft(/* @__PURE__ */ new Set()),
1870
+ disabled: busy || view === null || draft.size === 0
1871
+ }, t("transportModelsClear"))
1872
+ ),
1873
+ h("div", { className: "km-field-hint" }, t("transportModelsHint"))
1874
+ ) : null
1875
+ );
1876
+ }
1877
+ function ProviderCard({ provider, expanded, onToggle, now, transportAccount }) {
1652
1878
  const runtime = useRuntime();
1653
1879
  const t = useT();
1654
1880
  const [adding, setAdding] = import_react2.default.useState(false);
@@ -1663,11 +1889,15 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
1663
1889
  const keyCount = provider.keyCount;
1664
1890
  const warning = provider.warning;
1665
1891
  const isExtra = provider.extra;
1666
- const origin = provider.origin ?? (provider.extra ? "plugin" : "dsh");
1667
- const originLabel = origin === "plugin" ? t("pluginBuiltin") : origin === "dsh" ? t("dshBuiltin") : t("customProvider");
1892
+ const transport = provider.transport === true;
1893
+ const origin = originOf(provider);
1894
+ const originLabel = transport || origin === "plugin" ? t("pluginBuiltin") : origin === "dsh" ? t("dshBuiltin") : t("customProvider");
1668
1895
  const providerMeta = [
1669
1896
  provider.provider,
1670
- provider.active ? t("active") : provider.configured ? t("configured") : t("notConfigured"),
1897
+ // `active`/`configured` are read off the route's profile or the host's
1898
+ // provider list, neither of which describes a transport route: its state
1899
+ // is the account row it carries (signed out when the host lists none).
1900
+ transport ? t(transportStatusKey(transportAccount?.status ?? "signed-out")) : provider.active ? t("active") : provider.configured ? t("configured") : t("notConfigured"),
1671
1901
  provider.api ?? null
1672
1902
  ].filter((value) => Boolean(value));
1673
1903
  async function adopt() {
@@ -1719,7 +1949,7 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
1719
1949
  const adoptable = provider.supported || accountAdoptable;
1720
1950
  return h(
1721
1951
  "div",
1722
- { className: cx("km-card", "km-provider", expanded && "km-provider-open", managed && "km-provider-managed") },
1952
+ { className: cx("km-card", "km-provider", expanded && "km-provider-open", (managed || transport) && "km-provider-managed") },
1723
1953
  h(
1724
1954
  "button",
1725
1955
  {
@@ -1737,8 +1967,9 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
1737
1967
  { className: "km-provider-title-line" },
1738
1968
  h("span", { className: "km-provider-name" }, provider.displayName || provider.provider),
1739
1969
  h("span", { className: "km-tag" }, originLabel),
1970
+ transport ? h("span", { className: "km-tag km-tag-accent" }, t("transportTag")) : null,
1740
1971
  managed ? h("span", { className: "km-tag km-tag-accent" }, t("managedBadge")) : null,
1741
- !managed && !provider.supported && !accountAdoptable ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
1972
+ !transport && !managed && !provider.supported && !accountAdoptable ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
1742
1973
  ),
1743
1974
  h(
1744
1975
  "span",
@@ -1752,7 +1983,14 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
1752
1983
  h(
1753
1984
  "span",
1754
1985
  { className: "km-provider-side" },
1755
- managed ? h("span", { className: "km-count" }, countText(t, keyCount, "providerKeyOne", "providerKeys")) : h("span", { className: "km-count km-muted" }, adoptable ? t("unmanaged") : t("unsupported")),
1986
+ // I-000120 / D8: requests this route served without an attributed
1987
+ // account. Rendered only above zero, and the visible label carries the
1988
+ // count so it is the accessible name; the hint stays a description.
1989
+ (provider.unattributedRequests ?? 0) > 0 ? h("span", {
1990
+ className: "km-tag km-tag-warn km-unattributed",
1991
+ title: t("unattributedHint")
1992
+ }, t("unattributed", { n: provider.unattributedRequests ?? 0 })) : null,
1993
+ transport ? null : managed ? h("span", { className: "km-count" }, countText(t, keyCount, "providerKeyOne", "providerKeys")) : h("span", { className: "km-count km-muted" }, adoptable ? t("unmanaged") : t("unsupported")),
1756
1994
  managed ? h(StatusPill, { status: provider.status, t }) : null,
1757
1995
  h(Icon, { name: "chevron", size: 16, className: cx("km-chevron", expanded && "km-chevron-open") })
1758
1996
  )
@@ -1762,7 +2000,15 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
1762
2000
  { className: "km-provider-body" },
1763
2001
  error !== null ? h("div", { className: "km-inline-error", role: "alert" }, error) : null,
1764
2002
  warning !== void 0 || broken ? h(Notice, { tone: "warn", title: t("warning") }, broken ? t("bindingBroken") : warning) : null,
1765
- !managed ? h(
2003
+ // A transport route has no adopt step and no key pool: its whole
2004
+ // credential surface is the one account row its own plugin serves, plus
2005
+ // the model allow-list its adapter advertises.
2006
+ transport ? h(
2007
+ "div",
2008
+ { className: "km-transport-body" },
2009
+ transportAccount !== void 0 ? h("div", { className: "km-keys" }, h(TransportAccountRow, { account: transportAccount })) : h("div", { className: "km-muted" }, t("transportProviderHint")),
2010
+ h(TransportModels, { route: provider.provider })
2011
+ ) : !managed ? h(
1766
2012
  "div",
1767
2013
  { className: "km-adopt" },
1768
2014
  h(
@@ -2178,6 +2424,7 @@ function KeysManagerBody() {
2178
2424
  const state = snapshot.state;
2179
2425
  const now = snapshot.now;
2180
2426
  const [query, setQuery] = import_react2.default.useState("");
2427
+ const [filter, setFilter] = import_react2.default.useState("all");
2181
2428
  const [showAllAvailable, setShowAllAvailable] = import_react2.default.useState(false);
2182
2429
  const [expanded, setExpanded] = import_react2.default.useState(() => /* @__PURE__ */ new Set());
2183
2430
  const loadedRef = import_react2.default.useRef(false);
@@ -2201,9 +2448,23 @@ function KeysManagerBody() {
2201
2448
  ].filter((value) => Boolean(value)).join(" ").toLowerCase();
2202
2449
  return haystack.includes(normalizedQuery);
2203
2450
  };
2204
- const managed = providers.filter((provider) => provider.managed && matches(provider));
2205
- const available = providers.filter((provider) => !provider.managed && matches(provider));
2451
+ const managed = providers.filter((provider) => (provider.managed || provider.transport === true) && matchesFilter(provider, filter) && matches(provider));
2452
+ const available = providers.filter((provider) => !provider.managed && provider.transport !== true && matchesFilter(provider, filter) && matches(provider));
2206
2453
  const visibleAvailable = showAllAvailable || normalizedQuery.length > 0 ? available : available.slice(0, 12);
2454
+ const matchesAccount = (account) => {
2455
+ if (normalizedQuery.length === 0) return true;
2456
+ const haystack = [account.id, account.scope, account.displayName].join(" ").toLowerCase();
2457
+ return haystack.includes(normalizedQuery);
2458
+ };
2459
+ const orphanAccounts = orphanTransportAccounts(providers, transportAccounts).filter(matchesAccount);
2460
+ const visibleOrphans = filter === "all" || filter === "transport" ? orphanAccounts : [];
2461
+ const filterCount = (value) => {
2462
+ const searchable = providers.filter(matches);
2463
+ if (value === "all") return searchable.length + orphanAccounts.length;
2464
+ if (value === "transport") return searchable.filter((provider) => provider.transport === true).length + orphanAccounts.length;
2465
+ return searchable.filter((provider) => matchesFilter(provider, value)).length;
2466
+ };
2467
+ const managedHiddenByChip = filter !== "all" && providers.some((provider) => provider.managed || provider.transport === true);
2207
2468
  function toggle(provider) {
2208
2469
  setExpanded((current) => {
2209
2470
  const next = new Set(current);
@@ -2242,7 +2503,7 @@ function KeysManagerBody() {
2242
2503
  h(
2243
2504
  "div",
2244
2505
  { className: "km-summary" },
2245
- h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.managedCount ?? 0), h("span", null, t("managed"))),
2506
+ h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : managedEntryCount(providers, transportAccounts)), h("span", null, t("managed"))),
2246
2507
  h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.keyCount ?? 0), h("span", null, t("keys"))),
2247
2508
  h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.healthyKeyCount ?? 0), h("span", null, t("healthy"))),
2248
2509
  h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.coolingKeyCount ?? 0), h("span", null, t("cooling")))
@@ -2259,6 +2520,26 @@ function KeysManagerBody() {
2259
2520
  }, h(Icon, { name: "refresh", size: 14 }), t("refresh"))
2260
2521
  )
2261
2522
  ),
2523
+ // Quick filter: single select, view state only, composed with the text
2524
+ // search below. Counts are derived per predicate from the snapshot.
2525
+ !loading && !failed ? h("div", {
2526
+ className: "km-filters",
2527
+ role: "group",
2528
+ "aria-label": t("filterGroup")
2529
+ }, FILTER_CHIPS.map(
2530
+ (chip) => h(
2531
+ "button",
2532
+ {
2533
+ key: chip.value,
2534
+ type: "button",
2535
+ className: cx("km-chip", `km-chip-${chip.value}`, filter === chip.value && "km-chip-on"),
2536
+ "aria-pressed": filter === chip.value,
2537
+ onClick: () => setFilter(chip.value)
2538
+ },
2539
+ h("span", { className: "km-chip-label" }, t(chip.label)),
2540
+ h("span", { className: "km-chip-count" }, filterCount(chip.value))
2541
+ )
2542
+ )) : null,
2262
2543
  h(
2263
2544
  "div",
2264
2545
  { className: "km-search" },
@@ -2288,15 +2569,29 @@ function KeysManagerBody() {
2288
2569
  "div",
2289
2570
  { className: "km-section-head" },
2290
2571
  h("h2", { className: "km-section-title" }, t("managed")),
2291
- h("span", { className: "km-section-count" }, managed.length)
2572
+ // What is below it, like the available section: the tile above keeps
2573
+ // the global total.
2574
+ h("span", { className: "km-section-count" }, managed.length + visibleOrphans.length)
2292
2575
  ),
2293
- managed.length === 0 ? h("div", { className: "km-empty" }, h(Icon, { name: "key", size: 20 }), h("span", null, t("managedEmpty"))) : h("div", { className: "km-provider-list" }, managed.map((provider) => h(ProviderCard, {
2576
+ // The placeholder only when the section renders neither a card nor a row
2577
+ // the active chip selects.
2578
+ managed.length === 0 && visibleOrphans.length === 0 ? h("div", { className: "km-empty" }, h(Icon, { name: "key", size: 20 }), h(
2579
+ "span",
2580
+ null,
2581
+ normalizedQuery.length > 0 ? t("noSearch") : filter === "transport" ? t("transportEmpty") : managedHiddenByChip ? t("managedFilteredEmpty") : t("managedEmpty")
2582
+ )) : null,
2583
+ managed.length > 0 ? h("div", { className: "km-provider-list" }, managed.map((provider) => h(ProviderCard, {
2294
2584
  key: provider.provider,
2295
2585
  provider,
2296
2586
  now,
2587
+ transportAccount: provider.transport === true ? transportAccounts.find((account) => account.id === provider.provider) : void 0,
2297
2588
  expanded: expanded.has(provider.provider),
2298
2589
  onToggle: () => toggle(provider.provider)
2299
- })))
2590
+ }))) : null,
2591
+ visibleOrphans.length > 0 ? h("div", { className: "km-provider-list" }, visibleOrphans.map((account) => h(TransportAccountRow, {
2592
+ key: account.scope,
2593
+ account
2594
+ }))) : null
2300
2595
  ) : null,
2301
2596
  !loading && !failed ? h(
2302
2597
  "section",
@@ -2324,20 +2619,6 @@ function KeysManagerBody() {
2324
2619
  h(Button, { variant: "ghost", size: "small", onClick: () => setShowAllAvailable(false) }, t("showLess"))
2325
2620
  ) : null
2326
2621
  ) : null,
2327
- !loading && !failed && transportAccounts.length > 0 ? h(
2328
- "section",
2329
- { className: "km-section" },
2330
- h(
2331
- "div",
2332
- { className: "km-section-head" },
2333
- h("h2", { className: "km-section-title" }, t("transportAccounts")),
2334
- h("span", { className: "km-section-count" }, transportAccounts.length)
2335
- ),
2336
- h("div", { className: "km-provider-list" }, transportAccounts.map((account) => h(TransportAccountRow, {
2337
- key: account.scope,
2338
- account
2339
- })))
2340
- ) : null,
2341
2622
  !loading && !failed ? h("section", { className: "km-section" }, h(SettingsCard)) : null,
2342
2623
  h(
2343
2624
  "footer",
@@ -2353,8 +2634,9 @@ function KeysManagerCard({ view }) {
2353
2634
  if (view === "summary") {
2354
2635
  const state = snapshot.state;
2355
2636
  if (state === null) return h("span", { className: "km-summary-line" }, snapshot.status === "error" ? t("loadError") : t("loading"));
2356
- if (state.managedCount === 0) return h("span", { className: "km-summary-line" }, t("summaryEmpty"));
2357
- return h("span", { className: "km-summary-line" }, t("summaryText", { providers: state.managedCount, keys: state.keyCount }));
2637
+ const managed = managedEntryCount(state.providers, state.transportAccounts ?? []);
2638
+ if (managed === 0) return h("span", { className: "km-summary-line" }, t("summaryEmpty"));
2639
+ return h("span", { className: "km-summary-line" }, t("summaryText", { providers: managed, keys: state.keyCount }));
2358
2640
  }
2359
2641
  return h(KeysManagerBody);
2360
2642
  }
package/lib/core.d.ts CHANGED
@@ -1,11 +1,3 @@
1
- /**
2
- * Pure key-selection and cooldown policy for the Keys Manager.
3
- *
4
- * Nothing here touches Cordis, the filesystem, or a secret. The Host passes
5
- * plain provider records in and gets the same records back, which keeps the
6
- * round-robin, sticky-session, cooldown-escalation, and retry decisions
7
- * unit-testable without a running Harness.
8
- */
9
1
  import type { KeyRecord, KeyStatus, ManagerSettings, ProviderRecord, ProviderStatus } from './domain.js';
10
2
  import type { LlmFailureLike } from './host-types.js';
11
3
  /** Defaults also exposed in the manager's policy card. */
@@ -111,5 +103,12 @@ export declare function validateKeyPatch(patch: unknown): {
111
103
  };
112
104
  /** Maximum keys per provider (also used by the Host). */
113
105
  export declare const KEY_LIMIT = 32;
106
+ /**
107
+ * Finish kinds a connection test reports as a completed request. The installed
108
+ * `@deepseek-ai/dsh-llm` union is `stop | tool-calls | max-tokens | aborted |
109
+ * error`, so `length`/`toolUse` were never completions. Shared with the tests
110
+ * so the implementation and its fixtures cannot drift apart again.
111
+ */
112
+ export declare const TEST_SUCCESS_FINISH_KINDS: readonly ["stop", "tool-calls", "max-tokens"];
114
113
  /** Re-export the shared update type for callers that only import core. */
115
114
  export type { ManagerSettings };
package/lib/core.js CHANGED
@@ -279,4 +279,11 @@ export function validateKeyPatch(patch) {
279
279
  }
280
280
  /** Maximum keys per provider (also used by the Host). */
281
281
  export const KEY_LIMIT = MAX_KEYS_PER_PROVIDER;
282
+ /**
283
+ * Finish kinds a connection test reports as a completed request. The installed
284
+ * `@deepseek-ai/dsh-llm` union is `stop | tool-calls | max-tokens | aborted |
285
+ * error`, so `length`/`toolUse` were never completions. Shared with the tests
286
+ * so the implementation and its fixtures cannot drift apart again.
287
+ */
288
+ export const TEST_SUCCESS_FINISH_KINDS = ['stop', 'tool-calls', 'max-tokens'];
282
289
  //# sourceMappingURL=core.js.map