ck-pi-zen-session 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,8 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
25
25
  - 若请求已有工具,严格按照官方客户端规约(`localeCompare`)按函数名升序重排;
26
26
  - 自动对齐流式用量元数据 `stream_options: { include_usage: true }`。
27
27
  - 🌐 **独立供应商隔离与零干扰保障 (`isZenModelTarget`)**:严格将生命周期守卫、请求头与请求体拦截限制在 `opencode-zen-free` 命名空间内,对 `relayhub`、`deepseek`、`anthropic`、`openai`、`onerouter`、`apmix` 等其他任何模型及 Claude Code(`pi-cc-extensions`)插件 100% 保持完全静默放行,零副作用、零冲突。
28
- - 🧠 **精准上下文与思维链适配**:严格对齐免费模型的 `contextWindow`、`maxTokens` `thinkingLevelMap`(涵盖小米 MiMo、英伟达 NemotronMeta Muse Spark、Ling、Jev 等)。
28
+ - 🧠 **高精智能模型能力与参数解析引擎 (v0.1.7)**:无论输入新 Key 还是 OpenCode Zen 官方端点动态上线任何新免费/零额度模型,自动深度识别其上下文窗口(Context Window,如 1M, 256K, 200K, 128K)、最大输出(Max Tokens,如 128K, 64K, 32K, 16K)、推理思考能力与思考等级映射(thinkingLevelMap,涵盖 6 档深度推理 [minimal..xhigh]3 档标准推理 [low, high, max] 或快反模式)以及视觉多模态支持,并在 `/zen <key>` 和 `/zen list` 中呈现精美结构化卡片。
29
+ - 🛡️ **OpenAI 标准思考档位全链路映射与双重防线 (v0.1.8)**:彻底修复 OpenCode Zen 上游端点不支持 `max` / `xhigh` 思考档位导致抛出 `[400] Invalid request parameters` 的致命问题。全面将思考档位映射对齐 OpenAI 标准 `reasoning_effort`(`low` / `medium` / `high`),并在 `before_provider_request` 请求体守卫中设立兜底防线,自动将非标准或溢出档位降级规约为合法参数,确保小米 MiMo、英伟达 Nemotron、Meta Muse Spark 等全系模型在任意 Pi 默认思考模式下 100% 稳定响应。
29
30
  - 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
30
31
 
31
32
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ck-pi-zen-session",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenCode Zen session auto-refresh and free-tier model catalog synchronization for the Pi coding agent.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -10,7 +10,21 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
+ export {
14
+ formatModelCard,
15
+ formatThinkingSummary,
16
+ formatTokens,
17
+ inferModelCapabilities,
18
+ KNOWN_ZEN_FREE_MODELS,
19
+ OPENCODE_OFFICIAL_TOOLS,
20
+ ZEN_BASE_URL,
21
+ ZEN_PROVIDER_ID,
22
+ ZEN_USER_AGENT,
23
+ } from "./models-registry.js";
13
24
  import {
25
+ formatModelCard,
26
+ formatThinkingSummary,
27
+ formatTokens,
14
28
  KNOWN_ZEN_FREE_MODELS,
15
29
  OPENCODE_OFFICIAL_TOOLS,
16
30
  ZEN_BASE_URL,
@@ -68,8 +82,18 @@ export function registerZenProviderToPi(
68
82
  }
69
83
  }
70
84
 
