u1s1-cli 1.18.0 → 1.20.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.
Files changed (43) hide show
  1. package/dist/agents/agent-config.d.ts +1 -1
  2. package/dist/agents/agent-config.js +1 -1
  3. package/dist/agents/claude.d.ts +4 -1
  4. package/dist/agents/claude.js +6 -3
  5. package/dist/agents/codex.d.ts +4 -1
  6. package/dist/agents/codex.js +6 -3
  7. package/dist/agents/custom-models.d.ts +61 -0
  8. package/dist/agents/custom-models.js +175 -0
  9. package/dist/agents/dsh.d.ts +90 -0
  10. package/dist/agents/dsh.js +243 -0
  11. package/dist/agents/launch.d.ts +24 -2
  12. package/dist/agents/launch.js +115 -15
  13. package/dist/agents/models-list.d.ts +65 -0
  14. package/dist/agents/models-list.js +297 -0
  15. package/dist/agents/node-runtime.d.ts +91 -0
  16. package/dist/agents/node-runtime.js +223 -0
  17. package/dist/agents/runtime.d.ts +26 -3
  18. package/dist/agents/runtime.js +50 -14
  19. package/dist/agents/status.d.ts +4 -0
  20. package/dist/agents/status.js +32 -7
  21. package/dist/api.d.ts +2 -0
  22. package/dist/chat-protocol/anthropic.d.ts +29 -0
  23. package/dist/chat-protocol/anthropic.js +484 -0
  24. package/dist/chat-protocol/json.d.ts +20 -0
  25. package/dist/chat-protocol/json.js +22 -0
  26. package/dist/chat-protocol/responses.d.ts +97 -0
  27. package/dist/chat-protocol/responses.js +1076 -0
  28. package/dist/chat-protocol/sse.d.ts +19 -0
  29. package/dist/chat-protocol/sse.js +67 -0
  30. package/dist/chat-protocol/thinking.d.ts +22 -0
  31. package/dist/chat-protocol/thinking.js +58 -0
  32. package/dist/config-command.d.ts +1 -1
  33. package/dist/config-command.js +20 -10
  34. package/dist/config.d.ts +23 -1
  35. package/dist/config.js +52 -1
  36. package/dist/custom-endpoint-proxy.d.ts +92 -0
  37. package/dist/custom-endpoint-proxy.js +461 -0
  38. package/dist/device-auth.d.ts +14 -3
  39. package/dist/device-auth.js +36 -2
  40. package/dist/index.js +12 -3
  41. package/dist/launcher.d.ts +5 -1
  42. package/dist/launcher.js +35 -11
  43. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ /** One upstream SSE event cap: longer events are a broken or hostile upstream. */
