dsh-llm-fallbacks 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -18,7 +18,7 @@ dsh plugin --profile web add dsh-llm-fallbacks # pin a version with @<version>
18
18
  ## Features
19
19
 
20
20
  - **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
21
- - **Two-block config**: block 1 `rootChain` — the root agent's single fallback chain (empty = root does not fall back); block 2 declared roles — `roles.list` role entities (id/label/description/chain/fallback) that `roles.rules` reference by id (or the built-in `inherit`); no rule match → `inherit` → `rootChain`.
21
+ - **Two-block config**: block 1 `rootChain` — the root agent's single fallback chain (empty = root does not fall back); block 2 declared roles — `roles.list` role entities (id/persona/chain/fallback) that `roles.rules` reference by id (or the built-in `inherit`); no rule match → `inherit` → `rootChain`.
22
22
  - **Entry syntax**: chain entries are `provider/model` (exact switch) or `provider/*` (keep the failed model id, switch provider only) — the old chain-key namespace (provider/model keys, role-name keys) is gone.
23
23
  - **Cooldown and revert**: models that were switched away from / failed are not re-selected during the cooldown period; `revertPolicy: cooldown-expiry` automatically returns to the primary model when the cooldown expires, while `never` does not return within the session.
24
24
  - **Visible behavior**: every switch appends a persisted session event `fallbacks/switch` (from/to/role/reason), alongside info-level logs (candidate attempt order and skip reasons) and the read-only status block on the Settings → 插件配置 → Fallbacks card — no silent model switching.
@@ -85,8 +85,7 @@ fallbacks:
85
85
  roles: # block 2: declare roles first, then let rules reference them
86
86
  list:
87
87
  - id: reviewer # role entity: unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
88
- label: Reviewer
89
- description: Code-review subagents
88
+ persona: Code-review subagents
90
89
  chain:
91
90
  - openai/gpt-4o-mini
92
91
  fallback: inherit-root # default: role chain, then append rootChain
@@ -95,7 +94,7 @@ fallbacks:
95
94
  role: reviewer
96
95
  ```
97
96
 
98
- Roles are declared entities: `roles.list` holds role cards (id/label/description + chain/fallback), and `roles.rules` match origin/provider/model in order to a declared role id or the built-in `inherit` (first match wins) — **no rule match → `inherit` → `rootChain`**. A declared role is only ever hit when a rule references it. A role without a chain is meaningless — the settings card blocks saving it and the host warns at startup; give every declared role a chain, or point its rules at the built-in `inherit`. (A hand-written YAML role with a missing/empty chain is not fatal: with `fallback: inherit-root` (the default) an empty chain still falls back to `rootChain`; with `fallback: none` an empty chain leaves no candidates and the request passes through untouched — either way it is only a startup warn.) The legacy chain-key namespace and role-default field are gone (migration table: [docs/configuration.md](docs/configuration.md)).
97
+ Roles are declared entities: `roles.list` holds role cards (id/persona + chain/fallback), and `roles.rules` match origin/provider/model in order to a declared role id or the built-in `inherit` (first match wins) — **no rule match → `inherit` → `rootChain`**. A declared role is only ever hit when a rule references it. A role without a chain is meaningless — the settings card blocks saving it and the host warns at startup; give every declared role a chain, or point its rules at the built-in `inherit`. (A hand-written YAML role with a missing/empty chain is not fatal: with `fallback: inherit-root` (the default) an empty chain still falls back to `rootChain`; with `fallback: none` an empty chain leaves no candidates and the request passes through untouched — either way it is only a startup warn.) The legacy chain-key namespace and role-default field are gone (migration table: [docs/configuration.md](docs/configuration.md)).
99
98
 
100
99
  Save and restart the web session for the changes to take effect. The feature switch `fallbacks.enabled` **defaults to off (`false`)** — the plugin only engages once it is turned on; `triggerCodes` defaults to `AUTH` / `QUOTA` / `RATE_LIMIT`; and with **no `rootChain`/role chains configured the behavior is identical to not having the plugin installed**. More examples (role entities, fallback strategies, rules referencing `inherit`) → [docs/configuration.md](docs/configuration.md).
101
100
 
package/README.zh-CN.md CHANGED
@@ -18,7 +18,7 @@ dsh plugin --profile web add dsh-llm-fallbacks # 钉版本:加 @<version>
18
18
  ## 能力一览
19
19
 
20
20
  - **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
21
- - **两块制配置**:块 1 `rootChain`——root 主代理唯一降级链(空 = root 不降级);块 2 声明式角色——`roles.list` 角色实体(id/label/description/chain/fallback)供 `roles.rules` 按 id 引用(或内置 `inherit`);未命中规则 → `inherit` → `rootChain`。
21
+ - **两块制配置**:块 1 `rootChain`——root 主代理唯一降级链(空 = root 不降级);块 2 声明式角色——`roles.list` 角色实体(id/persona/chain/fallback)供 `roles.rules` 按 id 引用(或内置 `inherit`);未命中规则 → `inherit` → `rootChain`。
22
22
  - **条目语法**:链条目为 `provider/model`(精确切换)或 `provider/*`(保留失败模型 id 仅换 provider)——旧链键命名空间(provider/model 键、角色名键)已删除。
23
23
  - **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型,`never` 会话内不回。
24
24
  - **行为可见**:每次切换追加持久化会话事件 `fallbacks/switch`(from/to/role/reason),配合 info 级日志(候选尝试顺序与跳过原因)与设置 → 插件配置 → Fallbacks 卡片上的只读状态块,无静默换模型。
@@ -83,8 +83,7 @@ fallbacks:
83
83
  roles: # 块 2:先声明角色,再让规则引用
84
84
  list:
85
85
  - id: reviewer # 角色实体:id 唯一(/^[a-z0-9-]{1,32}$/);inherit 为保留字
86
- label: 审查者
87
- description: 代码审查子代理
86
+ persona: 代码审查子代理
88
87
  chain:
89
88
  - openai/gpt-4o-mini
90
89
  fallback: inherit-root # 默认:角色链后追加 rootChain
@@ -93,7 +92,7 @@ fallbacks:
93
92
  role: reviewer
94
93
  ```
95
94
 
96
- 角色是声明式实体:`roles.list` 存放角色卡(id/label/description + chain/fallback),`roles.rules` 按 origin/provider/model 顺序匹配到**已声明角色 id 或内置 `inherit`**(首个命中即停)——**未命中 → `inherit` → `rootChain`**。只声明不写规则的角色永不命中。角色无 chain 无意义——设置卡拦截保存、启动时告警;请为每个声明角色配置 chain,或让规则直接引用内置 `inherit`。(手写 YAML 角色链缺省/为空不致命:`fallback: inherit-root`(默认)时空链仍按防御语义回退 `rootChain`;`fallback: none` 时空链无候选 → no-op 透传;两种情况都仅启动告警。)旧链键命名空间与角色兜底字段已删除(迁移表见 [docs/configuration.md](docs/configuration.md))。
95
+ 角色是声明式实体:`roles.list` 存放角色卡(id/persona + chain/fallback),`roles.rules` 按 origin/provider/model 顺序匹配到**已声明角色 id 或内置 `inherit`**(首个命中即停)——**未命中 → `inherit` → `rootChain`**。只声明不写规则的角色永不命中。角色无 chain 无意义——设置卡拦截保存、启动时告警;请为每个声明角色配置 chain,或让规则直接引用内置 `inherit`。(手写 YAML 角色链缺省/为空不致命:`fallback: inherit-root`(默认)时空链仍按防御语义回退 `rootChain`;`fallback: none` 时空链无候选 → no-op 透传;两种情况都仅启动告警。)旧链键命名空间与角色兜底字段已删除(迁移表见 [docs/configuration.md](docs/configuration.md))。
97
96
 
98
97
  保存并重启 web 会话后生效。功能级开关 `fallbacks.enabled` **默认关闭(`false`)**——打开开关后插件才会介入;`triggerCodes` 默认覆盖 `AUTH` / `QUOTA` / `RATE_LIMIT`;**未配置任何 `rootChain`/角色链时行为与未安装插件完全一致**。更多示例(角色实体、fallback 策略、引用 `inherit` 的规则)见 [docs/configuration.md](docs/configuration.md)。
99
98
 
@@ -210,8 +210,7 @@ export declare function rowsToRootChain(rows: readonly RootChainRow[]): string[]
210
210
  */