71
- export function isZenModelTarget(model?: { provider?: string; id?: string }): boolean {
72
- if (!model) return false;
85
+ export function isZenModelTarget(model?: { provider?: string; id?: string }, payloadModel?: string): boolean {
86
+ if (!model) {
87
+ if (payloadModel) {
88
+ const lower = payloadModel.toLowerCase();
89
+ return (
90
+ lower.endsWith("-free") ||
91
+ lower === "big-pickle" ||
92
+ lower.includes("zen")
93
+ );
94
+ }
95
+ return false;
96
+ }
73
97
  // 严格隔离:仅处理 opencode-zen-free 或明确属于 opencode 官方的免费模型
74
98
  // 绝不干涉 relayhub、deepseek、anthropic、openai 等其他任何供应商或 CC 插件
75
99
  if (model.provider === ZEN_PROVIDER_ID) return true;
@@ -83,11 +107,20 @@ export function isZenModelTarget(model?: { provider?: string; id?: string }): bo
83
107
  }
84
108
 
85
109
  export default function piZenSession(pi: ExtensionAPI): void {
86
- // 1. 若本地已配置 Key,启动时即刻注册独立供应商 opencode-zen-free
87
- // 独立命名空间,彻底避开并兼容用户自带的官方登录 opencode / opencode-zen 套餐
88
110
  const existingKey = getStoredZenApiKey();
89
111
  if (existingKey) {
90
- registerZenProviderToPi(pi, existingKey);
112
+ const storedSession = getStoredZenSessionId();
113
+ const storedModels = getStoredZenModels();
114
+ registerZenProviderToPi(pi, existingKey, storedSession || generateZenSessionId(), storedModels);
115
+
116
+ // 若已有 Key 但还没有持久化模型列表,后台静默自动探测一次
117
+ if (!storedModels || storedModels.length === 0) {
118
+ syncZenConfiguration()
119
+ .then((res) => {
120
+ registerZenProviderToPi(pi, res.apiKey, res.sessionId, res.resolvedModels);
121
+ })
122
+ .catch(() => {});
123
+ }
91
124
  }
92
125
 
93
126
  // 2. 核心请求头拦截钩子:在每一次请求发往 Provider 前注入完整官方客户端对齐请求头
@@ -107,6 +140,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
107
140
  }
108
141
 
109
142
  // 注入全部官方客户端 7 维对齐请求头
143
+ delete event.headers["user-agent"];
110
144
  event.headers["User-Agent"] = ZEN_USER_AGENT;
111
145
  event.headers["x-opencode-client"] = "cli";
112
146
  event.headers["x-opencode-session"] = currentSession;
@@ -119,8 +153,9 @@ export default function piZenSession(pi: ExtensionAPI): void {
119
153
 
120
154
  // 3. 核心请求体守卫钩子:深度对齐 OpenCode 官方请求体结构与工具集
121
155
  pi.on("before_provider_request", (event, ctx) => {
122
- if (isZenModelTarget(ctx.model) && event.payload && typeof event.payload === "object") {
123
- const payload = event.payload as Record<string, unknown>;
156
+ const payload = event.payload as Record<string, unknown> | undefined;
157
+ const payloadModel = typeof payload?.model === "string" ? payload.model : undefined;
158
+ if (isZenModelTarget(ctx.model, payloadModel) && payload && typeof payload === "object") {
124
159
  const tools = payload.tools;
125
160
  let modified = false;
126
161
  const transformed = { ...payload };
@@ -145,6 +180,22 @@ export default function piZenSession(pi: ExtensionAPI): void {
145
180
  modified = true;
146
181
  }
147
182
 
183
+ // C. 规范化 reasoning_effort:OpenCode Zen 上游端点仅支持 "low", "medium", "high"
184
+ // 若为 "max" 或 "xhigh",降级映射为 "high";若为 "minimal" 映射为 "low",彻底根除 400 Invalid request parameters
185
+ if (typeof transformed.reasoning_effort === "string") {
186
+ const effort = transformed.reasoning_effort.toLowerCase();
187
+ if (effort === "max" || effort === "xhigh") {
188
+ transformed.reasoning_effort = "high";
189
+ modified = true;
190
+ } else if (effort === "minimal") {
191
+ transformed.reasoning_effort = "low";
192
+ modified = true;
193
+ } else if (!["low", "medium", "high"].includes(effort)) {
194
+ transformed.reasoning_effort = "high";
195
+ modified = true;
196
+ }
197
+ }
198
+
148
199
  if (modified) {
149
200
  return transformed;
150
201
  }
@@ -219,16 +270,21 @@ export async function handleZenCommand(
219
270
  ? `${status.sessionAgeMinutes} 分钟`
220
271
  : `${(status.sessionAgeMinutes / 60).toFixed(1)} 小时`;
221
272
 
273
+ const storedModels = getStoredZenModels();
274
+ const reasoningCount = storedModels.filter((m) => m.reasoning).length;
275
+ const visionCount = storedModels.filter((m) => m.input.includes("image")).length;
276
+
222
277
  const msg = [
223
278
  "✨ [OpenCode Zen 运行状态]",
224
279
  `• API Key: ${status.maskedKey}`,
225
280
  `• Session: ${status.sessionId || "未生成"} (${status.sessionExpired ? "已过期" : `有效,创建于 ${ageDesc}前`})`,
226
- `• 模型库: 已激活 ${status.modelsCount} 个免费模型`,
281
+ `• 模型库: 已激活 ${storedModels.length} 款 0 额度模型 (${reasoningCount} 款支持思考推理,${visionCount} 款支持多模态)`,
282
+ `• 请求头保护: 7维官方客户端签名 + 30分钟降序时间戳自愈`,
227
283
  "",
228
284
  "常用指令:",
229
- " /zen <key> - 更新 Key 并全量重新同步",
285
+ " /zen <key> - 更新 Key 并全自动识别免费模型/上下文/思考等级",
230
286
  " /zen refresh - 强制换新 Session ID",
231
- " /zen list - 查看所有可用免费模型规格",
287
+ " /zen list - 查看所有可用免费模型详细规格卡片",
232
288
  ].join("\n");
233
289
 
234
290
  notify(ctx, msg, "info");
@@ -237,17 +293,20 @@ export async function handleZenCommand(
237
293
 
238
294
  // 2. 查看模型清单: /zen list 或 /zen models
239
295
  if (command === "list" || command === "models") {
240
- const status = getZenStatusInfo();
241
- if (status.modelIds.length === 0) {
296
+ const storedModels = getStoredZenModels();
297
+ if (storedModels.length === 0) {
242
298
  notify(ctx, "当前尚未同步任何模型,请先运行 /zen <key>。", "warning");
243
299
  return;
244
300
  }
245
301
 
302
+ const modelCards = storedModels.map((m, idx) => formatModelCard(m, idx + 1));
246
303
  const listMsg = [
247
- `✨ [OpenCode Zen 免费模型列表 (${status.modelIds.length} )]`,
248
- ...status.modelIds.map((id) => ` • ${id}`),
304
+ `✨ [OpenCode Zen 免费模型列表 (${storedModels.length} )]`,
305
+ "智能识别精确上下文上限(Context)、最大输出(Max Tokens)及推理思考等级(Thinking):",
306
+ "",
307
+ ...modelCards,
249
308
  "",
250
- "提示:这些模型已就绪,可在 Pi 中直接切换使用!",
309
+ "提示:这些模型已全量注册至 Pi,使用 /model 或快捷键随时切换!",
251
310
  ].join("\n");
252
311
 
253
312
  notify(ctx, listMsg, "info");
@@ -262,10 +321,16 @@ export async function handleZenCommand(
262
321
  if (pi) {
263
322
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
264
323
  }
324
+ if ((ctx as { modelRegistry?: { refresh?: (arg: unknown) => Promise<unknown> } }).modelRegistry?.refresh) {
325
+ await (ctx as { modelRegistry: { refresh: (arg: unknown) => Promise<unknown> } }).modelRegistry
326
+ .refresh({ providers: [ZEN_PROVIDER_ID] })
327
+ .catch(() => {});
328
+ }
329
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
265
330
  const successMsg = [
266
331
  "✓ OpenCode Zen Session 已成功刷新!",
267
332
  `• 全新 Session: ${result.sessionId}`,
268
- `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度消耗模型`,
333
+ `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度模型 (${reasoningCount} 款支持思考推理)`,
269
334
  `• 请求头伪装: 7 维官方客户端签名已注入`,
270
335
  `• 同步状态: models.json ✓ | CC-Switch ${result.ccSwitchUpdated ? "✓" : "-(未安装或无此条目)"}`,
271
336
  ].join("\n");
@@ -279,19 +344,31 @@ export async function handleZenCommand(
279
344
  // 4. 输入了具体的 API Key: /zen oc_sk_... 或其它 key
280
345
  if (rawArg.length > 0 && !["status", "refresh", "list", "models"].includes(command)) {
281
346
  try {
282
- notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费与零额度模型...", "info");
347
+ notify(ctx, "正在验证 OpenCode Zen API Key 并智能探测免费模型上下文与思考等级...", "info");
283
348
  const result = await syncZenConfiguration({ apiKey: rawArg, forceSession: true });
284
349
  if (pi) {
285
350
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
286
351
  }
352
+ if ((ctx as { modelRegistry?: { refresh?: (arg: unknown) => Promise<unknown> } }).modelRegistry?.refresh) {
353
+ await (ctx as { modelRegistry: { refresh: (arg: unknown) => Promise<unknown> } }).modelRegistry
354
+ .refresh({ providers: [ZEN_PROVIDER_ID] })
355
+ .catch(() => {});
356
+ }
357
+
358
+ const modelCards = result.resolvedModels.map((m, idx) => formatModelCard(m, idx + 1));
359
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
360
+
287
361
  const msg = [
288
- "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已自动识别并全量对接:",
362
+ "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已全自动识别并对接:",
289
363
  `• API Key: ${maskKey(result.apiKey)} (已验证并保存)`,
290
- `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法)`,
291
- `• 自动对接免费模型 (${result.modelsCount} 款,全部标记 0 额度消耗):`,
292
- ...result.models.map((id) => ` • ${id}`),
364
+ `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法,30分钟自动轮换自愈)`,
365
+ `• 识别到 ${result.modelsCount} 0 额度免费模型 (${reasoningCount} 款支持深度推理/思考):`,
366
+ "",
367
+ ...modelCards,
368
+ "",
293
369
  `• 同步路径: ${result.modelsPath}`,
294
370
  result.ccSwitchUpdated ? "• CC-Switch: 本地数据库已同步更新" : "",
371
+ "提示:全部模型参数已热载入 Pi,输入 /model 或在模型选择器中可即刻选用!",
295
372
  ].filter(Boolean).join("\n");
296
373
  notify(ctx, msg, "info");
297
374
  } catch (error) {
@@ -310,17 +387,23 @@ export async function handleZenCommand(
310
387
  if (pi) {
311
388
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
312
389
  }
390
+ if ((ctx as { modelRegistry?: { refresh?: (arg: unknown) => Promise<unknown> } }).modelRegistry?.refresh) {
391
+ await (ctx as { modelRegistry: { refresh: (arg: unknown) => Promise<unknown> } }).modelRegistry
392
+ .refresh({ providers: [ZEN_PROVIDER_ID] })
393
+ .catch(() => {});
394
+ }
395
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
313
396
  const msg = [
314
397
  "✨ [OpenCode Zen 已自动刷新并就绪]",
315
398
  `• API Key: ${maskKey(result.apiKey)}`,
316
399
  `• 全新 Session: ${result.sessionId} (有效且已持久化)`,
317
- `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型`,
400
+ `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型 (${reasoningCount} 款支持思考推理)`,
318
401
  `• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
319
402
  "",
320
403
  "常用操作:",
321
- " /zen <key> - 更换 API Key",
404
+ " /zen <key> - 更换 API Key (自动重识免费模型/上下文/思考等级)",
322
405
  " /zen status - 详情状态",
323
- " /zen list - 查看所有可用免费模型",
406
+ " /zen list - 查看所有可用免费模型详细规格",
324
407
  ].join("\n");
325
408
  notify(ctx, msg, "info");
326
409
  } catch (error) {
@@ -11,23 +11,33 @@ export const ZEN_PROVIDER_ID = "opencode-zen-free";
11
11
  export const ZEN_BASE_URL = "https://opencode.ai/zen/v1";
12
12
  export const ZEN_USER_AGENT = "opencode/1.18.32 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14";
13
13
 
14
- const STANDARD_THINKING_LEVELS: Record<string, string | null> = {
14
+ export const STANDARD_THINKING_LEVELS: Record<string, string | null> = {
15
+ minimal: "low",
15
16
  low: "low",
17
+ medium: "medium",
18
+ high: "high",
19
+ xhigh: "high",
20
+ max: "high",
21
+ off: null,
22
+ };
23
+
24
+ export const MUSE_SPARK_THINKING_LEVELS: Record<string, string | null> = {
25
+ minimal: "low",
26
+ low: "low",
27
+ medium: "medium",
16
28
  high: "high",
17
- max: "max",
18
- medium: null,
19
- minimal: null,
29
+ xhigh: "high",
30
+ max: "high",
20
31
  off: null,
21
- xhigh: null,
22
32
  };
23
33
 
24
- const MUSE_SPARK_THINKING_LEVELS: Record<string, string | null> = {
25
- minimal: "minimal",
34
+ export const GRANULAR_THINKING_LEVELS: Record<string, string | null> = {
35
+ minimal: "low",
26
36
  low: "low",
27
37
  medium: "medium",
28
38
  high: "high",
29
- xhigh: "xhigh",
30
- max: "xhigh",
39
+ xhigh: "high",
40
+ max: "high",
31
41
  off: null,
32
42
  };
33
43
 
@@ -127,43 +137,306 @@ export const KNOWN_ZEN_FREE_MODELS: Record<string, ZenModelDefinition> = {
127
137
  };
128
138
 
129
139
  /**
130
- * 为未来可能新增的免费模型生成自适应默认配置
140
+ * 高精智能模型能力与参数解析引擎
141
+ * 当用户输入新 Key 或探测到未收录的免费/零额度模型时,全自动深度识别:
142
+ * 1. 上下文窗口限制 (Context Window)
143
+ * 2. 最大输出 Token 上限 (Max Tokens)
144
+ * 3. 思考推理能力与思考等级映射 (Reasoning & thinkingLevelMap)
145
+ * 4. 文本/视觉多模态输入支持 (Input Modalities)
146
+ * 5. 0 额度计费配置 (Zero-Cost Billing)
131
147
  */
132
- export function buildFallbackModelDefinition(modelId: string): ZenModelDefinition {
133
- const isReasoning = !modelId.includes("chat") && !modelId.includes("turbo");
134
- const friendlyName = modelId
148
+ export function inferModelCapabilities(
149
+ modelId: string,
150
+ raw?: Record<string, unknown>,
151
+ ): ZenModelDefinition {
152
+ // 1. 若为已知免费模型,以此为高精基准
153
+ if (KNOWN_ZEN_FREE_MODELS[modelId]) {
154
+ const base: ZenModelDefinition = { ...KNOWN_ZEN_FREE_MODELS[modelId] };
155
+ // 若在线接口返回了显式上下文或最大输出覆盖,进行动态微调
156
+ if (typeof raw?.context_window === "number" && raw.context_window > 0) {
157
+ base.contextWindow = raw.context_window;
158
+ } else if (typeof raw?.context_length === "number" && raw.context_length > 0) {
159
+ base.contextWindow = raw.context_length;
160
+ }
161
+ if (typeof raw?.max_tokens === "number" && raw.max_tokens > 0) {
162
+ base.maxTokens = raw.max_tokens;
163
+ } else if (typeof raw?.max_output_tokens === "number" && raw.max_output_tokens > 0) {
164
+ base.maxTokens = raw.max_output_tokens;
165
+ }
166
+ return base;
167
+ }
168
+
169
+ const lower = modelId.toLowerCase();
170
+
171
+ // 2. 上下文窗口识别 (contextWindow)
172
+ let contextWindow = 200000;
173
+ if (typeof raw?.context_window === "number" && raw.context_window > 0) {
174
+ contextWindow = raw.context_window;
175
+ } else if (typeof raw?.context_length === "number" && raw.context_length > 0) {
176
+ contextWindow = raw.context_length;
177
+ } else if (typeof raw?.max_context_length === "number" && raw.max_context_length > 0) {
178
+ contextWindow = raw.max_context_length;
179
+ } else if (typeof (raw?.limit as { context?: number } | undefined)?.context === "number") {
180
+ contextWindow = (raw!.limit as { context: number }).context;
181
+ } else if (lower.includes("2m") || lower.includes("2000k")) {
182
+ contextWindow = 2097152;
183
+ } else if (
184
+ lower.includes("1m") ||
185
+ lower.includes("1000k") ||
186
+ lower.includes("1024k") ||
187
+ lower.includes("muse") ||
188
+ lower.includes("spark") ||
189
+ lower.includes("gemini") ||
190
+ lower.includes("deepseek") ||
191
+ (lower.includes("nemotron") && lower.includes("ultra")) ||
192
+ (lower.includes("kimi") && lower.includes("k3")) ||
193
+ lower.includes("minimax")
194
+ ) {
195
+ contextWindow = lower.includes("muse") || lower.includes("gemini") ? 1048576 : 1000000;
196
+ } else if (lower.includes("512k")) {
197
+ contextWindow = 524288;
198
+ } else if (
199
+ lower.includes("256k") ||
200
+ (lower.includes("nemotron") && lower.includes("lightning")) ||
201
+ lower.includes("ling") ||
202
+ (lower.includes("kimi") && !lower.includes("k3"))
203
+ ) {
204
+ contextWindow = 262144;
205
+ } else if (
206
+ lower.includes("128k") ||
207
+ lower.includes("qwen") ||
208
+ lower.includes("grok") ||
209
+ lower.includes("jev") ||
210
+ lower.includes("system-1")
211
+ ) {
212
+ contextWindow = lower.includes("qwen") ? 131072 : 128000;
213
+ } else {
214
+ contextWindow = 200000;
215
+ }
216
+
217
+ // 3. 最大输出 Token 识别 (maxTokens)
218
+ let maxTokens = 32000;
219
+ if (typeof raw?.max_output_tokens === "number" && raw.max_output_tokens > 0) {
220
+ maxTokens = raw.max_output_tokens;
221
+ } else if (typeof raw?.max_tokens === "number" && raw.max_tokens > 0) {
222
+ maxTokens = raw.max_tokens;
223
+ } else if (typeof (raw?.limit as { output?: number } | undefined)?.output === "number") {
224
+ maxTokens = (raw!.limit as { output: number }).output;
225
+ } else if (lower.includes("nemotron") && lower.includes("lightning")) {
226
+ maxTokens = 262144;
227
+ } else if (lower.includes("muse") || lower.includes("spark")) {
228
+ maxTokens = 131072;
229
+ } else if (lower.includes("nemotron") && lower.includes("ultra")) {
230
+ maxTokens = 128000;
231
+ } else if (
232
+ lower.includes("gemini") ||
233
+ lower.includes("claude-3-7") ||
234
+ lower.includes("claude-sonnet") ||
235
+ lower.includes("claude-opus") ||
236
+ lower.includes("deepseek-v4")
237
+ ) {
238
+ maxTokens = 65536;
239
+ } else if (
240
+ lower.includes("deepseek") ||
241
+ lower.includes("ling") ||
242
+ lower.includes("glm") ||
243
+ lower.includes("qwen") ||
244
+ lower.includes("grok") ||
245
+ lower.includes("minimax") ||
246
+ lower.includes("gpt-5") ||
247
+ lower.includes("o1") ||
248
+ lower.includes("o3")
249
+ ) {
250
+ maxTokens = 32768;
251
+ } else if (lower.includes("jev") || lower.includes("system-1") || lower.includes("kimi-k2")) {
252
+ maxTokens = 16384;
253
+ } else {
254
+ maxTokens = 32000;
255
+ }
256
+
257
+ // 4. 是否支持推理思考 (reasoning)
258
+ let reasoning = true;
259
+ if (typeof raw?.reasoning === "boolean") {
260
+ reasoning = raw.reasoning;
261
+ } else if (typeof raw?.supports_thinking === "boolean") {
262
+ reasoning = raw.supports_thinking;
263
+ } else if (typeof raw?.thinking === "boolean") {
264
+ reasoning = raw.thinking;
265
+ } else if (
266
+ lower.includes("system-1") ||
267
+ lower.includes("reflex") ||
268
+ lower.includes("chat-only") ||
269
+ lower.includes("instruct-only") ||
270
+ lower.includes("base") ||
271
+ lower.includes("instant") ||
272
+ lower.includes("jev")
273
+ ) {
274
+ reasoning = false;
275
+ }
276
+
277
+ // 5. 思考等级映射 (thinkingLevelMap)
278
+ let thinkingLevelMap: ZenModelDefinition["thinkingLevelMap"] = undefined;
279
+ if (reasoning) {
280
+ if (lower.includes("muse") || lower.includes("spark")) {
281
+ thinkingLevelMap = MUSE_SPARK_THINKING_LEVELS;
282
+ } else if (
283
+ lower.includes("gemini") ||
284
+ lower.includes("claude") ||
285
+ lower.includes("gpt-5") ||
286
+ lower.includes("o1") ||
287
+ lower.includes("o3") ||
288
+ lower.includes("o4")
289
+ ) {
290
+ thinkingLevelMap = GRANULAR_THINKING_LEVELS;
291
+ } else {
292
+ thinkingLevelMap = STANDARD_THINKING_LEVELS;
293
+ }
294
+ }
295
+
296
+ // 6. 输入模态识别 (input)
297
+ let input: ("text" | "image")[] = ["text"];
298
+ if (Array.isArray(raw?.modalities) && raw.modalities.length > 0) {
299
+ input = raw.modalities.includes("image") || raw.modalities.includes("vision") ? ["text", "image"] : ["text"];
300
+ } else if (
301
+ lower.includes("vision") ||
302
+ lower.includes("vl") ||
303
+ lower.includes("multimodal") ||
304
+ lower.includes("image") ||
305
+ lower.includes("omni") ||
306
+ lower.includes("gemini") ||
307
+ lower.includes("claude") ||
308
+ lower.includes("mimo") ||
309
+ lower.includes("muse") ||
310
+ lower.includes("gpt-4o") ||
311
+ lower.includes("gpt-5")
312
+ ) {
313
+ input = ["text", "image"];
314
+ }
315
+
316
+ let baseCleanName = modelId
135
317
  .split(/[-_]/)
136
318
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
137
319
  .join(" ");
138
320
 
321
+ // 修正品牌专有大小写
322
+ baseCleanName = baseCleanName
323
+ .replace(/\bDeepseek\b/g, "DeepSeek")
324
+ .replace(/\bQwen\b/g, "Qwen")
325
+ .replace(/\bMinimax\b/g, "MiniMax")
326
+ .replace(/\bGpt\b/g, "GPT")
327
+ .replace(/\bGlm\b/g, "GLM")
328
+ .replace(/\bOpencode\b/g, "OpenCode");
329
+
330
+ let displayName = baseCleanName;
331
+ if (lower.includes("gemini") && !displayName.includes("Google")) {
332
+ displayName = `Google ${displayName}`;
333
+ } else if (lower.includes("claude") && !displayName.includes("Anthropic")) {
334
+ displayName = `Anthropic ${displayName}`;
335
+ } else if ((lower.includes("gpt") || lower.includes("o1") || lower.includes("o3")) && !displayName.includes("OpenAI")) {
336
+ displayName = `OpenAI ${displayName}`;
337
+ } else if (lower.includes("kimi") && !displayName.includes("Moonshot")) {
338
+ displayName = `Moonshot ${displayName}`;
339
+ } else if (lower.includes("grok") && !displayName.includes("xAI")) {
340
+ displayName = `xAI ${displayName}`;
341
+ } else if (lower.includes("nemotron") && !displayName.includes("NVIDIA")) {
342
+ displayName = `NVIDIA ${displayName}`;
343
+ } else if (lower.includes("mimo") && !displayName.includes("Xiaomi")) {
344
+ displayName = `Xiaomi ${displayName}`;
345
+ } else if (lower.includes("muse") && !displayName.includes("Meta")) {
346
+ displayName = `Meta ${displayName}`;
347
+ }
348
+
349
+ displayName = `${displayName} (OpenCode Zen)`;
350
+
139
351
  return {
140
352
  id: modelId,
141
- name: `${friendlyName} (OpenCode Zen)`,
142
- contextWindow: 200000,
143
- maxTokens: 32000,
144
- reasoning: isReasoning,
145
- input: ["text", "image"],
146
- thinkingLevelMap: isReasoning ? STANDARD_THINKING_LEVELS : undefined,
353
+ name: displayName,
354
+ contextWindow,
355
+ maxTokens,
356
+ reasoning,
357
+ input,
358
+ thinkingLevelMap,
147
359
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
148
360
  };
149
361
  }
150
362
 
363
+ /**
364
+ * 格式化 Token 数量为人类易读字符串 (如 1M, 256K, 200K)
365
+ */
366
+ export function formatTokens(tokens: number): string {
367
+ if (tokens >= 1000000) {
368
+ const m = Math.round(tokens / 100000) / 10;
369
+ return `${m}M (${tokens.toLocaleString("en-US")} tokens)`;
370
+ }
371
+ if (tokens >= 1000) {
372
+ const k = Math.round(tokens / 1024);
373
+ return `${k}K (${tokens.toLocaleString("en-US")} tokens)`;
374
+ }
375
+ return `${tokens.toLocaleString("en-US")} tokens`;
376
+ }
377
+
378
+ /**
379
+ * 格式化模型思考等级与推理能力描述
380
+ */
381
+ export function formatThinkingSummary(model: ZenModelDefinition): string {
382
+ if (!model.reasoning) {
383
+ return "不支持 (快反系统/无需思考)";
384
+ }
385
+ if (!model.thinkingLevelMap) {
386
+ return "支持思考 (默认档位)";
387
+ }
388
+ const activeLevels = Object.entries(model.thinkingLevelMap)
389
+ .filter(([_, v]) => v !== null && v !== undefined)
390
+ .map(([k]) => k);
391
+
392
+ if (activeLevels.length >= 6) {
393
+ return `6档深度思考 [${activeLevels.join(", ")}]`;
394
+ }
395
+ if (activeLevels.length > 0) {
396
+ return `${activeLevels.length}档思考 [${activeLevels.join(", ")}]`;
397
+ }
398
+ return "支持思考";
399
+ }
400
+
401
+ /**
402
+ * 格式化生成模型的结构化卡片展示信息
403
+ */
404
+ export function formatModelCard(model: ZenModelDefinition, index?: number): string {
405
+ const prefix = typeof index === "number" ? `${index}. ` : "• ";
406
+ const ctxStr = formatTokens(model.contextWindow);
407
+ const outStr = formatTokens(model.maxTokens);
408
+ const thinkStr = formatThinkingSummary(model);
409
+ const inputStr = model.input.includes("image") ? "文本 + 视觉 (多模态)" : "纯文本";
410
+
411
+ return [
412
+ `${prefix}${model.name} (${model.id})`,
413
+ ` └─ 上下文: ${ctxStr} | 最大输出: ${outStr} | 思考等级: ${thinkStr} | 模态: ${inputStr} | 额度: 0免费`,
414
+ ].join("\n");
415
+ }
416
+
417
+ /**
418
+ * 为未来可能新增的免费模型生成自适应默认配置(向后兼容)
419
+ */
420
+ export function buildFallbackModelDefinition(modelId: string): ZenModelDefinition {
421
+ return inferModelCapabilities(modelId);
422
+ }
423
+
151
424
  /**
152
425
  * 解析并生成完整的模型列表
153
426
  */
154
- export function resolveModelDefinitions(discoveredIds: string[]): ZenModelDefinition[] {
427
+ export function resolveModelDefinitions(
428
+ discovered: Array<string | { id: string; [key: string]: unknown }>,
429
+ ): ZenModelDefinition[] {
155
430
  const list: ZenModelDefinition[] = [];
156
431
  const seen = new Set<string>();
157
432
 
158
- for (const id of discoveredIds) {
159
- if (seen.has(id)) continue;
433
+ for (const item of discovered) {
434
+ const id = typeof item === "string" ? item : item?.id;
435
+ if (!id || seen.has(id)) continue;
160
436
  seen.add(id);
161
437
 
162
- if (KNOWN_ZEN_FREE_MODELS[id]) {
163
- list.push(KNOWN_ZEN_FREE_MODELS[id]);
164
- } else {
165
- list.push(buildFallbackModelDefinition(id));
166
- }
438
+ const rawItem = typeof item === "object" ? item : undefined;
439
+ list.push(inferModelCapabilities(id, rawItem));
167
440
  }
168
441
 
169
442
  return list;
package/src/sync.ts CHANGED
@@ -15,12 +15,16 @@ import { homedir } from "node:os";
15
15
  import { dirname, join } from "node:path";
16
16
  import { DatabaseSync } from "node:sqlite";
17
17
  import {
18
+ formatModelCard,
19
+ formatThinkingSummary,
20
+ formatTokens,
18
21
  KNOWN_ZEN_FREE_MODELS,
19
22
  resolveModelDefinitions,
20
23
  ZEN_BASE_URL,
21
24
  ZEN_PROVIDER_ID,
22
25
  ZEN_USER_AGENT,
23
26
  } from "./models-registry.js";
27
+ export { formatModelCard, formatThinkingSummary, formatTokens };
24
28
  import {
25
29
  DEFAULT_SESSION_MAX_AGE_MS,
26
30
  extractZenSessionTimestamp,
@@ -44,9 +48,17 @@ export function defaultCcSwitchDbPath(): string {
44
48
  }
45
49
 
46
50
  /**
47
- * 校验 API Key 并拉取 OpenCode Zen 在线模型清单,自动识别所有免费与零额度模型
51
+ * 校验 API Key 并拉取 OpenCode Zen 在线模型清单,自动识别所有免费与零额度模型 ID
48
52
  */
49
53
  export async function fetchZenFreeModels(apiKey: string): Promise<string[]> {
54
+ const catalog = await fetchZenModelCatalog(apiKey);
55
+ return catalog.map((m) => m.id);
56
+ }
57
+
58
+ /**
59
+ * 校验 API Key 并拉取 OpenCode Zen 完整免费模型目录,深度解析上下文与思考等级
60
+ */
61
+ export async function fetchZenModelCatalog(apiKey: string): Promise<ZenModelDefinition[]> {
50
62
  const res = await fetch(`${ZEN_BASE_URL}/models`, {
51
63
  method: "GET",
52
64
  headers: {
@@ -62,23 +74,38 @@ export async function fetchZenFreeModels(apiKey: string): Promise<string[]> {
62
74
 
63
75
  const data = (await res.json()) as { data?: Array<{ id: string; [key: string]: unknown }> };
64
76
  if (!data || !Array.isArray(data.data)) {
65
- return Object.keys(KNOWN_ZEN_FREE_MODELS);
77
+ return Object.values(KNOWN_ZEN_FREE_MODELS);
66
78
  }
67
79
 
68
- // 自动识别所有带 free / zero / pickle 标识的零额度消耗免费模型
69
- const freeIds = data.data
70
- .map((item) => String(item.id))
71
- .filter((id) => {
72
- const lower = id.toLowerCase();
73
- return (
74
- lower.includes("free") ||
75
- lower.includes("pickle") ||
76
- lower.includes("zero") ||
77
- lower.endsWith("-free")
78
- );
79
- });
80
+ // 自动识别所有带 free / zero / pickle / trial / demo / community 标识或零额度的免费模型
81
+ const freeItems = data.data.filter((item) => {
82
+ const id = String(item.id || "");
83
+ const lower = id.toLowerCase();
84
+ const isFreeByKeyword =
85
+ lower.includes("free") ||
86
+ lower.includes("pickle") ||
87
+ lower.includes("zero") ||
88
+ lower.includes("trial") ||
89
+ lower.includes("demo") ||
90
+ lower.includes("community") ||
91
+ lower.endsWith("-free");
92
+
93
+ const isFreeByMetadata =
94
+ (item as { free?: boolean }).free === true ||
95
+ (item as { is_free?: boolean }).is_free === true ||
96
+ (item as { tier?: string }).tier === "free" ||
97
+ ((item as { cost?: { input?: number; output?: number } }).cost &&
98
+ (item as { cost: { input: number; output: number } }).cost.input === 0 &&
99
+ (item as { cost: { input: number; output: number } }).cost.output === 0) ||
100
+ ((item as { pricing?: { input?: number } }).pricing &&
101
+ (item as { pricing: { input: number } }).pricing.input === 0);
102
+
103
+ const isKnown = KNOWN_ZEN_FREE_MODELS[id] !== undefined;
104
+
105
+ return isFreeByKeyword || isFreeByMetadata || isKnown;
106
+ });
80
107
 
81
- return freeIds.length > 0 ? freeIds : Object.keys(KNOWN_ZEN_FREE_MODELS);
108
+ return freeItems.length > 0 ? resolveModelDefinitions(freeItems) : Object.values(KNOWN_ZEN_FREE_MODELS);
82
109
  }
83
110
 
84
111
  /**
@@ -217,21 +244,31 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
217
244
  throw new Error("未检测到 OpenCode Zen API Key。请指定 Key,例如:/zen oc_sk_xxx");
218
245
  }
219
246
 
220
- // 1. 在线校验 API Key 并拉取最新免费模型
221
- const fetcher = options.fetchModels ?? fetchZenFreeModels;
222
- let freeModelIds: string[] = [];
223
- try {
224
- freeModelIds = await fetcher(resolvedApiKey);
225
- } catch (error) {
226
- // 若因网络波动失败但已有已知列表,允许回退
227
- if (!options.apiKey || options.fetchModels) {
228
- freeModelIds = Object.keys(KNOWN_ZEN_FREE_MODELS);
229
- } else {
230
- throw error;
247
+ // 1. 在线校验 API Key 并拉取最新免费模型及精确能力规格
248
+ let resolvedModels: ZenModelDefinition[] = [];
249
+ if (options.fetchModels) {
250
+ try {
251
+ const fetched = await options.fetchModels(resolvedApiKey);
252
+ resolvedModels = resolveModelDefinitions(fetched);
253
+ } catch (error) {
254
+ if (!options.apiKey) {
255
+ resolvedModels = Object.values(KNOWN_ZEN_FREE_MODELS);
256
+ } else {
257
+ throw error;
258
+ }
259
+ }
260
+ } else {
261
+ try {
262
+ resolvedModels = await fetchZenModelCatalog(resolvedApiKey);
263
+ } catch (error) {
264
+ if (!options.apiKey) {
265
+ resolvedModels = Object.values(KNOWN_ZEN_FREE_MODELS);
266
+ } else {
267
+ throw error;
268
+ }
231
269
  }
232
270
  }
233
-
234
- const resolvedModels = resolveModelDefinitions(freeModelIds);
271
+ const freeModelIds = resolvedModels.map((m) => m.id);
235
272
 
236
273
  // 2. 确定 Session ID:若显式强制更新或当前已过期,则生成全新合规 Session
237
274
  const currentSession = getStoredZenSessionId(modelsPath);
package/src/types.ts CHANGED
@@ -43,7 +43,7 @@ export interface ZenSyncOptions {
43
43
  modelsPath?: string;
44
44
  authPath?: string;
45
45
  dbPath?: string;
46
- fetchModels?: (apiKey: string) => Promise<string[]>;
46
+ fetchModels?: (apiKey: string) => Promise<Array<string | { id: string; [key: string]: unknown }>>;
47
47
  }
48
48
 
49
49
  export interface ZenSyncResult {