2
+ export declare const MAX_SSE_EVENT_CHARS: number;
3
+ /** Comment frame SSE clients ignore; forwarded so idle timers reset. */
4
+ export declare const SSE_KEEPALIVE_COMMENT = ": keepalive\n\n";
5
+ export interface SseEvent {
6
+ data: string;
7
+ event: string;
8
+ }
9
+ export declare class SseEventTooLargeError extends Error {
10
+ constructor();
11
+ }
12
+ /** Incremental SSE parser: feed decoded text, get complete events regardless of chunk boundaries. */
13
+ export declare class SseParser {
14
+ private buffered;
15
+ private readonly maxEventChars;
16
+ constructor(maxEventChars?: number);
17
+ push(text: string): SseEvent[];
18
+ finish(text?: string): SseEvent[];
19
+ }
@@ -0,0 +1,67 @@
1
+ // GENERATED from workers/gateway/src/chat-protocol/sse.ts; do not edit here. Edit the canonical file, then run: node scripts/sync-chat-protocol.mjs
2
+ // Server-sent events parsing for the converters (see json.ts for the rules of this directory).
3
+ /** One upstream SSE event cap: longer events are a broken or hostile upstream. */
4
+ export const MAX_SSE_EVENT_CHARS = 1 * 1_024 * 1_024;
5
+ /** Comment frame SSE clients ignore; forwarded so idle timers reset. */
6
+ export const SSE_KEEPALIVE_COMMENT = ": keepalive\n\n";
7
+ // Enumerate two consecutive SSE line endings explicitly. A compositional
8
+ // `(CRLF|CR|LF){2}` regex can backtrack and misread one CRLF as CR + LF.
9
+ const SSE_EVENT_BOUNDARY = /\r\n\r\n|\r\n\n|\r\n\r|\n\r\n|\n\n|\n\r|\r\r\n|\r\r/;
10
+ export class SseEventTooLargeError extends Error {
11
+ constructor() {
12
+ super("upstream SSE event exceeded limit");
13
+ this.name = "SseEventTooLargeError";
14
+ }
15
+ }
16
+ function eventFields(frame) {
17
+ const data = [];
18
+ let event = "";
19
+ for (const line of frame.split(/\r\n|\r|\n/)) {
20
+ if (!line || line.startsWith(":"))
21
+ continue;
22
+ const separator = line.indexOf(":");
23
+ const field = separator < 0 ? line : line.slice(0, separator);
24
+ let value = separator < 0 ? "" : line.slice(separator + 1);
25
+ if (value.startsWith(" "))
26
+ value = value.slice(1);
27
+ if (field === "data")
28
+ data.push(value);
29
+ else if (field === "event")
30
+ event = value;
31
+ }
32
+ return { data: data.join("\n"), event };
33
+ }
34
+ /** Incremental SSE parser: feed decoded text, get complete events regardless of chunk boundaries. */
35
+ export class SseParser {
36
+ buffered = "";
37
+ maxEventChars;
38
+ constructor(maxEventChars = MAX_SSE_EVENT_CHARS) {
39
+ this.maxEventChars = maxEventChars;
40
+ }
41
+ push(text) {
42
+ this.buffered += text;
43
+ const events = [];
44
+ let boundary = SSE_EVENT_BOUNDARY.exec(this.buffered);
45
+ while (boundary?.index !== undefined) {
46
+ if (boundary.index > this.maxEventChars) {
47
+ this.buffered = "";
48
+ throw new SseEventTooLargeError();
49
+ }
50
+ events.push(eventFields(this.buffered.slice(0, boundary.index)));
51
+ this.buffered = this.buffered.slice(boundary.index + boundary[0].length);
52
+ boundary = SSE_EVENT_BOUNDARY.exec(this.buffered);
53
+ }
54
+ if (this.buffered.length > this.maxEventChars) {
55
+ this.buffered = "";
56
+ throw new SseEventTooLargeError();
57
+ }
58
+ return events;
59
+ }
60
+ finish(text = "") {
61
+ const events = this.push(text);
62
+ if (this.buffered.trim())
63
+ events.push(eventFields(this.buffered));
64
+ this.buffered = "";
65
+ return events;
66
+ }
67
+ }
@@ -0,0 +1,22 @@
1
+ import { type JsonRecord } from "./json.ts";
2
+ /**
3
+ * Same contract as workers/gateway/src/model-thinking.ts and
4
+ * packages/cli/src/config.ts THINKING_LEVELS; the gateway passes its own
5
+ * thinkingWireFields here, so a drift fails its typecheck.
6
+ */
7
+ export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
8
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
9
+ /** Model id + requested level → reasoning fields of the Chat Completions body. */
10
+ export type ReasoningFields = (modelId: string, level: ThinkingLevel | null) => JsonRecord;
11
+ /**
12
+ * Anthropic Messages body → requested level. Claude Code sends
13
+ * `thinking: {type: "adaptive"}` + `output_config.effort`, and
14
+ * `thinking: {type: "disabled"}` when thinking is off; the legacy
15
+ * `{type: "enabled", budget_tokens}` is mapped by budget. No opinion → null.
16
+ */
17
+ export declare function requestedThinkingLevel(body: JsonRecord): ThinkingLevel | null;
18
+ /** Responses `reasoning.effort` → level. */
19
+ export declare const RESPONSES_REASONING_EFFORTS: Readonly<Record<string, ThinkingLevel>>;
20
+ export declare function responsesThinkingLevel(body: JsonRecord): ThinkingLevel | null;
21
+ /** Rough prompt-size estimate (ASCII ~4 chars/token, other ~1.5) for count_tokens style endpoints. */
22
+ export declare function estimateTokensFromText(text: string): number;
@@ -0,0 +1,58 @@
1
+ // GENERATED from workers/gateway/src/chat-protocol/thinking.ts; do not edit here. Edit the canonical file, then run: node scripts/sync-chat-protocol.mjs
2
+ // Thinking levels as clients request them (see json.ts for the rules of this directory).
3
+ import { asRecord } from "./json.js";
4
+ /**
5
+ * Same contract as workers/gateway/src/model-thinking.ts and
6
+ * packages/cli/src/config.ts THINKING_LEVELS; the gateway passes its own
7
+ * thinkingWireFields here, so a drift fails its typecheck.
8
+ */
9
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
10
+ /**
11
+ * Anthropic Messages body → requested level. Claude Code sends
12
+ * `thinking: {type: "adaptive"}` + `output_config.effort`, and
13
+ * `thinking: {type: "disabled"}` when thinking is off; the legacy
14
+ * `{type: "enabled", budget_tokens}` is mapped by budget. No opinion → null.
15
+ */
16
+ export function requestedThinkingLevel(body) {
17
+ const thinking = asRecord(body.thinking);
18
+ if (thinking?.type === "disabled")
19
+ return "off";
20
+ const effort = asRecord(body.output_config)?.effort;
21
+ if (typeof effort === "string" && effort !== "off" && THINKING_LEVELS.includes(effort)) {
22
+ return effort;
23
+ }
24
+ if (thinking?.type === "enabled") {
25
+ const budget = Number(thinking.budget_tokens);
26
+ if (!Number.isFinite(budget) || budget <= 0)
27
+ return null;
28
+ return budget <= 4096 ? "low" : budget <= 16_384 ? "medium" : "high";
29
+ }
30
+ return null;
31
+ }
32
+ /** Responses `reasoning.effort` → level. */
33
+ export const RESPONSES_REASONING_EFFORTS = {
34
+ none: "off",
35
+ minimal: "minimal",
36
+ low: "low",
37
+ medium: "medium",
38
+ high: "high",
39
+ xhigh: "xhigh",
40
+ };
41
+ export function responsesThinkingLevel(body) {
42
+ const effort = asRecord(body.reasoning)?.effort;
43
+ return typeof effort === "string" ? RESPONSES_REASONING_EFFORTS[effort] ?? null : null;
44
+ }
45
+ /** Rough prompt-size estimate (ASCII ~4 chars/token, other ~1.5) for count_tokens style endpoints. */
46
+ export function estimateTokensFromText(text) {
47
+ if (!text)
48
+ return 0;
49
+ let ascii = 0;
50
+ let wide = 0;
51
+ for (const char of text) {
52
+ if (char.charCodeAt(0) < 0x80)
53
+ ascii += 1;
54
+ else
55
+ wide += 1;
56
+ }
57
+ return Math.ceil(ascii / 4 + wide / 1.5);
58
+ }
@@ -1,4 +1,4 @@
1
- declare const CLIENTS: readonly ["claude", "codex"];
1
+ declare const CLIENTS: readonly ["claude", "codex", "dsh"];
2
2
  type Client = (typeof CLIENTS)[number];