211
211
  export interface RoleRow {
212
212
  id: string;
213
- label: string;
214
- description: string;
213
+ persona: string;
215
214
  selectors: ChainSelectorRow[];
216
215
  fallback: FallbackStrategy;
217
216
  }
@@ -4,33 +4,9 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- //#region \0rolldown/runtime.js
8
- var __create = Object.create;
9
- var __defProp = Object.defineProperty;
10
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
- var __getOwnPropNames = Object.getOwnPropertyNames;
12
- var __getProtoOf = Object.getPrototypeOf;
13
- var __hasOwnProp = Object.prototype.hasOwnProperty;
14
- var __copyProps = (to, from, except, desc) => {
15
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
- key = keys[i];
17
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
- get: ((k) => from[k]).bind(null, key),
19
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
- });
21
- }
22
- return to;
23
- };
24
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
- value: mod,
26
- enumerable: true
27
- }) : target, mod));
28
- //#endregion
29
7
  let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
30
8
  let react = require("react");
31
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
- let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
33
- _deepseek_ai_schemastery = __toESM(_deepseek_ai_schemastery, 1);
34
10
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
35
11
  let react_jsx_runtime = require("react/jsx-runtime");
36
12
  //#region src/selectors.ts
@@ -65,32 +41,6 @@ window.__ModuleLoader__.load({
65
41
  //#endregion
66
42
  //#region src/config.ts
67
43
  /**
68
- * The `fallbacks` settings namespace: plugin config schema + defaults.
69
- *
70
- * Two-block config model (plan fallbacks-role-config-model): block 1
71
- * `rootChain` — the root agent's single fallback chain (empty = no
72
- * degradation) — plus block 2 declared role entities: `roles.list`
73
- * (id/label/description/prompt?/permissions?/chain?/fallback) and
74
- * `roles.rules` enum references into the declared ids (or the built-in
75
- * `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
76
- * from the schema and type (zero residual, migration table excepted); the
77
- * runtime consumes the new shape directly and flags surviving legacy keys
78
- * at startup via `detectLegacyKeys` (see `src/index.ts` apply()).
79
- *
80
- * Spec §4 is authoritative for field names and default values — notably
81
- * `triggerCodes` defaults to dsh's stable failure codes `['AUTH', 'QUOTA',
82
- * 'RATE_LIMIT']` (there is no `QUOTA_EXCEEDED` code in dsh), and an
83
- * unconfigured install (`enabled: false`, empty `rootChain`, empty roles)
84
- * is a no-op pass-through exactly like an uninstalled plugin (AC-8).
85
- *
86
- * This module is pure logic: it must not import any `@deepseek-ai/*` package
87
- * (types included) — `FallbacksConfig` is the plugin's own type. Task 3
88
- * registers this schema with `installSettingsSection` under the `fallbacks`
89
- * settings namespace.
90
- *
91
- * @module dsh-llm-fallbacks/config
92
- */
93
- /**
94
44
  * Spec §4 defaults — `Config({})` must equal this (no-op install).
95
45
  * `enabled` defaults to `false` (readme-settings spec §1.2): the feature
96
46
  * switch is off until the user turns it on in the settings page; an
@@ -121,42 +71,6 @@ window.__ModuleLoader__.load({
121
71
  const INHERIT_ROLE_ID = "inherit";
122
72
  /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
123
73
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
124
- _deepseek_ai_schemastery.default.object({
125
- enabled: _deepseek_ai_schemastery.default.boolean().default(false),
126
- triggerCodes: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).default([
127
- "AUTH",
128
- "QUOTA",
129
- "RATE_LIMIT"
130
- ]),
131
- rootChain: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).default([]),
132
- roles: _deepseek_ai_schemastery.default.object({
133
- list: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.object({
134
- id: _deepseek_ai_schemastery.default.string().required(),
135
- label: _deepseek_ai_schemastery.default.string().default(""),
136
- description: _deepseek_ai_schemastery.default.string().default(""),
137
- prompt: _deepseek_ai_schemastery.default.string(),
138
- permissions: _deepseek_ai_schemastery.default.object({
139
- allow: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()),
140
- deny: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string())
141
- }),
142
- chain: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()),
143
- fallback: _deepseek_ai_schemastery.default.union([_deepseek_ai_schemastery.default.const("inherit-root"), _deepseek_ai_schemastery.default.const("none")]).default("inherit-root")
144
- })).default([]),
145
- rules: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.object({
146
- origin: _deepseek_ai_schemastery.default.union([_deepseek_ai_schemastery.default.const("root"), _deepseek_ai_schemastery.default.const("subagent")]),
147
- provider: _deepseek_ai_schemastery.default.string(),
148
- model: _deepseek_ai_schemastery.default.string(),
149
- role: _deepseek_ai_schemastery.default.string().required()
150
- })).default([])
151
- }).default({
152
- list: [],
153
- rules: []
154
- }),
155
- cooldownMs: _deepseek_ai_schemastery.default.number().default(3e5),
156
- revertPolicy: _deepseek_ai_schemastery.default.union([_deepseek_ai_schemastery.default.const("cooldown-expiry"), _deepseek_ai_schemastery.default.const("never")]).default("cooldown-expiry"),
157
- maxSwitchesPerStep: _deepseek_ai_schemastery.default.number().default(8),
158
- alwaysModeRetryCap: _deepseek_ai_schemastery.default.number().default(5)
159
- });
160
74
  //#endregion
161
75
  //#region src/client/fallbacks-store.ts
162
76
  /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
@@ -215,10 +129,8 @@ window.__ModuleLoader__.load({
215
129
  const roles = isRecord(value.roles) ? value.roles : {};
216
130
  const parsedList = (Array.isArray(roles.list) ? roles.list : []).map((role, index) => {
217
131
  if (!isRecord(role) || typeof role.id !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}] must have a string id`);
218
- const label = role.label;
219
- if (label !== void 0 && typeof label !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].label must be a string`);
220
- const description = role.description;
221
- if (description !== void 0 && typeof description !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].description must be a string`);
132
+ const persona = role.persona;
133
+ if (persona !== void 0 && typeof persona !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].persona must be a string`);
222
134
  const prompt = role.prompt;
223
135
  if (prompt !== void 0 && typeof prompt !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].prompt must be a string`);
224
136
  const permissions = role.permissions;
@@ -229,8 +141,7 @@ window.__ModuleLoader__.load({
229
141
  if (fallback !== void 0 && fallback !== "inherit-root" && fallback !== "none") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].fallback must be inherit-root|none`);
230
142
  return {
231
143
  id: role.id,
232
- label: label ?? "",
233
- description: description ?? "",
144
+ persona: persona ?? "",
234
145
  ...prompt === void 0 ? {} : { prompt },
235
146
  ...permissions === void 0 ? {} : { permissions },
236
147
  chain: chain ?? [],
@@ -421,8 +332,7 @@ window.__ModuleLoader__.load({
421
332
  function rolesToRows(roles, catalog) {
422
333
  return roles.map((role) => ({
423
334
  id: role.id,
424
- label: role.label,
425
- description: role.description,
335
+ persona: role.persona,
426
336
  selectors: (role.chain ?? []).map((entry) => entryToSelectorRow(entry, catalog)),
427
337
  fallback: role.fallback ?? "inherit-root"
428
338
  }));
@@ -431,8 +341,7 @@ window.__ModuleLoader__.load({
431
341
  function rowsToRoles(rows) {
432
342
  return rows.map((row) => ({
433
343
  id: row.id.trim(),
434
- label: row.label,
435
- description: row.description,
344
+ persona: row.persona,
436
345
  chain: row.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
437
346
  fallback: row.fallback
438
347
  }));
@@ -454,8 +363,7 @@ window.__ModuleLoader__.load({
454
363
  if (original === void 0) return role;
455
364
  return {
456
365
  id: role.id,
457
- label: role.label,
458
- description: role.description,
366
+ persona: role.persona,
459
367
  ...original.prompt === void 0 ? {} : { prompt: original.prompt },
460
368
  ...original.permissions === void 0 ? {} : { permissions: original.permissions },
461
369
  chain: role.chain,
@@ -850,6 +758,7 @@ window.__ModuleLoader__.load({
850
758
  "alwaysModeRetryCap.label": "always 模式重试上限",
851
759
  "alwaysModeRetryCap.hint": "达到上限次数后切换;0 表示禁用",
852
760
  "alwaysModeRetryCap.tooltip": "retryPolicy 为 always 的模型在同一请求内重试达到该次数后切换;0 表示禁用。",
761
+ "advanced.label": "高级选项",
853
762
  "rootChain.label": "root 主代理降级链",
854
763
  "rootChain.hint": "未配置 = root 不降级",
855
764
  "rootChain.tooltip": "root 主代理失败时按此有序选择器列表依次降级;未配置时行为与未安装插件一致。",
@@ -859,14 +768,15 @@ window.__ModuleLoader__.load({
859
768
  "chains.selector.modelPlaceholder": "选择 model",
860
769
  "chains.selector.wildcard": "通配该 provider(provider/*)",
861
770
  "chains.selector.noModels": "该 provider 暂无可用模型(目录查询失败),请使用通配或改选。",
771
+ "chains.selector.noModelsStrict": "该 provider 暂无可用模型(目录查询失败),请改选其他 provider。",
862
772
  "roles.list.label": "角色实体",
863
773
  "roles.list.hint": "先声明角色,规则才能引用",
864
774
  "roles.list.tooltip": "角色 id 须匹配 /^[a-z0-9-]{1,32}$/ 且唯一;\"inherit\" 为保留字,不能用作角色 id。",
865
775
  "roles.id": "id",
866
776
  "roles.id.hint": "小写字母/数字/连字符,1–32 字符",
867
777
  "roles.idPlaceholder": "例如 reviewer",
868
- "roles.label": "名称",
869
- "roles.description": "描述",
778
+ "roles.persona": "人格提示",
779
+ "roles.personaPlaceholder": "例如:你是资深代码审查员",
870
780
  "roles.fallback": "链拼接策略",
871
781
  "roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
872
782
  "roles.fallback.none": "仅角色链(不追加 rootChain)",
@@ -977,6 +887,7 @@ window.__ModuleLoader__.load({
977
887
  "alwaysModeRetryCap.label": "Always-mode retry cap",
978
888
  "alwaysModeRetryCap.hint": "Switches after the cap; 0 disables",
979
889
  "alwaysModeRetryCap.tooltip": "Models whose retryPolicy is always switch after this many retries within one request; 0 disables.",
890
+ "advanced.label": "Advanced options",
980
891
  "rootChain.label": "Root agent fallback chain",
981
892
  "rootChain.hint": "Unset = root does not fall back",
982
893
  "rootChain.tooltip": "When the root agent fails it falls back down this ordered selector list; unset behaves like an uninstalled plugin.",
@@ -986,14 +897,15 @@ window.__ModuleLoader__.load({
986
897
  "chains.selector.modelPlaceholder": "Select model",
987
898
  "chains.selector.wildcard": "Wildcard this provider (provider/*)",
988
899
  "chains.selector.noModels": "No models available for this provider (catalog lookup failed); use the wildcard or pick another provider.",
900
+ "chains.selector.noModelsStrict": "No models available for this provider (catalog lookup failed); pick another provider.",
989
901
  "roles.list.label": "Declared roles",
990
902
  "roles.list.hint": "Declare roles before rules can reference them",
991
903
  "roles.list.tooltip": "Role ids must match /^[a-z0-9-]{1,32}$/ and be unique; \"inherit\" is reserved and cannot be used as a role id.",
992
904
  "roles.id": "ID",
993
905
  "roles.id.hint": "lowercase letters, digits, hyphens; 1–32 chars",
994
906
  "roles.idPlaceholder": "e.g. reviewer",
995
- "roles.label": "Label",
996
- "roles.description": "Description",
907
+ "roles.persona": "Persona",
908
+ "roles.personaPlaceholder": "e.g. you are a senior code reviewer",
997
909
  "roles.fallback": "Chain append",
998
910
  "roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
999
911
  "roles.fallback.none": "Role chain only (no rootChain)",
@@ -1277,7 +1189,7 @@ window.__ModuleLoader__.load({
1277
1189
  * itself constrains normal edits), and illegal selector entries in
1278
1190
  * rootChain and role chains. Returns one localized message per violation;
1279
1191
  * a non-empty result blocks {@link save} — the draft is never written.
1280
- * `label`/`description` are free text and never validated.
1192
+ * `persona` is free text and never validated.
1281
1193
  */
1282
1194
  function validateDraft(draft, t) {
1283
1195
  const errors = [];
@@ -1377,7 +1289,10 @@ window.__ModuleLoader__.load({
1377
1289
  }
1378
1290
  /**
1379
1291
  * One chain entry selector row: provider select + model select (cascade) +
1380
- * wildcard checkbox (spec §2.5 D-3). The provider options are the catalog
1292
+ * wildcard checkbox (spec §2.5 D-3). `allowWildcard` gates the `provider/*`
1293
+ * checkbox — the root agent's chain editor passes false (the main agent's
1294
+ * chain stays provider/model lines; provider-any matching lives in the role
1295
+ * rules), role chain editors keep it. The provider options are the catalog
1381
1296
  * providers **configured on the Models page** (`configuredProviders`, the
1382
1297
  * Models-page `configured` join) — unconfigured directory providers never
1383
1298
  * become offerable. Out-of-catalog values read back from the server render as
@@ -1388,7 +1303,7 @@ window.__ModuleLoader__.load({
1388
1303
  * existing value is never hidden or dropped. New rows only offer configured
1389
1304
  * options.
1390
1305
  */
1391
- function ChainSelectorEditor({ selector, catalog, configuredProviders, disabled, t, onChange, onRemove }) {
1306
+ function ChainSelectorEditor({ selector, catalog, configuredProviders, disabled, t, onChange, onRemove, allowWildcard = true }) {
1392
1307
  const providerRaw = selectionToRaw(selector.provider);
1393
1308
  const providerOutside = selector.provider?.kind === "outside";
1394
1309
  const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !configuredProviders.some((entry) => entry.provider === providerRaw);
@@ -1470,11 +1385,11 @@ window.__ModuleLoader__.load({
1470
1385
  }),
1471
1386
  groupMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1472
1387
  className: FallbacksCard_module_css_default.hint,
1473
- children: t("chains.selector.noModels")
1388
+ children: t(allowWildcard ? "chains.selector.noModels" : "chains.selector.noModelsStrict")
1474
1389
  })
1475
1390
  ]
1476
1391
  }),
1477
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1392
+ allowWildcard && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1478
1393
  className: `${FallbacksCard_module_css_default.ruleCell} ${FallbacksCard_module_css_default.wildcardCell}`,
1479
1394
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1480
1395
  type: "checkbox",
@@ -1624,8 +1539,7 @@ window.__ModuleLoader__.load({
1624
1539
  const addRole = () => {
1625
1540
  setRoleRows((rows) => [...rows, {
1626
1541
  id: "",
1627
- label: "",
1628
- description: "",
1542
+ persona: "",
1629
1543
  selectors: [],
1630
1544
  fallback: "inherit-root"
1631
1545
  }]);
@@ -1841,211 +1755,6 @@ window.__ModuleLoader__.load({
1841
1755
  className: FallbacksCard_module_css_default.fieldset,
1842
1756
  disabled: !writable,
1843
1757
  children: [
1844
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1845
- className: FallbacksCard_module_css_default.field,
1846
- role: "group",
1847
- "aria-labelledby": "fallbacks-trigger-codes",
1848
- children: [
1849
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1850
- className: FallbacksCard_module_css_default.fieldLabel,
1851
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1852
- id: "fallbacks-trigger-codes",
1853
- children: t("triggerCodes.label")
1854
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
1855
- label: t("triggerCodes.tooltip"),
1856
- disabled: !writable
1857
- })]
1858
- }),
1859
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1860
- className: FallbacksCard_module_css_default.hint,
1861
- children: t("triggerCodes.hint")
1862
- }),
1863
- KNOWN_TRIGGER_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1864
- className: FallbacksCard_module_css_default.optionRow,
1865
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1866
- type: "checkbox",
1867
- checked: scalars.triggerCodes.includes(code),
1868
- onChange: (event) => {
1869
- updateScalars((draft) => {
1870
- draft.triggerCodes = withTriggerCode(draft.triggerCodes, code, event.target.checked);
1871
- });
1872
- }
1873
- }), t(TRIGGER_CODE_LABELS[code])]
1874
- }, code)),
1875
- unknownCodes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1876
- className: FallbacksCard_module_css_default.hint,
1877
- children: t("triggerCodes.extra", { codes: unknownCodes.join(", ") })
1878
- })
1879
- ]
1880
- }),
1881
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1882
- className: FallbacksCard_module_css_default.field,
1883
- role: "group",
1884
- "aria-labelledby": "fallbacks-revert-policy",
1885
- children: [
1886
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1887
- className: FallbacksCard_module_css_default.fieldLabel,
1888
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1889
- id: "fallbacks-revert-policy",
1890
- children: t("revertPolicy.label")
1891
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
1892
- label: t("revertPolicy.tooltip"),
1893
- disabled: !writable
1894
- })]
1895
- }),
1896
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1897
- className: FallbacksCard_module_css_default.hint,
1898
- children: t("revertPolicy.hint")
1899
- }),
1900
- ["cooldown-expiry", "never"].map((policy) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1901
- className: FallbacksCard_module_css_default.optionRow,
1902
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1903
- type: "radio",
1904
- name: "fallbacks-revert-policy",
1905
- checked: scalars.revertPolicy === policy,
1906
- onChange: () => {
1907
- updateScalars((draft) => {
1908
- draft.revertPolicy = policy;
1909
- });
1910
- }
1911
- }), t(`revertPolicy.${policy}`)]
1912
- }, policy))
1913
- ]
1914
- }),
1915
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1916
- className: FallbacksCard_module_css_default.numberFields,
1917
- children: [
1918
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1919
- className: FallbacksCard_module_css_default.field,
1920
- children: [
1921
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1922
- className: FallbacksCard_module_css_default.fieldLabel,
1923
- children: [
1924
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1925
- htmlFor: "fallbacks-cooldown-ms",
1926
- children: t("cooldownMs.label")
1927
- }),
1928
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
1929
- label: t("cooldownMs.tooltip"),
1930
- disabled: !writable
1931
- }),
1932
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1933
- className: FallbacksCard_module_css_default.defaultNote,
1934
- children: [
1935
- t("defaults.prefix"),
1936
- ": ",
1937
- state.config.cooldownMs
1938
- ]
1939
- })
1940
- ]
1941
- }),
1942
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1943
- id: "fallbacks-cooldown-ms",
1944
- className: FallbacksCard_module_css_default.input,
1945
- type: "number",
1946
- min: 0,
1947
- value: String(scalars.cooldownMs),
1948
- disabled: !writable,
1949
- onChange: (event) => {
1950
- updateScalars((draft) => {
1951
- draft.cooldownMs = parseCount(event.target.value);
1952
- });
1953
- }
1954
- }),
1955
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1956
- className: FallbacksCard_module_css_default.hint,
1957
- children: t("cooldownMs.hint")
1958
- })
1959
- ]
1960
- }),
1961
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1962
- className: FallbacksCard_module_css_default.field,
1963
- children: [
1964
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1965
- className: FallbacksCard_module_css_default.fieldLabel,
1966
- children: [
1967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1968
- htmlFor: "fallbacks-max-switches",
1969
- children: t("maxSwitchesPerStep.label")
1970
- }),
1971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
1972
- label: t("maxSwitchesPerStep.tooltip"),
1973
- disabled: !writable
1974
- }),
1975
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1976
- className: FallbacksCard_module_css_default.defaultNote,
1977
- children: [
1978
- t("defaults.prefix"),
1979
- ": ",
1980
- state.config.maxSwitchesPerStep
1981
- ]
1982
- })
1983
- ]
1984
- }),
1985
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1986
- id: "fallbacks-max-switches",
1987
- className: FallbacksCard_module_css_default.input,
1988
- type: "number",
1989
- min: 0,
1990
- value: String(scalars.maxSwitchesPerStep),
1991
- disabled: !writable,
1992
- onChange: (event) => {
1993
- updateScalars((draft) => {
1994
- draft.maxSwitchesPerStep = parseCount(event.target.value);
1995
- });
1996
- }
1997
- }),
1998
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1999
- className: FallbacksCard_module_css_default.hint,
2000
- children: t("maxSwitchesPerStep.hint")
2001
- })
2002
- ]
2003
- }),
2004
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2005
- className: FallbacksCard_module_css_default.field,
2006
- children: [
2007
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2008
- className: FallbacksCard_module_css_default.fieldLabel,
2009
- children: [
2010
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2011
- htmlFor: "fallbacks-always-cap",
2012
- children: t("alwaysModeRetryCap.label")
2013
- }),
2014
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2015
- label: t("alwaysModeRetryCap.tooltip"),
2016
- disabled: !writable
2017
- }),
2018
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2019
- className: FallbacksCard_module_css_default.defaultNote,
2020
- children: [
2021
- t("defaults.prefix"),
2022
- ": ",
2023
- state.config.alwaysModeRetryCap
2024
- ]
2025
- })
2026
- ]
2027
- }),
2028
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2029
- id: "fallbacks-always-cap",
2030
- className: FallbacksCard_module_css_default.input,
2031
- type: "number",
2032
- min: 0,
2033
- value: String(scalars.alwaysModeRetryCap),
2034
- disabled: !writable,
2035
- onChange: (event) => {
2036
- updateScalars((draft) => {
2037
- draft.alwaysModeRetryCap = parseCount(event.target.value);
2038
- });
2039
- }
2040
- }),
2041
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2042
- className: FallbacksCard_module_css_default.hint,
2043
- children: t("alwaysModeRetryCap.hint")
2044
- })
2045
- ]
2046
- })
2047
- ]
2048
- }),
2049
1758
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2050
1759
  className: FallbacksCard_module_css_default.field,
2051
1760
  role: "group",
@@ -2089,6 +1798,7 @@ window.__ModuleLoader__.load({
2089
1798
  configuredProviders: state.configuredProviders,
2090
1799
  disabled: !writable,
2091
1800
  t,
1801
+ allowWildcard: false,
2092
1802
  onChange: (patch) => {
2093
1803
  updateRootChainSelector(selectorIndex, patch);
2094
1804
  },
@@ -2134,64 +1844,51 @@ window.__ModuleLoader__.load({
2134
1844
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2135
1845
  className: FallbacksCard_module_css_default.editorCard,
2136
1846
  children: [
2137
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2138
1848
  className: FallbacksCard_module_css_default.ruleGrid,
2139
- children: [
2140
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2141
- className: FallbacksCard_module_css_default.ruleCell,
2142
- children: [
2143
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2144
- className: FallbacksCard_module_css_default.ruleCellLabel,
2145
- children: t("roles.id")
2146
- }),
2147
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2148
- className: `${FallbacksCard_module_css_default.input} ${invalid ? FallbacksCard_module_css_default.inputInvalid : ""}`,
2149
- value: row.id,
2150
- placeholder: t("roles.idPlaceholder"),
2151
- "aria-label": t("roles.id"),
2152
- "aria-invalid": invalid ? true : void 0,
2153
- disabled: !writable,
2154
- onChange: (event) => {
2155
- updateRoleRow(index, { id: event.target.value });
2156
- }
2157
- }),
2158
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2159
- className: FallbacksCard_module_css_default.hint,
2160
- children: t("roles.id.hint")
2161
- })
2162
- ]
2163
- }),
2164
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2165
- className: FallbacksCard_module_css_default.ruleCell,
2166
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2167
- className: FallbacksCard_module_css_default.ruleCellLabel,
2168
- children: t("roles.label")
2169
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2170
- className: FallbacksCard_module_css_default.input,
2171
- value: row.label,
2172
- "aria-label": t("roles.label"),
2173
- disabled: !writable,
2174
- onChange: (event) => {
2175
- updateRoleRow(index, { label: event.target.value });
2176
- }
2177
- })]
2178
- }),
2179
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2180
- className: FallbacksCard_module_css_default.ruleCell,
2181
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1849
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1850
+ className: FallbacksCard_module_css_default.ruleCell,
1851
+ children: [
1852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2182
1853
  className: FallbacksCard_module_css_default.ruleCellLabel,
2183
- children: t("roles.description")
2184
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2185
- className: FallbacksCard_module_css_default.input,
2186
- value: row.description,
2187
- "aria-label": t("roles.description"),
1854
+ children: t("roles.id")
1855
+ }),
1856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1857
+ className: `${FallbacksCard_module_css_default.input} ${invalid ? FallbacksCard_module_css_default.inputInvalid : ""}`,
1858
+ value: row.id,
1859
+ placeholder: t("roles.idPlaceholder"),
1860
+ "aria-label": t("roles.id"),
1861
+ "aria-invalid": invalid ? true : void 0,
2188
1862
  disabled: !writable,
