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 +1 -1
- package/package.json +1 -1
- package/src/index.ts +75 -20
- package/src/models-registry.ts +293 -20
- package/src/sync.ts +65 -28
- package/src/types.ts +1 -1
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
|
-
- 🧠
|
|
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
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
|
-
|
|
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
|
-
`• 模型库: 已激活 ${
|
|
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
|
|
241
|
-
if (
|
|
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 免费模型列表 (${
|
|
248
|
-
|
|
276
|
+
`✨ [OpenCode Zen 免费模型列表 (${storedModels.length} 款)]`,
|
|
277
|
+
"智能识别精确上下文上限(Context)、最大输出(Max Tokens)及推理思考等级(Thinking):",
|
|
278
|
+
"",
|
|
279
|
+
...modelCards,
|
|
249
280
|
"",
|
|
250
|
-
"
|
|
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
|
|
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
|
-
`•
|
|
292
|
-
|
|
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) {
|
package/src/models-registry.ts
CHANGED
|
@@ -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
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
142
|
-
contextWindow
|
|
143
|
-
maxTokens
|
|
144
|
-
reasoning
|
|
145
|
-
input
|
|
146
|
-
thinkingLevelMap
|
|
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(
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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.
|
|
77
|
+
return Object.values(KNOWN_ZEN_FREE_MODELS);
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
// 自动识别所有带 free / zero / pickle
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 {
|