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.
- package/dist/agents/claude.d.ts +4 -1
- package/dist/agents/claude.js +6 -3
- package/dist/agents/codex.d.ts +4 -1
- package/dist/agents/codex.js +6 -3
- package/dist/agents/custom-models.d.ts +61 -0
- package/dist/agents/custom-models.js +175 -0
- package/dist/agents/launch.d.ts +3 -0
- package/dist/agents/launch.js +17 -5
- package/dist/agents/models-list.d.ts +65 -0
- package/dist/agents/models-list.js +297 -0
- package/dist/api.d.ts +2 -0
- package/dist/chat-protocol/anthropic.d.ts +29 -0
- package/dist/chat-protocol/anthropic.js +484 -0
- package/dist/chat-protocol/json.d.ts +20 -0
- package/dist/chat-protocol/json.js +22 -0
- package/dist/chat-protocol/responses.d.ts +97 -0
- package/dist/chat-protocol/responses.js +1076 -0
- package/dist/chat-protocol/sse.d.ts +19 -0
- package/dist/chat-protocol/sse.js +67 -0
- package/dist/chat-protocol/thinking.d.ts +22 -0
- package/dist/chat-protocol/thinking.js +58 -0
- package/dist/config-command.js +5 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.js +51 -0
- package/dist/custom-endpoint-proxy.d.ts +92 -0
- package/dist/custom-endpoint-proxy.js +461 -0
- package/dist/device-auth.d.ts +13 -2
- package/dist/device-auth.js +36 -2
- package/dist/index.js +9 -1
- package/dist/launcher.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `u1s1 agents models [--json] [--offline]`:给外部前端(Hearth 等)列出每个客户端能用的模型,
|
|
3
|
+
* 带显示名;`id` 就是该客户端 `--model` / `-m` 要的值。
|
|
4
|
+
*
|
|
5
|
+
* 契约(docs/custom-endpoints-all-clients-plan-2026-11-13.md §3):
|
|
6
|
+
* { loggedIn, clients: { pi?: ClientModels, claude?: ClientModels, codex?: ClientModels } }
|
|
7
|
+
* - 未登录:{ loggedIn: false, clients: {} },退出码 0;绝不发起设备登录、不开浏览器、不联网。
|
|
8
|
+
* - 输出只含 id / name / source / endpoint / default / enabled,不带任何密钥、地址、token。
|
|
9
|
+
* - claude / codex 没有本站的 agent-config 缓存时不出现在 clients 里(调用方按「不知道」处理)。
|
|
10
|
+
*
|
|
11
|
+
* 数据来源(都按当前站点隔离):
|
|
12
|
+
* pi 官方模型:/v1/models(短超时)→ <agentDir>/models.json 里上次写的 u1s1 provider → 内置兜底列表;
|
|
13
|
+
* 自定义端点:/v1/endpoints(短超时,成功时刷新 endpoints.json,与启动 Pi 同一做法)→ endpoints.json。
|
|
14
|
+
* claude / codex:只读 ~/.u1s1/agents/<agent>/agent-config.json(拉新配置要起签名代理、拿客户端证明,
|
|
15
|
+
* 不适合做成秒回的列表命令;启动对应客户端时会刷新缓存)。
|
|
16
|
+
* --offline:只读缓存,不联网、不写任何文件。
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { agentDir, apiEndpointToCustom, apiModelToDef, ENDPOINT_ID_RE, loadConfig, MODELS, PROVIDER_ID, readRawConfigFile, resolvePreferredModel, saveEndpointsCache, setCustomEndpoints, setModelsFromApi, u1s1Dir, VERSION, } from "../config.js";
|
|
21
|
+
import { authorizedFetch, hasDeviceCredential } from "../device-auth.js";
|
|
22
|
+
import { tr } from "../i18n.js";
|
|
23
|
+
import { parseAgentConfig, readConfigCache } from "./agent-config.js";
|
|
24
|
+
import { customModelId, routableEndpoint } from "./custom-models.js";
|
|
25
|
+
import { chooseAgentModel, readAgentPrefs } from "./prefs.js";
|
|
26
|
+
const GATEWAY_AGENT = {
|
|
27
|
+
claude: "claude-code",
|
|
28
|
+
codex: "codex",
|
|
29
|
+
};
|
|
30
|
+
// ---------------------------------------------------------------- pure builders
|
|
31
|
+
/** Pi 的 --model 写法:provider/model(和 `u1s1 pi --list-models` 两列拼起来一致)。 */
|
|
32
|
+
export function piModelId(provider, modelId) {
|
|
33
|
+
return `${provider}/${modelId}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 自定义端点的模型 → 列表条目。idFor 决定该客户端认的 id:Pi 是 provider/model;
|
|
37
|
+
* Claude Code / Codex 接上本地路由后(A2)按各自的写法传进来。
|
|
38
|
+
*/
|
|
39
|
+
export function customModelEntries(endpoints, idFor) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const ep of endpoints) {
|
|
42
|
+
for (const m of ep.models) {
|
|
43
|
+
out.push({ id: idFor(ep, m), name: m.name || m.id, source: "custom", endpoint: ep.name });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function piClientModels(official, endpoints, preferred) {
|
|
49
|
+
return {
|
|
50
|
+
default: piModelId(preferred.provider, preferred.id),
|
|
51
|
+
models: [
|
|
52
|
+
...official.map((m) => ({ id: piModelId(PROVIDER_ID, m.id), name: m.name || m.id, source: "u1s1" })),
|
|
53
|
+
...customModelEntries(endpoints, (ep, m) => piModelId(ep.id, m.id)),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Claude Code / Codex:网关阵容(agent-config 缓存)+ 自定义端点的模型(只收本地路由转得了的
|
|
59
|
+
* openai-completions 端点,id = ep…/<模型>,见 custom-models.ts)。default 是启动时真正会用的模型:
|
|
60
|
+
* 用户在 u1s1 config 里选的(网关模型或自定义模型)还在就是它,否则网关默认——与 launch.ts 同一判断。
|
|
61
|
+
*/
|
|
62
|
+
export function gatewayClientModels(config, preferredModel, endpoints = []) {
|
|
63
|
+
const custom = customModelEntries(endpoints.filter(routableEndpoint), (ep, m) => customModelId(ep.id, m.id));
|
|
64
|
+
const lineup = { ...config, models: [...config.models, ...custom.map((m) => ({ id: m.id, name: m.name }))] };
|
|
65
|
+
return {
|
|
66
|
+
default: chooseAgentModel(lineup, preferredModel).model,
|
|
67
|
+
enabled: config.enabled,
|
|
68
|
+
models: [
|
|
69
|
+
...config.models.map((m) => ({ id: m.id, name: m.name || m.id, source: "u1s1" })),
|
|
70
|
+
...custom,
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------- caches (read-only)
|
|
75
|
+
function readJson(path) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isRecord(v) {
|
|
84
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
85
|
+
}
|
|
86
|
+
/** 与 config.ts 的端点缓存同一个文件(这里只读) */
|
|
87
|
+
function endpointsCacheFile() {
|
|
88
|
+
return join(u1s1Dir, "endpoints.json");
|
|
89
|
+
}
|
|
90
|
+
/** Pi 启动时把 u1s1 provider 的 baseUrl 写成本机签名代理(127.0.0.1 的随机端口)或站点地址。 */
|
|
91
|
+
function sameSiteProviderBaseUrl(value, baseUrl) {
|
|
92
|
+
if (typeof value !== "string")
|
|
93
|
+
return false;
|
|
94
|
+
if (value === baseUrl)
|
|
95
|
+
return true;
|
|
96
|
+
try {
|
|
97
|
+
const host = new URL(value).hostname;
|
|
98
|
+
return host === "127.0.0.1" || host === "localhost" || host === "[::1]";
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 上次启动 Pi 时写进 <agentDir>/models.json 的 u1s1 provider。那里的 baseUrl 通常是本机签名代理,
|
|
106
|
+
* 认不出站点;只排除明确指向别的站点的条目(换站后第一次启动 Pi 之前,离线时可能列出上一站的名字)。
|
|
107
|
+
* 那里的 name 拼了「 · 提示」(见 toProviderModels),这里去掉,只留显示名。
|
|
108
|
+
*/
|
|
109
|
+
export function officialModelsFromProviderFile(path, baseUrl) {
|
|
110
|
+
const root = readJson(path);
|
|
111
|
+
const providers = isRecord(root) ? root["providers"] : undefined;
|
|
112
|
+
const entry = isRecord(providers) ? providers[PROVIDER_ID] : undefined;
|
|
113
|
+
if (!isRecord(entry) || !sameSiteProviderBaseUrl(entry["baseUrl"], baseUrl) || !Array.isArray(entry["models"]))
|
|
114
|
+
return undefined;
|
|
115
|
+
const models = [];
|
|
116
|
+
for (const m of entry["models"]) {
|
|
117
|
+
if (!isRecord(m) || typeof m["id"] !== "string" || !m["id"])
|
|
118
|
+
continue;
|
|
119
|
+
const raw = typeof m["name"] === "string" ? m["name"] : "";
|
|
120
|
+
const sep = raw.indexOf(" · ");
|
|
121
|
+
const name = (sep > 0 ? raw.slice(0, sep) : raw).trim() || m["id"];
|
|
122
|
+
models.push({
|
|
123
|
+
id: m["id"],
|
|
124
|
+
name,
|
|
125
|
+
aliases: [],
|
|
126
|
+
reasoning: m["reasoning"] === true,
|
|
127
|
+
vision: false,
|
|
128
|
+
contextWindow: 0,
|
|
129
|
+
maxTokens: 0,
|
|
130
|
+
cost: null,
|
|
131
|
+
note: "",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return models.length ? models : undefined;
|
|
135
|
+
}
|
|
136
|
+
/** endpoints.json 只取列表要用的字段,形状不对的条目跳过。 */
|
|
137
|
+
export function endpointsFromCache(raw) {
|
|
138
|
+
if (!Array.isArray(raw))
|
|
139
|
+
return [];
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const e of raw) {
|
|
142
|
+
if (!isRecord(e) || typeof e["id"] !== "string" || !ENDPOINT_ID_RE.test(e["id"]) || !Array.isArray(e["models"]))
|
|
143
|
+
continue;
|
|
144
|
+
const models = [];
|
|
145
|
+
for (const m of e["models"]) {
|
|
146
|
+
if (!isRecord(m) || typeof m["id"] !== "string" || !m["id"])
|
|
147
|
+
continue;
|
|
148
|
+
models.push({
|
|
149
|
+
id: m["id"],
|
|
150
|
+
name: typeof m["name"] === "string" && m["name"] ? m["name"] : m["id"],
|
|
151
|
+
aliases: [],
|
|
152
|
+
reasoning: m["reasoning"] === true,
|
|
153
|
+
vision: m["vision"] === true,
|
|
154
|
+
contextWindow: 0,
|
|
155
|
+
maxTokens: 0,
|
|
156
|
+
cost: null,
|
|
157
|
+
note: "",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// api 不是密钥,留着让 Claude Code / Codex 的列表只收本地路由转得了的端点;baseUrl / apiKey 不读
|
|
161
|
+
const api = typeof e["api"] === "string" ? e["api"] : "";
|
|
162
|
+
out.push({ id: e["id"], name: typeof e["name"] === "string" ? e["name"] : e["id"], baseUrl: "", api, models });
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
export function cachedAgentConfig(cacheFile, site, agent) {
|
|
167
|
+
const cached = readConfigCache(cacheFile, site);
|
|
168
|
+
return cached ? parseAgentConfig(cached.body, agent) : undefined;
|
|
169
|
+
}
|
|
170
|
+
export const LIVE_REFRESH_TIMEOUT_MS = 2_500;
|
|
171
|
+
/**
|
|
172
|
+
* 并行拉 /v1/models 与 /v1/endpoints,整体不超过 timeoutMs;到点一起中止,
|
|
173
|
+
* 任何失败(断网、401、5xx、格式不对)都只是让对应字段缺席,调用方退回缓存。
|
|
174
|
+
*/
|
|
175
|
+
export async function fetchLiveCatalog(get, timeoutMs = LIVE_REFRESH_TIMEOUT_MS) {
|
|
176
|
+
const ctrl = new AbortController();
|
|
177
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
178
|
+
const json = async (path) => {
|
|
179
|
+
try {
|
|
180
|
+
const res = await get(path, ctrl.signal);
|
|
181
|
+
if (!res.ok)
|
|
182
|
+
return undefined;
|
|
183
|
+
const text = await res.text();
|
|
184
|
+
if (text.length > 2 * 1024 * 1024)
|
|
185
|
+
return undefined;
|
|
186
|
+
const body = JSON.parse(text);
|
|
187
|
+
return isRecord(body) ? body : undefined;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
try {
|
|
194
|
+
const [models, endpoints] = await Promise.all([json("/models"), json("/endpoints")]);
|
|
195
|
+
return {
|
|
196
|
+
...(Array.isArray(models?.["data"]) && models["data"].length ? { models: models["data"] } : {}),
|
|
197
|
+
...(Array.isArray(endpoints?.["endpoints"]) ? { endpoints: endpoints["endpoints"] } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
ctrl.abort();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function buildModelsListing(deps = {}) {
|
|
206
|
+
const cfg = loadConfig();
|
|
207
|
+
// 未登录:到此为止。不联网、不触发设备登录
|
|
208
|
+
if (!hasDeviceCredential(cfg))
|
|
209
|
+
return { loggedIn: false, clients: {} };
|
|
210
|
+
let live = {};
|
|
211
|
+
if (!deps.offline) {
|
|
212
|
+
const fetchLive = deps.fetchLive ?? (() => fetchLiveCatalog((path, signal) => authorizedFetch(cfg, `${cfg.baseUrl}${path}`, { headers: { "x-u1s1-version": VERSION }, signal })));
|
|
213
|
+
live = await fetchLive().catch(() => ({}));
|
|
214
|
+
}
|
|
215
|
+
// 官方模型
|
|
216
|
+
if (live.models) {
|
|
217
|
+
setModelsFromApi(live.models.map(apiModelToDef));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const cached = officialModelsFromProviderFile(join(agentDir, "models.json"), cfg.baseUrl);
|
|
221
|
+
if (cached)
|
|
222
|
+
setModelsFromApi(cached);
|
|
223
|
+
}
|
|
224
|
+
// 自定义端点(成功拉到时刷新缓存,与启动 Pi 的 loadCustomEndpoints 同一做法;--offline 不写)
|
|
225
|
+
let endpoints;
|
|
226
|
+
if (live.endpoints) {
|
|
227
|
+
try {
|
|
228
|
+
endpoints = live.endpoints.map(apiEndpointToCustom);
|
|
229
|
+
if (!deps.offline) {
|
|
230
|
+
try {
|
|
231
|
+
saveEndpointsCache(endpoints);
|
|
232
|
+
}
|
|
233
|
+
catch { /* 缓存写不进去不影响列表 */ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
endpoints = endpointsFromCache(readJson(endpointsCacheFile()));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
endpoints = endpointsFromCache(readJson(endpointsCacheFile()));
|
|
242
|
+
}
|
|
243
|
+
setCustomEndpoints(endpoints);
|
|
244
|
+
const clients = {
|
|
245
|
+
pi: piClientModels(MODELS, endpoints, resolvePreferredModel(cfg)),
|
|
246
|
+
};
|
|
247
|
+
const site = new URL(cfg.baseUrl).origin;
|
|
248
|
+
const raw = readRawConfigFile();
|
|
249
|
+
for (const client of ["claude", "codex"]) {
|
|
250
|
+
const config = cachedAgentConfig(join(u1s1Dir, "agents", client, "agent-config.json"), site, GATEWAY_AGENT[client]);
|
|
251
|
+
if (config)
|
|
252
|
+
clients[client] = gatewayClientModels(config, readAgentPrefs(client, raw).model, endpoints);
|
|
253
|
+
}
|
|
254
|
+
return { loggedIn: true, clients };
|
|
255
|
+
}
|
|
256
|
+
/** 人看的表。 */
|
|
257
|
+
export function formatModelsTable(listing) {
|
|
258
|
+
if (!listing.loggedIn)
|
|
259
|
+
return [tr("还没有登录,先运行 u1s1 login。", "Not signed in yet. Run u1s1 login first.")];
|
|
260
|
+
const lines = [];
|
|
261
|
+
for (const client of ["pi", "claude", "codex"]) {
|
|
262
|
+
const c = listing.clients[client];
|
|
263
|
+
lines.push("");
|
|
264
|
+
if (!c) {
|
|
265
|
+
lines.push(`${client} ${tr("(还没有模型列表,启动一次 u1s1 " + client + " 后可见)", "(no model list yet; start u1s1 " + client + " once to load it)")}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
lines.push(c.enabled === false ? `${client} ${tr("(暂未开放)", "(not available)")}` : client);
|
|
269
|
+
const width = Math.max(0, ...c.models.map((m) => m.id.length));
|
|
270
|
+
for (const m of c.models) {
|
|
271
|
+
const mark = m.id === c.default ? "●" : " ";
|
|
272
|
+
const tag = m.source === "custom" ? ` [${m.endpoint ?? tr("自定义", "custom")}]` : "";
|
|
273
|
+
lines.push(` ${mark} ${m.id.padEnd(width)} ${m.name}${tag}`);
|
|
274
|
+
}
|
|
275
|
+
if (c.models.length === 0)
|
|
276
|
+
lines.push(` ${tr("(没有可用模型)", "(no models)")}`);
|
|
277
|
+
}
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push(tr("● = 默认模型。启动时用 --model 传左列的 id。", "● = default model. Pass the left-hand id with --model."));
|
|
280
|
+
return lines;
|
|
281
|
+
}
|
|
282
|
+
export async function modelsCommand(args, deps = {}) {
|
|
283
|
+
const json = args.includes("--json");
|
|
284
|
+
const offline = deps.offline ?? args.includes("--offline");
|
|
285
|
+
const unknown = args.filter((a) => a !== "--json" && a !== "--offline");
|
|
286
|
+
if (unknown.length) {
|
|
287
|
+
console.error(tr("用法: u1s1 agents models [--json] [--offline]", "Usage: u1s1 agents models [--json] [--offline]"));
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
const listing = await buildModelsListing({ ...deps, offline });
|
|
291
|
+
if (json)
|
|
292
|
+
process.stdout.write(JSON.stringify(listing) + "\n");
|
|
293
|
+
else
|
|
294
|
+
for (const line of formatModelsTable(listing))
|
|
295
|
+
console.log(line);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
package/dist/api.d.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type JsonRecord } from "./json.ts";
|
|
2
|
+
import { type ReasoningFields } from "./thinking.ts";
|
|
3
|
+
export declare const MESSAGES_PATH = "/v1/messages";
|
|
4
|
+
/** Chat usage (any OpenAI-compatible shape) → Anthropic usage. */
|
|
5
|
+
export type AnthropicUsage = (usage: unknown) => JsonRecord;
|
|
6
|
+
/**
|
|
7
|
+
* Plain Chat Completions usage → Anthropic usage: cache reads
|
|
8
|
+
* (`prompt_tokens_details.cached_tokens`, or DeepSeek's
|
|
9
|
+
* `prompt_cache_hit_tokens`) are reported beside `input_tokens`, not in it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function chatUsageToAnthropic(value: unknown): JsonRecord;
|
|
12
|
+
/** Anthropic error body: `{type: "error", error: {type, message}}`. */
|
|
13
|
+
export declare function anthropicErrorBody(status: number, message: string): JsonRecord;
|
|
14
|
+
/**
|
|
15
|
+
* Anthropic Messages request → Chat Completions body. `thinking` and
|
|
16
|
+
* `output_config.effort` become whatever `reasoningFields` returns for the
|
|
17
|
+
* requested level (thinking.ts); `metadata` is dropped on purpose.
|
|
18
|
+
*/
|
|
19
|
+
export declare function anthropicRequestToChat(body: JsonRecord, reasoningFields: ReasoningFields): JsonRecord;
|
|
20
|
+
export declare function chatCompletionToAnthropic(value: unknown, requestedModel: string, usage?: AnthropicUsage): JsonRecord;
|
|
21
|
+
/** OpenAI chat.completion.chunk SSE → Anthropic Messages SSE event sequence. */
|
|
22
|
+
export declare function chatSseToAnthropic(requestedModel: string, usage?: AnthropicUsage): TransformStream<Uint8Array, Uint8Array>;
|
|
23
|
+
export declare function anthropicErrorType(status: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Anthropic SDKs retry 408/409/429/5xx unless the server sends
|
|
26
|
+
* `x-should-retry: false`. Quota and policy refusals are final: without the
|
|
27
|
+
* header the client would hammer the gateway with retries.
|
|
28
|
+
*/
|
|
29
|
+
export declare function anthropicShouldRetry(status: number, code: unknown): boolean;
|