2189
1863
  onChange: (event) => {
2190
- updateRoleRow(index, { description: event.target.value });
1864
+ updateRoleRow(index, { id: event.target.value });
2191
1865
  }
2192
- })]
2193
- })
2194
- ]
1866
+ }),
1867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1868
+ className: FallbacksCard_module_css_default.hint,
1869
+ children: t("roles.id.hint")
1870
+ })
1871
+ ]
1872
+ })
1873
+ }),
1874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1875
+ className: FallbacksCard_module_css_default.ruleGrid,
1876
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1877
+ className: FallbacksCard_module_css_default.ruleCell,
1878
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1879
+ className: FallbacksCard_module_css_default.ruleCellLabel,
1880
+ children: t("roles.persona")
1881
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1882
+ className: FallbacksCard_module_css_default.input,
1883
+ value: row.persona,
1884
+ placeholder: t("roles.personaPlaceholder"),
1885
+ "aria-label": t("roles.persona"),
1886
+ disabled: !writable,
1887
+ onChange: (event) => {
1888
+ updateRoleRow(index, { persona: event.target.value });
1889
+ }
1890
+ })]
1891
+ })
2195
1892
  }),
2196
1893
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2197
1894
  className: FallbacksCard_module_css_default.chainSelectors,
@@ -2484,6 +2181,225 @@ window.__ModuleLoader__.load({
2484
2181
  children: t("roles.addRule")
2485
2182
  })
2486
2183
  ]
2184
+ }),
2185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2186
+ className: FallbacksCard_module_css_default.field,
2187
+ role: "group",
2188
+ "aria-labelledby": "fallbacks-advanced",
2189
+ children: [
2190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2191
+ className: FallbacksCard_module_css_default.fieldLabel,
2192
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2193
+ id: "fallbacks-advanced",
2194
+ children: t("advanced.label")
2195
+ })
2196
+ }),
2197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2198
+ className: FallbacksCard_module_css_default.field,
2199
+ role: "group",
2200
+ "aria-labelledby": "fallbacks-trigger-codes",
2201
+ children: [
2202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2203
+ className: FallbacksCard_module_css_default.fieldLabel,
2204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2205
+ id: "fallbacks-trigger-codes",
2206
+ children: t("triggerCodes.label")
2207
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2208
+ label: t("triggerCodes.tooltip"),
2209
+ disabled: !writable
2210
+ })]
2211
+ }),
2212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2213
+ className: FallbacksCard_module_css_default.hint,
2214
+ children: t("triggerCodes.hint")
2215
+ }),
2216
+ KNOWN_TRIGGER_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2217
+ className: FallbacksCard_module_css_default.optionRow,
2218
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2219
+ type: "checkbox",
2220
+ checked: scalars.triggerCodes.includes(code),
2221
+ onChange: (event) => {
2222
+ updateScalars((draft) => {
2223
+ draft.triggerCodes = withTriggerCode(draft.triggerCodes, code, event.target.checked);
2224
+ });
2225
+ }
2226
+ }), t(TRIGGER_CODE_LABELS[code])]
2227
+ }, code)),
2228
+ unknownCodes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2229
+ className: FallbacksCard_module_css_default.hint,
2230
+ children: t("triggerCodes.extra", { codes: unknownCodes.join(", ") })
2231
+ })
2232
+ ]
2233
+ }),
2234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2235
+ className: FallbacksCard_module_css_default.field,
2236
+ role: "group",
2237
+ "aria-labelledby": "fallbacks-revert-policy",
2238
+ children: [
2239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2240
+ className: FallbacksCard_module_css_default.fieldLabel,
2241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2242
+ id: "fallbacks-revert-policy",
2243
+ children: t("revertPolicy.label")
2244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2245
+ label: t("revertPolicy.tooltip"),
2246
+ disabled: !writable
2247
+ })]
2248
+ }),
2249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2250
+ className: FallbacksCard_module_css_default.hint,
2251
+ children: t("revertPolicy.hint")
2252
+ }),
2253
+ ["cooldown-expiry", "never"].map((policy) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2254
+ className: FallbacksCard_module_css_default.optionRow,
2255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2256
+ type: "radio",
2257
+ name: "fallbacks-revert-policy",
2258
+ checked: scalars.revertPolicy === policy,
2259
+ onChange: () => {
2260
+ updateScalars((draft) => {
2261
+ draft.revertPolicy = policy;
2262
+ });
2263
+ }
2264
+ }), t(`revertPolicy.${policy}`)]
2265
+ }, policy))
2266
+ ]
2267
+ }),
2268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2269
+ className: FallbacksCard_module_css_default.numberFields,
2270
+ children: [
2271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2272
+ className: FallbacksCard_module_css_default.field,
2273
+ children: [
2274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2275
+ className: FallbacksCard_module_css_default.fieldLabel,
2276
+ children: [
2277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2278
+ htmlFor: "fallbacks-cooldown-ms",
2279
+ children: t("cooldownMs.label")
2280
+ }),
2281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2282
+ label: t("cooldownMs.tooltip"),
2283
+ disabled: !writable
2284
+ }),
2285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2286
+ className: FallbacksCard_module_css_default.defaultNote,
2287
+ children: [
2288
+ t("defaults.prefix"),
2289
+ ": ",
2290
+ state.config.cooldownMs
2291
+ ]
2292
+ })
2293
+ ]
2294
+ }),
2295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2296
+ id: "fallbacks-cooldown-ms",
2297
+ className: FallbacksCard_module_css_default.input,
2298
+ type: "number",
2299
+ min: 0,
2300
+ value: String(scalars.cooldownMs),
2301
+ disabled: !writable,
2302
+ onChange: (event) => {
2303
+ updateScalars((draft) => {
2304
+ draft.cooldownMs = parseCount(event.target.value);
2305
+ });
2306
+ }
2307
+ }),
2308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2309
+ className: FallbacksCard_module_css_default.hint,
2310
+ children: t("cooldownMs.hint")
2311
+ })
2312
+ ]
2313
+ }),
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2315
+ className: FallbacksCard_module_css_default.field,
2316
+ children: [
2317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2318
+ className: FallbacksCard_module_css_default.fieldLabel,
2319
+ children: [
2320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2321
+ htmlFor: "fallbacks-max-switches",
2322
+ children: t("maxSwitchesPerStep.label")
2323
+ }),
2324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2325
+ label: t("maxSwitchesPerStep.tooltip"),
2326
+ disabled: !writable
2327
+ }),
2328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2329
+ className: FallbacksCard_module_css_default.defaultNote,
2330
+ children: [
2331
+ t("defaults.prefix"),
2332
+ ": ",
2333
+ state.config.maxSwitchesPerStep
2334
+ ]
2335
+ })
2336
+ ]
2337
+ }),
2338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2339
+ id: "fallbacks-max-switches",
2340
+ className: FallbacksCard_module_css_default.input,
2341
+ type: "number",
2342
+ min: 0,
2343
+ value: String(scalars.maxSwitchesPerStep),
2344
+ disabled: !writable,
2345
+ onChange: (event) => {
2346
+ updateScalars((draft) => {
2347
+ draft.maxSwitchesPerStep = parseCount(event.target.value);
2348
+ });
2349
+ }
2350
+ }),
2351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2352
+ className: FallbacksCard_module_css_default.hint,
2353
+ children: t("maxSwitchesPerStep.hint")
2354
+ })
2355
+ ]
2356
+ }),
2357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2358
+ className: FallbacksCard_module_css_default.field,
2359
+ children: [
2360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2361
+ className: FallbacksCard_module_css_default.fieldLabel,
2362
+ children: [
2363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2364
+ htmlFor: "fallbacks-always-cap",
2365
+ children: t("alwaysModeRetryCap.label")
2366
+ }),
2367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2368
+ label: t("alwaysModeRetryCap.tooltip"),
2369
+ disabled: !writable
2370
+ }),
2371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2372
+ className: FallbacksCard_module_css_default.defaultNote,
2373
+ children: [
2374
+ t("defaults.prefix"),
2375
+ ": ",
2376
+ state.config.alwaysModeRetryCap
2377
+ ]
2378
+ })
2379
+ ]
2380
+ }),
2381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2382
+ id: "fallbacks-always-cap",
2383
+ className: FallbacksCard_module_css_default.input,
2384
+ type: "number",
2385
+ min: 0,
2386
+ value: String(scalars.alwaysModeRetryCap),
2387
+ disabled: !writable,
2388
+ onChange: (event) => {
2389
+ updateScalars((draft) => {
2390
+ draft.alwaysModeRetryCap = parseCount(event.target.value);
2391
+ });
2392
+ }
2393
+ }),
2394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2395
+ className: FallbacksCard_module_css_default.hint,
2396
+ children: t("alwaysModeRetryCap.hint")
2397
+ })
2398
+ ]
2399
+ })
2400
+ ]
2401
+ })
2402
+ ]
2487
2403
  })
