@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 +3 -3
- package/cordis.patch.yml +6 -6
- package/lib/client.js +56 -46
- package/lib/index.js +41 -27
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@ AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一
|
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
|
-
- **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`
|
|
8
|
-
- **模型列表可编辑**:设置卡片里直接列出这条路由的模型,`更新` 向中转站询问它现有的模型 ID(只补缺失的、不动已有条目),每个模型的 ID / 显示名 / 上下文窗口 / 最大输出 / 输入模态 / 各推理档位与线上取值都可改,保存后下一个请求即生效;新补入的模型默认 1048576 上下文、131072 输出、text+image、off..max 七档。改的就是路由自己的 `models
|
|
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
|
-
-
|
|
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: 设置 →
|
|
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,
|
|
64
|
-
# llm-pi-ai row's whole config object: nothing a default config
|
|
65
|
-
# at any depth. That is safe today only because dsh-base
|
|
66
|
-
# `- id: llm-pi-ai` with a `name:` and no `config:` at all
|
|
67
|
-
# (dsh-base/cordis.patch.yml:
|
|
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
|
|
189
|
-
* schema documents when the
|
|
190
|
-
* @param {unknown} value - the
|
|
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
|
|
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
|
|
211
|
-
*
|
|
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 `
|
|
217
|
-
* two halves are independent: each reads and writes its own
|
|
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
|
|
220
|
+
* @param {object} props - the injected config forms plus the bound translator.
|
|
221
221
|
* @returns {JSX.Element} the card.
|
|
222
222
|
*/
|
|
223
|
-
function EndpointCard({
|
|
223
|
+
function EndpointCard({ form, t, routeForm, operations }) {
|
|
224
224
|
const snapshot = react.useSyncExternalStore(
|
|
225
|
-
react.useCallback((listener) =>
|
|
226
|
-
() =>
|
|
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(() =>
|
|
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
|
-
|
|
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
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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 —
|
|
685
|
-
* or a
|
|
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
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
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
|
|
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({
|
|
715
|
+
function ModelList({ routeForm, operations, t }) {
|
|
709
716
|
const snapshot = react.useSyncExternalStore(
|
|
710
|
-
react.useCallback((listener) =>
|
|
711
|
-
() =>
|
|
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
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
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", "
|
|
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
|
|
1154
|
-
// The model list is not this plugin's own
|
|
1155
|
-
// route inside the adapter's
|
|
1156
|
-
//
|
|
1157
|
-
const
|
|
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 =
|
|
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: () => ({
|
|
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
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
|
126
|
-
const
|
|
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(
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
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
|
+
"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.
|
|
55
|
+
"dsh": ">=0.1.7-rc.2"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.4"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
64
64
|
"@deepseek-ai/cordis": {
|