3
3
  export type ConfigKey = "default-client" | "pi.model" | `${Client}.model` | `${Client}.auto-update` | `${Client}.pin`;
4
4
  export declare const CONFIG_KEYS: readonly ConfigKey[];
@@ -9,14 +9,16 @@
9
9
  * u1s1 config unset <键> 恢复默认
10
10
  *
11
11
  * 键:
12
- * default-client pi | claude | codex 同 u1s1 use
12
+ * default-client pi | claude | codex | dsh 同 u1s1 use
13
13
  * pi.model 模型 id 或别名 同 u1s1 model <模型>
14
14
  * claude.model 模型 id u1s1 claude 的默认模型(--settings 的 model)
15
15
  * codex.model 模型 id u1s1 codex 的默认模型(config.toml 的 model)
16
+ * dsh.model 模型 id u1s1 dsh 的默认模型(--patch 里 agent-default-model)
16
17
  * claude.auto-update on | off 每天查新、下次启动前更新;缺省 on
17
18
  * codex.auto-update on | off
18
19
  * claude.pin 版本号 同 u1s1 agents pin / unpin
19
20
  * codex.pin 版本号
21
+ * dsh.auto-update / dsh.pin 同上
20
22
  *
21
23
  * 客户端的模型只按网关上次下发的阵容(启动配置缓存)核对:还没启动过、没有缓存时照收,启动时再核对,