2488
2404
  ]
2489
2405
  })
@@ -34,6 +34,7 @@ export declare const zh: {
34
34
  'alwaysModeRetryCap.label': string;
35
35
  'alwaysModeRetryCap.hint': string;
36
36
  'alwaysModeRetryCap.tooltip': string;
37
+ 'advanced.label': string;
37
38
  'rootChain.label': string;
38
39
  'rootChain.hint': string;
39
40
  'rootChain.tooltip': string;
@@ -43,14 +44,15 @@ export declare const zh: {
43
44
  'chains.selector.modelPlaceholder': string;
44
45
  'chains.selector.wildcard': string;
45
46
  'chains.selector.noModels': string;
47
+ 'chains.selector.noModelsStrict': string;
46
48
  'roles.list.label': string;
47
49
  'roles.list.hint': string;
48
50
  'roles.list.tooltip': string;
49
51
  'roles.id': string;
50
52
  'roles.id.hint': string;
51
53
  'roles.idPlaceholder': string;
52
- 'roles.label': string;
53
- 'roles.description': string;
54
+ 'roles.persona': string;
55
+ 'roles.personaPlaceholder': string;
54
56
  'roles.fallback': string;
55
57
  'roles.fallback.inherit-root': string;
56
58
  'roles.fallback.none': string;
@@ -163,6 +165,7 @@ export declare const en: {
163
165
  'alwaysModeRetryCap.label': string;
164
166
  'alwaysModeRetryCap.hint': string;
165
167
  'alwaysModeRetryCap.tooltip': string;
168
+ 'advanced.label': string;
166
169
  'rootChain.label': string;
167
170
  'rootChain.hint': string;
168
171
  'rootChain.tooltip': string;
@@ -172,14 +175,15 @@ export declare const en: {
172
175
  'chains.selector.modelPlaceholder': string;
173
176
  'chains.selector.wildcard': string;
174
177
  'chains.selector.noModels': string;
178
+ 'chains.selector.noModelsStrict': string;
175
179
  'roles.list.label': string;
176
180
  'roles.list.hint': string;
177
181
  'roles.list.tooltip': string;
178
182
  'roles.id': string;
179
183
  'roles.id.hint': string;
180
184
  'roles.idPlaceholder': string;
181
- 'roles.label': string;
182
- 'roles.description': string;
185
+ 'roles.persona': string;
186
+ 'roles.personaPlaceholder': string;
183
187
  'roles.fallback': string;
184
188
  'roles.fallback.inherit-root': string;
185
189
  'roles.fallback.none': string;
package/dist/config.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
- * The `fallbacks` settings namespace: plugin config schema + defaults.
2
+ * The `fallbacks` settings namespace: config types + defaults (the
3
+ * schemastery `Config` schema lives in `./schema.ts`, host-only — this
4
+ * module must stay free of `@deepseek-ai/*` value imports so the client
5
+ * bundle never reaches schemastery).
3
6
  *
4
7
  * Two-block config model (plan fallbacks-role-config-model): block 1
5
8
  * `rootChain` — the root agent's single fallback chain (empty = no
6
9
  * degradation) — plus block 2 declared role entities: `roles.list`
7
- * (id/label/description/prompt?/permissions?/chain?/fallback) and
10
+ * (id/persona/prompt?/permissions?/chain?/fallback) and
8
11
  * `roles.rules` enum references into the declared ids (or the built-in
9
12
  * `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
10
13
  * from the schema and type (zero residual, migration table excepted); the
@@ -19,12 +22,11 @@
19
22
  *
20
23
  * This module is pure logic: it must not import any `@deepseek-ai/*` package
21
24
  * (types included) — `FallbacksConfig` is the plugin's own type. Task 3
22
- * registers this schema with `installSettingsSection` under the `fallbacks`
23
- * settings namespace.
25
+ * registers the schemastery schema (see `./schema.ts`) with
26
+ * `installSettingsSection` under the `fallbacks` settings namespace.
24
27
  *
25
28
  * @module dsh-llm-fallbacks/config
26
29
  */
27
- import z from '@deepseek-ai/schemastery';
28
30
  /** How a cooled-down model comes back (spec §4). */
29
31
  export type RevertPolicy = 'cooldown-expiry' | 'never';
30
32
  /** A single role rule: match on origin/provider/model patterns (spec §3). */
@@ -49,8 +51,8 @@ export type FallbackStrategy = 'inherit-root' | 'none';
49
51
  */
50
52
  export interface FallbacksRole {
51
53
  id: string;
52
- label: string;
53
- description: string;
54
+ /** Personality hint (人格提示) — free text, never validated. */
55
+ persona: string;
54
56
  /** Reserved for next iteration — no consumer this round. */
55
57
  prompt?: string;
56
58
  /** Reserved for next iteration — no consumer this round. */
@@ -110,7 +112,7 @@ export interface FallbacksConfigLogger {
110
112
  * `rootChain`/role-chain selector legality, and the role model-config
111
113
  * requirement (a declared role with a missing/empty chain warns — a role
112
114
  * without a model config is meaningless, plan fallbacks-feedback-round
113
- * T2). `label`/`description` are free text and are deliberately not
115
+ * T2). `persona` is free text and is deliberately not
114
116
  * validated. Each violation emits one `llm-fallbacks: ...` warn and "does
115
117
  * not take effect" — the config stays usable (spec §4 / AC-4
116
118
  * warn-not-crash semantics).
@@ -119,21 +121,13 @@ export declare function validateFallbacksConfig(config: FallbacksConfig, logger:
119
121
  /**
120
122
  * Detect legacy (two-block-era) leftovers in a config SOURCE — the composed
121
123
  * object `source()` returns, or a raw settings document. Recognizes the
122
- * removed `chains` key, the removed `roles.default` field, and
123
- * `roles.rules[].role` values that reference no declared `roles.list` id
124
- * and are not the built-in `'inherit'`. Returns descriptive key/role names;
125
- * the gateway attaches them as `get().legacyKeys` so the client can show a
126
- * migration banner (spec §9 — the source is read directly because
127
- * schemastery retains unknown keys, verified plan Task 1 Step 1).
124
+ * removed `chains` key, the removed `roles.default` field, the removed
125
+ * role-entity fields `roles.list[].label` / `roles.list[].description`
126
+ * (renamed to `persona`), and `roles.rules[].role` values that reference no
127
+ * declared `roles.list` id and are not the built-in `'inherit'`. Returns
128
+ * descriptive key/role names; the gateway attaches them as `get().legacyKeys`
129
+ * so the client can show a migration banner (spec §9 — the source is read
130
+ * directly because schemastery retains unknown keys, verified plan Task 1
131
+ * Step 1).
128
132
  */
129
133
  export declare function detectLegacyKeys(source: Record<string, unknown>): string[];
130
- /**
131
- * Plugin Config schema (schemastery), mirroring {@link FallbacksConfig}.
132
- * Object fields are optional by default in schemastery; `.default()` fills
133
- * the spec defaults, `.required()` keeps mandatory fields. Unknown keys are
134
- * RETAINED by the composition (verified plan Task 1 Step 1) — that is what
135
- * lets `detectLegacyKeys` flag two-block-era leftovers (`chains` /
136
- * `roles.default`) on the composed object at startup (warn + gateway
137
- * `legacyKeys`, see `src/index.ts` apply()).
138
- */
139
- export declare const Config: z<FallbacksConfig>;
package/dist/index.d.ts CHANGED
@@ -29,7 +29,8 @@
29
29
  */
30
30
  import type { Context } from '@deepseek-ai/cordis';
31
31
  import type { Session } from '@deepseek-ai/dsh-session';
32
- import { Config, detectLegacyKeys, validateFallbacksConfig, type FallbacksConfig } from './config.ts';
32
+ import { detectLegacyKeys, validateFallbacksConfig, type FallbacksConfig } from './config.ts';
33
+ import { Config } from './schema.ts';
33
34
  import { resolveChain } from './chains.ts';
34
35
  import { resolveRole } from './roles.ts';
35
36
  import { FallbackStateStore } from './state.ts';
package/dist/index.js CHANGED
@@ -86,8 +86,11 @@ function detectLegacyKeys(source) {
86
86
  if (isRecordLike(roles)) {
87
87
  if (Object.hasOwn(roles, "default")) keys.push("roles.default");
88
88
  const declared = new Set();
89
- if (Array.isArray(roles.list)) {
90
- for (const item of roles.list) if (isRecordLike(item) && typeof item.id === "string") declared.add(item.id);
89
+ if (Array.isArray(roles.list)) for (const item of roles.list) {
90
+ if (!isRecordLike(item)) continue;
91
+ if (typeof item.id === "string") declared.add(item.id);
92
+ if (Object.hasOwn(item, "label")) keys.push("roles.list[].label");
93
+ if (Object.hasOwn(item, "description")) keys.push("roles.list[].description");
91
94
  }
92
95
  if (Array.isArray(roles.rules)) {
93
96
  for (const rule of roles.rules) if (isRecordLike(rule) && typeof rule.role === "string" && rule.role !== "inherit" && !declared.has(rule.role)) keys.push(`roles.rules[].role: ${rule.role}`);
@@ -98,6 +101,8 @@ function detectLegacyKeys(source) {
98
101
  function isRecordLike(value) {
99
102
  return typeof value === "object" && value !== null && !Array.isArray(value);
100
103
  }
104
+ //#endregion
105
+ //#region .build/host/schema.js
101
106
  const Config = z.object({
102
107
  enabled: z.boolean().default(false),
103
108
  triggerCodes: z.array(z.string()).default([
@@ -109,8 +114,7 @@ const Config = z.object({
109
114
  roles: z.object({
110
115
  list: z.array(z.object({
111
116
  id: z.string().required(),
112
- label: z.string().default(""),
113
- description: z.string().default(""),
117
+ persona: z.string().default(""),
114
118
  prompt: z.string(),
115
119
  permissions: z.object({
116
120
  allow: z.array(z.string()),
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The `fallbacks` settings schema (schemastery), mirroring
3
+ * {@link FallbacksConfig}.
4
+ *
5
+ * Host-only module: `Config` is the schemastery schema the settings section
6
+ * validates/composes against, and `@deepseek-ai/schemastery` is an
7
+ * `@deepseek-ai/*` RUNTIME value import — it must never enter the client
8
+ * bundle, because the web loader module table cannot answer that require
9
+ * (build-time externals drift, 20260815: the client bundle previously
10
+ * externalized `@deepseek-ai/schemastery` and the web settings card failed
11
+ * to load). The client half consumes `FallbacksConfig` and the other
12
+ * config types from `./config.ts` type-only, so the schema stays here, out
13
+ * of the client module graph.
14
+ *
15
+ * Object fields are optional by default in schemastery; `.default()` fills
16
+ * the spec defaults, `.required()` keeps mandatory fields. Unknown keys are
17
+ * RETAINED by the composition (verified plan Task 1 Step 1) — that is what
18
+ * lets `detectLegacyKeys` flag two-block-era leftovers (`chains` /
19
+ * `roles.default`) on the composed object at startup (warn + gateway
20
+ * `legacyKeys`, see `src/index.ts` apply()).
21
+ *
22
+ * @module dsh-llm-fallbacks/schema
23
+ */
24
+ import z from '@deepseek-ai/schemastery';
25
+ import type { FallbacksConfig } from './config.ts';
26
+ export declare const Config: z<FallbacksConfig>;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.1.2",
8
+ "version": "0.1.3",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -66,11 +66,6 @@ export const CLIENT_EXTERNALS: readonly string[] = [
66
66
  '@deepseek-ai/dsh-client-ui-attachment',
67
67
  '@deepseek-ai/dsh-client-schema-form',
68
68
  '@deepseek-ai/dsh-client-runtime/client',
69
- // @deepseek-ai/schemastery: config.ts's schema value import reaches the
70
- // client module graph (client consumes FallbacksConfig types via config.ts);
71
- // the dsh host provides the package in-box (dsh-settings depends on it), so
72
- // it stays external like the other @deepseek-ai/* runtime faces.
73
- '@deepseek-ai/schemastery',
74
69
  ]
75
70
 
76
71
  /**