@lynn123411/dsh-llm-agentrouter 0.3.1 → 0.4.1

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
@@ -4,12 +4,12 @@ AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一
4
4
 
5
5
  ## 特性
6
6
 
7
- - **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`reasoningEffort` 档位与兼容开关按实测探针手写声明,模型列表不翻倍。
8
- - **模型列表可编辑**:设置卡片里直接列出这条路由的模型,`更新` 向中转站询问它现有的模型 ID(只补缺失的、不动已有条目),每个模型的 ID / 显示名 / 上下文窗口 / 最大输出 / 输入模态 / 各推理档位与线上取值都可改,保存后下一个请求即生效;新补入的模型默认 1048576 上下文、131072 输出、text+image、off..max 七档。改的就是路由自己的 `models`(与内核「设置 - 模型」页同一份数据),patch 里手写的那几条是「重置」的基线。
7
+ - **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`reasoningEfforts` 各档位与 `compat` 开关按实测探针手写声明,模型列表不翻倍。
8
+ - **模型列表可编辑**:设置卡片里直接列出这条路由的模型,`更新` 向中转站询问它现有的模型 ID(只补缺失的、不动已有条目),每个模型的 ID / 显示名 / 上下文窗口 / 最大输出 / 输入模态 / 各推理档位与线上取值都可改,保存后下一个请求即生效;新补入的模型默认 1048576 上下文、131072 输出、text+image、off..max 七档。改的就是路由自己的 `models`——`llm-pi-ai` 这条 profile 插件配置里的 `providers.agentrouter.models`,内核「设置 - 模型」页对该路由注册的写入地址也正是这里,两边同一份数据;patch 里手写的那几条是「重置」的基线。
9
9
  - **端点一键切换**:设置卡片两个大选项(国内端点 / 国际端点,附带真实 host 展示),点击即写即生效,无需重启;API Key 只存 `$DSH_HOME/.credentials.yaml` 的 `AGENTROUTER_API_KEY` 引用。
10
10
  - **共享「API中转」设置页**:本插件的端点卡片与 `dsh-a6api` 的面板挂在同一个设置页里(`settings.section` id `relay` 的页内两个 tab,参与者可增删)。内核不允许一页被多个插件共同声明,因此每个参与者各持一份运行时逻辑一致的页壳,**先加载者当选页面宿主**、其余只注册卡片;当选者被卸载后下次启动自动改选。做法与约束见仓库 `docs/rules/shared-settings-page.md`。
11
11
  - **请求围栏**:重写 relay 要求的 `User-Agent`(适配器强制署名下沉到 `fetch` 层替换),哨兵 host(`.internal` 不可解析)兜底防裸奔,402 配额耗尽的 JSON 错误体改写为可读提示。
12
- - **内核设置接入**:宿主用 `ctx.settings.installSection` 注册设置段(`ctx.inject(['settings'])`),客户端卡片挂在共享设置页的子 slot;设置命名空间为 `llm-agentrouter`。
12
+ - **内核设置接入**:本插件 `Config` 里需要实时生效的字段都声明为 `.volatile()`,设置面据此派生表单、并把配置写回同一批引用(`ctx.inject(['settings'])` + `ctx.settings.configure({ auto: false })` 记下「本插件自带 UI」);客户端卡片挂在共享设置页的子 slot,读写的是 `llm-agentrouter` 这条 profile 插件配置,条目 id 即设置命名空间。
13
13
 
14
14
  ## 安装
15
15
 
package/cordis.patch.yml CHANGED
@@ -56,15 +56,15 @@
56
56
  # name and an adapter-supplied description a hand-declared pi-ai route has no
57
57
  # field for. The group title is therefore the only reachable surface — and it is
58
58
  # a name, not a notice, so it stays a name. The endpoint is explained where it is
59
- # changed: 设置 → 插件 → AgentRouter 中转站.
59
+ # changed: 设置 → API中转 → AgentRouter 中转站.
60
60
  #
61
61
  # WHOLE-OBJECT OVERWRITE, NOT A DEEP MERGE — RE-CHECK ON EVERY dsh-base BUMP
62
62
  # The loader applies a patch entry as `target[key] = value`
63
- # (applyEntryPatches, 0.1.6-alpha.2), so the `config:` below REPLACES the
64
- # llm-pi-ai row's whole config object: nothing a default config carried survives,
65
- # at any depth. That is safe today only because dsh-base mounts
66
- # `- id: llm-pi-ai` with a `name:` and no `config:` at all
67
- # (dsh-base/cordis.patch.yml:114-115), so there is nothing to lose. If a future
63
+ # (applyEntryPatches, @deepseek-ai/dsh-app-boot), so the `config:` below
64
+ # REPLACES the llm-pi-ai row's whole config object: nothing a default config
65
+ # carried survives, at any depth. That is safe today only because dsh-base
66
+ # mounts `- id: llm-pi-ai` with a `name:` and no `config:` at all
67
+ # (dsh-base/cordis.patch.yml:127-128), so there is nothing to lose. If a future
68
68
  # dsh-base gives that row a default config, this patch swallows it SILENTLY —
69
69
  # diff the shipped llm-pi-ai row against this file on every dsh-base bump and
70
70
  # fold whatever it adds into the `providers:` map below.
package/lib/client.js CHANGED
@@ -185,9 +185,9 @@ window.__ModuleLoader__.load({
185
185
  const FIELD = "endpoint";
186
186
 
187
187
  /**
188
- * Read the endpoint from a resolved section, falling back to the default the
189
- * schema documents when the section is not readable yet.
190
- * @param {unknown} value - the scope snapshot's resolved value.
188
+ * Read the endpoint from a resolved entry, falling back to the default the
189
+ * schema documents when the entry is not readable yet.
190
+ * @param {unknown} value - the form snapshot's resolved value.
191
191
  * @returns {string} an endpoint key.
192
192
  */
193
193
  function endpointOf(value) {
@@ -198,7 +198,7 @@ window.__ModuleLoader__.load({
198
198
  /**
199
199
  * Read the host table from a resolved section so each choice can show the
200
200
  * origin it actually means.
201
- * @param {unknown} value - the scope snapshot's resolved value.
201
+ * @param {unknown} value - the form snapshot's resolved value.
202
202
  * @returns {Record<string, string>} host per endpoint key.
203
203
  */
204
204
  function hostsOf(value) {
@@ -207,23 +207,23 @@ window.__ModuleLoader__.load({
207
207
  }
208
208
 
209
209
  /**
210
- * The relay card: the endpoint radio group over this plugin's own settings
211
- * namespace, above the model list the route in `llm-pi-ai` actually serves.
210
+ * The relay card: the endpoint radio group over this plugin's own profile
211
+ * entry config, above the model list the route in `llm-pi-ai` actually serves.
212
212
  *
213
213
  * A choice writes immediately rather than staging behind a Save button. The
214
214
  * namespace has exactly one user-facing field and the write is reversible in
215
215
  * one click, so a staged form would add a step without protecting anything —
216
- * and `scope.set` already fences the write with the revision it read. The
217
- * two halves are independent: each reads and writes its own namespace, so a
218
- * deployment that exposes only one of them still renders the other.
216
+ * and the form's own `set` already fences the write with the revision it read.
217
+ * The two halves are independent: each reads and writes its own entry's form,
218
+ * so a deployment that exposes only one of them still renders the other.
219
219
  *
220
- * @param {object} props - the injected scope faces plus the bound translator.
220
+ * @param {object} props - the injected config forms plus the bound translator.
221
221
  * @returns {JSX.Element} the card.
222
222
  */
223
- function EndpointCard({ scope, t, routeScope, operations }) {
223
+ function EndpointCard({ form, t, routeForm, operations }) {
224
224
  const snapshot = react.useSyncExternalStore(
225
- react.useCallback((listener) => scope.subscribe(listener), [scope]),
226
- () => scope.getSnapshot(),
225
+ react.useCallback((listener) => form.subscribe(listener), [form]),
226
+ () => form.getSnapshot(),
227
227
  );
228
228
  const [pending, setPending] = react.useState(null);
229
229
  const [failed, setFailed] = react.useState(false);
@@ -238,10 +238,14 @@ window.__ModuleLoader__.load({
238
238
  setPending(endpoint);
239
239
  setFailed(false);
240
240
  Promise.resolve()
241
- .then(() => scope.set(FIELD, endpoint))
241
+ .then(() => form.set(FIELD, endpoint))
242
242
  .then(
243
- () => {
243
+ // The form answers whether the Host accepted the write and runs its own
244
+ // recovery read; a refusal and a transport rejection are one outcome here:
245
+ // the choice did not land, so the card says so and leaves the section alone.
246
+ (accepted) => {
244
247
  setPending(null);
248
+ if (!accepted) setFailed(true);
245
249
  },
246
250
  () => {
247
251
  setPending(null);
@@ -313,7 +317,7 @@ window.__ModuleLoader__.load({
313
317
  role: shown.kind === "error" ? "alert" : "status",
314
318
  children: shown.text,
315
319
  }),
316
- routeScope === undefined ? null : jsx.jsx(ModelList, { routeScope, operations, t }),
320
+ routeForm === undefined ? null : jsx.jsx(ModelList, { routeForm, operations, t }),
317
321
  ],
318
322
  });
319
323
  }
@@ -321,12 +325,14 @@ window.__ModuleLoader__.load({
321
325
 
322
326
  //#region model list
323
327
  /**
324
- * The pi-ai settings namespace the relay route is declared in. This plugin's
325
- * model list is NOT its own preference: it is the `agentrouter` route's
326
- * `models` array, the same data the kernel's Models page edits, which is why
327
- * editing it is what changes the model picker. Spelled rather than imported,
328
- * for the same reason as SETTINGS_NS in the plugin region below: a browser
329
- * bundle must not depend on a Host package.
328
+ * The profile entry id the relay route is declared in — `llm-pi-ai`'s own
329
+ * patch row, which the adapter also registers as the settings namespace of
330
+ * its configurable providers (`settingsPath: ["providers", route]`). This
331
+ * plugin's model list is NOT its own preference: it is the `agentrouter`
332
+ * route's `models` array, the same entry and the same path the kernel's
333
+ * Models page edits, which is why editing it is what changes the model
334
+ * picker. Spelled rather than imported, for the same reason as SETTINGS_NS in
335
+ * the plugin region below: a browser bundle must not depend on a Host package.
330
336
  */
331
337
  const PI_AI_SETTINGS_NS = "llm-pi-ai";
332
338
  /** The route key inside that namespace this plugin owns. */
@@ -681,8 +687,8 @@ window.__ModuleLoader__.load({
681
687
  }
682
688
 
683
689
  /**
684
- * Store faces for a caller that injects no Host operations — an older Host,
685
- * or a test rendering this component directly.
690
+ * Store faces for a caller that injects no Host operations — a test rendering
691
+ * this component directly, or a registration whose optional Remote is absent.
686
692
  */
687
693
  const NO_OPERATIONS_STORE = { subscribe: () => () => {}, snapshot: () => 0 };
688
694
 
@@ -691,24 +697,25 @@ window.__ModuleLoader__.load({
691
697
  * serves, the 更新 action that asks the relay what it currently offers, and
692
698
  * the write that puts the result back on the route.
693
699
  *
694
- * Reads come from the shared settings mirror through the scope bound to
695
- * `llm-pi-ai`, so the card and the kernel's Models page can never disagree
696
- * about the route. Writes go through the Remote call the operations were
697
- * built with, NOT through `scope.set`: the mirror's write path reports a
698
- * refusal by silently reloading, while this card has to show the Host's own
699
- * diagnostic — a stale revision, or a payload the adapter's schema refused.
700
+ * Reads come from the shared config form of the `llm-pi-ai` profile entry, so
701
+ * the card and the kernel's Models page can never disagree about the route.
702
+ * Writes go through the Remote call the operations were built with, NOT
703
+ * through the form's `mutate`: that path answers only whether the Host
704
+ * accepted, while this card has to show the Host's own diagnostic — a stale
705
+ * revision, or a payload the adapter's schema refused. The kernel's own
706
+ * Models page writes the same namespace the same way.
700
707
  *
701
708
  * An edit lives in this component until 保存; 重置 drops the draft and, when
702
709
  * the user layer owns the list, removes that override so the composition's
703
710
  * hand-declared list serves again.
704
711
  *
705
- * @param {object} props - the route scope, the Host operations, and `t`.
712
+ * @param {object} props - the route form, the Host operations, and `t`.
706
713
  * @returns {JSX.Element} the model list.
707
714
  */
708
- function ModelList({ routeScope, operations, t }) {
715
+ function ModelList({ routeForm, operations, t }) {
709
716
  const snapshot = react.useSyncExternalStore(
710
- react.useCallback((listener) => routeScope.subscribe(listener), [routeScope]),
711
- () => routeScope.getSnapshot(),
717
+ react.useCallback((listener) => routeForm.subscribe(listener), [routeForm]),
718
+ () => routeForm.getSnapshot(),
712
719
  );
713
720
  const ops = operations ?? {};
714
721
  // The optional Remote faces mount after this plugin activates, so the
@@ -1134,27 +1141,30 @@ window.__ModuleLoader__.load({
1134
1141
  /** Dictionary namespace owned by this plugin. */
1135
1142
  const NS = "settings.agentrouter";
1136
1143
  /**
1137
- * Settings namespace the Host half registers. Spelled rather than imported:
1138
- * a browser bundle must not depend on a Host package, so both halves state
1139
- * the same literal (the Host's is `AGENTROUTER_SETTINGS_NAMESPACE`).
1144
+ * This plugin's profile entry id — its settings namespace, and the key its
1145
+ * config form is addressed by. Spelled rather than imported: a browser bundle
1146
+ * must not depend on a Host package, so both halves state the same literal
1147
+ * (the Host's is `AGENTROUTER_SETTINGS_NAMESPACE`).
1140
1148
  */
1141
1149
  const SETTINGS_NS = "llm-agentrouter";
1142
1150
  /** Services this plugin needs from the browser runtime. */
1143
- const inject = ["slots", "locale", "settingsScope"];
1151
+ const inject = ["slots", "locale", "configForms"];
1144
1152
 
1145
1153
  /**
1146
1154
  * Register the endpoint card inside the shared 「API中转」 page, which this
1147
- * plugin carries the shell for and claims when it activates first.
1155
+ * plugin carries the shell for and claims when it activates first, and bind
1156
+ * the two config forms it reads (this plugin's own entry, and the adapter's
1157
+ * route) out of the shared configuration forms service.
1148
1158
  * @param {object} ctx - the browser plugin context.
1149
1159
  */
1150
1160
  function apply(ctx) {
1151
1161
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-agentrouter: dictionaries");
1152
1162
  const t = ctx.locale.bind(NS);
1153
- const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
1154
- // The model list is not this plugin's own section: it is the `agentrouter`
1155
- // route inside the adapter's namespace, the same document the kernel's
1156
- // Models page edits, so the card derives from that namespace's mirror.
1157
- const routeScope = ctx.settingsScope.bind({ namespace: PI_AI_SETTINGS_NS });
1163
+ const form = ctx.configForms.get(SETTINGS_NS);
1164
+ // The model list is not this plugin's own entry: it is the `agentrouter`
1165
+ // route inside the adapter's entry, the same document the kernel's Models
1166
+ // page edits, so the card derives from that entry's shared config form.
1167
+ const routeForm = ctx.configForms.get(PI_AI_SETTINGS_NS);
1158
1168
  // Both Host faces are OPTIONAL reads: a deployment whose Client assembly
1159
1169
  // mounts neither still shows the endpoint switch, and the model list
1160
1170
  // degrades to read-only with the reason rendered instead of going blank.
@@ -1220,14 +1230,14 @@ window.__ModuleLoader__.load({
1220
1230
  ctx.slots.register(
1221
1231
  {
1222
1232
  name: RELAY_ITEM_SLOT,
1223
- // id = the Host settings namespace; the shared page filters its
1233
+ // id = this plugin's profile entry id; the shared page filters its
1224
1234
  // panels by this id, so it must be the plugin's own key.
1225
1235
  id: SETTINGS_NS,
1226
1236
  order: 20,
1227
1237
  // Rendered as the card's tab title inside the shared page.
1228
1238
  label: () => t("title"),
1229
1239
  locale: NS,
1230
- inject: () => ({ scope, t, routeScope, operations }),
1240
+ inject: () => ({ form, t, routeForm, operations }),
1231
1241
  },
1232
1242
  EndpointCard,
1233
1243
  ),
package/lib/index.js CHANGED
@@ -38,10 +38,11 @@ import z from '@deepseek-ai/schemastery'
38
38
  const name = 'llm-agentrouter'
39
39
 
40
40
  /**
41
- * Settings namespace this plugin owns.
41
+ * This plugin's profile entry id, which is also its settings namespace and the
42
+ * license key its configuration form is addressed by.
42
43
  *
43
- * It is also the key the browser half registers its card under, so the two
44
- * halves meet here without either importing the other.
44
+ * The id is declared by the bundle patch (`cordis.patch.yml`), and the browser
45
+ * half addresses the same literal without importing this module.
45
46
  */
46
47
  const AGENTROUTER_SETTINGS_NAMESPACE = 'llm-agentrouter'
47
48
 
@@ -53,6 +54,7 @@ const Config = z.object({
53
54
  endpoint: z
54
55
  .union([z.const('cn'), z.const('intl')])
55
56
  .default('cn')
57
+ .volatile()
56
58
  .description('relay endpoint requests are sent to: cn (domestic) or intl (international)'),
57
59
  /**
58
60
  * Host per endpoint key. Configuration rather than a constant so a moved
@@ -61,6 +63,7 @@ const Config = z.object({
61
63
  endpoints: z
62
64
  .dict(z.string())
63
65
  .default({ cn: 'ps.air-outer.com', intl: 'agentrouter.org' })
66
+ .volatile()
64
67
  .description('host for each endpoint key'),
65
68
  /**
66
69
  * The host the route's `baseURL` names. Requests to it are rewritten to the
@@ -70,6 +73,7 @@ const Config = z.object({
70
73
  sentinel: z
71
74
  .string()
72
75
  .default('relay.agentrouter.internal')
76
+ .volatile()
73
77
  .description('placeholder host in the route baseURL that the fence replaces with the selected endpoint'),
74
78
  /**
75
79
  * The exact User-Agent the relay accepts. It is the whole authentication of
@@ -79,6 +83,7 @@ const Config = z.object({
79
83
  userAgent: z
80
84
  .string()
81
85
  .default('claude-cli/2.1.161 (external, cli)')
86
+ .volatile()
82
87
  .description('User-Agent value sent to the relay in place of the harness attribution'),
83
88
  /** Report the installed fence once on activation. */
84
89
  announce: z.boolean().default(true),
@@ -91,9 +96,19 @@ const Config = z.object({
91
96
  quotaHint: z
92
97
  .string()
93
98
  .default('Claude / GPT 本批额度已用完,请等待下一批投放。')
99
+ .volatile()
94
100
  .description('text appended to relay 402 quota errors'),
95
101
  })
96
102
 
103
+ /**
104
+ * Every field the fence reads carries `.volatile()` and is therefore a live
105
+ * reference rather than a snapshot: the settings plane writes the reference in
106
+ * place, the next request reads the new value, and no reload intervenes. That
107
+ * is also what makes these fields the ones the schema-derived config form
108
+ * shows and accepts writes to (`ctx.settings.describe`/`mutate`). `announce`
109
+ * is deliberately plain: it is read once, on activation.
110
+ */
111
+
97
112
  /**
98
113
  * Resolve a `fetch` argument to its URL without consuming a request body.
99
114
  * @param {unknown} input - the first `fetch` argument.
@@ -122,12 +137,13 @@ function urlOf(input) {
122
137
  */
123
138
  function routingTable(config) {
124
139
  const table = new Map()
125
- const selected = config.endpoints[config.endpoint]
126
- const sentinel = config.sentinel.trim().toLowerCase()
140
+ const endpoints = config.endpoints.get()
141
+ const selected = endpoints[config.endpoint.get()]
142
+ const sentinel = config.sentinel.get().trim().toLowerCase()
127
143
  if (sentinel.length > 0 && typeof selected === 'string' && selected.trim().length > 0) {
128
144
  table.set(sentinel, selected.trim())
129
145
  }
130
- for (const host of Object.values(config.endpoints)) {
146
+ for (const host of Object.values(endpoints)) {
131
147
  if (typeof host !== 'string') continue
132
148
  const trimmed = host.trim()
133
149
  if (trimmed.length > 0) table.set(trimmed.toLowerCase(), trimmed)
@@ -151,21 +167,22 @@ function routingTable(config) {
151
167
  * rebuilds it as `application/json`. Every other response is left untouched.
152
168
  *
153
169
  * @param {typeof fetch} native - the fetch this wrapper delegates to.
154
- * @param {() => ReturnType<typeof Config>} current - reads the live section.
170
+ * @param {ReturnType<typeof Config>} config - the entry configuration; every
171
+ * field read here is a `.volatile()` reference, so the value is the one the
172
+ * settings plane last wrote, not the one activation resolved.
155
173
  * @returns {typeof fetch} the wrapping fetch.
156
174
  */
157
- function fenceFetch(native, current) {
175
+ function fenceFetch(native, config) {
158
176
  return function agentRouterFetch(input, init) {
159
177
  const url = urlOf(input)
160
178
  if (url === undefined) return native(input, init)
161
179
 
162
- const config = current()
163
180
  const destination = routingTable(config).get(url.host.toLowerCase())
164
181
  if (destination === undefined) return native(input, init)
165
182
 
166
183
  const isRequest = typeof Request === 'function' && input instanceof Request
167
184
  const headers = new Headers(init?.headers ?? (isRequest ? input.headers : undefined))
168
- headers.set('user-agent', config.userAgent)
185
+ headers.set('user-agent', config.userAgent.get())
169
186
 
170
187
  // Same host means the sentinel was not involved: rewrite the header only,
171
188
  // and leave the caller's own URL object or Request identity alone.
@@ -185,7 +202,8 @@ function fenceFetch(native, current) {
185
202
  // as an event stream, which provider SDKs otherwise surface as an opaque
186
203
  // transport failure. Annotate those with the configured hint; every other
187
204
  // response passes through untouched.
188
- return config.quotaHint === '' ? pending : annotateQuotaError(pending, config.quotaHint)
205
+ const hint = config.quotaHint.get()
206
+ return hint === '' ? pending : annotateQuotaError(pending, hint)
189
207
  }
190
208
  }
191
209
 
@@ -257,22 +275,18 @@ function requestInitOf(request) {
257
275
  }
258
276
 
259
277
  /**
260
- * Install the relay fence and expose its endpoint choice as a settings section.
278
+ * Install the relay fence and declare how this plugin's configuration is edited.
261
279
  * @param {import('@deepseek-ai/cordis').Context} ctx - the plugin's context.
262
280
  * @param {ReturnType<typeof Config>} config - resolved entry configuration.
263
281
  */
264
282
  function apply(ctx, config) {
265
- // The section is the authority while a settings service exists; the composed
266
- // entry is the fallback, so the fence works identically with no settings
267
- // plane at all (headless, or before the service mounts).
268
- let current = () => config
269
- ctx.inject(['settings'], (settingsCtx) => {
270
- settingsCtx.settings.installSection(ctx, AGENTROUTER_SETTINGS_NAMESPACE, Config, config, {
271
- setSource: (source) => {
272
- current = source
273
- },
274
- onChange: () => {},
275
- })
283
+ // The settings plane derives this plugin's form from the Config schema above
284
+ // and writes it back into the same `.volatile()` references the fence reads.
285
+ // `auto: false` because the endpoint card owns this plugin's UI (lib/client.js);
286
+ // the registration is optional, so a deployment with no settings service at all
287
+ // (headless, or before the service mounts) keeps the composed entry as-is.
288
+ ctx.inject(['settings'], (child) => {
289
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber))
276
290
  })
277
291
 
278
292
  ctx.effect(() => {
@@ -281,7 +295,7 @@ function apply(ctx, config) {
281
295
  ctx.logger.warn('llm-agentrouter: no global fetch to fence; relay requests will be unroutable and rejected')
282
296
  return () => {}
283
297
  }
284
- const fenced = fenceFetch(previous, () => current())
298
+ const fenced = fenceFetch(previous, config)
285
299
  globalThis.fetch = fenced
286
300
  return () => {
287
301
  // Restore only what this plugin installed: a later wrapper layered on top
@@ -294,9 +308,9 @@ function apply(ctx, config) {
294
308
  const table = routingTable(config)
295
309
  ctx.logger.info(
296
310
  'llm-agentrouter: endpoint %s (%s), sending %s',
297
- config.endpoint,
298
- table.get(config.sentinel.trim().toLowerCase()) ?? 'unrouted',
299
- config.userAgent,
311
+ config.endpoint.get(),
312
+ table.get(config.sentinel.get().trim().toLowerCase()) ?? 'unrouted',
313
+ config.userAgent.get(),
300
314
  )
301
315
  }
302
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-llm-agentrouter",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "AgentRouter 中转聚合:一条 pi-ai 路由承载多模型(设置卡片内可从中转站拉取、逐个编辑模型与参数)+ 国内/国际端点一键切换 + 出站请求 User-Agent/402-配额改写围栏。模型列表写入内核「设置 - 模型」页所编辑的同一份 llm-pi-ai 路由数据。",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -52,13 +52,13 @@
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22",
55
- "dsh": "0.1.6-alpha.2"
55
+ "dsh": ">=0.1.7-rc.2"
56
56
  },
57
57
  "dependencies": {
58
- "@deepseek-ai/schemastery": "^3.18.2"
58
+ "@deepseek-ai/schemastery": "^3.18.4"
59
59
  },
60
60
  "peerDependencies": {
61
- "@deepseek-ai/cordis": "^4.0.2"
61
+ "@deepseek-ai/cordis": "^4.0.4"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@deepseek-ai/cordis": {