@aiwayds/dsh-llm-proxy 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/lib/match.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Origin matcher for the LLM proxy routing list.
3
+ *
4
+ * A match pattern is one of:
5
+ * - a full origin ("https://api.deepseek.org") — case-insensitive exact match
6
+ * against the request origin;
7
+ * - a host authority ("api.deepseek.org", "api.deepseek.org:8443") — matches
8
+ * the host exactly, or with a leading `*.` wildcard also any subdomain and
9
+ * the bare domain itself ("*.volces.com" matches "volces.com" and
10
+ * "a.volces.com"). An explicit port in the pattern must equal the request
11
+ * origin's port (scheme defaults count: https=443, http=80).
12
+ *
13
+ * All comparisons are lowercase; matching is purely lexical — no DNS, no I/O.
14
+ * Hostnames are normalized the way WHATWG URL parsing normalizes request
15
+ * origins: lowercase ASCII with IDN labels converted to punycode, so a
16
+ * pattern and an origin written in either the unicode or the xn-- form of
17
+ * the same domain compare equal.
18
+ */
19
+ const DEFAULT_PORTS = new Map([
20
+ ['https:', '443'],
21
+ ['http:', '80'],
22
+ ]);
23
+ /**
24
+ * Canonical origin of a full-origin URL: lowercase scheme/host with IDN
25
+ * labels punycoded; `undefined` when the value does not parse.
26
+ */
27
+ function canonicalOrigin(value) {
28
+ try {
29
+ return new URL(value).origin;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /**
36
+ * Normalize a bare hostname (no port, no scheme) the way WHATWG URL parsing
37
+ * would for a request origin: lowercase ASCII, IDN labels to punycode.
38
+ * Falls back to plain lowercasing when the value cannot be parsed.
39
+ */
40
+ function normalizeHost(host) {
41
+ try {
42
+ return new URL(`http://${host}`).hostname;
43
+ }
44
+ catch {
45
+ return host.toLowerCase();
46
+ }
47
+ }
48
+ /** Split "host[:port]" into host and an optional numeric port. */
49
+ function splitAuthority(authority) {
50
+ const colon = authority.lastIndexOf(':');
51
+ if (colon === -1)
52
+ return { host: authority, port: undefined };
53
+ const port = authority.slice(colon + 1);
54
+ if (/^\d+$/.test(port))
55
+ return { host: authority.slice(0, colon), port };
56
+ // Not a numeric suffix (e.g. IPv6 literals); treat the whole token as host.
57
+ return { host: authority, port: undefined };
58
+ }
59
+ /** Wildcard-aware host comparison: "*.base" matches base and subdomains. */
60
+ function hostMatches(pattern, host) {
61
+ if (pattern.startsWith('*.')) {
62
+ const base = pattern.slice(2);
63
+ return base.length > 0 && (host === base || host.endsWith(`.${base}`));
64
+ }
65
+ return host === pattern;
66
+ }
67
+ /**
68
+ * Whether a request origin hits a routing-list match pattern.
69
+ * @param match - pattern from the llmProxy list entry.
70
+ * @param origin - request origin, e.g. "https://api.example.com" or
71
+ * "https://api.example.com:8443"; a bare host[:port] is tolerated.
72
+ */
73
+ export function matchOrigin(match, origin) {
74
+ const normalizedMatch = match.trim().toLowerCase();
75
+ if (!normalizedMatch)
76
+ return false;
77
+ const trimmedOrigin = origin.trim();
78
+ if (normalizedMatch.includes('://')) {
79
+ // Full-origin form: compare canonical origins, so scheme/host casing and
80
+ // punycode vs unicode IDN spellings do not affect the outcome.
81
+ const patternOrigin = canonicalOrigin(normalizedMatch);
82
+ return patternOrigin !== undefined && patternOrigin === canonicalOrigin(trimmedOrigin);
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = new URL(trimmedOrigin.includes('://') ? trimmedOrigin : `http://${trimmedOrigin}`);
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ const { host: patternHost, port: patternPort } = splitAuthority(normalizedMatch);
92
+ if (!patternHost)
93
+ return false;
94
+ if (patternPort !== undefined) {
95
+ const actualPort = parsed.port || DEFAULT_PORTS.get(parsed.protocol) || '';
96
+ if (patternPort !== actualPort)
97
+ return false;
98
+ }
99
+ // parsed.hostname is already punycode-normalized by URL parsing; normalize
100
+ // the pattern host the same way so both sides compare symmetrically.
101
+ return hostMatches(normalizeHost(patternHost), parsed.hostname);
102
+ }
103
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The routing dispatcher: forwards every request to the first llmProxy entry
3
+ * whose match pattern hits the request origin, or to a shared fallback
4
+ * (system) dispatcher otherwise. Priority is decided here; the fallback
5
+ * itself — an EnvHttpProxyAgent for env semantics, or a direct Agent — is
6
+ * supplied by the caller.
7
+ */
8
+ import * as undici from 'undici';
9
+ /** One configured routing entry: origin pattern plus its proxy URL. */
10
+ export interface LlmProxyEntry {
11
+ readonly match: string;
12
+ readonly proxy: string;
13
+ }
14
+ /**
15
+ * Route decision for an origin: either the first matching llmProxy entry, or
16
+ * the shared system fallback.
17
+ */
18
+ export type RouteDecision = {
19
+ readonly kind: 'llm';
20
+ readonly index: number;
21
+ readonly proxyUrl: string;
22
+ } | {
23
+ readonly kind: 'system';
24
+ };
25
+ export declare function withUndiciErrorListener<T extends undici.Dispatcher>(dispatcher: T): T;
26
+ /**
27
+ * Which route an origin takes under the given entry list.
28
+ * @param entries - ordered llmProxy list; first hit wins.
29
+ * @param origin - request origin string.
30
+ */
31
+ export declare function resolveRoute(entries: readonly LlmProxyEntry[], origin: string): RouteDecision;
32
+ /** Injectable seams for tests and custom construction. */
33
+ export interface RouterDeps {
34
+ /** Fallback for requests no llmProxy entry matches. Defaults to a direct Agent. */
35
+ systemDispatcher?: undici.Dispatcher;
36
+ /** Creates the per-proxy dispatcher; defaults to ProxyAgent. Overridable in tests. */
37
+ proxyFactory?: (proxyUrl: string) => undici.Dispatcher;
38
+ }
39
+ /**
40
+ * Create the process-global routing dispatcher.
41
+ * @param entries - ordered llmProxy list; first hit wins.
42
+ * @param deps - optional injectable system dispatcher / proxy factory.
43
+ */
44
+ export declare function createProxyRouterDispatcher(entries: readonly LlmProxyEntry[], deps?: RouterDeps): undici.Dispatcher;
package/lib/router.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The routing dispatcher: forwards every request to the first llmProxy entry
3
+ * whose match pattern hits the request origin, or to a shared fallback
4
+ * (system) dispatcher otherwise. Priority is decided here; the fallback
5
+ * itself — an EnvHttpProxyAgent for env semantics, or a direct Agent — is
6
+ * supplied by the caller.
7
+ */
8
+ import { EventEmitter } from 'node:events';
9
+ import * as undici from 'undici';
10
+ import { matchOrigin } from "./match.js";
11
+ const ignoreUndiciDispatcherError = (_error) => { };
12
+ // Undici can emit an internal Client "error" while terminating a mid-stream
13
+ // fetch body. The body stream still rejects through reader.read(); this
14
+ // listener only prevents EventEmitter's unhandled "error" special case from
15
+ // crashing the host process. Applied to every dispatcher this plugin creates.
16
+ export function withUndiciErrorListener(dispatcher) {
17
+ if (dispatcher instanceof EventEmitter) {
18
+ EventEmitter.prototype.on.call(dispatcher, 'error', ignoreUndiciDispatcherError);
19
+ }
20
+ return dispatcher;
21
+ }
22
+ /**
23
+ * Which route an origin takes under the given entry list.
24
+ * @param entries - ordered llmProxy list; first hit wins.
25
+ * @param origin - request origin string.
26
+ */
27
+ export function resolveRoute(entries, origin) {
28
+ const index = entries.findIndex((entry) => matchOrigin(entry.match, origin));
29
+ if (index === -1)
30
+ return { kind: 'system' };
31
+ return { kind: 'llm', index, proxyUrl: entries[index].proxy };
32
+ }
33
+ function originToString(origin) {
34
+ if (origin instanceof URL)
35
+ return origin.origin;
36
+ if (typeof origin === 'string')
37
+ return origin;
38
+ return '';
39
+ }
40
+ class ProxyRouterDispatcher extends undici.Dispatcher {
41
+ #entries;
42
+ #systemDispatcher;
43
+ #proxyFactory;
44
+ #proxies = new Map();
45
+ constructor(entries, deps) {
46
+ super();
47
+ this.#entries = entries;
48
+ this.#systemDispatcher = deps.systemDispatcher ?? withUndiciErrorListener(new undici.Agent());
49
+ this.#proxyFactory = deps.proxyFactory ?? ((proxyUrl) => withUndiciErrorListener(new undici.ProxyAgent(proxyUrl)));
50
+ }
51
+ dispatch(options, handler) {
52
+ const decision = resolveRoute(this.#entries, originToString(options.origin));
53
+ const target = decision.kind === 'llm'
54
+ ? this.#proxyFor(decision.proxyUrl)
55
+ : this.#systemDispatcher;
56
+ return target.dispatch(options, handler);
57
+ }
58
+ #proxyFor(proxyUrl) {
59
+ let proxy = this.#proxies.get(proxyUrl);
60
+ if (!proxy) {
61
+ proxy = this.#proxyFactory(proxyUrl);
62
+ this.#proxies.set(proxyUrl, proxy);
63
+ }
64
+ return proxy;
65
+ }
66
+ /** All dispatchers owned by this router, including the system fallback. */
67
+ #owned() {
68
+ return [this.#systemDispatcher, ...this.#proxies.values()];
69
+ }
70
+ async close() {
71
+ await Promise.all(this.#owned().map((dispatcher) => dispatcher.close()));
72
+ }
73
+ async destroy() {
74
+ await Promise.all(this.#owned().map((dispatcher) => dispatcher.destroy()));
75
+ }
76
+ }
77
+ /**
78
+ * Create the process-global routing dispatcher.
79
+ * @param entries - ordered llmProxy list; first hit wins.
80
+ * @param deps - optional injectable system dispatcher / proxy factory.
81
+ */
82
+ export function createProxyRouterDispatcher(entries, deps = {}) {
83
+ // The router itself performs no I/O and currently emits no 'error', but it
84
+ // is an EventEmitter; attach the guard here so the factory exit always
85
+ // hands out a protected dispatcher even if that ever changes.
86
+ return withUndiciErrorListener(new ProxyRouterDispatcher(entries, deps));
87
+ }
88
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@aiwayds/dsh-llm-proxy",
3
+ "version": "0.2.0",
4
+ "description": "dsh plugin: SYSTEM proxy (env) + LLM traffic-splitting proxy via a process-global undici routing dispatcher",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "dsh",
11
+ "dsh-plugin",
12
+ "plugin",
13
+ "proxy",
14
+ "undici",
15
+ "deepseek-harness",
16
+ "llm"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/fan56/dsh-llm-proxy.git"
21
+ },
22
+ "homepage": "https://github.com/fan56/dsh-llm-proxy",
23
+ "engines": {
24
+ "node": ">=22.19.0"
25
+ },
26
+ "files": [
27
+ "lib",
28
+ "cordis.patch.yml",
29
+ "src",
30
+ "skills"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "check": "tsc --noEmit -p tsconfig.json",
35
+ "pretest": "npm run build",
36
+ "prepare": "npm run build",
37
+ "test": "node --test test/match.test.mjs test/router.test.mjs test/config.test.mjs test/skill.test.mjs test/dispose.test.mjs test/e2e.test.mjs"
38
+ },
39
+ "dependencies": {
40
+ "undici": "8.9.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@deepseek-ai/cordis": "4.0.1",
44
+ "@deepseek-ai/schemastery": "3.18.1",
45
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-skill": "0.1.1-rc.2"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@deepseek-ai/cordis": {
50
+ "optional": true
51
+ },
52
+ "@deepseek-ai/schemastery": {
53
+ "optional": true
54
+ },
55
+ "@deepseek-ai/dsh-skill": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@deepseek-ai/cordis": "4.0.1",
61
+ "@deepseek-ai/schemastery": "3.18.1",
62
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
63
+ "@deepseek-ai/dsh-skill": "0.1.1-rc.2",
64
+ "@types/node": "^24.0.0",
65
+ "typescript": "^5.9.0"
66
+ },
67
+ "dsh": {
68
+ "bundle": {
69
+ "patch": "./cordis.patch.yml"
70
+ }
71
+ },
72
+ "author": "fliu56",
73
+ "publishConfig": {
74
+ "access": "public"
75
+ }
76
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: dsh-llm-proxy
3
+ description: "dsh 出站代理 / LLM 分流插件(@aiwayds/dsh-llm-proxy)使用指南。凡给 dsh 插件配置 HTTP 代理、LLM 出站分流,或排查代理网络问题时先读本指南:settings.yaml 顶层 `dsh-llm-proxy:` 段(enabled/systemMode/llmProxy)、llmProxy match 规则、代理 407、CONNECT 挂起、socks5 报错(SOCKS 不支持)、NODE_USE_ENV_PROXY 等价替代。触发词:dsh 代理、HTTP 代理、LLM 分流、llmProxy、llm-proxy、407、CONNECT 挂起、SOCKS、HTTPS_PROXY、NO_PROXY、出站代理、NODE_USE_ENV_PROXY。"
4
+ ---
5
+
6
+ # dsh-llm-proxy 使用指南(LLM 分流代理)
7
+
8
+ > dsh 插件:**SYSTEM proxy + LLM 分流代理**。进程级 undici 路由 Dispatcher 把 LLM 流量
9
+ > 按配置列表分流到指定代理,其余流量走系统代理(启动环境变量),都没配则直连。
10
+
11
+ ## 配置入口(settings.yaml 顶层命名空间段)
12
+
13
+ 在 `~/.dsh/settings.yaml` 写**顶层 `dsh-llm-proxy:` 段**(与插件 id 一致,同 harness 内置
14
+ 插件 `llm-deepseek:` 段机制):
15
+
16
+ ```yaml
17
+ dsh-llm-proxy:
18
+ enabled: true # 总开关,默认 true;false 时完全不动全局
19
+ systemMode: env # 'env'(默认,读环境变量)| 'off'(全部直连,仅 llmProxy 生效)
20
+ llmProxy: # 匹配列表,按顺序首条命中生效
21
+ - match: "api.deepseek.org" # host 精确匹配(含任意端口)
22
+ proxy: "http://127.0.0.1:7890"
23
+ - match: "*.volces.com" # 通配:volces.com 本身 + 任意子域
24
+ proxy: "http://127.0.0.1:7891"
25
+ - match: "https://api.openai.com" # 完整 origin 精确匹配(大小写不敏感)
26
+ proxy: "http://127.0.0.1:7892"
27
+ ```
28
+
29
+ 要点:
30
+
31
+ - 编辑 **热生效**:无需重启,插件拆旧 router 按新配置重建。
32
+ - bundle entry config(cordis.patch.yml 的 `config:`)是 base 层;settings.yaml 段覆盖其上
33
+ 同名键。未挂载 settings 服务的宿主自动回退 entry config,插件照常工作。
34
+
35
+ ## 优先级语义
36
+
37
+ ```
38
+ LLM 列表命中 > SYSTEM proxy(env 语义)> 直连
39
+ ```
40
+
41
+ - 命中 `llmProxy` 条目 → 走该条目指定的代理(undici `ProxyAgent`);**命中条目直接进指定
42
+ 代理,不读 env `no_proxy`**。
43
+ - 未命中 → 共享 `EnvHttpProxyAgent`:`HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` /
44
+ `NO_PROXY`(大小写均兼容);`systemMode: off` 时未命中流量一律直连。
45
+ - 环境变量**只读不写**:插件不改 `process.env.HTTP_PROXY`(对已启动进程无意义)。
46
+ - 与 `NODE_USE_ENV_PROXY` 的关系:等价替代——无需该变量、无需重启,对新请求即时生效,
47
+ 且额外提供 LLM 分流。
48
+
49
+ ## match 规则
50
+
51
+ | 写法 | 含义 |
52
+ |------|------|
53
+ | `api.example.com` | host 精确匹配,**任意端口**都算命中 |
54
+ | `api.example.com:8443` | host + 显式端口必须相等(https 默认 443 / http 默认 80 会补全比较) |
55
+ | `*.example.com` | `example.com` 本身及任意层级子域 |
56
+ | `https://api.example.com` | 完整 origin 精确匹配(scheme 也要一致) |
57
+
58
+ 匹配全部大小写不敏感、纯词法比较(无 DNS/IO);IDN 域名两侧统一归一化为 punycode。
59
+
60
+ **proxy 仅支持 http:// 与 https://**(undici ProxyAgent 限制);`socks5://` 等在启动校验
61
+ 时直接报错(fail-fast):**SOCKS 不支持**。
62
+
63
+ ## 排障清单
64
+
65
+ 1. **完全不生效** → 先查顶层键名:必须是 `dsh-llm-proxy:`;写成 `llm-proxy:` 等其他键
66
+ 不会被读到,**静默失效**。
67
+ 2. **启动即报错** → proxy 用了 `socks5://` 等不支持的 schema;换 http(s) 代理。
68
+ 3. **走了代理但目标返回 407** → 代理自身认证问题,检查代理凭证;本插件日志/error
69
+ message 中 proxy URL 的 userinfo 一律脱敏为 `***@host`。
70
+ 4. **疑似代理问题** → 先用 `curl -x <proxy> <目标URL>` 复验代理本身通不通,通了再查 dsh
71
+ 侧配置;随后按仓库 README「手动 e2e 验证」观察代理访问日志确认分流是否按预期。