u1s1-cli 0.11.1 → 0.12.1

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,4 +1,4 @@
1
- import { type CliConfig } from "./config.js";
1
+ import { type CliConfig, type CustomEndpoint, type ModelDef } from "./config.js";
2
2
  /** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
3
3
  export declare function ensureBrandPrompt(): void;
4
4
  /** Defaults that don't overwrite values the user already set. */
@@ -29,4 +29,23 @@ export declare function writeWebToolsExtension(cfg: CliConfig, webSearchEnabled:
29
29
  * 其他 provider 的凭据原样保留;文件损坏时不动它,交给 SDK 自己报错。
30
30
  */
31
31
  export declare function ensureAuthCredential(): void;
32
+ /** pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。 */
33
+ export declare function toProviderModels(models: ModelDef[]): {
34
+ id: string;
35
+ name: string;
36
+ reasoning: boolean;
37
+ input: ("text" | "image")[];
38
+ cost: ModelDef["cost"];
39
+ contextWindow: number;
40
+ maxTokens: number;
41
+ }[];
42
+ /**
43
+ * 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
44
+ * endpointKeyEnv),文件里不落明文;没配密钥的端点(本机 Ollama 等)写
45
+ * 字面量 "local" —— pi 没有凭据会直接拒启("No API key found"),
46
+ * 这是 pi 自带 llama provider 的同款兜底,本地服务会忽略 Authorization 头。
47
+ */
48
+ export declare function endpointProviderEntry(ep: CustomEndpoint): Record<string, unknown>;
49
+ /** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
50
+ export declare function endpointKeyEnv(): Record<string, string>;
32
51
  export declare function ensureProviderModels(cfg: CliConfig): void;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { agentDir, MODELS, PROVIDER_ID, VERSION } from "./config.js";
3
+ import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
4
4
  const BRAND_APPEND = `## u1s1
5
5
 
6
6
  你是 u1s1(有一说一) —— 说人话的 AI 编程搭子,一个面向编程新手的中文 AI 编程助手。用户很可能不熟悉编程术语:
@@ -133,6 +133,42 @@ export function ensureAuthCredential() {
133
133
  root[PROVIDER_ID] = { type: "api_key", key: "$U1S1_API_KEY" };
134
134
  writeFileSync(p, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
135
135
  }
136
+ /** pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。 */
137
+ export function toProviderModels(models) {
138
+ return models.map((m) => ({
139
+ id: m.id,
140
+ name: m.name,
141
+ reasoning: m.reasoning,
142
+ input: ["text"],
143
+ cost: m.cost,
144
+ contextWindow: m.contextWindow,
145
+ maxTokens: m.maxTokens,
146
+ }));
147
+ }
148
+ /**
149
+ * 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
150
+ * endpointKeyEnv),文件里不落明文;没配密钥的端点(本机 Ollama 等)写
151
+ * 字面量 "local" —— pi 没有凭据会直接拒启("No API key found"),
152
+ * 这是 pi 自带 llama provider 的同款兜底,本地服务会忽略 Authorization 头。
153
+ */
154
+ export function endpointProviderEntry(ep) {
155
+ return {
156
+ name: ep.name,
157
+ baseUrl: ep.baseUrl,
158
+ api: ep.api,
159
+ apiKey: ep.apiKey ? `$${endpointKeyEnvName(ep.id)}` : "local",
160
+ models: toProviderModels(ep.models),
161
+ };
162
+ }
163
+ /** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
164
+ export function endpointKeyEnv() {
165
+ const env = {};
166
+ for (const ep of CUSTOM_ENDPOINTS) {
167
+ if (ep.apiKey)
168
+ env[endpointKeyEnvName(ep.id)] = ep.apiKey;
169
+ }
170
+ return env;
171
+ }
136
172
  export function ensureProviderModels(cfg) {
137
173
  mkdirSync(agentDir, { recursive: true });
138
174
  const p = join(agentDir, "models.json");
@@ -155,16 +191,18 @@ export function ensureProviderModels(cfg) {
155
191
  apiKey: "$U1S1_API_KEY",
156
192
  // 网关按这个头识别客户端版本;不带头的旧版会在会话首轮被追加升级提示
157
193
  headers: { "x-u1s1-version": VERSION },
158
- models: MODELS.map((m) => ({
159
- id: m.id,
160
- name: m.name,
161
- reasoning: m.reasoning,
162
- input: ["text"],
163
- cost: m.cost,
164
- contextWindow: m.contextWindow,
165
- maxTokens: m.maxTokens,
166
- })),
194
+ models: toProviderModels(MODELS),
167
195
  };
196
+ // 自定义端点:先清掉我们此前写入、如今已删除的条目(键形状 ep+32hex,
197
+ // 用户手工加的其他 provider 不受影响),再写当前列表
198
+ for (const key of Object.keys(providers)) {
199
+ if (ENDPOINT_ID_RE.test(key) && !CUSTOM_ENDPOINTS.some((e) => e.id === key)) {
200
+ delete providers[key];
201
+ }
202
+ }
203
+ for (const ep of CUSTOM_ENDPOINTS) {
204
+ providers[ep.id] = endpointProviderEntry(ep);
205
+ }
168
206
  root["providers"] = providers;
169
207
  writeFileSync(p, JSON.stringify(root, null, 2) + "\n");
170
208
  }
package/dist/api.d.ts CHANGED
@@ -36,6 +36,30 @@ export interface ModelsResponse {
36
36
  features: ApiFeatures;
37
37
  }
38
38
  export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
39
+ export interface ApiEndpoint {
40
+ id: string;
41
+ name: string;
42
+ base_url: string;
43
+ api: string;
44
+ api_key: string | null;
45
+ models: {
46
+ id: string;
47
+ name?: string;
48
+ reasoning?: boolean;
49
+ context_window?: number;
50
+ max_tokens?: number;
51
+ }[];
52
+ }
53
+ /**
54
+ * 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
55
+ * 老网关没有这个路由(GET 落到静态资产 404),调用方失败时回退本地缓存。
56
+ */
57
+ export declare function fetchUserEndpoints(cfg: CliConfig): Promise<ApiEndpoint[]>;
58
+ /**
59
+ * 拉取云端配置的自定义端点并装载进 CUSTOM_ENDPOINTS;失败(离线/老网关)回退
60
+ * 上次缓存,保证本地端点断网时依旧可用。成功时刷新缓存(~/.u1s1/endpoints.json)。
61
+ */
62
+ export declare function loadCustomEndpoints(cfg: CliConfig): Promise<void>;
39
63
  export interface SearchResult {
40
64
  title: string;
41
65
  url: string;
package/dist/api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { VERSION } from "./config.js";
1
+ import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
2
2
  /** 网关按 x-u1s1-version 识别客户端版本(旧版 CLI 不带,提示升级)。 */
3
3
  function authHeaders(apiKey) {
4
4
  return { authorization: `Bearer ${apiKey}`, "x-u1s1-version": VERSION };
@@ -20,6 +20,39 @@ export async function fetchModels(cfg) {
20
20
  const body = (await resp.json());
21
21
  return { models: body.data, features: body.features ?? {} };
22
22
  }
23
+ /**
24
+ * 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
25
+ * 老网关没有这个路由(GET 落到静态资产 404),调用方失败时回退本地缓存。
26
+ */
27
+ export async function fetchUserEndpoints(cfg) {
28
+ let resp;
29
+ try {
30
+ resp = await fetch(`${cfg.baseUrl}/endpoints`, {
31
+ headers: authHeaders(cfg.apiKey),
32
+ });
33
+ }
34
+ catch {
35
+ throw new Error(`连不上 ${cfg.baseUrl}`);
36
+ }
37
+ if (!resp.ok)
38
+ throw new Error(`服务端返回 ${resp.status}`);
39
+ const body = (await resp.json());
40
+ return Array.isArray(body.endpoints) ? body.endpoints : [];
41
+ }
42
+ /**
43
+ * 拉取云端配置的自定义端点并装载进 CUSTOM_ENDPOINTS;失败(离线/老网关)回退
44
+ * 上次缓存,保证本地端点断网时依旧可用。成功时刷新缓存(~/.u1s1/endpoints.json)。
45
+ */
46
+ export async function loadCustomEndpoints(cfg) {
47
+ try {
48
+ const endpoints = (await fetchUserEndpoints(cfg)).map(apiEndpointToCustom);
49
+ setCustomEndpoints(endpoints);
50
+ saveEndpointsCache(endpoints);
51
+ }
52
+ catch {
53
+ setCustomEndpoints(loadEndpointsCache());
54
+ }
55
+ }
23
56
  /** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
24
57
  export async function searchWeb(cfg, query, maxResults, signal) {
25
58
  if (!cfg.apiKey)
package/dist/config.d.ts CHANGED
@@ -45,11 +45,61 @@ export declare const MODELS: ModelDef[];
45
45
  export declare function setModelsFromApi(apiModels: ModelDef[]): void;
46
46
  export declare const DEFAULT_MODEL_ID: string;
47
47
  export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
48
+ /** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
49
+ export declare const ENDPOINT_ID_RE: RegExp;
50
+ export interface CustomEndpoint {
51
+ /** 云端端点 id,同时作为 pi provider 的注册键 */
52
+ id: string;
53
+ name: string;
54
+ baseUrl: string;
55
+ /** pi 协议类型:openai-completions / anthropic-messages */
56
+ api: string;
57
+ apiKey?: string;
58
+ models: ModelDef[];
59
+ }
60
+ export declare const CUSTOM_ENDPOINTS: CustomEndpoint[];
61
+ export declare function setCustomEndpoints(endpoints: CustomEndpoint[]): void;
62
+ /** 端点密钥不写进 models.json,走这个环境变量引用(pi 读取时解析)。 */
63
+ export declare function endpointKeyEnvName(endpointId: string): string;
64
+ /** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
65
+ export declare function apiEndpointToCustom(e: {
66
+ id: string;
67
+ name: string;
68
+ base_url: string;
69
+ api: string;
70
+ api_key: string | null;
71
+ models: {
72
+ id: string;
73
+ name?: string;
74
+ reasoning?: boolean;
75
+ context_window?: number;
76
+ max_tokens?: number;
77
+ }[];
78
+ }): CustomEndpoint;
79
+ /** 模型引用 = provider 注册键 + 模型 id;provider 是 u1s1 或某个端点 id。 */
80
+ export interface ModelRef {
81
+ provider: string;
82
+ id: string;
83
+ }
84
+ export interface ResolvedModel extends ModelRef {
85
+ /** 展示用:u1s1 或端点名 */
86
+ providerName: string;
87
+ model: ModelDef;
88
+ }
89
+ /** ref 指向的模型仍然存在吗(服务端下架/端点删除后失效)。 */
90
+ export declare function refValid(ref: ModelRef): boolean;
91
+ /**
92
+ * 按名字/别名找模型,支持三种写法:模型 id、别名、`端点名:模型id`。
93
+ * u1s1 官方模型优先,再按端点顺序找,重名取先命中。
94
+ */
95
+ export declare function findModel(query: string): ResolvedModel | undefined;
48
96
  export interface CliConfig {
49
97
  apiKey?: string;
50
98
  baseUrl: string;
51
- /** preferred model id; must be one of MODELS */
99
+ /** preferred model id (within modelProvider) */
52
100
  model?: string;
101
+ /** preferred model 所属 provider;缺省 = u1s1(老配置兼容) */
102
+ modelProvider?: string;
53
103
  }
54
104
  export declare const u1s1Dir: string;
55
105
  /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
@@ -63,15 +113,18 @@ export interface AgentSettings {
63
113
  /** Read u1s1 agent settings (settings.json). Returns empty object if missing or invalid. */
64
114
  export declare function readSettings(): AgentSettings;
65
115
  /** Model last chosen in-session via /model (pi writes this). */
66
- export declare function readAgentDefaultModel(): string | undefined;
116
+ export declare function readAgentDefaultModel(): ModelRef | undefined;
67
117
  /** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
68
- export declare function writeAgentDefaultModel(modelId: string): void;
118
+ export declare function writeAgentDefaultModel(provider: string, modelId: string): void;
69
119
  /**
70
120
  * After this fix both stores stay in sync. If they still disagree (old installs),
71
121
  * prefer the in-session /model value — that's the one users thought they had set.
122
+ * 校验依赖 CUSTOM_ENDPOINTS,须在 setCustomEndpoints 之后调用才认得端点模型。
72
123
  */
73
- export declare function resolvePreferredModel(configModel: string | undefined): string;
124
+ export declare function resolvePreferredModel(cfg: Pick<CliConfig, "model" | "modelProvider">): ModelRef;
74
125
  export declare function loadConfig(): CliConfig;
75
126
  export declare function saveConfig(cfg: CliConfig): void;
76
127
  /** Persist the user's preferred model to both stores. */
77
- export declare function persistPreferredModel(cfg: CliConfig, modelId: string): CliConfig;
128
+ export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
129
+ export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
130
+ export declare function loadEndpointsCache(): CustomEndpoint[];
package/dist/config.js CHANGED
@@ -81,6 +81,71 @@ export function resolveModel(nameOrAlias) {
81
81
  const q = nameOrAlias.trim().toLowerCase();
82
82
  return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
83
83
  }
84
+ // ---- 用户自定义端点(云端 dashboard 配置,启动时经 /v1/endpoints 拉取)----
85
+ /** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
86
+ export const ENDPOINT_ID_RE = /^ep[0-9a-f]{32}$/;
87
+ export const CUSTOM_ENDPOINTS = [];
88
+ export function setCustomEndpoints(endpoints) {
89
+ CUSTOM_ENDPOINTS.length = 0;
90
+ CUSTOM_ENDPOINTS.push(...endpoints);
91
+ }
92
+ /** 端点密钥不写进 models.json,走这个环境变量引用(pi 读取时解析)。 */
93
+ export function endpointKeyEnvName(endpointId) {
94
+ return `U1S1_EP_KEY_${endpointId}`;
95
+ }
96
+ /** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
97
+ export function apiEndpointToCustom(e) {
98
+ return {
99
+ id: e.id,
100
+ name: e.name,
101
+ baseUrl: e.base_url,
102
+ api: e.api,
103
+ apiKey: e.api_key ?? undefined,
104
+ models: e.models.map((m) => ({
105
+ id: m.id,
106
+ name: m.name || m.id,
107
+ aliases: defaultAliases(m.id),
108
+ reasoning: !!m.reasoning,
109
+ contextWindow: m.context_window ?? 128_000,
110
+ maxTokens: m.max_tokens ?? 16_384,
111
+ // 自有端点不经网关计费,价格未知,展示按 0 处理
112
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
113
+ note: "",
114
+ })),
115
+ };
116
+ }
117
+ /** ref 指向的模型仍然存在吗(服务端下架/端点删除后失效)。 */
118
+ export function refValid(ref) {
119
+ if (ref.provider === PROVIDER_ID)
120
+ return MODELS.some((m) => m.id === ref.id);
121
+ const ep = CUSTOM_ENDPOINTS.find((e) => e.id === ref.provider);
122
+ return !!ep && ep.models.some((m) => m.id === ref.id);
123
+ }
124
+ /**
125
+ * 按名字/别名找模型,支持三种写法:模型 id、别名、`端点名:模型id`。
126
+ * u1s1 官方模型优先,再按端点顺序找,重名取先命中。
127
+ */
128
+ export function findModel(query) {
129
+ const q = query.trim();
130
+ const colon = q.indexOf(":");
131
+ if (colon > 0) {
132
+ const epName = q.slice(0, colon).toLowerCase();
133
+ const rest = q.slice(colon + 1).trim().toLowerCase();
134
+ const ep = CUSTOM_ENDPOINTS.find((e) => e.name.toLowerCase() === epName);
135
+ const m = ep?.models.find((x) => x.id.toLowerCase() === rest || x.aliases.includes(rest));
136
+ return ep && m ? { provider: ep.id, providerName: ep.name, id: m.id, model: m } : undefined;
137
+ }
138
+ const builtin = resolveModel(q);
139
+ if (builtin)
140
+ return { provider: PROVIDER_ID, providerName: "u1s1", id: builtin.id, model: builtin };
141
+ const lower = q.toLowerCase();
142
+ for (const ep of CUSTOM_ENDPOINTS) {
143
+ const m = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
144
+ if (m)
145
+ return { provider: ep.id, providerName: ep.name, id: m.id, model: m };
146
+ }
147
+ return undefined;
148
+ }
84
149
  export const u1s1Dir = join(homedir(), ".u1s1");
85
150
  const configFile = join(u1s1Dir, "config.json");
86
151
  /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
@@ -105,35 +170,49 @@ export function readAgentDefaultModel() {
105
170
  const settings = readJsonFile(agentSettingsFile);
106
171
  if (!settings)
107
172
  return undefined;
108
- if (settings["defaultProvider"] !== PROVIDER_ID)
109
- return undefined;
173
+ const provider = settings["defaultProvider"];
110
174
  const id = settings["defaultModel"];
111
- return typeof id === "string" && resolveModel(id) ? id : undefined;
175
+ if (typeof provider !== "string" || typeof id !== "string")
176
+ return undefined;
177
+ if (provider !== PROVIDER_ID && !ENDPOINT_ID_RE.test(provider))
178
+ return undefined;
179
+ const ref = { provider, id };
180
+ return refValid(ref) ? ref : undefined;
112
181
  }
113
182
  /** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
114
- export function writeAgentDefaultModel(modelId) {
183
+ export function writeAgentDefaultModel(provider, modelId) {
115
184
  mkdirSync(agentDir, { recursive: true });
116
185
  const settings = readJsonFile(agentSettingsFile) ?? {};
117
- if (settings["defaultProvider"] === PROVIDER_ID && settings["defaultModel"] === modelId)
186
+ if (settings["defaultProvider"] === provider && settings["defaultModel"] === modelId)
118
187
  return;
119
- settings["defaultProvider"] = PROVIDER_ID;
188
+ settings["defaultProvider"] = provider;
120
189
  settings["defaultModel"] = modelId;
121
190
  writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
122
191
  }
123
192
  /**
124
193
  * After this fix both stores stay in sync. If they still disagree (old installs),
125
194
  * prefer the in-session /model value — that's the one users thought they had set.
195
+ * 校验依赖 CUSTOM_ENDPOINTS,须在 setCustomEndpoints 之后调用才认得端点模型。
126
196
  */
127
- export function resolvePreferredModel(configModel) {
128
- const agentModel = readAgentDefaultModel();
129
- return agentModel ?? configModel ?? DEFAULT_MODEL_ID;
197
+ export function resolvePreferredModel(cfg) {
198
+ const agentRef = readAgentDefaultModel();
199
+ if (agentRef)
200
+ return agentRef;
201
+ if (cfg.model) {
202
+ const ref = { provider: cfg.modelProvider ?? PROVIDER_ID, id: cfg.model };
203
+ if (refValid(ref))
204
+ return ref;
205
+ }
206
+ return { provider: PROVIDER_ID, id: DEFAULT_MODEL_ID };
130
207
  }
131
208
  export function loadConfig() {
132
209
  const file = (readJsonFile(configFile) ?? {});
133
210
  return {
134
211
  apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
135
212
  baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
136
- model: file.model && resolveModel(file.model) ? file.model : undefined,
213
+ // 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
214
+ model: file.model,
215
+ modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
137
216
  };
138
217
  }
139
218
  export function saveConfig(cfg) {
@@ -142,9 +221,20 @@ export function saveConfig(cfg) {
142
221
  chmodSync(configFile, 0o600);
143
222
  }
144
223
  /** Persist the user's preferred model to both stores. */
145
- export function persistPreferredModel(cfg, modelId) {
146
- const next = { ...cfg, model: modelId };
224
+ export function persistPreferredModel(cfg, provider, modelId) {
225
+ const next = { ...cfg, model: modelId, modelProvider: provider };
147
226
  saveConfig(next);
148
- writeAgentDefaultModel(modelId);
227
+ writeAgentDefaultModel(provider, modelId);
149
228
  return next;
150
229
  }
230
+ // ---- 端点本地缓存:离线/网关不可达时沿用上次拉到的配置(含密钥,0600)----
231
+ const endpointsCacheFile = join(u1s1Dir, "endpoints.json");
232
+ export function saveEndpointsCache(endpoints) {
233
+ mkdirSync(u1s1Dir, { recursive: true, mode: 0o700 });
234
+ writeFileSync(endpointsCacheFile, JSON.stringify(endpoints, null, 2) + "\n");
235
+ chmodSync(endpointsCacheFile, 0o600);
236
+ }
237
+ export function loadEndpointsCache() {
238
+ const raw = readJsonFile(endpointsCacheFile);
239
+ return Array.isArray(raw) ? raw : [];
240
+ }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { SessionManager } from "@earendil-works/pi-coding-agent";
4
- import { agentDir, loadConfig, PROVIDER_ID, resolvePreferredModel } from "../config.js";
4
+ import { agentDir, loadConfig, resolvePreferredModel } from "../config.js";
5
5
  const EMPTY_USAGE = {
6
6
  input: 0,
7
7
  output: 0,
@@ -73,7 +73,8 @@ export function writeConvertedSession(converted, destCwd) {
73
73
  if (title)
74
74
  sm.appendSessionInfo(title);
75
75
  // Resume should keep using u1s1 models, not the original Claude/Codex id.
76
- sm.appendModelChange(PROVIDER_ID, resolvePreferredModel(loadConfig().model));
76
+ const pref = resolvePreferredModel(loadConfig());
77
+ sm.appendModelChange(pref.provider, pref.id);
77
78
  const dest = sm.getSessionFile();
78
79
  if (!dest)
79
80
  throw new Error("会话写盘失败");
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
- import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, toProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { printConsoleBanner } from "./brand.js";
6
- import { agentDir, apiModelToDef, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
6
+ import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
7
  import { ensureSearchTools } from "./search-tools.js";
8
8
  import { applyBrandUi, setUpdateNotice } from "./style.js";
9
9
  import { offerStarterTemplates } from "./templates.js";
10
- import { fetchModels } from "./api.js";
10
+ import { fetchModels, loadCustomEndpoints } from "./api.js";
11
11
  const PACKAGE_NAME = "u1s1-cli";
12
12
  /**
13
13
  * 启动时自动检查 npm 最新版:autoUpdate 开着就静默安装,关着也在启动横幅的
@@ -123,7 +123,9 @@ async function runAgent(cfg, args) {
123
123
  const searchToolsReady = ensureSearchTools(cfg);
124
124
  // Fetch model list from server; fall back to built-in MODELS on error.
125
125
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search,模型就不会白调
126
+ // 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
126
127
  let webSearchEnabled = true;
128
+ const endpointsReady = loadCustomEndpoints(cfg);
127
129
  try {
128
130
  const { models, features } = await fetchModels(cfg);
129
131
  setModelsFromApi(models.map(apiModelToDef));
@@ -132,6 +134,7 @@ async function runAgent(cfg, args) {
132
134
  catch (e) {
133
135
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
134
136
  }
137
+ await endpointsReady;
135
138
  ensureProviderModels(cfg);
136
139
  // 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
137
140
  writeWebToolsExtension(cfg, webSearchEnabled);
@@ -140,6 +143,8 @@ async function runAgent(cfg, args) {
140
143
  process.env["PI_CODING_AGENT_DIR"] = agentDir;
141
144
  process.env["U1S1_API_KEY"] = cfg.apiKey;
142
145
  process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
146
+ // 自定义端点的密钥走环境变量引用(models.json 里只有 $VAR,不落明文)
147
+ Object.assign(process.env, endpointKeyEnv());
143
148
  // hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
144
149
  process.env["PI_SKIP_VERSION_CHECK"] = "1";
145
150
  await searchToolsReady;
@@ -197,25 +202,20 @@ async function runAgent(cfg, args) {
197
202
  baseUrl: cfg.baseUrl,
198
203
  api: "openai-completions",
199
204
  apiKey: "$U1S1_API_KEY",
200
- models: MODELS.map((m) => ({
201
- id: m.id,
202
- name: m.name,
203
- reasoning: m.reasoning,
204
- input: ["text"],
205
- cost: m.cost,
206
- contextWindow: m.contextWindow,
207
- maxTokens: m.maxTokens,
208
- })),
205
+ models: toProviderModels(MODELS),
209
206
  });
207
+ // 用户在云端配置的自定义端点,一个端点一个 provider,/model 里即可切换
208
+ for (const ep of CUSTOM_ENDPOINTS) {
209
+ pi.registerProvider(ep.id, endpointProviderEntry(ep));
210
+ }
210
211
  // /model and Ctrl+P already write pi settings; also keep ~/.u1s1/config.json in sync
211
212
  pi.on("model_select", (event) => {
212
213
  if (event.source === "restore")
213
214
  return;
214
- if (event.model.provider !== PROVIDER_ID)
215
- return;
216
- if (!MODELS.some((m) => m.id === event.model.id))
215
+ const ref = { provider: event.model.provider, id: event.model.id };
216
+ if (!refValid(ref))
217
217
  return;
218
- persistPreferredModel(loadConfig(), event.model.id);
218
+ persistPreferredModel(loadConfig(), ref.provider, ref.id);
219
219
  });
220
220
  pi.on("session_start", (_event, ctx) => {
221
221
  const previous = ctx.ui.getEditorComponent();
@@ -233,10 +233,11 @@ async function runAgent(cfg, args) {
233
233
  },
234
234
  ];
235
235
  const hasModelArg = args.some((a) => a === "--model" || a.startsWith("--model=") || a === "--provider");
236
- const defaultModel = resolvePreferredModel(cfg.model);
237
- if (cfg.model !== defaultModel)
238
- persistPreferredModel(cfg, defaultModel);
239
- const finalArgs = hasModelArg ? args : ["--model", `${PROVIDER_ID}/${defaultModel}`, ...args];
236
+ const pref = resolvePreferredModel(cfg);
237
+ if (cfg.model !== pref.id || (cfg.modelProvider ?? PROVIDER_ID) !== pref.provider) {
238
+ persistPreferredModel(cfg, pref.provider, pref.id);
239
+ }
240
+ const finalArgs = hasModelArg ? args : ["--model", `${pref.provider}/${pref.id}`, ...args];
240
241
  await main(finalArgs, { extensionFactories: extension });
241
242
  }
242
243
  async function run() {
package/dist/model.js CHANGED
@@ -1,11 +1,14 @@
1
- import { loadConfig, MODELS, persistPreferredModel, resolveModel, resolvePreferredModel } from "./config.js";
1
+ import { loadCustomEndpoints } from "./api.js";
2
+ import { CUSTOM_ENDPOINTS, findModel, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, } from "./config.js";
2
3
  export async function modelCommand(nameOrAlias) {
3
4
  const cfg = loadConfig();
4
- const current = resolvePreferredModel(cfg.model);
5
+ // 云端可能刚改过端点配置;拉一次(失败回退本地缓存),再解析当前默认
6
+ await loadCustomEndpoints(cfg);
7
+ const current = resolvePreferredModel(cfg);
5
8
  if (!nameOrAlias) {
6
9
  console.log("");
7
10
  for (const m of MODELS) {
8
- const mark = m.id === current ? "●" : " ";
11
+ const mark = current.provider === PROVIDER_ID && m.id === current.id ? "●" : " ";
9
12
  console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
10
13
  if (m.note) {
11
14
  console.log(` ${m.note} · $${m.cost.input}/$${m.cost.output} 每百万 token`);
@@ -14,16 +17,37 @@ export async function modelCommand(nameOrAlias) {
14
17
  console.log(` $${m.cost.input}/$${m.cost.output} 每百万 token`);
15
18
  }
16
19
  }
20
+ for (const ep of CUSTOM_ENDPOINTS) {
21
+ console.log("");
22
+ console.log(` ─ 自定义端点 ${ep.name}(${ep.baseUrl},走本地直连不计费)`);
23
+ for (const m of ep.models) {
24
+ const mark = current.provider === ep.id && m.id === current.id ? "●" : " ";
25
+ console.log(` ${mark} ${m.id.padEnd(10)} ${m.name === m.id ? "" : m.name}`);
26
+ }
27
+ }
17
28
  console.log("");
18
29
  console.log(" 切换:u1s1 model grok / u1s1 model deepseek(对话里 /model 同样会记住)");
30
+ if (CUSTOM_ENDPOINTS.length) {
31
+ console.log(" 端点模型重名时可加限定:u1s1 model 端点名:模型id");
32
+ }
33
+ else {
34
+ console.log(" 想接自己的 API(含本机 Ollama)?去 https://u1s1.io/dashboard 配「自定义模型端点」");
35
+ }
19
36
  console.log("");
20
37
  return;
21
38
  }
22
- const m = resolveModel(nameOrAlias);
39
+ const m = findModel(nameOrAlias);
23
40
  if (!m) {
24
- console.error(` 没有叫「${nameOrAlias}」的模型,可选:${MODELS.map((x) => x.aliases[0]).join(" / ")}`);
41
+ const custom = CUSTOM_ENDPOINTS.flatMap((e) => e.models.map((x) => x.id));
42
+ const all = [...MODELS.map((x) => x.aliases[0]), ...custom];
43
+ console.error(` 没有叫「${nameOrAlias}」的模型,可选:${all.join(" / ")}`);
25
44
  process.exit(1);
26
45
  }
27
- persistPreferredModel(cfg, m.id);
28
- console.log(` ✓ 默认模型已切到 ${m.name}(${m.note})`);
46
+ persistPreferredModel(cfg, m.provider, m.id);
47
+ if (m.provider === PROVIDER_ID) {
48
+ console.log(` ✓ 默认模型已切到 ${m.model.name}${m.model.note ? `(${m.model.note})` : ""}`);
49
+ }
50
+ else {
51
+ console.log(` ✓ 默认模型已切到 ${m.model.name}(自定义端点 ${m.providerName},本地直连不计费)`);
52
+ }
29
53
  }
package/dist/web.js CHANGED
@@ -2,11 +2,11 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
- import { cleanupBrandThemes, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
5
+ import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
6
6
  import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
7
7
  import { ensureSearchTools } from "./search-tools.js";
8
8
  import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
9
- import { fetchModels } from "./api.js";
9
+ import { fetchModels, loadCustomEndpoints } from "./api.js";
10
10
  const require = createRequire(import.meta.url);
11
11
  /**
12
12
  * `u1s1 web` — 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
@@ -26,7 +26,9 @@ export async function prepareWebEnv(cfg) {
26
26
  const searchToolsReady = ensureSearchTools(cfg);
27
27
  // Fetch model list from server; fall back to built-in MODELS on error.
28
28
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
29
+ // 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
29
30
  let webSearchEnabled = true;
31
+ const endpointsReady = loadCustomEndpoints(cfg);
30
32
  try {
31
33
  const { models, features } = await fetchModels(cfg);
32
34
  setModelsFromApi(models.map(apiModelToDef));
@@ -35,6 +37,7 @@ export async function prepareWebEnv(cfg) {
35
37
  catch (e) {
36
38
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
37
39
  }
40
+ await endpointsReady;
38
41
  ensureProviderModels(cfg);
39
42
  // pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
40
43
  ensureAuthCredential();
@@ -42,7 +45,8 @@ export async function prepareWebEnv(cfg) {
42
45
  writeWebToolsExtension(cfg, webSearchEnabled);
43
46
  // 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
44
47
  // 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
45
- writeAgentDefaultModel(resolvePreferredModel(cfg.model));
48
+ const pref = resolvePreferredModel(cfg);
49
+ writeAgentDefaultModel(pref.provider, pref.id);
46
50
  const dataDir = join(u1s1Dir, "web");
47
51
  mkdirSync(dataDir, { recursive: true });
48
52
  await searchToolsReady;
@@ -51,6 +55,8 @@ export async function prepareWebEnv(cfg) {
51
55
  U1S1_API_KEY: cfg.apiKey,
52
56
  U1S1_TOOLS_VIA_EXTENSION: "1",
53
57
  PI_WEB_DATA_DIR: dataDir,
58
+ // 自定义端点的密钥经环境变量传给 web 子进程(models.json 里只有 $VAR 引用)
59
+ ...endpointKeyEnv(),
54
60
  };
55
61
  }
56
62
  export async function webCommand(cfg, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {