dsh-router-core 0.4.6 → 0.4.8
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 +156 -76
- package/cordis.patch.yml +2 -7
- package/docs/ext.md +4 -18
- package/docs/protocols.md +198 -0
- package/lib/client-registry.js +193 -668
- package/lib/client.js +193 -668
- package/lib/ext-prompt/index.js +202 -32
- package/lib/index.js +2805 -547
- package/lib/types/ext/contract.d.ts +20 -1
- package/lib/types/ext-prompt/content.d.ts +18 -1
- package/lib/types/ext-prompt/index.d.ts +7 -9
- package/lib/types/ext-prompt/mount.d.ts +14 -3
- package/lib/types/ext-prompt/plugin.d.ts +13 -5
- package/lib/types/ext-prompt/render.d.ts +14 -3
- package/lib/types/ext-prompt/sections.d.ts +95 -0
- package/lib/types/protocol/anthropic.d.ts +35 -0
- package/lib/types/protocol/chat.d.ts +25 -0
- package/lib/types/protocol/entry.d.ts +137 -0
- package/lib/types/protocol/gemini.d.ts +57 -0
- package/lib/types/protocol/index.d.ts +13 -0
- package/lib/types/protocol/responses.d.ts +41 -0
- package/lib/types/protocol/sse.d.ts +41 -0
- package/lib/types/protocol/stream.d.ts +92 -0
- package/lib/types/protocol/types.d.ts +224 -0
- package/lib/types/router/index.d.ts +42 -1
- package/lib/types/router/types.d.ts +19 -0
- package/lib/types/shared.d.ts +11 -0
- package/lib/types/suppliers/contract.d.ts +8 -0
- package/lib/types/suppliers/registry.d.ts +0 -1
- package/package.json +6 -15
- package/src/client/ExtDetail.tsx +26 -18
- package/src/client/group-controls.test.ts +53 -0
- package/src/client/group-controls.ts +43 -0
- package/src/client/index.tsx +0 -17
- package/src/client/router-components.test.ts +1 -1
- package/src/client/router.css +23 -355
- package/src/ext/contract.ts +20 -1
- package/src/ext-prompt/assembly.test.ts +63 -0
- package/src/ext-prompt/content.ts +25 -22
- package/src/ext-prompt/ext-prompt.test.ts +41 -31
- package/src/ext-prompt/index.ts +57 -11
- package/src/ext-prompt/mount.test.ts +18 -1
- package/src/ext-prompt/mount.ts +24 -9
- package/src/ext-prompt/plugin.ts +67 -6
- package/src/ext-prompt/render.test.ts +1 -1
- package/src/ext-prompt/render.ts +17 -6
- package/src/ext-prompt/section-controls.test.ts +148 -0
- package/src/ext-prompt/section-filter.test.ts +140 -0
- package/src/ext-prompt/sections.test.ts +123 -0
- package/src/ext-prompt/sections.ts +193 -0
- package/src/ext-route.test.ts +16 -0
- package/src/index.ts +109 -1
- package/src/protocol/anthropic.ts +662 -0
- package/src/protocol/chat.ts +272 -0
- package/src/protocol/entry.ts +440 -0
- package/src/protocol/gemini.ts +548 -0
- package/src/protocol/index.ts +31 -0
- package/src/protocol/protocol.test.ts +728 -0
- package/src/protocol/responses.ts +551 -0
- package/src/protocol/sse.ts +94 -0
- package/src/protocol/stream.ts +195 -0
- package/src/protocol/types.ts +249 -0
- package/src/protocol/wire-e2e.test.ts +208 -0
- package/src/protocol/wiring.test.ts +225 -0
- package/src/router/index.ts +144 -26
- package/src/router/models-cache.test.ts +5 -6
- package/src/router/supplier-switch.test.ts +1 -1
- package/src/router/types.ts +19 -0
- package/src/shared.ts +11 -0
- package/src/suppliers/contract.ts +8 -0
- package/src/suppliers/registry.ts +0 -71
- package/lib/contract-D4BWZ7Xe.js +0 -13
- package/lib/ext-test/index.js +0 -36
- package/lib/ext-test/locale/en.json +0 -6
- package/lib/ext-test/locale/zh.json +0 -6
- package/lib/ext-test/package.json +0 -5
- package/lib/types/ext-test/index.d.ts +0 -21
- package/lib/types/ext-test/plugin.d.ts +0 -23
- package/lib/types/suppliers/probe.d.ts +0 -160
- package/src/client/ExtTestPanel.tsx +0 -392
- package/src/client/comp-button-scope.test.ts +0 -58
- package/src/client/comp-panel-layout.test.ts +0 -58
- package/src/client/ext-panels.test.ts +0 -44
- package/src/client/ext-panels.ts +0 -49
- package/src/client/model-choice.test.ts +0 -41
- package/src/client/model-choice.ts +0 -37
- package/src/ext-test/ext-test.test.ts +0 -60
- package/src/ext-test/index.ts +0 -35
- package/src/ext-test/locale/en.json +0 -6
- package/src/ext-test/locale/zh.json +0 -6
- package/src/ext-test/plugin.ts +0 -34
- package/src/ext-test/row.json +0 -5
- package/src/suppliers/probe.test.ts +0 -266
- package/src/suppliers/probe.ts +0 -475
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE 帧的编解码小工具 —— 四种协议共用。
|
|
3
|
+
*
|
|
4
|
+
* 只做三件事:把字节流按 SSE 规则切成「一帧」、把一帧拆成 `event:`/`data:`
|
|
5
|
+
* 两部分、把若干帧拼回字节流。**不**知道任何协议语义 —— 那在各协议的
|
|
6
|
+
* `ProtocolCodec` 里。
|
|
7
|
+
*
|
|
8
|
+
* 为什么不用现成库:宿主没有 SSE 依赖,而我们要的能力只有「分帧」这一件,
|
|
9
|
+
* 引入一个包换三十行代码不值;且分帧必须跨 chunk 边界缓冲(上游一帧被拆在
|
|
10
|
+
* 两个 TCP 段里是常态),现成库普遍假设整帧到达。
|
|
11
|
+
*/
|
|
12
|
+
/** 一帧 SSE。 */
|
|
13
|
+
export interface SseFrame {
|
|
14
|
+
/** `event:` 行的值(没有该行时为空串)。 */
|
|
15
|
+
event: string;
|
|
16
|
+
/** `data:` 各行的拼接(多行 `data:` 用 `\n` 连接,SSE 规范如此)。 */
|
|
17
|
+
data: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 增量分帧器:喂字节进去,吐出完整帧。
|
|
21
|
+
*
|
|
22
|
+
* 用类而不是纯函数,是因为「半个帧」必须留在实例里 —— 每次调用都是无状态的
|
|
23
|
+
* 函数做不到这件事,而每个流都要一个独立缓冲。
|
|
24
|
+
*/
|
|
25
|
+
export declare class SseSplitter {
|
|
26
|
+
private buf;
|
|
27
|
+
/** 喂入一块字节,返回其中包含的完整帧(可能 0..n 个)。 */
|
|
28
|
+
push(bytes: Uint8Array): SseFrame[];
|
|
29
|
+
/** 流结束时冲刷:把残留的、不以空行结尾的尾帧也吐出来(上游不发尾空行是有的)。 */
|
|
30
|
+
end(): SseFrame[];
|
|
31
|
+
/** 把已缓冲的完整帧全部切出来。 */
|
|
32
|
+
private drain;
|
|
33
|
+
}
|
|
34
|
+
/** 把一帧原文拆成 `event` + `data`。空 data 的帧(注释/keep-alive)返回空 data。 */
|
|
35
|
+
export declare function parseFrame(raw: string): SseFrame;
|
|
36
|
+
/** 拼一帧字节(`data:` 帧;不带 `event:`)。 */
|
|
37
|
+
export declare function dataFrame(data: string): string;
|
|
38
|
+
/** 拼一帧字节(带 `event:`)。 */
|
|
39
|
+
export declare function eventFrame(event: string, data: string): string;
|
|
40
|
+
/** 把若干帧原文拼成字节。 */
|
|
41
|
+
export declare function framesToBytes(frames: string[]): Uint8Array;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流的协议转码 —— 把「一种协议的 SSE 字节流」翻译成「另一种协议的 SSE 字节流」。
|
|
3
|
+
*
|
|
4
|
+
* ════════════════════════════════════════════════════════════════════
|
|
5
|
+
* 为什么拆成 transform + state 两层
|
|
6
|
+
* ════════════════════════════════════════════════════════════════════
|
|
7
|
+
* 逐帧翻译需要一个**有状态**的累加器(usage 分散在多帧、工具名要先于参数、
|
|
8
|
+
* 收尾帧要在流末补),直接写成一个 200 行的 async generator 会把「状态」和
|
|
9
|
+
* 「翻译」搅在一起。所以:
|
|
10
|
+
* - `StreamState`:一次流的有状态累加(纯内存,可单测,无 IO)。
|
|
11
|
+
* - `translateStream`:把它接到字节流上(只管分帧/拼帧/生命周期)。
|
|
12
|
+
*
|
|
13
|
+
* 同一条流里**同时只有一个转码器**(上游协议固定),但出站协议由客户端决定 ——
|
|
14
|
+
* 所以是「一个 decode + 一个 encode」,而不是各自独立的两趟。
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ **不缓冲、不聚合**:每个事件到立刻吐对应帧。首字节延迟与上游一致,
|
|
17
|
+
* 不会因为转码多等一个 chunk(Gemini 的工具调用除外:它的参数是一次性整块,
|
|
18
|
+
* 天然没有增量可言)。
|
|
19
|
+
*/
|
|
20
|
+
import type { ProtocolId } from './types.ts';
|
|
21
|
+
/** 一次流式转码的有状态累加。 */
|
|
22
|
+
export declare class StreamState {
|
|
23
|
+
/** 上游协议(解码用)。 */
|
|
24
|
+
readonly from: ProtocolId;
|
|
25
|
+
/** 客户端协议(编码用)。 */
|
|
26
|
+
readonly to: ProtocolId;
|
|
27
|
+
/** 上游 usage(跨帧合并,字段级 max —— 见 mergeUsage 的注释)。 */
|
|
28
|
+
private usage;
|
|
29
|
+
/** 已经吐过起始帧没有(起始帧只能在流最前发一次)。 */
|
|
30
|
+
private started;
|
|
31
|
+
/** 收尾原因(用于补 endFrames;缺省 stop)。 */
|
|
32
|
+
private finish;
|
|
33
|
+
/** 模型名(回显进 startFrames/endFrames,如 Anthropic 的 message_start)。 */
|
|
34
|
+
private model;
|
|
35
|
+
/**
|
|
36
|
+
* 解码侧会话与编码侧会话,各一份。
|
|
37
|
+
*
|
|
38
|
+
* **必须每帧复用同一个**:Gemini 的工具名↔index 映射就存在这里;每帧新建
|
|
39
|
+
* 会让 `tool-args` 找不到 `tool-start` 给的名字(参数被丢),而放模块级会
|
|
40
|
+
* 跨请求串名。会话在 `StreamState` 构造时建、随流结束丢弃 —— 生命周期与
|
|
41
|
+
* 「一条流」严格对齐。
|
|
42
|
+
*/
|
|
43
|
+
private readonly decodeSession;
|
|
44
|
+
private readonly encodeSession;
|
|
45
|
+
constructor(from: ProtocolId, to: ProtocolId, model?: string);
|
|
46
|
+
/** 当前累计的用量。 */
|
|
47
|
+
currentUsage(): {
|
|
48
|
+
promptTokens: number;
|
|
49
|
+
completionTokens: number;
|
|
50
|
+
cachedTokens: number;
|
|
51
|
+
};
|
|
52
|
+
/** 把上游一帧原文(`data:` 载荷)翻成出站帧原文列表。 */
|
|
53
|
+
translateFrame(frame: string, event?: string): string[];
|
|
54
|
+
/**
|
|
55
|
+
* 上游流结束:补发收尾帧。
|
|
56
|
+
*
|
|
57
|
+
* **顺序固定**:finish → usage → endFrames。各协议的收尾帧都要求这个次序
|
|
58
|
+
* (Anthropic 的 message_delta 必须在 message_stop 前,Responses 的
|
|
59
|
+
* `response.completed` 是最后一件且自带 usage)。
|
|
60
|
+
*
|
|
61
|
+
* `endFrames` 拿得到 finish 与 usage —— 有的协议(Responses)只有收尾帧
|
|
62
|
+
* 里才有地方放 usage 与「是否被截断」。
|
|
63
|
+
*/
|
|
64
|
+
finishStream(): string[];
|
|
65
|
+
/**
|
|
66
|
+
* 带当前用量派生的编码会话。
|
|
67
|
+
*
|
|
68
|
+
* 不能只在 `encodeSession` 存在时才派生:`createSession` 是**可选**的
|
|
69
|
+
* (只有 Gemini 这类有跨帧映射的协议实现它),Anthropic 没有 —— 按
|
|
70
|
+
* `encodeSession === undefined` 就返回 undefined 会让 usage 永远送不进去
|
|
71
|
+
* (2026-10-21 实测:output_tokens 恒为 0)。所以无论如何都造一个,
|
|
72
|
+
* 有 state 就带上。
|
|
73
|
+
*/
|
|
74
|
+
private finishSessionWithUsage;
|
|
75
|
+
/**
|
|
76
|
+
* usage 合并:字段级 max。
|
|
77
|
+
*
|
|
78
|
+
* 与 `router/usage-tokens.ts` 的 `mergeUsage` 同规则(那里照 9router):
|
|
79
|
+
* Claude 把输入用量放 `message_start`、输出放 `message_delta`,两帧都给
|
|
80
|
+
* `usage` 字段,**覆盖**会让后一帧把前面的输入清零。
|
|
81
|
+
*/
|
|
82
|
+
private mergeUsage;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 把上游 SSE 字节流转成客户端协议的 SSE 字节流。
|
|
86
|
+
*
|
|
87
|
+
* 生命周期:上游正常读完 / 上游 error / 消费方取消,三条路径都要**释放上游
|
|
88
|
+
* reader** 并把取消传下去 —— 否则上游 fetch body 永不 cancel,长跑会漏连接
|
|
89
|
+
* (`router/index.ts` 的 `normalizeSSEStream` 里同一个坑,判据在
|
|
90
|
+
* `router/disconnect-leak.test.ts`)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function translateStream(upstream: ReadableStream<Uint8Array>, state: StreamState): ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议矩阵的**规范形态**(canonical form)与编解码契约。
|
|
3
|
+
*
|
|
4
|
+
* ════════════════════════════════════════════════════════════════════
|
|
5
|
+
* 为什么内部只留一种形态
|
|
6
|
+
* ════════════════════════════════════════════════════════════════════
|
|
7
|
+
* 四种协议两两互转是 4×3 = 12 条路径,逐条实现必然长出一打互相不一致的
|
|
8
|
+
* 实现。收敛成「全部先转成同一个规范形态、再从规范形态转出去」之后:
|
|
9
|
+
*
|
|
10
|
+
* 外来请求 ──decode──┐ ┌──encode── 外来响应
|
|
11
|
+
* ├─ 规范形态 ────┤
|
|
12
|
+
* 供应商请求 ◀─encode─┘ └─decode──▶ 供应商响应
|
|
13
|
+
*
|
|
14
|
+
* 只需要 N 个协议模块(N=4),而不是 N² 条翻译路径;新增第五种协议只是再加
|
|
15
|
+
* 一个模块,已有路径一行不改。
|
|
16
|
+
*
|
|
17
|
+
* 规范形态选 **OpenAI Chat Completions**(请求体)与**一组扁平事件**(响应流),
|
|
18
|
+
* 不是随便挑的:供应商契约(`suppliers/contract.ts` 的 `chatOnce`)今天就是
|
|
19
|
+
* 「吃 Chat Completions 请求体、吐 OpenAI SSE」,选别的形态等于把每个已装
|
|
20
|
+
* 供应商插件都改一遍。
|
|
21
|
+
*
|
|
22
|
+
* 天花板:规范形态只覆盖**能表达的部分**。各家私有的、没有对应概念的东西
|
|
23
|
+
* (Anthropic 的 `cache_control`、Responses 的 `previous_response_id`、
|
|
24
|
+
* Gemini 的 `safetySettings`)在跨协议时会被**丢掉**,不是被翻译。每条丢弃
|
|
25
|
+
* 在对应模块的注释里点名。
|
|
26
|
+
*/
|
|
27
|
+
/** 四种协议。`chat` 是规范形态本身,因此它的编解码是恒等变换。 */
|
|
28
|
+
export type ProtocolId = 'chat' | 'anthropic' | 'responses' | 'gemini';
|
|
29
|
+
/** 所有协议 id(顺序 = 文档与面板展示顺序)。 */
|
|
30
|
+
export declare const PROTOCOL_IDS: readonly ProtocolId[];
|
|
31
|
+
/** 宽松 JSON 对象(外来请求体一律不可信,逐字段校验)。 */
|
|
32
|
+
export type JsonObject = Record<string, unknown>;
|
|
33
|
+
/** 编解码时需要知道的上下文。 */
|
|
34
|
+
export interface CodecContext {
|
|
35
|
+
/**
|
|
36
|
+
* 请求里声明的模型名(已剥掉本插件的 `alias/` 前缀)。
|
|
37
|
+
*
|
|
38
|
+
* 有些协议把模型名放在 URL 路径里(Gemini 的
|
|
39
|
+
* `/v1beta/models/<model>:generateContent`),有些放在请求体里;模块统一从
|
|
40
|
+
* 这里取,不各自去猜。
|
|
41
|
+
*/
|
|
42
|
+
model: string;
|
|
43
|
+
/** 客户端是否要流式。 */
|
|
44
|
+
stream: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* 收尾原因(仅 `endFrames` 会读)。
|
|
47
|
+
*
|
|
48
|
+
* 有些协议的收尾帧要**同时**带状态与用量(Responses 的 `response.completed`
|
|
49
|
+
* 里 `status: 'completed' | 'incomplete'` 决定 Codex 是否认为被截断),
|
|
50
|
+
* 而 `finish` 事件本身在这些协议里没有独立帧可发 —— 于是原因必须通过
|
|
51
|
+
* 上下文传进 `endFrames`,不能只靠「收到过 finish 事件」这个事实。
|
|
52
|
+
*/
|
|
53
|
+
finish?: FinishReason;
|
|
54
|
+
/** 累计用量(仅 `endFrames` 会读,同上:Responses 只在收尾帧给 usage)。 */
|
|
55
|
+
usage?: {
|
|
56
|
+
promptTokens: number;
|
|
57
|
+
completionTokens: number;
|
|
58
|
+
cachedTokens: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 规范形态的**流式事件**。
|
|
63
|
+
*
|
|
64
|
+
* 刻意做得很小:只要四家能无损落进来的都在这,落不进来的都不在。工具调用用
|
|
65
|
+
* `tool-start` + `tool-args` 两段表达(各家都是「先给名字、再给参数增量」,
|
|
66
|
+
* 只有 Gemini 把参数整块给一次 —— 它在 decode 时拆成一段即可)。
|
|
67
|
+
*/
|
|
68
|
+
export type ChatEvent =
|
|
69
|
+
/** 可见文本增量。 */
|
|
70
|
+
{
|
|
71
|
+
type: 'text';
|
|
72
|
+
text: string;
|
|
73
|
+
}
|
|
74
|
+
/** 推理过程增量(`reasoning_content` / `thinking` / `thought`)。 */
|
|
75
|
+
| {
|
|
76
|
+
type: 'reasoning';
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
/** 一个工具调用的开始(名字先到,参数随后)。同一 index 只发一次。 */
|
|
80
|
+
| {
|
|
81
|
+
type: 'tool-start';
|
|
82
|
+
index: number;
|
|
83
|
+
id?: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
}
|
|
86
|
+
/** 工具调用的参数**增量**(JSON 片段,逐段拼接后才是完整参数)。 */
|
|
87
|
+
| {
|
|
88
|
+
type: 'tool-args';
|
|
89
|
+
index: number;
|
|
90
|
+
delta: string;
|
|
91
|
+
}
|
|
92
|
+
/** 收尾原因。一条流最多一个,且必须是最后一个内容事件。 */
|
|
93
|
+
| {
|
|
94
|
+
type: 'finish';
|
|
95
|
+
reason: FinishReason;
|
|
96
|
+
}
|
|
97
|
+
/** 用量。多数上游在最后一帧才给。 */
|
|
98
|
+
| {
|
|
99
|
+
type: 'usage';
|
|
100
|
+
promptTokens: number;
|
|
101
|
+
completionTokens: number;
|
|
102
|
+
cachedTokens: number;
|
|
103
|
+
}
|
|
104
|
+
/** 上游在流内报的错(HTTP 200 + 流内 error 是真实存在的形态)。 */
|
|
105
|
+
| {
|
|
106
|
+
type: 'error';
|
|
107
|
+
message: string;
|
|
108
|
+
};
|
|
109
|
+
/** 收尾原因(规范形态)。 */
|
|
110
|
+
export type FinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error';
|
|
111
|
+
/**
|
|
112
|
+
* 一条流里的编解码会话状态。
|
|
113
|
+
*
|
|
114
|
+
* 刻意做成**不透明的空接口 + 可选**:绝大多数协议(chat / Anthropic /
|
|
115
|
+
* Responses)逐帧无状态,不需要它;只有 Gemini 这类「用名字而非 index 指代
|
|
116
|
+
* 工具」的协议需要跨帧记住名字↔index。让它可选,是为了不给无状态协议强加
|
|
117
|
+
* 一个空对象参数。
|
|
118
|
+
*/
|
|
119
|
+
export interface CodecSession {
|
|
120
|
+
/** 会话内的临时槽位(各协议自己的形状,核心不解读)。 */
|
|
121
|
+
readonly state?: unknown;
|
|
122
|
+
/**
|
|
123
|
+
* 本流已累计的用量。
|
|
124
|
+
*
|
|
125
|
+
* 为什么编码器需要它:有的协议把「收尾原因」与「输出用量」绑在**同一帧**
|
|
126
|
+
* (Anthropic 的 `message_delta`),而 finish 事件本身不带 usage。不给这个
|
|
127
|
+
* 槽位,收尾帧只能写 `output_tokens: 0` 或者再多发一帧 —— 后者会让客户端
|
|
128
|
+
* 取到第一个(占位)值,把用量读成 0(2026-10-21 在 3080 实测到)。
|
|
129
|
+
*/
|
|
130
|
+
readonly usage?: {
|
|
131
|
+
promptTokens: number;
|
|
132
|
+
completionTokens: number;
|
|
133
|
+
cachedTokens: number;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 一个协议的编解码器。
|
|
138
|
+
*
|
|
139
|
+
* 四个方法分成两组,两组都必须实现 —— 少了任何一组就只能单向,而「双向互转」
|
|
140
|
+
* 正是这个模块存在的理由:
|
|
141
|
+
* - `toChat*`:**入站**用。客户端说这个协议,我们得听懂它、并用它回答。
|
|
142
|
+
* - `fromChat*`:**出站**用。供应商说这个协议,我们得用它的语言请求、并把
|
|
143
|
+
* 它的回答翻回规范形态。
|
|
144
|
+
*/
|
|
145
|
+
export interface ProtocolCodec {
|
|
146
|
+
readonly id: ProtocolId;
|
|
147
|
+
/**
|
|
148
|
+
* 一条流的编解码会话。
|
|
149
|
+
*
|
|
150
|
+
* 部分协议是**有状态**的:Gemini 的 `functionCall` 靠工具名认,而规范形态
|
|
151
|
+
* 靠 index,跨帧必须有映射;映射若放模块级就会串请求(并发两条流各自的
|
|
152
|
+
* `ls` 抢同一个 index,或上一条流的残留映射漏到下一条)。
|
|
153
|
+
*
|
|
154
|
+
* 所以状态由调用方在**请求边界**建一次、在整条流里传给每一帧:
|
|
155
|
+
* - `translateStream` / `StreamState` 在一次转码里建一个;
|
|
156
|
+
* - 单帧转换(测试、非流式一次性)可以不传,此时每帧各建一份 —— 结果
|
|
157
|
+
* 等价于无状态,代价只有一次 Map 分配。
|
|
158
|
+
*/
|
|
159
|
+
createSession?(): CodecSession;
|
|
160
|
+
/** 外来请求体 → 规范请求体。`chat` 是恒等。 */
|
|
161
|
+
toChat(body: JsonObject, ctx: CodecContext): JsonObject;
|
|
162
|
+
/** 规范请求体 → 外来请求体。`chat` 是恒等。 */
|
|
163
|
+
fromChat(body: JsonObject, ctx: CodecContext): JsonObject;
|
|
164
|
+
/**
|
|
165
|
+
* 外来响应流的**单帧**解码:一帧 SSE 载荷(已剥掉 `data:` 前缀)→ 0..n 个
|
|
166
|
+
* 规范事件。`[DONE]` 与无法识别的帧返回空数组,**不抛**:一条坏帧不该让
|
|
167
|
+
* 整轮响应作废(上游发半截 JSON 是常态,见 `llm/adapter.ts` 的同类处理)。
|
|
168
|
+
*
|
|
169
|
+
* @param frame SSE 的 `data:` 载荷原文;`[DONE]` 由调用方过滤,不会传进来。
|
|
170
|
+
* @param event 该帧的 `event:` 名(Anthropic 用它区分事件类型;其余协议为空)。
|
|
171
|
+
*/
|
|
172
|
+
decodeFrame(frame: string, event?: string, session?: CodecSession): ChatEvent[];
|
|
173
|
+
/**
|
|
174
|
+
* 规范事件 → 0..n 个外来 SSE 帧。
|
|
175
|
+
*
|
|
176
|
+
* 返回的是**完整帧**(含 `event:`/`data:` 行与结尾空行),调用方只管拼字节。
|
|
177
|
+
* 做成整帧而不是载荷,是因为 Anthropic 这类协议每个事件都要带 `event:` 行,
|
|
178
|
+
* 让编码器自己拼比让调用方猜事件名干净。
|
|
179
|
+
*/
|
|
180
|
+
encodeFrame(event: ChatEvent, session?: CodecSession): string[];
|
|
181
|
+
/**
|
|
182
|
+
* 非流式响应体的解码:整个 JSON body → 规范事件列表。
|
|
183
|
+
*
|
|
184
|
+
* 与 `decodeFrame` 同构(同样返回事件列表),区别只在输入是**一个完整的
|
|
185
|
+
* 响应对象**而不是一帧增量。两种都必须实现:客户端可能发 `stream:false`,
|
|
186
|
+
* 供应商也可能只给 JSON(有些上游对短请求直接回非流式)。
|
|
187
|
+
*/
|
|
188
|
+
decodeJson(body: JsonObject): ChatEvent[];
|
|
189
|
+
/**
|
|
190
|
+
* 规范事件 → 非流式响应体。
|
|
191
|
+
*
|
|
192
|
+
* 与 `encodeFrame` 同构:把攒起来的事件折成一个完整响应对象。
|
|
193
|
+
* `events` 里最多一个 `finish`、一个 `usage`,其余是内容。
|
|
194
|
+
*/
|
|
195
|
+
encodeJson(events: ChatEvent[], ctx: CodecContext): JsonObject;
|
|
196
|
+
/**
|
|
197
|
+
* 流开始时的一次性帧(Anthropic 的 `message_start`、Responses 的
|
|
198
|
+
* `response.created`)。恒等协议返回空数组。
|
|
199
|
+
*/
|
|
200
|
+
startFrames(ctx: CodecContext): string[];
|
|
201
|
+
/**
|
|
202
|
+
* 流结束时的一次性帧(`message_stop`、`response.completed`、`[DONE]`)。
|
|
203
|
+
* 与 `startFrames` 对称;`finish` 事件本身走 `encodeFrame`。
|
|
204
|
+
*
|
|
205
|
+
* 带上 `session` 是因为有的协议**必须**在流末才发得出东西:Gemini 的
|
|
206
|
+
* `functionCall` 是「名字+完整参数」一个 part,而上游逐帧发参数增量,
|
|
207
|
+
* 只能攒到流末(见 gemini.ts 的 endFrames)。
|
|
208
|
+
*/
|
|
209
|
+
endFrames(ctx: CodecContext, session?: CodecSession): string[];
|
|
210
|
+
}
|
|
211
|
+
/** 是否为已知协议 id。 */
|
|
212
|
+
export declare function isProtocolId(v: unknown): v is ProtocolId;
|
|
213
|
+
/** 取对象字段(不是对象则 undefined)。 */
|
|
214
|
+
export declare function obj(v: unknown): JsonObject | undefined;
|
|
215
|
+
/** 取数组字段(不是数组则 undefined)。 */
|
|
216
|
+
export declare function arr(v: unknown): unknown[] | undefined;
|
|
217
|
+
/** 取非空字符串字段。 */
|
|
218
|
+
export declare function str(v: unknown): string | undefined;
|
|
219
|
+
/** 取有限数字字段。 */
|
|
220
|
+
export declare function num(v: unknown): number | undefined;
|
|
221
|
+
/** 取布尔字段。 */
|
|
222
|
+
export declare function bool(v: unknown): boolean | undefined;
|
|
223
|
+
/** 把 `finish` 原因在各协议间对齐(各家词汇不同,这里是唯一的对照表)。 */
|
|
224
|
+
export declare function finishOf(v: unknown): FinishReason;
|
|
@@ -12,6 +12,31 @@ import { UsageStore } from './usage-store.ts';
|
|
|
12
12
|
* 谁也没法再改结果)。核心也是所有供应商唯一的收口点,套一次全都受益。
|
|
13
13
|
*/
|
|
14
14
|
export declare function withFirstByteDeadline(stream: ReadableStream<Uint8Array>, ms: number): ReadableStream<Uint8Array>;
|
|
15
|
+
/** 丢弃响应的假 ServerResponse(测试模型用:记录状态+内容,用于判定成败)。 */
|
|
16
|
+
/**
|
|
17
|
+
* 响应收集器:把 Router 本来要写给 `res` 的东西收进来,让调用方翻译后自己写。
|
|
18
|
+
*
|
|
19
|
+
* 只给**非 chat 协议**用(`/v1/messages`、`/v1/responses`、Gemini):Router 只会
|
|
20
|
+
* 写 OpenAI 形态,而客户端要的是别的协议,中间必须插一层翻译。插入点在
|
|
21
|
+
* `chatCompletions` 的入口 —— 那里是唯一能同时看到「响应还没开始写」与
|
|
22
|
+
* 「协议是什么」的地方。
|
|
23
|
+
*
|
|
24
|
+
* 三条路径都要接住,漏一条就是那个协议下的一种故障:
|
|
25
|
+
* - `head(status, headers)` —— 非流式的状态行(503 也走这里)
|
|
26
|
+
* - `write(bytes)` —— 流式的 SSE 字节(可能是多段)
|
|
27
|
+
* - `end()` —— 流结束
|
|
28
|
+
*
|
|
29
|
+
* 流式的 `writeHead` 是**延迟到第一个字节**才发生的(见 writeChatResult),
|
|
30
|
+
* 所以 `head` 在流式下可能一次都不来;调用方不能假设它来过。
|
|
31
|
+
*/
|
|
32
|
+
export interface ResponseSink {
|
|
33
|
+
/** 状态行 + 响应头(流式下可能不来;来了就一次)。 */
|
|
34
|
+
head?: (status: number, headers: Record<string, string>) => void;
|
|
35
|
+
/** 一段响应体字节。 */
|
|
36
|
+
write: (bytes: Uint8Array) => void;
|
|
37
|
+
/** 响应体结束。 */
|
|
38
|
+
end: () => void;
|
|
39
|
+
}
|
|
15
40
|
/** 路由器。 */
|
|
16
41
|
export declare class Router {
|
|
17
42
|
/**
|
|
@@ -108,6 +133,13 @@ export declare class Router {
|
|
|
108
133
|
* pi-ai 等 DSH provider 通过它发现 Router 的模型目录。
|
|
109
134
|
*/
|
|
110
135
|
listModels(): Promise<ModelInfo[]>;
|
|
136
|
+
/**
|
|
137
|
+
* 该供应商的 `chatOnce`,已按它声明的 wire 包好协议翻译。
|
|
138
|
+
*
|
|
139
|
+
* 每次调用都现取(而不是构造时缓存):供应商会被开关/热重载替换,
|
|
140
|
+
* 缓存会把旧实例的翻译绑在新实例上。
|
|
141
|
+
*/
|
|
142
|
+
private wireOf;
|
|
111
143
|
/** 组合列表(面板用):用户自定义组合。 */
|
|
112
144
|
combos(): Promise<Combo[]>;
|
|
113
145
|
/**
|
|
@@ -196,7 +228,16 @@ export declare class Router {
|
|
|
196
228
|
* - model 命中组合名 → 在组合模型里按策略选一个,交给供应商;失败按回退顺序尝试剩余模型。
|
|
197
229
|
* - 否则 → 依次尝试供应商,直到某个返回 true(已写响应)或全部返回 false。
|
|
198
230
|
*/
|
|
199
|
-
|
|
231
|
+
/**
|
|
232
|
+
* 规范请求入口。
|
|
233
|
+
*
|
|
234
|
+
* @param res 真实的客户端响应。协议非 chat 时**不直接用它**,而是经
|
|
235
|
+
* `sink` 先收集再翻译(见 `protocol/entry.ts`)—— 因为 Router 只知道
|
|
236
|
+
* 怎么写 OpenAI 形态,而客户端说的可能是 Anthropic/Gemini。
|
|
237
|
+
* @param sink 可选的响应收集器:给了就把「写给 res 的东西」改成写给 sink,
|
|
238
|
+
* 由调用方翻译后自己写真正的 res。只给非 chat 协议用。
|
|
239
|
+
*/
|
|
240
|
+
chatCompletions(req: ChatRequest, res: ServerResponse, sink?: ResponseSink): Promise<void>;
|
|
200
241
|
/** 调指定供应商。
|
|
201
242
|
* 模型名**原样**传给插件(不在这剥 alias/)——插件自己认得自己的别名
|
|
202
243
|
* (如 traework 会剥第一段)。核心在这剥就会剥两次,把模型 id 里的
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AccountState, ChatOnceResult, SupplierAccountNow } from '../suppliers/contract.ts';
|
|
7
7
|
import type { AccountPool } from './account-pool.ts';
|
|
8
|
+
import type { ProtocolId } from '../protocol/types.ts';
|
|
8
9
|
/** 面板展示的账号状态(脱敏)。 */
|
|
9
10
|
export interface SupplierAccount {
|
|
10
11
|
uid: string;
|
|
@@ -64,6 +65,15 @@ export interface ChatRequest {
|
|
|
64
65
|
* 能反查是哪个客户端在打)。缺省 = 匿名外部客户端。
|
|
65
66
|
*/
|
|
66
67
|
session?: string;
|
|
68
|
+
/**
|
|
69
|
+
* 客户端说的协议(入站)。
|
|
70
|
+
*
|
|
71
|
+
* 缺省 `'chat'`(今天所有入口的现状)。四种协议(chat / anthropic /
|
|
72
|
+
* responses / gemini)在入站路由上就被翻成规范形态,所以**核心与供应商
|
|
73
|
+
* 只认 chat**;这个字段存在的理由是让用量记录与日志能说出「客户端原本
|
|
74
|
+
* 说的哪种协议」,排查「Claude Code 打过来为什么不通」时省一次猜。
|
|
75
|
+
*/
|
|
76
|
+
protocol?: ProtocolId;
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* 供应商接口。核心负责账号池(选号/冷却/禁用/错误累计/遍历回退),
|
|
@@ -90,6 +100,15 @@ export interface Supplier {
|
|
|
90
100
|
accounts(): SupplierAccountNow[];
|
|
91
101
|
/** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
|
|
92
102
|
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
103
|
+
/**
|
|
104
|
+
* 上游说哪种协议(缺省 `'chat'`)。
|
|
105
|
+
*
|
|
106
|
+
* 声明成别种时,核心在 `chatOnce` 前后各做一次翻译:请求体翻成该协议、
|
|
107
|
+
* 响应流翻回 OpenAI SSE。于是**插件实现里永远只看见 OpenAI 形态**,
|
|
108
|
+
* 契约不变 —— 一个只会说 Anthropic 的上游可以接一个只写了几行 fetch 的
|
|
109
|
+
* 插件,路由/选号/冷却/用量全都照旧。
|
|
110
|
+
*/
|
|
111
|
+
readonly wire?: ProtocolId;
|
|
93
112
|
/** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
|
|
94
113
|
removeLink?(uid: string): Promise<boolean>;
|
|
95
114
|
dispose(): void;
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -185,6 +185,17 @@ export interface ExtControlItem {
|
|
|
185
185
|
body?: string;
|
|
186
186
|
/** 文本可编辑(缺省 = 只读;见 `ExtControl.editable`)。 */
|
|
187
187
|
editable?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* 分组名(可选;缺省 = 无组)—— 详情页按它分节渲染,组间插组标题行;
|
|
190
|
+
* 无组照旧平铺(见 `ExtControl.group`)。
|
|
191
|
+
*/
|
|
192
|
+
group?: string;
|
|
193
|
+
/**
|
|
194
|
+
* 参与拖动排序(可选;缺省 = 参与)—— `false` 的条目不给拖把手
|
|
195
|
+
* (见 `ExtControl.sortable`)。⚠️ 服务端**带值透传**,`false` 也会收到;
|
|
196
|
+
* 判断必须写 `sortable !== false`,`!sortable` 会把缺省(参与)当不参与。
|
|
197
|
+
*/
|
|
198
|
+
sortable?: boolean;
|
|
188
199
|
/**
|
|
189
200
|
* 自建条目(可删)还是内置(不可删、只有开关与还原)。
|
|
190
201
|
*
|
|
@@ -13,6 +13,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
13
13
|
import type { ChatRequest, ModelInfo, SupplierAccount } from '../router/types.ts';
|
|
14
14
|
import type { SupplierConfigStore } from '../supplier-config.ts';
|
|
15
15
|
import type { CredentialStore } from '../credential-store.ts';
|
|
16
|
+
import type { ProtocolId } from '../protocol/types.ts';
|
|
16
17
|
/** 供应商配置:loader 注入的运行时环境。 */
|
|
17
18
|
export interface SupplierEnv {
|
|
18
19
|
/** 数据目录(state.json / keys.json 所在目录)。 */
|
|
@@ -45,9 +46,16 @@ export interface SupplierEnv {
|
|
|
45
46
|
* `source` 是**可选标签**,声明这个供应商是从哪儿来的。核心用它把 `/health` 的
|
|
46
47
|
* `source` 报准(面板按它分「内置 / 插件」两组,面板和插件页都看这个值)。
|
|
47
48
|
* 不声明 = `external`(独立安装的供应商插件的现状,一个字段都不用改)。
|
|
49
|
+
*
|
|
50
|
+
* `wire` 是**协议标签**(可选):这个供应商的上游说哪种协议。缺省
|
|
51
|
+
* `'chat'`(= OpenAI Chat Completions,今天所有供应商的现状,一个都不用改)。
|
|
52
|
+
* 声明成别种时,核心在把 `req` 交给插件前把规范请求体翻成它的协议,
|
|
53
|
+
* 并把它的响应流翻回规范形态 —— 插件看到的仍是「OpenAI SSE」,
|
|
54
|
+
* 于是**契约不变、已有插件全都不动**。详见 `protocol/`。
|
|
48
55
|
*/
|
|
49
56
|
export type SupplierFactory = ((env: SupplierEnv) => SupplierModule) & {
|
|
50
57
|
source?: 'builtin';
|
|
58
|
+
wire?: ProtocolId;
|
|
51
59
|
};
|
|
52
60
|
/**
|
|
53
61
|
* 账户此刻的状态 —— 插件**解读**上游信号后的语义状态。
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 通用能力(dsh-router 核心,所有供应商自动可用,js 无需实现):
|
|
5
5
|
* GET /suppliers/:id/models models + alias(listModels 合并启用状态)
|
|
6
|
-
* POST /suppliers/:id/probe 契约体检:逐成员报「实现/可用/不执行」
|
|
7
6
|
* PATCH /suppliers/:id/enabled {enabled} —— 供应商开关(关掉不参与路由)
|
|
8
7
|
* POST /suppliers/:id/models/toggle {id, enabled}
|
|
9
8
|
* POST /suppliers/:id/models/add {id}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -36,12 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"./suppliers/nvidia/package.json": "./lib/suppliers/nvidia/package.json",
|
|
38
38
|
"./suppliers/nvidia/locale/*.json": "./lib/suppliers/nvidia/locale/*.json",
|
|
39
|
-
"./ext-test": {
|
|
40
|
-
"types": "./lib/types/ext-test/index.d.ts",
|
|
41
|
-
"default": "./lib/ext-test/index.js"
|
|
42
|
-
},
|
|
43
|
-
"./ext-test/package.json": "./lib/ext-test/package.json",
|
|
44
|
-
"./ext-test/locale/*.json": "./lib/ext-test/locale/*.json",
|
|
45
39
|
"./ext-prompt": {
|
|
46
40
|
"types": "./lib/types/ext-prompt/index.d.ts",
|
|
47
41
|
"default": "./lib/ext-prompt/index.js"
|
|
@@ -79,9 +73,6 @@
|
|
|
79
73
|
"lib/suppliers/*/index.js",
|
|
80
74
|
"lib/suppliers/*/package.json",
|
|
81
75
|
"lib/suppliers/*/locale/*.json",
|
|
82
|
-
"lib/ext-test/index.js",
|
|
83
|
-
"lib/ext-test/package.json",
|
|
84
|
-
"lib/ext-test/locale/*.json",
|
|
85
76
|
"lib/ext-prompt/index.js",
|
|
86
77
|
"lib/ext-prompt/package.json",
|
|
87
78
|
"lib/ext-prompt/locale/*.json",
|
|
@@ -105,7 +96,7 @@
|
|
|
105
96
|
},
|
|
106
97
|
"license": "MIT",
|
|
107
98
|
"dependencies": {
|
|
108
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
99
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
109
100
|
},
|
|
110
101
|
"peerDependencies": {
|
|
111
102
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
@@ -113,8 +104,8 @@
|
|
|
113
104
|
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
|
|
114
105
|
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
115
106
|
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
116
|
-
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
117
107
|
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
108
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
118
109
|
"cordis": "^4.0.0-rc.7",
|
|
119
110
|
"react": "^18.2.0",
|
|
120
111
|
"react-dom": "^18.2.0"
|
|
@@ -134,13 +125,13 @@
|
|
|
134
125
|
}
|
|
135
126
|
},
|
|
136
127
|
"devDependencies": {
|
|
137
|
-
"@types/node": "^26.6.
|
|
128
|
+
"@types/node": "^26.6.5",
|
|
138
129
|
"@types/react": "~19.3.0",
|
|
139
130
|
"@types/react-dom": "~19.3.0",
|
|
140
131
|
"cordis": "4.0.0-rc.10",
|
|
141
|
-
"react": "^19.3.
|
|
132
|
+
"react": "^19.3.1",
|
|
142
133
|
"react-dom": "19.3.0",
|
|
143
|
-
"tsdown": "^0.23.
|
|
134
|
+
"tsdown": "^0.23.1",
|
|
144
135
|
"typescript": "^7.0.2",
|
|
145
136
|
"vite": "8.3.0",
|
|
146
137
|
"vite-plugin-css-injected-by-js": "^5.0.2"
|