u1s1-cli-cn 1.18.0 → 1.19.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.
@@ -1,5 +1,6 @@
1
1
  import { type McpLaunchEnv } from "../mcp-server.js";
2
2
  import { type AgentConfig } from "./agent-config.js";
3
+ import { type AgentCustomModel } from "./custom-models.js";
3
4
  import { type RuntimePaths } from "./runtime.js";
4
5
  export { agentsRoot, CLAUDE_TERMS_URL, cliInstallHooks, consentLines, notOpenText, overMaxTestedText, printRuntimeFailure, runtimeErrorText, spawnAgent, } from "./launch.js";
5
6
  export declare const CLAUDE_GATEWAY_AGENT: "claude-code";
@@ -72,7 +73,9 @@ export declare function sanitizeClaudeSettings(settings: Record<string, unknown>
72
73
  * model 用调用方选好的(用户在 u1s1 config 里定的、仍在阵容里的模型),没给时保留网关的,
73
74
  * 网关也没有就用 default_model。
74
75
  */
75
- export declare function claudeSettingsFromConfig(config: AgentConfig, model?: string | null): Record<string, unknown>;
76
+ export declare function claudeSettingsFromConfig(config: AgentConfig, model?: string | null,
77
+ /** 自定义端点模型:接在 /model 列表后面(custom-models.ts withCustomClaudeModels) */
78
+ customModels?: readonly AgentCustomModel[]): Record<string, unknown>;
76
79
  /** 调用方的 --settings 有问题(缺值、读不了、不是 JSON 对象);runClaude 报出来后以非 0 退出。 */
77
80
  export declare class ClaudeSettingsArgError extends Error {
78
81
  name: string;
@@ -22,6 +22,7 @@ import { join, resolve } from "node:path";
22
22
  import { writePrivateFile } from "../config.js";
23
23
  import { lang, tr } from "../i18n.js";
24
24
  import { MCP_ENV_NAMES, MCP_SERVER_NAME, MCP_TRACE_ENV, mcpInjectionDisabled, mcpLaunchEnv, mcpServerCommand, } from "../mcp-server.js";
25
+ import { withCustomClaudeModels } from "./custom-models.js";
25
26
  import { AGENT_PROFILES, agentsRoot, prepareAgentLaunch, runPreparedAgent, } from "./launch.js";
26
27
  import { runtimePaths } from "./runtime.js";
27
28
  import { stripSecretEnv } from "../secret-env.js";
@@ -198,13 +199,15 @@ export function sanitizeClaudeSettings(settings) {
198
199
  * model 用调用方选好的(用户在 u1s1 config 里定的、仍在阵容里的模型),没给时保留网关的,
199
200
  * 网关也没有就用 default_model。
200
201
  */
201
- export function claudeSettingsFromConfig(config, model) {
202
+ export function claudeSettingsFromConfig(config, model,
203
+ /** 自定义端点模型:接在 /model 列表后面(custom-models.ts withCustomClaudeModels) */
204
+ customModels = []) {
202
205
  const out = sanitizeClaudeSettings(config.settings ?? {});
203
206
  if (model)
204
207
  out["model"] = model;
205
208
  else if (out["model"] === undefined && config.defaultModel)
206
209
  out["model"] = config.defaultModel;
207
- return out;
210
+ return withCustomClaudeModels(out, customModels, out["model"]);
208
211
  }
209
212
  // ---------------------------------------------------------------- 调用方的 --settings
210
213
  /** 调用方的 --settings 有问题(缺值、读不了、不是 JSON 对象);runClaude 报出来后以非 0 退出。 */
@@ -385,7 +388,7 @@ export async function runClaude(userArgs) {
385
388
  const mcp = mcpInjectionDisabled()
386
389
  ? undefined
387
390
  : mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
388
- const settings = claudeSettingsFromConfig(prepared.config, prepared.model);
391
+ const settings = claudeSettingsFromConfig(prepared.config, prepared.model, prepared.customModels);
389
392
  cleanStaleSettingsFiles(paths.root);
390
393
  const settingsFile = callerSettings.length ? processSettingsFile(paths) : paths.settingsFile;
391
394
  writeClaudeSettings(settingsFile, finalClaudeSettings(settings, callerSettings, !!mcp));
@@ -1,5 +1,6 @@
1
1
  import { type McpLaunchEnv } from "../mcp-server.js";
2
2
  import type { AgentConfig } from "./agent-config.js";
3
+ import { type AgentCustomModel } from "./custom-models.js";
3
4
  import { type RuntimePaths } from "./runtime.js";
4
5
  export declare const CODEX_GATEWAY_AGENT: "codex";
5
6
  /** config.toml 里我们的 provider id(不能用 openai、ollama 等内置保留名)。 */
@@ -98,7 +99,9 @@ export declare function modelCatalogFromConfig(config: AgentConfig): Record<stri
98
99
  */
99
100
  export declare function writeCodexConfig(paths: CodexPaths, config: AgentConfig, now?: () => number, mcp?: CodexMcpServer | null,
100
101
  /** 这次启动用的模型(launch.ts 按 u1s1 config 选好);undefined = 网关默认 */
101
- model?: string | null): string | undefined;
102
+ model?: string | null,
103
+ /** 自定义端点模型:接在网关目录后面(custom-models.ts withCustomCodexCatalog) */
104
+ customModels?: readonly AgentCustomModel[]): string | undefined;
102
105
  /** [mcp_servers.u1s1] 的内容:同一个 node、同一份 CLI;只列要转发的变量名。 */
103
106
  export declare function codexMcpServer(command?: {
104
107
  command: string;
@@ -25,6 +25,7 @@ import { writePrivateFile } from "../config.js";
25
25
  import { lang, tr } from "../i18n.js";
26
26
  import { MCP_ENV_NAMES, MCP_SERVER_NAME, MCP_TRACE_ENV, mcpInjectionDisabled, mcpLaunchEnv, mcpServerCommand, } from "../mcp-server.js";
27
27
  import { stripSecretEnv } from "../secret-env.js";
28
+ import { withCustomCodexCatalog } from "./custom-models.js";
28
29
  import { AGENT_PROFILES, agentsRoot, prepareAgentLaunch, runPreparedAgent } from "./launch.js";
29
30
  import { runtimePaths } from "./runtime.js";
30
31
  export const CODEX_GATEWAY_AGENT = "codex";
@@ -334,9 +335,11 @@ export function modelCatalogFromConfig(config) {
334
335
  */
335
336
  export function writeCodexConfig(paths, config, now = Date.now, mcp = null,
336
337
  /** 这次启动用的模型(launch.ts 按 u1s1 config 选好);undefined = 网关默认 */
337
- model = config.defaultModel) {
338
+ model = config.defaultModel,
339
+ /** 自定义端点模型:接在网关目录后面(custom-models.ts withCustomCodexCatalog) */
340
+ customModels = []) {
338
341
  mkdirSync(paths.configDir, { recursive: true, mode: 0o700 });
339
- const catalog = modelCatalogFromConfig(config);
342
+ const catalog = withCustomCodexCatalog(modelCatalogFromConfig(config), customModels);
340
343
  if (catalog)
341
344
  writePrivateFile(paths.catalogFile, JSON.stringify(catalog) + "\n");
342
345
  else
@@ -382,7 +385,7 @@ export async function runCodex(userArgs) {
382
385
  const mcp = mcpInjectionDisabled()
383
386
  ? undefined
384
387
  : mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
385
- const backup = writeCodexConfig(paths, prepared.config, Date.now, mcp ? codexMcpServer() : null, prepared.model);
388
+ const backup = writeCodexConfig(paths, prepared.config, Date.now, mcp ? codexMcpServer() : null, prepared.model, prepared.customModels);
386
389
  if (backup) {
387
390
  console.error(tr(` ${paths.configToml} 里有 u1s1 没法逐项改写的写法,已整份重写;原文件备份在 ${backup}。`, ` ${paths.configToml} had entries u1s1 couldn't update in place, so it was rewritten; the original is saved as ${backup}.`));
388
391
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 自定义端点的模型给 Claude Code / Codex 用(规划 docs/custom-endpoints-all-clients-plan-2026-11-13.md 的 A2)。
3
+ *
4
+ * 客户端里的模型 id 是 `<端点 id>/<模型 id>`(端点 id = ep + 32 位 hex,模型 id 原样,可以再带 `/`),
5
+ * 与 Pi 的 `provider/model` 写法一致;本机签名代理按这个 id 认出自定义模型,在本机转成 Chat Completions
6
+ * 直连端点(custom-endpoint-proxy.ts),不经网关、不计费。网关模型的 id 里没有这种前缀,照旧走网关。
7
+ *
8
+ * 只收 api = openai-completions 的端点(本地路由只会转 Chat Completions)。
9
+ */
10
+ import { type CustomEndpoint, type ModelDef } from "../config.js";
11
+ /** 本地路由认的 id:`ep<32 hex>/<模型 id>`。 */
12
+ export declare const CUSTOM_MODEL_ID_RE: RegExp;
13
+ export declare const CUSTOM_ENDPOINT_API = "openai-completions";
14
+ export interface AgentCustomModel {
15
+ /** 客户端里用的 id(--model / -m / /model 里的值) */
16
+ id: string;
17
+ endpoint: CustomEndpoint;
18
+ model: ModelDef;
19
+ }
20
+ export declare function customModelId(endpointId: string, modelId: string): string;
21
+ /** 端点能不能给 Claude Code / Codex 用:只认 Chat Completions 协议。 */
22
+ export declare function routableEndpoint(ep: Pick<CustomEndpoint, "id" | "api">): boolean;
23
+ /** 某客户端(Claude Code / Codex)可用的自定义模型,端点顺序、端点内模型顺序。A1 的模型列表也用它。 */
24
+ export declare function agentCustomModels(endpoints: readonly CustomEndpoint[]): AgentCustomModel[];
25
+ export type CustomModelLookup =
26
+ /** 不是自定义模型的写法:交给网关 */
27
+ {
28
+ kind: "gateway";
29
+ }
30
+ /** 自定义模型的写法,但端点或模型已不在(删了、改了名):本机直接报错,不发给网关 */
31
+ | {
32
+ kind: "missing";
33
+ endpointId: string;
34
+ modelId: string;
35
+ } | {
36
+ kind: "custom";
37
+ found: AgentCustomModel;
38
+ };
39
+ export declare function lookupCustomModel(endpoints: readonly CustomEndpoint[], requested: unknown): CustomModelLookup;
40
+ /**
41
+ * 把自定义模型接在 /model 列表(modelPicker.options)后面。Claude Code 不认识这些 id,窗口只有一个全局的
42
+ * CLAUDE_CODE_MAX_CONTEXT_TOKENS:启动模型是自定义模型时设成它的窗口,否则不动(不让一个小窗口的本地模型
43
+ * 拖低网关模型的压缩点;会话中途从网关模型换到更小窗口的自定义模型时可能超窗,由端点报错)。
44
+ */
45
+ export declare function withCustomClaudeModels(settings: Record<string, unknown>, customs: readonly AgentCustomModel[], launchModel: string | null | undefined): Record<string, unknown>;
46
+ /**
47
+ * Codex /model 里给自定义模型列的思考档位(本地路由按 chatReasoningFields 转成 reasoning_effort):
48
+ * 带思考契约的按契约(关不掉的不列 none);只标了推理的给 none/low/medium/high,默认 medium;非推理不列。
49
+ */
50
+ export declare function codexCustomReasoning(model: Pick<ModelDef, "reasoning" | "thinking">): {
51
+ levels: string[];
52
+ defaultLevel: string | null;
53
+ };
54
+ /**
55
+ * 自定义模型的 model_catalog 条目。指令模板等 Codex 必填但与模型无关的字段取网关目录的第一条
56
+ * (网关目录每条都带同一份模板);目录为空时没有模板可用,返回 undefined(Codex 对目录外的模型用
57
+ * 兜底元数据,照样能用)。
58
+ */
59
+ export declare function codexCatalogEntry(c: AgentCustomModel, template: Record<string, unknown>, priority: number): Record<string, unknown>;
60
+ /** 网关的 model_catalog + 自定义模型(排在后面)。没有网关目录或目录为空时原样返回。 */
61
+ export declare function withCustomCodexCatalog(catalog: Record<string, unknown> | undefined, customs: readonly AgentCustomModel[]): Record<string, unknown> | undefined;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * 自定义端点的模型给 Claude Code / Codex 用(规划 docs/custom-endpoints-all-clients-plan-2026-11-13.md 的 A2)。
3
+ *
4
+ * 客户端里的模型 id 是 `<端点 id>/<模型 id>`(端点 id = ep + 32 位 hex,模型 id 原样,可以再带 `/`),
5
+ * 与 Pi 的 `provider/model` 写法一致;本机签名代理按这个 id 认出自定义模型,在本机转成 Chat Completions
6
+ * 直连端点(custom-endpoint-proxy.ts),不经网关、不计费。网关模型的 id 里没有这种前缀,照旧走网关。
7
+ *
8
+ * 只收 api = openai-completions 的端点(本地路由只会转 Chat Completions)。
9
+ */
10
+ import { ENDPOINT_ID_RE } from "../config.js";
11
+ /** 本地路由认的 id:`ep<32 hex>/<模型 id>`。 */
12
+ export const CUSTOM_MODEL_ID_RE = /^(ep[0-9a-f]{32})\/(.+)$/;
13
+ export const CUSTOM_ENDPOINT_API = "openai-completions";
14
+ export function customModelId(endpointId, modelId) {
15
+ return `${endpointId}/${modelId}`;
16
+ }
17
+ /** 端点能不能给 Claude Code / Codex 用:只认 Chat Completions 协议。 */
18
+ export function routableEndpoint(ep) {
19
+ return ENDPOINT_ID_RE.test(ep.id) && ep.api === CUSTOM_ENDPOINT_API;
20
+ }
21
+ /** 某客户端(Claude Code / Codex)可用的自定义模型,端点顺序、端点内模型顺序。A1 的模型列表也用它。 */
22
+ export function agentCustomModels(endpoints) {
23
+ const out = [];
24
+ const seen = new Set();
25
+ for (const endpoint of endpoints) {
26
+ if (!routableEndpoint(endpoint))
27
+ continue;
28
+ for (const model of endpoint.models) {
29
+ const id = customModelId(endpoint.id, model.id);
30
+ if (!model.id || seen.has(id))
31
+ continue;
32
+ seen.add(id);
33
+ out.push({ id, endpoint, model });
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ export function lookupCustomModel(endpoints, requested) {
39
+ if (typeof requested !== "string")
40
+ return { kind: "gateway" };
41
+ const m = CUSTOM_MODEL_ID_RE.exec(requested);
42
+ if (!m)
43
+ return { kind: "gateway" };
44
+ const [, endpointId, modelId] = m;
45
+ const endpoint = endpoints.find((ep) => ep.id === endpointId && routableEndpoint(ep));
46
+ const model = endpoint?.models.find((x) => x.id === modelId);
47
+ if (!endpoint || !model)
48
+ return { kind: "missing", endpointId, modelId };
49
+ return { kind: "custom", found: { id: requested, endpoint, model } };
50
+ }
51
+ // ---------------------------------------------------------------- Claude Code
52
+ function contextLabel(tokens) {
53
+ if (tokens >= 1_000_000)
54
+ return `${Math.round(tokens / 100_000) / 10}M context`;
55
+ return `${Math.round(tokens / 1000)}K context`;
56
+ }
57
+ function customDescription(c) {
58
+ const parts = [];
59
+ if (c.model.contextWindow > 0)
60
+ parts.push(contextLabel(c.model.contextWindow));
61
+ parts.push(`custom endpoint ${c.endpoint.name}`);
62
+ return parts.join(" - ");
63
+ }
64
+ function isPlainObject(value) {
65
+ return !!value && typeof value === "object" && !Array.isArray(value);
66
+ }
67
+ /**
68
+ * 把自定义模型接在 /model 列表(modelPicker.options)后面。Claude Code 不认识这些 id,窗口只有一个全局的
69
+ * CLAUDE_CODE_MAX_CONTEXT_TOKENS:启动模型是自定义模型时设成它的窗口,否则不动(不让一个小窗口的本地模型
70
+ * 拖低网关模型的压缩点;会话中途从网关模型换到更小窗口的自定义模型时可能超窗,由端点报错)。
71
+ */
72
+ export function withCustomClaudeModels(settings, customs, launchModel) {
73
+ if (customs.length === 0)
74
+ return settings;
75
+ const out = { ...settings };
76
+ const picker = isPlainObject(out["modelPicker"]) ? { ...out["modelPicker"] } : {};
77
+ const options = Array.isArray(picker["options"]) ? [...picker["options"]] : [];
78
+ const listed = new Set(options.map((o) => (isPlainObject(o) ? o["model"] : undefined)));
79
+ for (const c of customs) {
80
+ if (listed.has(c.id))
81
+ continue;
82
+ options.push({ model: c.id, label: c.model.name || c.model.id, description: customDescription(c) });
83
+ }
84
+ picker["options"] = options;
85
+ out["modelPicker"] = picker;
86
+ const launched = customs.find((c) => c.id === launchModel);
87
+ if (launched && launched.model.contextWindow > 0) {
88
+ const env = isPlainObject(out["env"]) ? { ...out["env"] } : {};
89
+ env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = String(launched.model.contextWindow);
90
+ out["env"] = env;
91
+ }
92
+ return out;
93
+ }
94
+ // ---------------------------------------------------------------- Codex
95
+ const CODEX_EFFORT_TEXT = {
96
+ none: "No reasoning",
97
+ minimal: "Minimal reasoning",
98
+ low: "Fast responses with lighter reasoning",
99
+ medium: "Balanced speed and reasoning depth",
100
+ high: "Deeper reasoning for complex problems",
101
+ xhigh: "Extra deep reasoning",
102
+ };
103
+ /** u1s1 思考档位 → Codex 的 reasoning effort;max 在 Responses 里没有对应档,不列。 */
104
+ const CODEX_EFFORT = {
105
+ off: "none",
106
+ minimal: "minimal",
107
+ low: "low",
108
+ medium: "medium",
109
+ high: "high",
110
+ xhigh: "xhigh",
111
+ };
112
+ /**
113
+ * Codex /model 里给自定义模型列的思考档位(本地路由按 chatReasoningFields 转成 reasoning_effort):
114
+ * 带思考契约的按契约(关不掉的不列 none);只标了推理的给 none/low/medium/high,默认 medium;非推理不列。
115
+ */
116
+ export function codexCustomReasoning(model) {
117
+ if (!model.reasoning)
118
+ return { levels: [], defaultLevel: null };
119
+ const caps = model.thinking;
120
+ if (caps && caps.levels.length) {
121
+ const levels = caps.levels
122
+ .filter((level) => level !== "off" || caps.canDisable)
123
+ .map((level) => CODEX_EFFORT[level])
124
+ .filter((effort) => typeof effort === "string");
125
+ const preferred = CODEX_EFFORT[caps.defaultLevel];
126
+ const defaultLevel = preferred && levels.includes(preferred) ? preferred : levels.filter((e) => e !== "none").at(-1) ?? null;
127
+ return { levels, defaultLevel };
128
+ }
129
+ return { levels: ["none", "low", "medium", "high"], defaultLevel: "medium" };
130
+ }
131
+ /** 与网关 codex-config.ts 一致:模型行没设压缩预算时按 200K 压。 */
132
+ const CODEX_DEFAULT_COMPACT_TOKENS = 200_000;
133
+ /**
134
+ * 自定义模型的 model_catalog 条目。指令模板等 Codex 必填但与模型无关的字段取网关目录的第一条
135
+ * (网关目录每条都带同一份模板);目录为空时没有模板可用,返回 undefined(Codex 对目录外的模型用
136
+ * 兜底元数据,照样能用)。
137
+ */
138
+ export function codexCatalogEntry(c, template, priority) {
139
+ const reasoning = codexCustomReasoning(c.model);
140
+ const levels = reasoning.levels;
141
+ const window = c.model.contextWindow > 0 ? c.model.contextWindow : undefined;
142
+ const entry = {
143
+ ...template,
144
+ slug: c.id,
145
+ display_name: c.model.name || c.model.id,
146
+ description: customDescription(c),
147
+ supported_reasoning_levels: levels.map((effort) => ({ effort, description: CODEX_EFFORT_TEXT[effort] ?? effort })),
148
+ priority,
149
+ input_modalities: c.model.vision ? ["text", "image"] : ["text"],
150
+ };
151
+ delete entry["default_reasoning_level"];
152
+ if (reasoning.defaultLevel)
153
+ entry["default_reasoning_level"] = reasoning.defaultLevel;
154
+ if (window) {
155
+ entry["context_window"] = window;
156
+ entry["max_context_window"] = window;
157
+ // 压缩点:200K 与窗口的 90% 取小(本地模型窗口常比 200K 小)
158
+ entry["auto_compact_token_limit"] = Math.min(CODEX_DEFAULT_COMPACT_TOKENS, Math.floor(window * 0.9));
159
+ }
160
+ return entry;
161
+ }
162
+ /** 网关的 model_catalog + 自定义模型(排在后面)。没有网关目录或目录为空时原样返回。 */
163
+ export function withCustomCodexCatalog(catalog, customs) {
164
+ if (!catalog || customs.length === 0)
165
+ return catalog;
166
+ const models = Array.isArray(catalog["models"]) ? catalog["models"] : [];
167
+ const template = models.find(isPlainObject);
168
+ if (!template)
169
+ return catalog;
170
+ const slugs = new Set(models.map((m) => (isPlainObject(m) ? m["slug"] : undefined)));
171
+ const extra = customs
172
+ .filter((c) => !slugs.has(c.id))
173
+ .map((c, i) => codexCatalogEntry(c, template, models.length + i));
174
+ return { ...catalog, models: [...models, ...extra] };
175
+ }
@@ -9,6 +9,7 @@ import { spawn } from "node:child_process";
9
9
  import { type ApiFeatures } from "../api.js";
10
10
  import { type SiteId } from "../site.js";
11
11
  import { type AgentConfig, type GatewayAgentName } from "./agent-config.js";
12
+ import { type AgentCustomModel } from "./custom-models.js";
12
13
  import { RuntimePlanError, type DownloadInfo, type EnsuredRuntime, type InstallHooks, type ManagedAgent, type RuntimePaths, type SkippedRuntime } from "./runtime.js";
13
14
  export declare function agentsRoot(): string;
14
15
  export interface AgentProfile {
@@ -76,6 +77,8 @@ export interface PreparedLaunch {
76
77
  site: SiteId;
77
78
  /** /v1/models 的能力开关(搜索、云端渲染、生图);没拉到时为 undefined。MCP 工具服务按它决定提供哪些工具 */
78
79
  features: ApiFeatures | undefined;
80
+ /** 账号里的自定义端点模型(id = ep…/<模型>):接在客户端模型阵容后面,请求由签名代理在本机直连端点 */
81
+ customModels: AgentCustomModel[];
79
82
  }
80
83
  /**
81
84
  * 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
@@ -11,12 +11,13 @@ import { constants as osConstants } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import { Agent, fetch as undiciFetch } from "undici";
14
- import { AccessDeniedError, AuthError, fetchModels } from "../api.js";
15
- import { u1s1Dir } from "../config.js";
14
+ import { AccessDeniedError, AuthError, fetchModels, loadCustomEndpoints } from "../api.js";
15
+ import { CUSTOM_ENDPOINTS, u1s1Dir } from "../config.js";
16
16
  import { ensureSigningProxy } from "../device-auth.js";
17
17
  import { tr } from "../i18n.js";
18
18
  import { siteLinks, siteOf } from "../site.js";
19
19
  import { loadAgentConfig } from "./agent-config.js";
20
+ import { agentCustomModels } from "./custom-models.js";
20
21
  import { readAgentPrefs, resolveLaunchModel } from "./prefs.js";
21
22
  import { dailyUpdateCheck, ensureAgentRuntime, fetchLatestVersion, npmRegistryFor, RuntimeInstallError, RuntimePlanError, runtimeSpec, } from "./runtime.js";
22
23
  export function agentsRoot() {
@@ -319,8 +320,16 @@ export async function prepareAgentLaunch(profile, paths) {
319
320
  return 1;
320
321
  }
321
322
  const config = result.config;
323
+ // 自定义端点:拉不到时用上次的缓存(与启动 Pi 同一做法)
324
+ await loadCustomEndpoints(cfg).catch(() => undefined);
325
+ const customModels = agentCustomModels(CUSTOM_ENDPOINTS);
322
326
  const prefs = readAgentPrefs(profile.agent);
323
- const model = resolveLaunchModel(profile.agent, config, {
327
+ // 用户在 u1s1 config 里选的模型可以是自定义模型
328
+ const lineup = {
329
+ ...config,
330
+ models: [...config.models, ...customModels.map((c) => ({ id: c.id, name: c.model.name, contextWindow: c.model.contextWindow }))],
331
+ };
332
+ const model = resolveLaunchModel(profile.agent, lineup, {
324
333
  prefs,
325
334
  notify: (missing, fallback) => console.error(modelMissingText(profile.agent, missing, fallback)),
326
335
  });
@@ -343,8 +352,11 @@ export async function prepareAgentLaunch(profile, paths) {
343
352
  }
344
353
  printRuntimeNotes(runtime, profile.agent);
345
354
  // 子进程用的代理:带上客户端名与版本,网关据此核对请求形状
346
- const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version });
347
- return { config, model, autoUpdate: prefs.autoUpdate, runtime, proxy, site: site.id, features: models?.features };
355
+ // 自定义模型的请求在这个代理里本机直连端点(custom-endpoint-proxy.ts),不经网关
356
+ const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version }, {
357
+ endpoints: [...CUSTOM_ENDPOINTS],
358
+ });
359
+ return { config, model, autoUpdate: prefs.autoUpdate, runtime, proxy, site: site.id, features: models?.features, customModels };
348
360
  }
349
361
  /** 用户在 u1s1 config 里选的模型不在当前阵容里(只提示一次,见 prefs.ts)。 */
350
362
  export function modelMissingText(agent, missing, fallback) {
@@ -0,0 +1,65 @@
1
+ import type { ApiEndpoint, ApiModel } from "../api.js";
2
+ import { type CustomEndpoint, type ModelDef, type ModelRef } from "../config.js";
3
+ import { type AgentConfig, type GatewayAgentName } from "./agent-config.js";
4
+ export type ModelsClient = "pi" | "claude" | "codex";
5
+ export interface ModelEntry {
6
+ /** 该客户端 --model 要的值 */
7
+ id: string;
8
+ /** 显示名;拿不到时等于 id */
9
+ name: string;
10
+ source: "u1s1" | "custom";
11
+ /** source = custom 时:端点名(后台填的) */
12
+ endpoint?: string;
13
+ }
14
+ export interface ClientModels {
15
+ default: string | null;
16
+ /** 网关是否开放这个客户端(Pi 恒开放,不带这个字段) */
17
+ enabled?: boolean;
18
+ models: ModelEntry[];
19
+ }
20
+ export interface ModelsListing {
21
+ loggedIn: boolean;
22
+ clients: Partial<Record<ModelsClient, ClientModels>>;
23
+ }
24
+ /** Pi 的 --model 写法:provider/model(和 `u1s1 pi --list-models` 两列拼起来一致)。 */
25
+ export declare function piModelId(provider: string, modelId: string): string;
26
+ /**
27
+ * 自定义端点的模型 → 列表条目。idFor 决定该客户端认的 id:Pi 是 provider/model;
28
+ * Claude Code / Codex 接上本地路由后(A2)按各自的写法传进来。
29
+ */
30
+ export declare function customModelEntries(endpoints: readonly CustomEndpoint[], idFor: (endpoint: CustomEndpoint, model: ModelDef) => string): ModelEntry[];
31
+ export declare function piClientModels(official: readonly ModelDef[], endpoints: readonly CustomEndpoint[], preferred: ModelRef): ClientModels;
32
+ /**
33
+ * Claude Code / Codex:网关阵容(agent-config 缓存)+ 自定义端点的模型(只收本地路由转得了的
34
+ * openai-completions 端点,id = ep…/<模型>,见 custom-models.ts)。default 是启动时真正会用的模型:
35
+ * 用户在 u1s1 config 里选的(网关模型或自定义模型)还在就是它,否则网关默认——与 launch.ts 同一判断。
36
+ */
37
+ export declare function gatewayClientModels(config: AgentConfig, preferredModel: string | undefined, endpoints?: readonly CustomEndpoint[]): ClientModels;
38
+ /**
39
+ * 上次启动 Pi 时写进 <agentDir>/models.json 的 u1s1 provider。那里的 baseUrl 通常是本机签名代理,
40
+ * 认不出站点;只排除明确指向别的站点的条目(换站后第一次启动 Pi 之前,离线时可能列出上一站的名字)。
41
+ * 那里的 name 拼了「 · 提示」(见 toProviderModels),这里去掉,只留显示名。
42
+ */
43
+ export declare function officialModelsFromProviderFile(path: string, baseUrl: string): ModelDef[] | undefined;
44
+ /** endpoints.json 只取列表要用的字段,形状不对的条目跳过。 */
45
+ export declare function endpointsFromCache(raw: unknown): CustomEndpoint[];
46
+ export declare function cachedAgentConfig(cacheFile: string, site: string, agent: GatewayAgentName): AgentConfig | undefined;
47
+ export interface LiveCatalog {
48
+ models?: ApiModel[];
49
+ endpoints?: ApiEndpoint[];
50
+ }
51
+ export declare const LIVE_REFRESH_TIMEOUT_MS = 2500;
52
+ /**
53
+ * 并行拉 /v1/models 与 /v1/endpoints,整体不超过 timeoutMs;到点一起中止,
54
+ * 任何失败(断网、401、5xx、格式不对)都只是让对应字段缺席,调用方退回缓存。
55
+ */
56
+ export declare function fetchLiveCatalog(get: (path: "/models" | "/endpoints", signal: AbortSignal) => Promise<Response>, timeoutMs?: number): Promise<LiveCatalog>;
57
+ export interface ModelsCommandDeps {
58
+ offline?: boolean;
59
+ /** 测试注入;默认经 authorizedFetch 请求当前站点 */
60
+ fetchLive?: () => Promise<LiveCatalog>;
61
+ }
62
+ export declare function buildModelsListing(deps?: ModelsCommandDeps): Promise<ModelsListing>;
63
+ /** 人看的表。 */
64
+ export declare function formatModelsTable(listing: ModelsListing): string[];
65
+ export declare function modelsCommand(args: readonly string[], deps?: ModelsCommandDeps): Promise<number>;