@lynn123411/dsh-llm-agentrouter 0.2.0
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/LICENSE +22 -0
- package/README.md +16 -0
- package/cordis.patch.yml +147 -0
- package/lib/client.js +256 -0
- package/lib/index.js +304 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aqiu817
|
|
4
|
+
Copyright (c) 2026 lynn123411 (alpha.5 compatibility fork: settings/slots API migration, package rename)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @lynn123411/dsh-llm-agentrouter
|
|
2
|
+
|
|
3
|
+
AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一条 pi-ai 路由(模型选择器只出现一个 AgentRouter 分组),国内 / 国际端点在「设置 - 插件 - AgentRouter 中转站」一键切换、下一请求即生效,外加出站请求的 User-Agent 改写与 402 配额耗尽提示围栏。分叉自 `aqiu817/dsh-llm-agentrouter`,适配 DSH `0.1.2-alpha.5` 新 settings/slots API。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`reasoningEffort` 档位与兼容开关按实测探针手写声明,模型列表不翻倍。
|
|
8
|
+
- **端点一键切换**:设置卡片两个大选项(国内端点 / 国际端点,附带真实 host 展示),点击即写即生效,无需重启;API Key 只存 `$DSH_HOME/.credentials.yaml` 的 `AGENTROUTER_API_KEY` 引用。
|
|
9
|
+
- **请求围栏**:重写 relay 要求的 `User-Agent`(适配器强制署名下沉到 `fetch` 层替换),哨兵 host(`.internal` 不可解析)兜底防裸奔,402 配额耗尽的 JSON 错误体改写为可读提示。
|
|
10
|
+
- **alpha.5 兼容**:宿主改用 `ctx.settings.installSection` + `ctx.inject(['settings'])`,客户端卡片改挂 `settings.plugins.tab`;沿用 `llm-agentrouter` 设置命名空间,老配置无缝继承。
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
dsh plugin --profile web add @lynn123411/dsh-llm-agentrouter
|
|
16
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# dsh-llm-agentrouter bundle patch — the AgentRouter relay as ONE provider route
|
|
2
|
+
# whose endpoint is a runtime choice, plus the fence that makes both possible.
|
|
3
|
+
#
|
|
4
|
+
# Applied after the dsh-base / dsh-web-app bundle layers, so the `llm-pi-ai`
|
|
5
|
+
# row below overrides the dormant zero-route entry dsh-base mounts.
|
|
6
|
+
#
|
|
7
|
+
# WHY ONE ROUTE AND NOT TWO
|
|
8
|
+
# The relay serves the same three models at a domestic and an international
|
|
9
|
+
# endpoint; only the origin differs. Declaring a route per endpoint put every
|
|
10
|
+
# model in the picker twice and made "which endpoint" a choice the user had to
|
|
11
|
+
# re-make on every model switch. It is not a model property — it is one
|
|
12
|
+
# deployment-wide setting, so it lives in this plugin's own settings section and
|
|
13
|
+
# the picker shows one AgentRouter group.
|
|
14
|
+
#
|
|
15
|
+
# WHY THE BASE URL IS A SENTINEL
|
|
16
|
+
# `baseURL` is resolved once per request by the adapter, but it cannot read this
|
|
17
|
+
# plugin's endpoint choice — the two live in different namespaces. So the route
|
|
18
|
+
# addresses a host that deliberately does not exist, and the fence rewrites it
|
|
19
|
+
# to the chosen endpoint on the way out (`.internal` is reserved for exactly
|
|
20
|
+
# this, so an unfenced request fails to resolve instead of reaching anything
|
|
21
|
+
# real). The fence has to be in the request path regardless: the relay
|
|
22
|
+
# authenticates its client by User-Agent, and the adapter strips profile headers
|
|
23
|
+
# colliding with its mandatory attribution. See lib/index.js.
|
|
24
|
+
#
|
|
25
|
+
# CREDENTIALS
|
|
26
|
+
# `apiKeyEnv` is a credential *reference* resolved per request; the secret lives
|
|
27
|
+
# in $DSH_HOME/.credentials.yaml (or the environment), never in this file. Do
|
|
28
|
+
# NOT move a key into `headers`: that dict is returned verbatim by the adapter's
|
|
29
|
+
# `describe()` and rendered by the Models settings page. One reference serves
|
|
30
|
+
# both endpoints because it is one relay account.
|
|
31
|
+
#
|
|
32
|
+
# MODEL CATALOG
|
|
33
|
+
# pi-ai's installed catalog describes all three models, but under their vendor
|
|
34
|
+
# providers (`anthropic`, `openai`) and other protocols — Opus via
|
|
35
|
+
# anthropic-messages, gpt-5.6-sol via openai-responses. A hand-declared route
|
|
36
|
+
# inherits nothing, so capacities, reasoning levels, and compat switches are
|
|
37
|
+
# restated here from that catalog, adjusted for Chat Completions.
|
|
38
|
+
#
|
|
39
|
+
# `reasoningEfforts` keys are the levels the picker offers; each value is the
|
|
40
|
+
# wire spelling sent as `reasoning_effort`. All three models were probed against
|
|
41
|
+
# the live relay: every level below answered 200. `off:` (declared, no value)
|
|
42
|
+
# offers Off and sends nothing, which is what an OpenAI-shaped endpoint wants.
|
|
43
|
+
# WHY THE PICKER SAYS NOTHING ABOUT THE ENDPOINT
|
|
44
|
+
# The model picker renders no child slots, so a plugin cannot inject anything
|
|
45
|
+
# into that menu; per group it shows only `displayName`, and per model only the
|
|
46
|
+
# name and an adapter-supplied description a hand-declared pi-ai route has no
|
|
47
|
+
# field for. The group title is therefore the only reachable surface — and it is
|
|
48
|
+
# a name, not a notice, so it stays a name. The endpoint is explained where it is
|
|
49
|
+
# changed: 设置 → 插件 → AgentRouter 中转站.
|
|
50
|
+
- id: llm-pi-ai
|
|
51
|
+
name: '@deepseek-ai/dsh-llm-pi-ai'
|
|
52
|
+
config:
|
|
53
|
+
providers:
|
|
54
|
+
agentrouter:
|
|
55
|
+
displayName: AgentRouter
|
|
56
|
+
apiKeyEnv: AGENTROUTER_API_KEY
|
|
57
|
+
api: openai-completions
|
|
58
|
+
# Rewritten per request by llm-agentrouter to the selected endpoint.
|
|
59
|
+
baseURL: https://relay.agentrouter.internal/v1
|
|
60
|
+
defaultInput:
|
|
61
|
+
- text
|
|
62
|
+
# The relay is not an endpoint pi-ai can recognize, so its detection
|
|
63
|
+
# would address it as OpenAI itself: `developer` system role and
|
|
64
|
+
# `max_completion_tokens`. Probes show it accepts plain `system`,
|
|
65
|
+
# `max_tokens`, top-level `reasoning_effort`, and `strict` tools.
|
|
66
|
+
compat:
|
|
67
|
+
thinkingFormat: openai
|
|
68
|
+
supportsReasoningEffort: true
|
|
69
|
+
supportsDeveloperRole: false
|
|
70
|
+
maxTokensField: max_tokens
|
|
71
|
+
supportsStore: false
|
|
72
|
+
supportsStrictMode: true
|
|
73
|
+
supportsUsageInStreaming: true
|
|
74
|
+
models:
|
|
75
|
+
- id: claude-opus-5
|
|
76
|
+
name: Claude Opus 5
|
|
77
|
+
contextWindow: 1000000
|
|
78
|
+
maxTokens: 128000
|
|
79
|
+
reasoningEfforts: &opus-efforts
|
|
80
|
+
off:
|
|
81
|
+
low: low
|
|
82
|
+
medium: medium
|
|
83
|
+
high: high
|
|
84
|
+
xhigh: xhigh
|
|
85
|
+
max: max
|
|
86
|
+
- id: claude-opus-4-8
|
|
87
|
+
name: Claude Opus 4.8
|
|
88
|
+
contextWindow: 1000000
|
|
89
|
+
maxTokens: 128000
|
|
90
|
+
reasoningEfforts: *opus-efforts
|
|
91
|
+
- id: gpt-5.6-sol
|
|
92
|
+
name: GPT 5.6 Sol
|
|
93
|
+
contextWindow: 272000
|
|
94
|
+
maxTokens: 128000
|
|
95
|
+
reasoningEfforts:
|
|
96
|
+
off:
|
|
97
|
+
minimal: minimal
|
|
98
|
+
low: low
|
|
99
|
+
medium: medium
|
|
100
|
+
high: high
|
|
101
|
+
xhigh: xhigh
|
|
102
|
+
max: max
|
|
103
|
+
# Tiers mirror what the first-party `deepseek-official` route offers for
|
|
104
|
+
# DeepSeek-V4-Flash — off / low / high / max — rather than the relay's
|
|
105
|
+
# full enum. The relay does accept `minimal`, `medium`, and `xhigh` for
|
|
106
|
+
# this model, but the levels a model is *offered* under should match the
|
|
107
|
+
# vendor's own selection; add them here if that ever changes.
|
|
108
|
+
#
|
|
109
|
+
# `off` carries the wire value `none` instead of being left empty, and
|
|
110
|
+
# that is not cosmetic: omitting `reasoning_effort` entirely still
|
|
111
|
+
# returns `reasoning_content` with a nonzero `reasoning_tokens`, so an
|
|
112
|
+
# empty `off:` would offer a switch that does nothing. `none` is the
|
|
113
|
+
# only spelling that actually stops the thinking, and it is the relay's
|
|
114
|
+
# own word — `off` is rejected with a 400 naming the accepted enum.
|
|
115
|
+
- id: deepseek-v4-flash
|
|
116
|
+
name: DeepSeek V4 Flash
|
|
117
|
+
contextWindow: 1000000
|
|
118
|
+
maxTokens: 256000
|
|
119
|
+
reasoningEfforts:
|
|
120
|
+
off: none
|
|
121
|
+
low: low
|
|
122
|
+
high: high
|
|
123
|
+
max: max
|
|
124
|
+
# This model declares no `off` at all, and that is the honest shape
|
|
125
|
+
# rather than an omission: every level outside low / high / max — `none`,
|
|
126
|
+
# `minimal`, `medium`, `xhigh` — is refused with 400 «该模型始终思考,不
|
|
127
|
+
# 支持关闭思考;请使用 low、high 或 max。». It always thinks. Leaving `off`
|
|
128
|
+
# out withholds the level, so the picker never offers a switch the
|
|
129
|
+
# upstream would reject.
|
|
130
|
+
#
|
|
131
|
+
# `maxTokens` is the ceiling the relay states for this model: a larger
|
|
132
|
+
# value comes back as «max_tokens参数非法:限制数值范围[1,131072]».
|
|
133
|
+
- id: glm-5.3
|
|
134
|
+
name: GLM 5.3
|
|
135
|
+
contextWindow: 1000000
|
|
136
|
+
maxTokens: 131072
|
|
137
|
+
reasoningEfforts:
|
|
138
|
+
low: low
|
|
139
|
+
high: high
|
|
140
|
+
max: max
|
|
141
|
+
|
|
142
|
+
# The endpoint switch and the User-Agent fence. Both are one plugin because both
|
|
143
|
+
# are one rewrite of one outbound request, and splitting them would mean two
|
|
144
|
+
# wrappers over the same global for the same hostname.
|
|
145
|
+
- insert:
|
|
146
|
+
- id: llm-agentrouter
|
|
147
|
+
name: '@lynn123411/dsh-llm-agentrouter'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@lynn123411/dsh-llm-agentrouter",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let jsx = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
|
|
10
|
+
//#region styles
|
|
11
|
+
/*
|
|
12
|
+
* Written by hand rather than emitted from a CSS module: the `clientBundle`
|
|
13
|
+
* tsdown preset that produces those hashed class names is not published, so
|
|
14
|
+
* this bundle owns a prefixed class set and injects it once. Every colour is
|
|
15
|
+
* a shell design token, so the card follows the active theme.
|
|
16
|
+
*/
|
|
17
|
+
const CSS = [
|
|
18
|
+
".dshAr_card{display:flex;flex-direction:column;gap:10px}",
|
|
19
|
+
".dshAr_lead{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}",
|
|
20
|
+
".dshAr_choices{display:flex;gap:10px;margin:0;padding:0;border:0;flex-wrap:wrap}",
|
|
21
|
+
".dshAr_choice{flex:1 1 200px;min-width:0;display:flex;gap:9px;align-items:flex-start;padding:11px 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);cursor:pointer}",
|
|
22
|
+
".dshAr_choice:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
23
|
+
".dshAr_choice[data-selected=true]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary) inset}",
|
|
24
|
+
".dshAr_choice[data-disabled=true]{cursor:default;opacity:.55}",
|
|
25
|
+
".dshAr_choice input{margin:3px 0 0}",
|
|
26
|
+
".dshAr_choiceText{display:flex;flex-direction:column;gap:2px;min-width:0}",
|
|
27
|
+
".dshAr_choiceTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:20px}",
|
|
28
|
+
".dshAr_choiceHost{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code);font-size:12px;line-height:17px}",
|
|
29
|
+
".dshAr_choiceHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}",
|
|
30
|
+
".dshAr_status{margin:0;min-height:18px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
31
|
+
".dshAr_status[data-kind=error]{color:var(--dsw-alias-state-error-primary)}",
|
|
32
|
+
].join("");
|
|
33
|
+
const CSS_TAG_ID = "dsh-llm-agentrouter/EndpointCard.css";
|
|
34
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_TAG_ID) + "]") === null) {
|
|
35
|
+
const tag = document.createElement("style");
|
|
36
|
+
tag.setAttribute("data-plugin-css", CSS_TAG_ID);
|
|
37
|
+
tag.textContent = CSS;
|
|
38
|
+
document.head.append(tag);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
|
|
42
|
+
//#region locales
|
|
43
|
+
/** Simplified Chinese dictionary and key source of truth. */
|
|
44
|
+
const zh = {
|
|
45
|
+
title: "AgentRouter 中转站",
|
|
46
|
+
description: "选择请求发往的端点。切换后立即生效,无需重启;模型列表不受影响。",
|
|
47
|
+
cn: "国内端点",
|
|
48
|
+
cnHint: "如您区域处于中国大陆则更适用于该端点",
|
|
49
|
+
intl: "国际端点",
|
|
50
|
+
intlHint: "请确保您的网络环境支持访问该端点后切换",
|
|
51
|
+
loading: "正在读取设置…",
|
|
52
|
+
unavailable: "此浏览器无法读取该设置。",
|
|
53
|
+
readOnly: "当前部署不允许写入设置。",
|
|
54
|
+
saving: "正在保存…",
|
|
55
|
+
saved: "已切换到%s。",
|
|
56
|
+
failed: "保存失败,设置未更改。",
|
|
57
|
+
};
|
|
58
|
+
/** English dictionary checked against the Chinese key set. */
|
|
59
|
+
const en = {
|
|
60
|
+
title: "AgentRouter relay",
|
|
61
|
+
description: "Which endpoint requests are sent to. A switch applies to the next request; the model list is unaffected.",
|
|
62
|
+
cn: "Domestic endpoint",
|
|
63
|
+
cnHint: "Better suited if you are in mainland China",
|
|
64
|
+
intl: "International endpoint",
|
|
65
|
+
intlHint: "Make sure your network can reach this endpoint before switching",
|
|
66
|
+
loading: "Reading settings…",
|
|
67
|
+
unavailable: "This browser cannot read these settings.",
|
|
68
|
+
readOnly: "This deployment does not accept settings writes.",
|
|
69
|
+
saving: "Saving…",
|
|
70
|
+
saved: "Switched to %s.",
|
|
71
|
+
failed: "The save failed; the setting is unchanged.",
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
|
|
75
|
+
//#region endpoint card
|
|
76
|
+
/** The endpoint keys this card offers, in display order. */
|
|
77
|
+
const ENDPOINTS = ["cn", "intl"];
|
|
78
|
+
/** Field this card writes inside its namespace. */
|
|
79
|
+
const FIELD = "endpoint";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the endpoint from a resolved section, falling back to the default the
|
|
83
|
+
* schema documents when the section is not readable yet.
|
|
84
|
+
* @param {unknown} value - the scope snapshot's resolved value.
|
|
85
|
+
* @returns {string} an endpoint key.
|
|
86
|
+
*/
|
|
87
|
+
function endpointOf(value) {
|
|
88
|
+
const endpoint = typeof value === "object" && value !== null ? value.endpoint : undefined;
|
|
89
|
+
return ENDPOINTS.includes(endpoint) ? endpoint : "cn";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read the host table from a resolved section so each choice can show the
|
|
94
|
+
* origin it actually means.
|
|
95
|
+
* @param {unknown} value - the scope snapshot's resolved value.
|
|
96
|
+
* @returns {Record<string, string>} host per endpoint key.
|
|
97
|
+
*/
|
|
98
|
+
function hostsOf(value) {
|
|
99
|
+
const hosts = typeof value === "object" && value !== null ? value.endpoints : undefined;
|
|
100
|
+
return typeof hosts === "object" && hosts !== null ? hosts : {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The relay endpoint card: one radio group over this plugin's own settings
|
|
105
|
+
* namespace.
|
|
106
|
+
*
|
|
107
|
+
* A choice writes immediately rather than staging behind a Save button. The
|
|
108
|
+
* namespace has exactly one user-facing field and the write is reversible in
|
|
109
|
+
* one click, so a staged form would add a step without protecting anything —
|
|
110
|
+
* and `scope.set` already fences the write with the revision it read.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} props - the injected scope face plus the bound translator.
|
|
113
|
+
* @returns {JSX.Element} the card.
|
|
114
|
+
*/
|
|
115
|
+
function EndpointCard({ scope, t }) {
|
|
116
|
+
const snapshot = react.useSyncExternalStore(
|
|
117
|
+
react.useCallback((listener) => scope.subscribe(listener), [scope]),
|
|
118
|
+
() => scope.getSnapshot(),
|
|
119
|
+
);
|
|
120
|
+
const [pending, setPending] = react.useState(null);
|
|
121
|
+
const [failed, setFailed] = react.useState(false);
|
|
122
|
+
const groupName = react.useId();
|
|
123
|
+
|
|
124
|
+
const selected = endpointOf(snapshot.value);
|
|
125
|
+
const hosts = hostsOf(snapshot.value);
|
|
126
|
+
const disabled = snapshot.status !== "ready" || !snapshot.writable || pending !== null;
|
|
127
|
+
|
|
128
|
+
const choose = (endpoint) => {
|
|
129
|
+
if (endpoint === selected || disabled) return;
|
|
130
|
+
setPending(endpoint);
|
|
131
|
+
setFailed(false);
|
|
132
|
+
Promise.resolve()
|
|
133
|
+
.then(() => scope.set(FIELD, endpoint))
|
|
134
|
+
.then(
|
|
135
|
+
() => {
|
|
136
|
+
setPending(null);
|
|
137
|
+
},
|
|
138
|
+
() => {
|
|
139
|
+
setPending(null);
|
|
140
|
+
setFailed(true);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const status = () => {
|
|
146
|
+
if (snapshot.status === "loading") return { kind: "info", text: t("loading") };
|
|
147
|
+
if (snapshot.status === "unavailable") return { kind: "error", text: t("unavailable") };
|
|
148
|
+
if (failed) return { kind: "error", text: t("failed") };
|
|
149
|
+
if (pending !== null) return { kind: "info", text: t("saving") };
|
|
150
|
+
if (!snapshot.writable) return { kind: "info", text: t("readOnly") };
|
|
151
|
+
return { kind: "info", text: "" };
|
|
152
|
+
};
|
|
153
|
+
const shown = status();
|
|
154
|
+
|
|
155
|
+
return jsx.jsxs("section", {
|
|
156
|
+
className: "dshAr_card",
|
|
157
|
+
"data-plugin-card": "llm-agentrouter",
|
|
158
|
+
"aria-busy": snapshot.status === "loading" || pending !== null,
|
|
159
|
+
children: [
|
|
160
|
+
jsx.jsx("h3", { children: t("title") }),
|
|
161
|
+
jsx.jsx("p", { className: "dshAr_lead", children: t("description") }),
|
|
162
|
+
jsx.jsxs("fieldset", {
|
|
163
|
+
className: "dshAr_choices",
|
|
164
|
+
children: [
|
|
165
|
+
jsx.jsx("legend", { hidden: true, children: t("title") }),
|
|
166
|
+
...ENDPOINTS.map((endpoint) => {
|
|
167
|
+
const active = (pending ?? selected) === endpoint;
|
|
168
|
+
return jsx.jsxs(
|
|
169
|
+
"label",
|
|
170
|
+
{
|
|
171
|
+
className: "dshAr_choice",
|
|
172
|
+
"data-endpoint": endpoint,
|
|
173
|
+
"data-selected": active ? "true" : undefined,
|
|
174
|
+
"data-disabled": disabled ? "true" : undefined,
|
|
175
|
+
children: [
|
|
176
|
+
jsx.jsx("input", {
|
|
177
|
+
type: "radio",
|
|
178
|
+
name: groupName,
|
|
179
|
+
value: endpoint,
|
|
180
|
+
checked: active,
|
|
181
|
+
disabled: disabled && !active,
|
|
182
|
+
onChange: () => choose(endpoint),
|
|
183
|
+
}),
|
|
184
|
+
jsx.jsxs("span", {
|
|
185
|
+
className: "dshAr_choiceText",
|
|
186
|
+
children: [
|
|
187
|
+
jsx.jsx("span", { className: "dshAr_choiceTitle", children: t(endpoint) }),
|
|
188
|
+
jsx.jsx("span", {
|
|
189
|
+
className: "dshAr_choiceHost",
|
|
190
|
+
children: hosts[endpoint] ?? "",
|
|
191
|
+
}),
|
|
192
|
+
jsx.jsx("span", { className: "dshAr_choiceHint", children: t(endpoint + "Hint") }),
|
|
193
|
+
],
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
endpoint,
|
|
198
|
+
);
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
jsx.jsx("p", {
|
|
203
|
+
className: "dshAr_status",
|
|
204
|
+
"data-kind": shown.kind,
|
|
205
|
+
role: shown.kind === "error" ? "alert" : "status",
|
|
206
|
+
children: shown.text,
|
|
207
|
+
}),
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
|
|
213
|
+
//#region plugin
|
|
214
|
+
/** Dictionary namespace owned by this plugin. */
|
|
215
|
+
const NS = "settings.agentrouter";
|
|
216
|
+
/**
|
|
217
|
+
* Settings namespace the Host half registers. Spelled rather than imported:
|
|
218
|
+
* a browser bundle must not depend on a Host package, so both halves state
|
|
219
|
+
* the same literal (the Host's is `AGENTROUTER_SETTINGS_NAMESPACE`).
|
|
220
|
+
*/
|
|
221
|
+
const SETTINGS_NS = "llm-agentrouter";
|
|
222
|
+
/** Services this plugin needs from the browser runtime. */
|
|
223
|
+
const inject = ["slots", "locale", "settingsScope"];
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Register the endpoint card into the plugin configuration tab.
|
|
227
|
+
* @param {object} ctx - the browser plugin context.
|
|
228
|
+
*/
|
|
229
|
+
function apply(ctx) {
|
|
230
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-agentrouter: dictionaries");
|
|
231
|
+
const t = ctx.locale.bind(NS);
|
|
232
|
+
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
|
|
233
|
+
ctx.slots.inject("settings.plugins.tab", () =>
|
|
234
|
+
ctx.slots.register(
|
|
235
|
+
{
|
|
236
|
+
name: "settings.plugins.tab",
|
|
237
|
+
id: SETTINGS_NS,
|
|
238
|
+
order: 30,
|
|
239
|
+
label: () => t("title"),
|
|
240
|
+
locale: NS,
|
|
241
|
+
inject: () => ({ scope, t }),
|
|
242
|
+
},
|
|
243
|
+
EndpointCard,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
|
|
249
|
+
exports.NS = NS;
|
|
250
|
+
exports.SETTINGS_NS = SETTINGS_NS;
|
|
251
|
+
exports.EndpointCard = EndpointCard;
|
|
252
|
+
exports.apply = apply;
|
|
253
|
+
exports.inject = inject;
|
|
254
|
+
return module.exports;
|
|
255
|
+
}
|
|
256
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dsh-llm-agentrouter — the runtime half of the bundle of the same name.
|
|
5
|
+
*
|
|
6
|
+
* The bundle's `cordis.patch.yml` declares ONE relay route on the pi-ai adapter
|
|
7
|
+
* (`llm-pi-ai`); everything a request needs — protocol, model catalog, reasoning
|
|
8
|
+
* levels — is configuration there, not code.
|
|
9
|
+
*
|
|
10
|
+
* This plugin owns the two things that configuration cannot express, both of
|
|
11
|
+
* which are rewrites of the same outbound request:
|
|
12
|
+
*
|
|
13
|
+
* 1. **The User-Agent.** The relay authenticates its *client* by `User-Agent`,
|
|
14
|
+
* accepting only the exact value it requires, and `dsh-llm-pi-ai`
|
|
15
|
+
* deliberately strips any profile header colliding with
|
|
16
|
+
* its own attribution before appending
|
|
17
|
+
* `user-agent: deepseek-harness/<version>`. Attribution is mandatory by
|
|
18
|
+
* design ("a white-label deployment may replace it, but not suppress it"), so
|
|
19
|
+
* the substitution has to happen below the adapter — at the `fetch` the
|
|
20
|
+
* provider SDK resolves from the global scope when it builds its client.
|
|
21
|
+
*
|
|
22
|
+
* 2. **The endpoint.** The relay serves the same models at a domestic and an
|
|
23
|
+
* international origin. That is one deployment-wide preference, not a model
|
|
24
|
+
* property, so it is a single setting here rather than a route per endpoint —
|
|
25
|
+
* which would list every model twice in the picker. The adapter cannot read
|
|
26
|
+
* this namespace, so the route's `baseURL` names a deliberately unresolvable
|
|
27
|
+
* sentinel host and the fence substitutes the chosen origin on the way out.
|
|
28
|
+
*
|
|
29
|
+
* The fence is therefore deliberately narrow: it rewrites one header and, for
|
|
30
|
+
* the sentinel alone, one origin; every other request goes to the previous
|
|
31
|
+
* `fetch` untouched. It is installed through `ctx.effect()`, so stopping or
|
|
32
|
+
* reloading the plugin restores the `fetch` it replaced.
|
|
33
|
+
*
|
|
34
|
+
* @module dsh-llm-agentrouter
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Stable Cordis plugin name. */
|
|
38
|
+
const name = 'llm-agentrouter'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Settings namespace this plugin owns.
|
|
42
|
+
*
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
const AGENTROUTER_SETTINGS_NAMESPACE = 'llm-agentrouter'
|
|
47
|
+
|
|
48
|
+
const Config = z.object({
|
|
49
|
+
/**
|
|
50
|
+
* Which relay origin outbound requests are sent to. The whole point of the
|
|
51
|
+
* plugin's settings card: one choice, applied to every model on the route.
|
|
52
|
+
*/
|
|
53
|
+
endpoint: z
|
|
54
|
+
.union([z.const('cn'), z.const('intl')])
|
|
55
|
+
.default('cn')
|
|
56
|
+
.description('relay endpoint requests are sent to: cn (domestic) or intl (international)'),
|
|
57
|
+
/**
|
|
58
|
+
* Host per endpoint key. Configuration rather than a constant so a moved
|
|
59
|
+
* origin is a settings edit, not a release.
|
|
60
|
+
*/
|
|
61
|
+
endpoints: z
|
|
62
|
+
.dict(z.string())
|
|
63
|
+
.default({ cn: 'ps.air-outer.com', intl: 'agentrouter.org' })
|
|
64
|
+
.description('host for each endpoint key'),
|
|
65
|
+
/**
|
|
66
|
+
* The host the route's `baseURL` names. Requests to it are rewritten to the
|
|
67
|
+
* selected endpoint; it must stay unresolvable so an unfenced request fails
|
|
68
|
+
* loudly instead of reaching some real server (`.internal` is reserved).
|
|
69
|
+
*/
|
|
70
|
+
sentinel: z
|
|
71
|
+
.string()
|
|
72
|
+
.default('relay.agentrouter.internal')
|
|
73
|
+
.description('placeholder host in the route baseURL that the fence replaces with the selected endpoint'),
|
|
74
|
+
/**
|
|
75
|
+
* The exact User-Agent the relay accepts. It is the whole authentication of
|
|
76
|
+
* the client (the API key authenticates the account), so it is configuration
|
|
77
|
+
* rather than a constant: the relay may require a different value later.
|
|
78
|
+
*/
|
|
79
|
+
userAgent: z
|
|
80
|
+
.string()
|
|
81
|
+
.default('claude-cli/2.1.161 (external, cli)')
|
|
82
|
+
.description('User-Agent value sent to the relay in place of the harness attribution'),
|
|
83
|
+
/** Report the installed fence once on activation. */
|
|
84
|
+
announce: z.boolean().default(true),
|
|
85
|
+
/**
|
|
86
|
+
* Hint appended to a 402 quota error from the relay. The relay answers
|
|
87
|
+
* Claude / GPT budget-pool exhaustion with HTTP 402 and a JSON error body
|
|
88
|
+
* mislabelled as an event stream; the fence keeps that original message and
|
|
89
|
+
* appends this plain-language explanation. Empty string disables it.
|
|
90
|
+
*/
|
|
91
|
+
quotaHint: z
|
|
92
|
+
.string()
|
|
93
|
+
.default('Claude / GPT 本批额度已用完,请等待下一批投放。')
|
|
94
|
+
.description('text appended to relay 402 quota errors'),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a `fetch` argument to its URL without consuming a request body.
|
|
99
|
+
* @param {unknown} input - the first `fetch` argument.
|
|
100
|
+
* @returns {URL | undefined} the parsed URL, or undefined when it is not one.
|
|
101
|
+
*/
|
|
102
|
+
function urlOf(input) {
|
|
103
|
+
try {
|
|
104
|
+
if (typeof input === 'string') return new URL(input)
|
|
105
|
+
if (input instanceof URL) return input
|
|
106
|
+
if (typeof input === 'object' && input !== null && typeof input.url === 'string') return new URL(input.url)
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The hosts one resolved section wants rewritten, and how.
|
|
115
|
+
*
|
|
116
|
+
* Derived per request from the live section so a settings change takes effect on
|
|
117
|
+
* the next call with no reload. Endpoint hosts are included as themselves: a
|
|
118
|
+
* request already addressed to a real relay origin still needs the User-Agent.
|
|
119
|
+
*
|
|
120
|
+
* @param {ReturnType<typeof Config>} config - the resolved section.
|
|
121
|
+
* @returns {Map<string, string>} lowercase source host to destination host.
|
|
122
|
+
*/
|
|
123
|
+
function routingTable(config) {
|
|
124
|
+
const table = new Map()
|
|
125
|
+
const selected = config.endpoints[config.endpoint]
|
|
126
|
+
const sentinel = config.sentinel.trim().toLowerCase()
|
|
127
|
+
if (sentinel.length > 0 && typeof selected === 'string' && selected.trim().length > 0) {
|
|
128
|
+
table.set(sentinel, selected.trim())
|
|
129
|
+
}
|
|
130
|
+
for (const host of Object.values(config.endpoints)) {
|
|
131
|
+
if (typeof host !== 'string') continue
|
|
132
|
+
const trimmed = host.trim()
|
|
133
|
+
if (trimmed.length > 0) table.set(trimmed.toLowerCase(), trimmed)
|
|
134
|
+
}
|
|
135
|
+
return table
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Wrap one `fetch` so relay requests carry the relay User-Agent, and sentinel
|
|
140
|
+
* requests additionally go to the selected endpoint.
|
|
141
|
+
*
|
|
142
|
+
* The wrapper never reads or rebuilds a body as a rule: for the shape every
|
|
143
|
+
* provider SDK in this harness uses (url plus an init object) it copies
|
|
144
|
+
* `init` and replaces only its headers. A bare `Request` is re-created with
|
|
145
|
+
* `duplex: 'half'` so a streamed body survives the clone.
|
|
146
|
+
*
|
|
147
|
+
* The one exception is a relay 402 quota-exhaustion response: the body is a
|
|
148
|
+
* small JSON error body mislabelled as `text/event-stream`, which provider
|
|
149
|
+
* SDKs otherwise surface as an opaque transport failure. The fence reads that
|
|
150
|
+
* body, keeps the original error message, appends the configured hint, and
|
|
151
|
+
* rebuilds it as `application/json`. Every other response is left untouched.
|
|
152
|
+
*
|
|
153
|
+
* @param {typeof fetch} native - the fetch this wrapper delegates to.
|
|
154
|
+
* @param {() => ReturnType<typeof Config>} current - reads the live section.
|
|
155
|
+
* @returns {typeof fetch} the wrapping fetch.
|
|
156
|
+
*/
|
|
157
|
+
function fenceFetch(native, current) {
|
|
158
|
+
return function agentRouterFetch(input, init) {
|
|
159
|
+
const url = urlOf(input)
|
|
160
|
+
if (url === undefined) return native(input, init)
|
|
161
|
+
|
|
162
|
+
const config = current()
|
|
163
|
+
const destination = routingTable(config).get(url.host.toLowerCase())
|
|
164
|
+
if (destination === undefined) return native(input, init)
|
|
165
|
+
|
|
166
|
+
const isRequest = typeof Request === 'function' && input instanceof Request
|
|
167
|
+
const headers = new Headers(init?.headers ?? (isRequest ? input.headers : undefined))
|
|
168
|
+
headers.set('user-agent', config.userAgent)
|
|
169
|
+
|
|
170
|
+
// Same host means the sentinel was not involved: rewrite the header only,
|
|
171
|
+
// and leave the caller's own URL object or Request identity alone.
|
|
172
|
+
let pending
|
|
173
|
+
if (destination.toLowerCase() === url.host.toLowerCase()) {
|
|
174
|
+
if (isRequest && init === undefined) pending = native(new Request(input, { headers, duplex: 'half' }))
|
|
175
|
+
else pending = native(input, { ...init, headers })
|
|
176
|
+
} else {
|
|
177
|
+
const target = new URL(url)
|
|
178
|
+
target.host = destination
|
|
179
|
+
if (isRequest) pending = native(new Request(target, { ...(init ?? {}), ...requestInitOf(input), headers, duplex: 'half' }))
|
|
180
|
+
else pending = native(target, { ...init, headers })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The one response this wrapper rewrites: the relay answers Claude / GPT
|
|
184
|
+
// budget-pool exhaustion with HTTP 402 and a JSON error body mislabelled
|
|
185
|
+
// as an event stream, which provider SDKs otherwise surface as an opaque
|
|
186
|
+
// transport failure. Annotate those with the configured hint; every other
|
|
187
|
+
// response passes through untouched.
|
|
188
|
+
return config.quotaHint === '' ? pending : annotateQuotaError(pending, config.quotaHint)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The relay answers Claude / GPT budget-pool exhaustion with HTTP 402 and a
|
|
194
|
+
* JSON error body mislabelled as 'text/event-stream'; left alone, provider
|
|
195
|
+
* SDKs surface it as an opaque transport failure rather than the API error it
|
|
196
|
+
* is. This rebuilds such a response as 'application/json', keeping the
|
|
197
|
+
* original error message and appending the hint. Every other response passes
|
|
198
|
+
* through untouched.
|
|
199
|
+
*
|
|
200
|
+
* @param {Promise<Response>} pending - the relay fetch promise.
|
|
201
|
+
* @param {string} hint - text appended to the original error message; empty
|
|
202
|
+
* disables the rewrite (the caller should not call this then).
|
|
203
|
+
* @returns {Promise<Response>} the (possibly rebuilt) response.
|
|
204
|
+
*/
|
|
205
|
+
function annotateQuotaError(pending, hint) {
|
|
206
|
+
return pending.then((response) => {
|
|
207
|
+
if (response.status !== 402) return response
|
|
208
|
+
const type = (response.headers.get('content-type') ?? '').toLowerCase()
|
|
209
|
+
if (!type.includes('event-stream') && !type.includes('json')) return response
|
|
210
|
+
return response.text().then((text) => {
|
|
211
|
+
let parsed
|
|
212
|
+
let message
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(text)
|
|
215
|
+
message = parsed?.error?.message
|
|
216
|
+
} catch {
|
|
217
|
+
message = undefined
|
|
218
|
+
}
|
|
219
|
+
const headers = new Headers(response.headers)
|
|
220
|
+
if (typeof message !== 'string' || message.length === 0) {
|
|
221
|
+
// Not a JSON error body; hand the bytes back unchanged.
|
|
222
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers })
|
|
223
|
+
}
|
|
224
|
+
headers.set('content-type', 'application/json')
|
|
225
|
+
// Only annotate quota-exhaustion errors; other 402s pass through with
|
|
226
|
+
// the original body so the provider SDK builds its own error message.
|
|
227
|
+
if (/\b(?:quota|budget)\b/i.test(message)) {
|
|
228
|
+
parsed.error.message = message + '\n' + hint
|
|
229
|
+
}
|
|
230
|
+
return new Response(JSON.stringify(parsed), { status: response.status, statusText: response.statusText, headers })
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The fields of a `Request` that must survive re-addressing it.
|
|
237
|
+
*
|
|
238
|
+
* `new Request(url, request)` is not available — the second argument must be an
|
|
239
|
+
* init — so the parts a relay call depends on are copied explicitly. The body is
|
|
240
|
+
* passed by reference, never read.
|
|
241
|
+
*
|
|
242
|
+
* @param {Request} request - the request being re-addressed.
|
|
243
|
+
* @returns {RequestInit} an init carrying its method, body, and transfer flags.
|
|
244
|
+
*/
|
|
245
|
+
function requestInitOf(request) {
|
|
246
|
+
return {
|
|
247
|
+
method: request.method,
|
|
248
|
+
...(request.body === null || request.method === 'GET' || request.method === 'HEAD' ? {} : { body: request.body }),
|
|
249
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
250
|
+
credentials: request.credentials,
|
|
251
|
+
redirect: request.redirect,
|
|
252
|
+
referrer: request.referrer,
|
|
253
|
+
integrity: request.integrity,
|
|
254
|
+
keepalive: request.keepalive,
|
|
255
|
+
mode: request.mode,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Install the relay fence and expose its endpoint choice as a settings section.
|
|
261
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin's context.
|
|
262
|
+
* @param {ReturnType<typeof Config>} config - resolved entry configuration.
|
|
263
|
+
*/
|
|
264
|
+
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
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
ctx.effect(() => {
|
|
279
|
+
const previous = globalThis.fetch
|
|
280
|
+
if (typeof previous !== 'function') {
|
|
281
|
+
ctx.logger.warn('llm-agentrouter: no global fetch to fence; relay requests will be unroutable and rejected')
|
|
282
|
+
return () => {}
|
|
283
|
+
}
|
|
284
|
+
const fenced = fenceFetch(previous, () => current())
|
|
285
|
+
globalThis.fetch = fenced
|
|
286
|
+
return () => {
|
|
287
|
+
// Restore only what this plugin installed: a later wrapper layered on top
|
|
288
|
+
// owns the global now, and clobbering it would drop that one's rewrite.
|
|
289
|
+
if (globalThis.fetch === fenced) globalThis.fetch = previous
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
if (config.announce) {
|
|
294
|
+
const table = routingTable(config)
|
|
295
|
+
ctx.logger.info(
|
|
296
|
+
'llm-agentrouter: endpoint %c (%c), sending %c',
|
|
297
|
+
config.endpoint,
|
|
298
|
+
table.get(config.sentinel.trim().toLowerCase()) ?? 'unrouted',
|
|
299
|
+
config.userAgent,
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { AGENTROUTER_SETTINGS_NAMESPACE, Config, apply, name }
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lynn123411/dsh-llm-agentrouter",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AgentRouter 中转聚合:一条 pi-ai 路由承载多模型 + 国内/国际端点一键切换(设置卡片)+ 出站请求 User-Agent/403-配额改写围栏。aqiu817/dsh-llm-agentrouter 的 alpha.5 兼容分叉。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"cordis",
|
|
9
|
+
"llm",
|
|
10
|
+
"agentrouter"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tttnny/my-dsh#readme",
|
|
13
|
+
"bugs": "https://github.com/tttnny/my-dsh/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/tttnny/my-dsh.git",
|
|
17
|
+
"directory": "plugins/dsh-llm-agentrouter"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "lynn123411",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "lib/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./lib/index.js",
|
|
25
|
+
"./client": "./lib/client.js",
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
28
|
+
},
|
|
29
|
+
"dsh": {
|
|
30
|
+
"bundle": {
|
|
31
|
+
"patch": "./cordis.patch.yml"
|
|
32
|
+
},
|
|
33
|
+
"client": {
|
|
34
|
+
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
37
|
+
],
|
|
38
|
+
"platform": "web"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"lib/",
|
|
43
|
+
"cordis.patch.yml",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node --test test/*.test.mjs"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@deepseek-ai/cordis": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"js-yaml": "^5.4.1",
|
|
66
|
+
"react": "18.3.1",
|
|
67
|
+
"react-test-renderer": "18.3.1"
|
|
68
|
+
}
|
|
69
|
+
}
|