ck-pi-zen-session 0.1.5 → 0.1.7

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,7 @@ 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
29
  - 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
30
30
 
31
31
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ck-pi-zen-session",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,
@@ -83,11 +97,20 @@ export function isZenModelTarget(model?: { provider?: string; id?: string }): bo
83
97
  }
84
98
 
85
99
  export default function piZenSession(pi: ExtensionAPI): void {
86
- // 1. 若本地已配置 Key,启动时即刻注册独立供应商 opencode-zen-free
87
- // 独立命名空间,彻底避开并兼容用户自带的官方登录 opencode / opencode-zen 套餐
88
100
  const existingKey = getStoredZenApiKey();
89
101
  if (existingKey) {
90
- registerZenProviderToPi(pi, existingKey);
102
+ const storedSession = getStoredZenSessionId();
103
+ const storedModels = getStoredZenModels();
104
+ registerZenProviderToPi(pi, existingKey, storedSession || generateZenSessionId(), storedModels);
105
+
106
+ // 若已有 Key 但还没有持久化模型列表,后台静默自动探测一次
107
+ if (!storedModels || storedModels.length === 0) {
108
+ syncZenConfiguration()
109
+ .then((res) => {
110
+ registerZenProviderToPi(pi, res.apiKey, res.sessionId, res.resolvedModels);
111
+ })
112
+ .catch(() => {});
113
+ }
91
114
  }
92
115
 
93
116
  // 2. 核心请求头拦截钩子:在每一次请求发往 Provider 前注入完整官方客户端对齐请求头
@@ -219,16 +242,21 @@ export async function handleZenCommand(
219
242
  ? `${status.sessionAgeMinutes} 分钟`
220
243
  : `${(status.sessionAgeMinutes / 60).toFixed(1)} 小时`;
221
244
 
245
+ const storedModels = getStoredZenModels();
246
+ const reasoningCount = storedModels.filter((m) => m.reasoning).length;
247
+ const visionCount = storedModels.filter((m) => m.input.includes("image")).length;
248
+
222
249
  const msg = [
223
250
  "✨ [OpenCode Zen 运行状态]",
224
251
  `• API Key: ${status.maskedKey}`,
225
252
  `• Session: ${status.sessionId || "未生成"} (${status.sessionExpired ? "已过期" : `有效,创建于 ${ageDesc}前`})`,
226
- `• 模型库: 已激活 ${status.modelsCount} 个免费模型`,
253
+ `• 模型库: 已激活 ${storedModels.length} 款 0 额度模型 (${reasoningCount} 款支持思考推理,${visionCount} 款支持多模态)`,
254
+ `• 请求头保护: 7维官方客户端签名 + 30分钟降序时间戳自愈`,
227
255
  "",
228
256
  "常用指令:",
229
- " /zen <key> - 更新 Key 并全量重新同步",
257
+ " /zen <key> - 更新 Key 并全自动识别免费模型/上下文/思考等级",
230
258
  " /zen refresh - 强制换新 Session ID",
231
- " /zen list - 查看所有可用免费模型规格",
259
+ " /zen list - 查看所有可用免费模型详细规格卡片",
232
260
  ].join("\n");
233
261
 
234
262
  notify(ctx, msg, "info");
@@ -237,17 +265,20 @@ export async function handleZenCommand(
237
265
 
238
266
  // 2. 查看模型清单: /zen list 或 /zen models
239
267
  if (command === "list" || command === "models") {
240
- const status = getZenStatusInfo();
241
- if (status.modelIds.length === 0) {
268
+ const storedModels = getStoredZenModels();
269
+ if (storedModels.length === 0) {
242
270
  notify(ctx, "当前尚未同步任何模型,请先运行 /zen <key>。", "warning");
243
271
  return;
244
272
  }
245
273
 
274
+ const modelCards = storedModels.map((m, idx) => formatModelCard(m, idx + 1));
246
275
  const listMsg = [
247
- `✨ [OpenCode Zen 免费模型列表 (${status.modelIds.length} )]`,
248
- ...status.modelIds.map((id) => ` • ${id}`),
276
+ `✨ [OpenCode Zen 免费模型列表 (${storedModels.length} )]`,
277
+ "智能识别精确上下文上限(Context)、最大输出(Max Tokens)及推理思考等级(Thinking):",
278
+ "",
279
+ ...modelCards,
249
280
  "",
250
- "提示:这些模型已就绪,可在 Pi 中直接切换使用!",
281
+ "提示:这些模型已全量注册至 Pi,使用 /model 或快捷键随时切换!",
251
282
  ].join("\n");
252
283
 
253
284
  notify(ctx, listMsg, "info");
@@ -262,10 +293,16 @@ export async function handleZenCommand(
262
293
  if (pi) {
263
294
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
264
295
  }
296
+ if ((ctx as { modelRegistry?: { refresh?: (arg: unknown) => Promise<unknown> } }).modelRegistry?.refresh) {
297
+ await (ctx as { modelRegistry: { refresh: (arg: unknown) => Promise<unknown> } }).modelRegistry
298
+ .refresh({ providers: [ZEN_PROVIDER_ID] })
299
+ .catch(() => {});
300
+ }
301
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
265
302
  const successMsg = [
266
303
  "✓ OpenCode Zen Session 已成功刷新!",
267
304
  `• 全新 Session: ${result.sessionId}`,
268
- `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度消耗模型`,
305
+ `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度模型 (${reasoningCount} 款支持思考推理)`,
269
306
  `• 请求头伪装: 7 维官方客户端签名已注入`,
270
307
  `• 同步状态: models.json ✓ | CC-Switch ${result.ccSwitchUpdated ? "✓" : "-(未安装或无此条目)"}`,
271
308
  ].join("\n");
@@ -279,19 +316,31 @@ export async function handleZenCommand(
279
316
  // 4. 输入了具体的 API Key: /zen oc_sk_... 或其它 key
280
317
  if (rawArg.length > 0 && !["status", "refresh", "list", "models"].includes(command)) {
281
318
  try {
282
- notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费与零额度模型...", "info");
319
+ notify(ctx, "正在验证 OpenCode Zen API Key 并智能探测免费模型上下文与思考等级...", "info");
283
320
  const result = await syncZenConfiguration({ apiKey: rawArg, forceSession: true });
284
321
  if (pi) {
285
322
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
286
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
+
330
+ const modelCards = result.resolvedModels.map((m, idx) => formatModelCard(m, idx + 1));
331
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
332
+
287
333
  const msg = [
288
- "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已自动识别并全量对接:",
334
+ "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已全自动识别并对接:",
289
335
  `• API Key: ${maskKey(result.apiKey)} (已验证并保存)`,
290
- `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法)`,
291
- `• 自动对接免费模型 (${result.modelsCount} 款,全部标记 0 额度消耗):`,
292
- ...result.models.map((id) => ` • ${id}`),
336
+ `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法,30分钟自动轮换自愈)`,
337
+ `• 识别到 ${result.modelsCount} 0 额度免费模型 (${reasoningCount} 款支持深度推理/思考):`,
338
+ "",
339
+ ...modelCards,
340
+ "",
293
341
  `• 同步路径: ${result.modelsPath}`,
294
342
  result.ccSwitchUpdated ? "• CC-Switch: 本地数据库已同步更新" : "",
343
+ "提示:全部模型参数已热载入 Pi,输入 /model 或在模型选择器中可即刻选用!",
295
344
  ].filter(Boolean).join("\n");
296
345
  notify(ctx, msg, "info");
297
346
  } catch (error) {
@@ -310,17 +359,23 @@ export async function handleZenCommand(
310
359
  if (pi) {
311
360
  registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
312
361
  }
362
+ if ((ctx as { modelRegistry?: { refresh?: (arg: unknown) => Promise<unknown> } }).modelRegistry?.refresh) {
363
+ await (ctx as { modelRegistry: { refresh: (arg: unknown) => Promise<unknown> } }).modelRegistry
364
+ .refresh({ providers: [ZEN_PROVIDER_ID] })
365
+ .catch(() => {});
366
+ }
367
+ const reasoningCount = result.resolvedModels.filter((m) => m.reasoning).length;
313
368
  const msg = [
314
369
  "✨ [OpenCode Zen 已自动刷新并就绪]",
315
370
  `• API Key: ${maskKey(result.apiKey)}`,
316
371
  `• 全新 Session: ${result.sessionId} (有效且已持久化)`,
317
- `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型`,
372
+ `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型 (${reasoningCount} 款支持思考推理)`,
318
373
  `• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
319
374
  "",
320
375
  "常用操作:",
321
- " /zen <key> - 更换 API Key",
376
+ " /zen <key> - 更换 API Key (自动重识免费模型/上下文/思考等级)",
322
377
  " /zen status - 详情状态",
323
- " /zen list - 查看所有可用免费模型",
378
+ " /zen list - 查看所有可用免费模型详细规格",
324
379
  ].join("\n");
325
380
  notify(ctx, msg, "info");
326
381
  } catch (error) {
@@ -11,7 +11,7 @@ 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
15
  low: "low",
16
16
  high: "high",
17
17
  max: "max",
@@ -21,7 +21,7 @@ const STANDARD_THINKING_LEVELS: Record<string, string | null> = {
21
21
  xhigh: null,
22
22
  };
23
23
 
24
- const MUSE_SPARK_THINKING_LEVELS: Record<string, string | null> = {
24
+ export const MUSE_SPARK_THINKING_LEVELS: Record<string, string | null> = {
25
25
  minimal: "minimal",
26
26
  low: "low",
27
27
  medium: "medium",
@@ -31,6 +31,16 @@ const MUSE_SPARK_THINKING_LEVELS: Record<string, string | null> = {
31
31
  off: null,
32
32
  };
33
33
 
34
+ export const GRANULAR_THINKING_LEVELS: Record<string, string | null> = {
35
+ minimal: "low",
36
+ low: "low",
37
+ medium: "medium",
38
+ high: "high",
39
+ xhigh: "high",
40
+ max: "max",
41
+ off: null,
42
+ };
43
+
34
44
  /**
35
45
  * 官方已知免费模型精确规格字典
36
46
  */
@@ -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 {