22
24
  * 不在阵容里就退回默认模型并提示一次(prefs.ts)。
@@ -25,23 +27,26 @@ import { existsSync } from "node:fs";
25
27
  import { createInterface } from "node:readline/promises";
26
28
  import { agentSettingsFile, isDefaultAgent, loadConfig, readRawConfigFile, saveDefaultAgent, } from "./config.js";
27
29
  import { tr } from "./i18n.js";
28
- const CLIENTS = ["claude", "codex"];
30
+ const CLIENTS = ["claude", "codex", "dsh"];
29
31
  export const CONFIG_KEYS = [
30
32
  "default-client",
31
33
  "pi.model",
32
34
  "claude.model",
33
35
  "codex.model",
36
+ "dsh.model",
34
37
  "claude.auto-update",
35
38
  "codex.auto-update",
39
+ "dsh.auto-update",
36
40
  "claude.pin",
37
41
  "codex.pin",
42
+ "dsh.pin",
38
43
  ];
39
44
  export function isConfigKey(value) {
40
45
  return typeof value === "string" && CONFIG_KEYS.includes(value);
41
46
  }
42
47
  function clientOf(key) {
43
48
  const prefix = key.split(".")[0];
44
- return prefix === "claude" || prefix === "codex" ? prefix : undefined;
49
+ return CLIENTS.includes(prefix ?? "") ? prefix : undefined;
45
50
  }
46
51
  function describe(key) {
47
52
  const client = clientOf(key);
@@ -97,7 +102,12 @@ async function cachedLineup(client) {
97
102
  const config = cached ? parseAgentConfig(cached.body, AGENT_PROFILES[client].gatewayAgent) : undefined;
98
103
  if (!config || !config.enabled)
99
104
  return undefined;
100
- return { ids: config.models.map((m) => m.id), defaultModel: config.defaultModel };
105
+ // 自定义端点的模型(ep…/<模型>)也能选:按上次拉到的端点缓存核对,不联网
106
+ const { agentCustomModels } = await import("./agents/custom-models.js");
107
+ const { loadEndpointsCache } = await import("./config.js");
108
+ // dsh 走 /v1/chat/completions,本地路由只转 /v1/messages、/v1/responses:不列自定义模型
109
+ const custom = client === "dsh" ? [] : agentCustomModels(loadEndpointsCache()).map((c) => c.id);
110
+ return { ids: [...config.models.map((m) => m.id), ...custom], defaultModel: config.defaultModel };
101
111
  }
102
112
  function parseSwitch(value) {
103
113
  const v = value.trim().toLowerCase();
@@ -117,7 +127,7 @@ export async function writeConfigValue(key, value) {
117
127
  return 0;
118
128
  }
119
129
  if (!isDefaultAgent(value)) {
120
- console.error(tr("default-client 只能是 pi、claude 或 codex。", "default-client must be pi, claude or codex."));
130
+ console.error(tr("default-client 只能是 pi、claude、codex 或 dsh。", "default-client must be pi, claude, codex or dsh."));
121
131
  return 1;
122
132
  }
123
133
  saveDefaultAgent(value);
@@ -192,11 +202,11 @@ export function configHelpLines() {
192
202
  ["u1s1 config unset <键>", tr("恢复默认", "Restore the default")],
193
203
  ];
194
204
  const keyRows = [
195
- ["default-client", "pi | claude | codex"],
205
+ ["default-client", "pi | claude | codex | dsh"],
196
206
  ["pi.model", tr("模型 id 或别名", "model id or alias")],
197
- ["claude.model, codex.model", tr("模型 id;default = 跟随网关默认", "model id; default = gateway default")],
198
- ["claude.auto-update, codex.auto-update", "on | off"],
199
- ["claude.pin, codex.pin", tr("版本号", "version")],
207
+ ["claude.model, codex.model, dsh.model", tr("模型 id;default = 跟随网关默认", "model id; default = gateway default")],
208
+ ["<client>.auto-update", "on | off (claude / codex / dsh)"],
209
+ ["<client>.pin", tr("版本号(claude / codex / dsh)", "version (claude / codex / dsh)")],
200
210
  ];
201
211
  const usage = rows.map(([a, b]) => ` ${tr(a, a.replace("<键>", "<key>").replace("<值>", "<value>")).padEnd(30)}${b}`);
202
212
  return [
@@ -265,7 +275,7 @@ function padDisplay(text, width) {
265
275
  async function choicesFor(key) {
266
276
  const client = clientOf(key);
267
277
  if (key === "default-client") {
268
- return ["pi", "claude", "codex"].map((a) => ({ label: a, value: a }));
278
+ return ["pi", "claude", "codex", "dsh"].map((a) => ({ label: a, value: a }));
269
279
  }
270
280
  if (key.endsWith(".auto-update")) {
271
281
  return [
package/dist/config.d.ts CHANGED
@@ -139,6 +139,26 @@ export declare const CUSTOM_ENDPOINTS: CustomEndpoint[];
139
139
  export declare function setCustomEndpoints(endpoints: CustomEndpoint[]): void;
140
140
  /** 端点密钥不写进 models.json,走这个环境变量引用(pi 读取时解析)。 */
141
141
  export declare function endpointKeyEnvName(endpointId: string): string;
142
+ /**
143
+ * 自定义模型的「推理参数格式」(dashboard 模型行第 6 栏,/v1/endpoints 的 models[].thinking_format)。
144
+ * 与 workers/gateway/src/endpoint-validation.ts 的 THINKING_FORMATS 是同一取值空间。
145
+ * - openai(默认;老服务端不下发该字段):沿用 pi 的通用 OpenAI 行为——只发 reasoning_effort,
146
+ * 关思考时什么都不发(后端按自己的默认值思考)。
147
+ * - deepseek:DeepSeek V4 的档位(off / low / high / max,默认 high)。2026-11-13 实测
148
+ * LiteLLM + vLLM 部署的 DeepSeek-V4-Flash:顶层 reasoning_effort 原样透传、能控级别,
149
+ * "none" 能关思考;顶层 thinking 字段会被 LiteLLM 以 UnsupportedParamsError 400 拒掉。
150
+ * 所以只发 reasoning_effort(关 = "none");只有官方 api.deepseek.com 用 DeepSeek 自己的
151
+ * thinking {type} + reasoning_effort 格式。
152
+ */
153
+ export declare const CUSTOM_THINKING_FORMATS: readonly ["openai", "deepseek"];
154
+ export type CustomThinkingFormat = (typeof CUSTOM_THINKING_FORMATS)[number];
155
+ /** 未知取值(更新的服务端加了新格式)按默认 openai 处理,不让老 CLI 拒收整个端点。 */
156
+ export declare function parseCustomThinkingFormat(value: unknown): CustomThinkingFormat;
157
+ /**
158
+ * 推理参数格式 → 交给 pi 的思考能力描述(与网关模型的 thinking 同一结构,toProviderModels 统一翻译)。
159
+ * openai 返回 undefined = 不声明档位,pi 走原来的通用行为。
160
+ */
161
+ export declare function customThinkingCapabilities(format: CustomThinkingFormat, baseUrl: string): ThinkingCapabilities | undefined;
142
162
  /** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
143
163
  export declare function apiEndpointToCustom(e: {
144
164
  id: string;
@@ -153,6 +173,8 @@ export declare function apiEndpointToCustom(e: {
153
173
  vision?: boolean;
154
174
  context_window?: number;
155
175
  max_tokens?: number;
176
+ /** 老服务端没有这个字段 → openai */
177
+ thinking_format?: string;
156
178
  }[];
157
179
  }): CustomEndpoint;
158
180
  /** 模型引用 = provider 注册键 + 模型 id;provider 是 u1s1 或某个端点 id。 */
@@ -189,7 +211,7 @@ export interface CliConfig {
189
211
  /** 默认客户端(`u1s1 use`/首次选择器写入);缺省 = 存量用户,按 Pi 处理(见 launcher.ts)。 */
190
212
  defaultAgent?: DefaultAgent;
191
213
  }
192
- export type DefaultAgent = "pi" | "claude" | "codex";
214
+ export type DefaultAgent = "pi" | "claude" | "codex" | "dsh";
193
215
  export declare function isDefaultAgent(value: unknown): value is DefaultAgent;
194
216
  export declare const u1s1Dir: string;
195
217
  export declare const configFile: string;
package/dist/config.js CHANGED
@@ -216,6 +216,50 @@ export function setCustomEndpoints(endpoints) {
216
216
  export function endpointKeyEnvName(endpointId) {
217
217
  return `U1S1_EP_KEY_${endpointId}`;
218
218
  }
219
+ /**
220
+ * 自定义模型的「推理参数格式」(dashboard 模型行第 6 栏,/v1/endpoints 的 models[].thinking_format)。
221
+ * 与 workers/gateway/src/endpoint-validation.ts 的 THINKING_FORMATS 是同一取值空间。
222
+ * - openai(默认;老服务端不下发该字段):沿用 pi 的通用 OpenAI 行为——只发 reasoning_effort,
223
+ * 关思考时什么都不发(后端按自己的默认值思考)。
224
+ * - deepseek:DeepSeek V4 的档位(off / low / high / max,默认 high)。2026-11-13 实测
225
+ * LiteLLM + vLLM 部署的 DeepSeek-V4-Flash:顶层 reasoning_effort 原样透传、能控级别,
226
+ * "none" 能关思考;顶层 thinking 字段会被 LiteLLM 以 UnsupportedParamsError 400 拒掉。
227
+ * 所以只发 reasoning_effort(关 = "none");只有官方 api.deepseek.com 用 DeepSeek 自己的
228
+ * thinking {type} + reasoning_effort 格式。
229
+ */
230
+ export const CUSTOM_THINKING_FORMATS = ["openai", "deepseek"];
231
+ /** 未知取值(更新的服务端加了新格式)按默认 openai 处理,不让老 CLI 拒收整个端点。 */
232
+ export function parseCustomThinkingFormat(value) {
233
+ const v = typeof value === "string" ? value.trim().toLowerCase() : "";
234
+ return CUSTOM_THINKING_FORMATS.includes(v) ? v : "openai";
235
+ }
236
+ /**
237
+ * 官方 DeepSeek API 按域名特判:它的开关是 thinking {type: enabled|disabled},不认 reasoning_effort "none";
238
+ * 自建(vLLM / LiteLLM 等)认 "none" 却会拒 thinking 字段。只认精确主机名,仿冒子域名不算。
239
+ */
240
+ function isOfficialDeepSeekUrl(baseUrl) {
241
+ try {
242
+ return new URL(baseUrl).hostname.toLowerCase() === "api.deepseek.com";
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
248
+ /**
249
+ * 推理参数格式 → 交给 pi 的思考能力描述(与网关模型的 thinking 同一结构,toProviderModels 统一翻译)。
250
+ * openai 返回 undefined = 不声明档位,pi 走原来的通用行为。
251
+ */
252
+ export function customThinkingCapabilities(format, baseUrl) {
253
+ if (format !== "deepseek")
254
+ return undefined;
255
+ return {
256
+ levels: ["off", "low", "high", "max"],
257
+ defaultLevel: "high",
258
+ canDisable: true,
259
+ levelMap: { off: "none", low: "low", high: "high", max: "max" },
260
+ requestFormat: isOfficialDeepSeekUrl(baseUrl) ? "deepseek" : "openai",
261
+ };
262
+ }
219
263
  /** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
220
264
  export function apiEndpointToCustom(e) {
221
265
  return {
@@ -229,6 +273,10 @@ export function apiEndpointToCustom(e) {
229
273
  name: m.name || m.id,
230
274
  aliases: defaultAliases(m.id),
231
275
  reasoning: !!m.reasoning,
276
+ // 只对推理模型生效:非推理模型 pi 本来就不发推理参数
277
+ ...(m.reasoning
278
+ ? withThinking(customThinkingCapabilities(parseCustomThinkingFormat(m.thinking_format), e.base_url))
279
+ : {}),
232
280
  vision: !!m.vision,
233
281
  contextWindow: m.context_window ?? 128_000,
234
282
  maxTokens: m.max_tokens ?? 16_384,
@@ -238,6 +286,9 @@ export function apiEndpointToCustom(e) {
238
286
  })),
239
287
  };
240
288
  }
289
+ function withThinking(thinking) {
290
+ return thinking ? { thinking } : {};
291
+ }
241
292
  /** ref 指向的模型仍然存在吗(服务端下架/端点删除后失效)。 */
242
293
  export function refValid(ref) {
243
294
  if (ref.provider === PROVIDER_ID)
@@ -271,7 +322,7 @@ export function findModel(query) {
271
322
  return undefined;
272
323
  }
273
324
  export function isDefaultAgent(value) {
274
- return value === "pi" || value === "claude" || value === "codex";
325
+ return value === "pi" || value === "claude" || value === "codex" || value === "dsh";
275
326
  }
276
327
  export const u1s1Dir = join(homedir(), ".u1s1");
277
328
  export const configFile = join(u1s1Dir, "config.json");
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 本机签名代理的本地路由:Claude Code / Codex 选了自定义端点的模型(`ep<32 hex>/<模型 id>`,见
3
+ * agents/custom-models.ts)时,请求不发往网关,在本机转成 OpenAI Chat Completions 直连该端点,
4
+ * 回来的结果再转回客户端的协议:
5
+ * POST /v1/messages Anthropic Messages ↔ Chat(流式 / 非流式)
6
+ * POST /v1/messages/count_tokens 本机粗估,不请求端点
7
+ * POST /v1/responses OpenAI Responses ↔ Chat(流式 / 非流式)
8
+ * 转换代码与网关共用(chat-protocol/,正本在 workers/gateway/src/chat-protocol)。
9
+ *
10
+ * 安全:发往端点的请求头从零构造,只有内容类型、端点自己的 apiKey(为空就不带 Authorization)和客户端的
11
+ * 会话 id(x-session-id,端点做会话亲和用);u1s1 的设备凭证、DPoP、客户端证明、本地 token、x-u1s1-* 一律不带。
12
+ * 不经网关,也就不计费。
13
+ *
14
+ * 推理参数见 chatReasoningFields:只发 reasoning_effort(关思考发 "none")。
15
+ */
16
+ import type { IncomingHttpHeaders, ServerResponse } from "node:http";
17
+ import { type JsonRecord } from "./chat-protocol/json.js";
18
+ import { type ResponsesContext } from "./chat-protocol/responses.js";
19
+ import { type ThinkingLevel } from "./chat-protocol/thinking.js";
20
+ import { type CustomModelLookup } from "./agents/custom-models.js";
21
+ import type { CustomEndpoint, ModelDef } from "./config.js";
22
+ export type CustomProtocol = "messages" | "count_tokens" | "responses";
23
+ /** 本地路由只看这三条;其余路径(/v1/models、/v1/agents/…)照旧走网关。 */
24
+ export declare function customProtocolFor(method: string | undefined, pathname: string): CustomProtocol | null;
25
+ export interface CustomRouteMatch {
26
+ protocol: CustomProtocol;
27
+ body: JsonRecord;
28
+ lookup: Exclude<CustomModelLookup, {
29
+ kind: "gateway";
30
+ }>;
31
+ }
32
+ /**
33
+ * 这个请求要不要本地处理。只有 model 是 `ep…/…` 写法时才是;端点或模型已不在时也本地报错
34
+ * (网关不认识这种 id,发过去只会多一次网关请求)。body 里连 `"ep` 都没有时不解析 JSON。
35
+ */
36
+ export declare function matchCustomRoute(method: string | undefined, pathname: string, rawBody: Buffer | undefined, endpoints: readonly CustomEndpoint[]): CustomRouteMatch | null;
37
+ /**
38
+ * 客户端请求的思考设置 → Chat 请求里的推理参数(自定义端点只发 `reasoning_effort`;不发 DeepSeek 的顶层
39
+ * `thinking`、不发 chat_template_kwargs:有的端点见到 thinking 直接 400)。
40
+ * - 非推理模型、客户端没表态(level = null):什么都不发,用端点默认;
41
+ * - 关思考:发 "none"(省略等于让后端按默认开着想);
42
+ * - 其余档位:原样发(max 收到 xhigh,OpenAI 兼容端点不认 max)。
43
+ * 模型带思考契约(ModelDef.thinking:levels / canDisable / levelMap,后台「推理参数格式」字段)时,
44
+ * 档位取它支持的最近一档、按 levelMap 改写;关不掉的模型收到「关」退到最低一档。requestFormat 不影响
45
+ * 字段名:自定义端点一律 reasoning_effort。
46
+ */
47
+ export declare function chatReasoningFields(model: Pick<ModelDef, "reasoning" | "thinking">, level: ThinkingLevel | null): JsonRecord;
48
+ export declare function clientSessionId(headers: IncomingHttpHeaders): string | undefined;
49
+ /** 发往端点的请求头:从零构造,见文件头的安全说明。 */
50
+ export declare function endpointRequestHeaders(endpoint: Pick<CustomEndpoint, "apiKey">, stream: boolean, sessionId?: string): Headers;
51
+ export declare function chatCompletionsUrl(baseUrl: string): string;
52
+ /** 转换后的 Chat 体 → 真正发给端点的体:模型 id 换成端点自己的,流式要 usage,输出上限不超过模型登记的值。 */
53
+ export declare function endpointChatBody(chat: JsonRecord, model: ModelDef): JsonRecord;
54
+ export type FailureKind = "connect" | "timeout" | "http" | "malformed" | "context";
55
+ export interface EndpointFailure {
56
+ kind: FailureKind;
57
+ /** 给客户端的 HTTP 状态 */
58
+ status: number;
59
+ /** 给用户看的话(已带端点名) */
60
+ message: string;
61
+ /** 重试有没有可能好:连不上、请求本身有问题、返回不成形都不重试 */
62
+ retry: boolean;
63
+ }
64
+ export declare function httpFailure(endpoint: Pick<CustomEndpoint, "name">, status: number, text: string): EndpointFailure;
65
+ export declare function transportFailure(endpoint: Pick<CustomEndpoint, "name">, error: unknown): EndpointFailure;
66
+ export declare function malformedFailure(endpoint: Pick<CustomEndpoint, "name">, what: string): EndpointFailure;
67
+ /** Anthropic 错误形状(Claude Code):`{type: "error", error: {type, message}}` + x-should-retry。 */
68
+ export declare function anthropicFailureResponse(failure: EndpointFailure): Response;
69
+ /**
70
+ * Responses 错误形状(Codex)。不重试的错误用网关同款的最终形状(HTTP 400 invalid_prompt,Codex 只发一次、
71
+ * 显示 message);超窗给 context_length_exceeded;能重试的(超时、429、5xx)按网关「上游失败」的口径:
72
+ * 流式请求回流内 response.failed(Codex 重试 6 次而不是 30 次),非流式回原状态码。
73
+ */
74
+ export declare function responsesFailureResponse(failure: EndpointFailure, ctx: ResponsesContext): Promise<Response>;
75
+ export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
76
+ /**
77
+ * 端点的 SSE 原文,读到一半出错(断线、超时)时补一帧 Chat 形状的 error,让下游转换器按「流中途失败」
78
+ * 收尾(Claude Code 收到 error 事件,Codex 收到 response.failed),而不是静默截断。
79
+ */
80
+ export declare function guardedEventStream(body: ReadableStream<Uint8Array>, endpoint: Pick<CustomEndpoint, "name">): ReadableStream<Uint8Array>;
81
+ /** 一条完整的 Anthropic message → 等价的 SSE 事件序列(端点不支持流式时用)。 */
82
+ export declare function anthropicMessageAsSse(message: JsonRecord): string;
83
+ /** chat.completion → 等价的单个 chat.completion.chunk(message 换成 delta,工具调用补 index)。 */
84
+ export declare function completionAsChunk(payload: JsonRecord): JsonRecord;
85
+ /** 处理一个本地路由的请求,得到给客户端的 Web Response。 */
86
+ export declare function customRouteResponse(match: CustomRouteMatch, input: {
87
+ headers: IncomingHttpHeaders;
88
+ signal: AbortSignal;
89
+ fetchImpl?: FetchLike;
90
+ }): Promise<Response>;
91
+ /** Web Response → 本机 http 响应(流式逐块写)。 */
92
+ export declare function writeWebResponse(res: ServerResponse, response: Response): Promise<void>;