@kkmila/cpc 1.0.1 → 1.0.4

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
@@ -36,6 +36,7 @@ cpc install
36
36
  | `cpc export [name]` | 导出提供商配置 |
37
37
  | `cpc import <file>` | 导入提供商配置 |
38
38
  | `cpc list-installed` | 列出已安装的提供商 |
39
+ | `cpc doctor` | 检查配置和运行状态 |
39
40
 
40
41
  ## 使用示例
41
42
 
@@ -73,6 +74,7 @@ cpc test openai
73
74
  - omp
74
75
  - mimo-code
75
76
  - cursor
77
+ - opencode
76
78
 
77
79
  ## 配置文件
78
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkmila/cpc",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Custom Provider CLI for managing model providers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,10 @@
10
10
  "exports": {
11
11
  ".": "./src/commands/provider.mjs"
12
12
  },
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/"
16
+ },
13
17
  "files": [
14
18
  "bin",
15
19
  "src",
@@ -18,7 +22,7 @@
18
22
  ],
19
23
  "scripts": {
20
24
  "dev": "node bin/cpc.mjs",
21
- "test": "node --test src/**/*.test.mjs"
25
+ "test": "node --test 'src/**/*.test.mjs'"
22
26
  },
23
27
  "keywords": [
24
28
  "cli",
package/registry.yaml CHANGED
@@ -41,3 +41,8 @@ agents:
41
41
  name: Cursor
42
42
  home: .
43
43
  description: "Cursor editor (project-level config)"
44
+
45
+ opencode:
46
+ name: OpenCode
47
+ home: ~/.config/opencode
48
+ description: "OpenCode AI coding assistant"
@@ -5,6 +5,7 @@ import { resolve, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import yaml from "js-yaml";
7
7
  import { loadRegistry, getAgents, resolveAgentPaths } from "../lib/agents.mjs";
8
+ import { enrichModelWithBaseline } from "../lib/baseline-models.mjs";
8
9
 
9
10
  /**
10
11
  * Get the config directory path (~/.config/cpc)
@@ -387,6 +388,27 @@ function getAgentProviderConfig(agentId, provider) {
387
388
  },
388
389
  };
389
390
 
391
+ case "opencode":
392
+ return {
393
+ opencodeJson: {
394
+ provider: {
395
+ [provider.name]: {
396
+ models: provider.models.reduce((acc, m) => {
397
+ acc[m.id] = {
398
+ name: m.name || m.id,
399
+ };
400
+ return acc;
401
+ }, {}),
402
+ npm: "@ai-sdk/openai-compatible",
403
+ options: {
404
+ apiKey: provider.apiKey,
405
+ baseURL: provider.baseUrl,
406
+ },
407
+ },
408
+ },
409
+ },
410
+ };
411
+
390
412
  default:
391
413
  return null;
392
414
  }
@@ -511,6 +533,22 @@ function installProviderToAgent(agentId, provider, agentHome) {
511
533
  return true;
512
534
  }
513
535
 
536
+ case "opencode": {
537
+ const configPath = join(agentHome, "opencode.json");
538
+ let configData = {};
539
+ if (existsSync(configPath)) {
540
+ configData = JSON.parse(readFileSync(configPath, "utf8"));
541
+ }
542
+ if (!configData.provider) configData.provider = {};
543
+ configData.provider = {
544
+ ...configData.provider,
545
+ ...config.opencodeJson.provider,
546
+ };
547
+ writeFileSync(configPath, JSON.stringify(configData, null, 2));
548
+ console.log(chalk.green(` ✓ 更新 OpenCode opencode.json`));
549
+ return true;
550
+ }
551
+
514
552
  default:
515
553
  console.log(chalk.yellow(` ⚠ 未知 agent: ${agentId}`));
516
554
  return false;
@@ -521,6 +559,21 @@ function installProviderToAgent(agentId, provider, agentHome) {
521
559
  }
522
560
  }
523
561
 
562
+ /**
563
+ * Check if an agent is installed by looking at its home directory
564
+ */
565
+ function isAgentInstalled(agentId, agents) {
566
+ const agent = agents[agentId];
567
+ if (!agent || !agent.home) return false;
568
+
569
+ // Expand ~ to home directory
570
+ const home = agent.home.replace(
571
+ /^~/,
572
+ process.env.HOME || process.env.USERPROFILE,
573
+ );
574
+ return existsSync(home);
575
+ }
576
+
524
577
  export function registerProvider(program, repoRoot) {
525
578
  // ── cpc add ──────────────────────────────────────────────────
526
579
  program
@@ -647,14 +700,23 @@ export function registerProvider(program, repoRoot) {
647
700
  for (const modelId of selected) {
648
701
  const model = result.models.find((m) => m.id === modelId);
649
702
  const defaults = getModelDefaults(modelId);
650
- const configured = {
703
+ // 先用 baseline 字典补全 contextWindow / input / description,
704
+ // 未命中时回退到 API 响应与 MODEL_DEFAULTS
705
+ const configured = enrichModelWithBaseline({
651
706
  id: modelId,
652
707
  name: model?.name || modelId,
653
708
  contextWindow: model?.contextWindow || defaults.contextWindow,
654
709
  maxTokens: model?.maxTokens || defaults.maxTokens,
655
710
  reasoning: model?.reasoning || defaults.reasoning,
656
711
  input: model?.input || ["text"],
657
- };
712
+ });
713
+ if (configured.description) {
714
+ console.log(
715
+ chalk.dim(
716
+ ` ↪ ${modelId}: 命中 baseline, 已注入 contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`,
717
+ ),
718
+ );
719
+ }
658
720
  models.push(configured);
659
721
  }
660
722
  } else {
@@ -976,14 +1038,23 @@ export function registerProvider(program, repoRoot) {
976
1038
  for (const modelId of selected) {
977
1039
  const model = result.models.find((m) => m.id === modelId);
978
1040
  const defaults = getModelDefaults(modelId);
979
- const configured = {
1041
+ // 先用 baseline 字典补全 contextWindow / input / description,
1042
+ // 未命中时回退到 API 响应与 MODEL_DEFAULTS
1043
+ const configured = enrichModelWithBaseline({
980
1044
  id: modelId,
981
1045
  name: model?.name || modelId,
982
1046
  contextWindow: model?.contextWindow || defaults.contextWindow,
983
1047
  maxTokens: model?.maxTokens || defaults.maxTokens,
984
1048
  reasoning: model?.reasoning || defaults.reasoning,
985
1049
  input: model?.input || ["text"],
986
- };
1050
+ });
1051
+ if (configured.description) {
1052
+ console.log(
1053
+ chalk.dim(
1054
+ ` ↪ ${modelId}: 命中 baseline, 已注入 contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`,
1055
+ ),
1056
+ );
1057
+ }
987
1058
 
988
1059
  if (opts.merge) {
989
1060
  config.models = config.models || [];
@@ -1134,24 +1205,37 @@ export function registerProvider(program, repoRoot) {
1134
1205
  }
1135
1206
 
1136
1207
  // Step 3: Select agent(s)
1137
- let agentId;
1208
+ let agentIds;
1138
1209
  if (opts.agent) {
1139
- agentId = opts.agent;
1210
+ agentIds = opts.agent === "all" ? Object.keys(agents) : [opts.agent];
1140
1211
  } else {
1141
- agentId = await select({
1142
- message: "Select target agent:",
1212
+ // Auto-detect installed agents
1213
+ const selected = await checkbox({
1214
+ message: "Select target agents (installed agents are pre-selected):",
1143
1215
  choices: [
1144
- ...Object.entries(agents).map(([id, a]) => ({
1145
- name: `${id.padEnd(14)} ${a.description || ""}`,
1146
- value: id,
1147
- })),
1148
- { name: "─────────", value: "__separator__", disabled: true },
1149
- { name: "all agents", value: "all" },
1216
+ ...Object.entries(agents).map(([id, a]) => {
1217
+ const installed = isAgentInstalled(id, agents);
1218
+ return {
1219
+ name: `${id.padEnd(14)} ${a.description || ""}${installed ? " ✓" : ""}`,
1220
+ value: id,
1221
+ checked: installed,
1222
+ };
1223
+ }),
1150
1224
  ],
1225
+ instructions: {
1226
+ navigator: "↑↓ navigate",
1227
+ select: "space select",
1228
+ all: "a toggle all",
1229
+ },
1151
1230
  });
1152
- }
1153
1231
 
1154
- const agentIds = agentId === "all" ? Object.keys(agents) : [agentId];
1232
+ if (selected.length === 0) {
1233
+ console.log(chalk.yellow("No agents selected. Cancelled."));
1234
+ return;
1235
+ }
1236
+
1237
+ agentIds = selected;
1238
+ }
1155
1239
 
1156
1240
  // Step 4: Install provider to agents
1157
1241
  console.log();
@@ -1563,6 +1647,29 @@ export function registerProvider(program, repoRoot) {
1563
1647
  break;
1564
1648
  }
1565
1649
 
1650
+ case "opencode": {
1651
+ const configPath = join(agent.home, "opencode.json");
1652
+ if (existsSync(configPath)) {
1653
+ const configData = JSON.parse(readFileSync(configPath, "utf8"));
1654
+ if (configData.provider && Object.keys(configData.provider).length > 0) {
1655
+ for (const [name, config] of Object.entries(
1656
+ configData.provider,
1657
+ )) {
1658
+ console.log(` ${chalk.blue(name)}`);
1659
+ console.log(` URL: ${chalk.dim(config.options?.baseURL || "not set")}`);
1660
+ console.log(
1661
+ ` Models: ${chalk.dim(Object.keys(config.models || {}).length)}`,
1662
+ );
1663
+ }
1664
+ } else {
1665
+ console.log(chalk.dim(" No providers configured"));
1666
+ }
1667
+ } else {
1668
+ console.log(chalk.dim(" Config file not found"));
1669
+ }
1670
+ break;
1671
+ }
1672
+
1566
1673
  default:
1567
1674
  console.log(chalk.dim(" Unsupported agent"));
1568
1675
  }
@@ -1570,7 +1677,88 @@ export function registerProvider(program, repoRoot) {
1570
1677
  console.log(chalk.red(` Error reading config: ${err.message}`));
1571
1678
  }
1572
1679
 
1573
- console.log();
1680
+ console.log();
1681
+ }
1682
+ });
1683
+
1684
+ // ── cpc doctor ───────────────────────────────────────────────
1685
+ program
1686
+ .command("doctor")
1687
+ .description("Check if cpc is properly configured and working")
1688
+ .action(() => {
1689
+ console.log(chalk.bold("\n🔍 CPC Doctor\n"));
1690
+
1691
+ let issues = 0;
1692
+
1693
+ // Check Node.js version
1694
+ const nodeVersion = process.version;
1695
+ const requiredVersion = "18.0.0";
1696
+ const nodeOk = compareVersions(nodeVersion.slice(1), requiredVersion) >= 0;
1697
+ console.log(`${nodeOk ? "✅" : "❌"} Node.js version: ${nodeVersion} (required: >=${requiredVersion})`);
1698
+ if (!nodeOk) issues++;
1699
+
1700
+ // Check registry.yaml
1701
+ try {
1702
+ const registry = loadRegistry(repoRoot);
1703
+ const agents = getAgents(registry);
1704
+ const agentCount = Object.keys(agents).length;
1705
+ console.log(`✅ registry.yaml: ${agentCount} agents defined`);
1706
+ } catch (err) {
1707
+ console.log(`❌ registry.yaml: ${err.message}`);
1708
+ issues++;
1709
+ }
1710
+
1711
+ // Check config directory
1712
+ const configDir = getConfigDir();
1713
+ if (existsSync(configDir)) {
1714
+ console.log(`✅ Config directory: ${configDir}`);
1715
+
1716
+ // Check providers.json
1717
+ try {
1718
+ const providers = loadProviders();
1719
+ const providerCount = Object.keys(providers.providers).length;
1720
+ console.log(`✅ providers.json: ${providerCount} provider(s) configured`);
1721
+ } catch (err) {
1722
+ console.log(`❌ providers.json: ${err.message}`);
1723
+ issues++;
1724
+ }
1725
+ } else {
1726
+ console.log(`⚠️ Config directory: ${configDir} (will be created on first use)`);
1727
+ }
1728
+
1729
+ // Check installed agents
1730
+ console.log(chalk.bold("\nInstalled Agents:"));
1731
+ try {
1732
+ const registry = loadRegistry(repoRoot);
1733
+ const agents = getAgents(registry);
1734
+ for (const [id, agent] of Object.entries(agents)) {
1735
+ const installed = isAgentInstalled(id, agents);
1736
+ console.log(`${installed ? "✅" : "⬜"} ${id.padEnd(14)} ${agent.description || ""}`);
1737
+ }
1738
+ } catch (err) {
1739
+ console.log(chalk.red(` Error: ${err.message}`));
1574
1740
  }
1741
+
1742
+ // Summary
1743
+ console.log(chalk.bold(`\n${"═".repeat(40)}`));
1744
+ if (issues === 0) {
1745
+ console.log(chalk.green.bold("✅ All checks passed! CPC is ready to use."));
1746
+ } else {
1747
+ console.log(chalk.red.bold(`❌ Found ${issues} issue(s). Please fix them before using CPC.`));
1748
+ }
1749
+ console.log();
1575
1750
  });
1576
1751
  }
1752
+
1753
+ /**
1754
+ * Compare semver versions
1755
+ */
1756
+ function compareVersions(a, b) {
1757
+ const pa = a.split(".").map(Number);
1758
+ const pb = b.split(".").map(Number);
1759
+ for (let i = 0; i < 3; i++) {
1760
+ if (pa[i] > pb[i]) return 1;
1761
+ if (pa[i] < pb[i]) return -1;
1762
+ }
1763
+ return 0;
1764
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * 基础模型字典 (Baseline)
3
+ *
4
+ * 来源: providers-baseline.json
5
+ * 用途: 当从外部 API (/v1/models) 获取到模型 ID 后, 用本字典补全
6
+ * - contextWindow: 模型上下文窗口
7
+ * - input: 模型支持的输入类型 (text / image / ...)
8
+ * - description: 模型中文描述
9
+ * 然后随 model 一起持久化到 ~/.config/cpc/providers.json
10
+ *
11
+ * 注意: 本字典不包含 maxTokens / reasoning, 仍由 src/commands/provider.mjs
12
+ * 中的 MODEL_DEFAULTS 处理. 安装到 Agent 时, 各 agent schema 不消费
13
+ * description, 该字段仅作为 provider.json 内的元信息保留.
14
+ */
15
+
16
+ export const BASELINE_MODELS = [
17
+ {
18
+ modelId: "mimo-v2-omni",
19
+ contextWindow: 32768,
20
+ input: ["text"],
21
+ description: "小米 MiMo V2 全模态旗舰,文本/图像/音频/视频统一理解与生成。",
22
+ },
23
+ {
24
+ modelId: "mimo-v2-pro",
25
+ contextWindow: 32768,
26
+ input: ["text"],
27
+ description: "小米 MiMo V2 推理增强版,强项为复杂推理与长程任务。",
28
+ },
29
+ {
30
+ modelId: "mimo-v2-tts",
31
+ contextWindow: 32768,
32
+ input: ["text"],
33
+ description: "小米 MiMo V2 文本转语音合成模型,多音色/多语种。",
34
+ },
35
+ {
36
+ modelId: "mimo-v2.5",
37
+ contextWindow: 32768,
38
+ input: ["text"],
39
+ description: "小米 MiMo V2.5 通用对话基座,性价比首选。",
40
+ },
41
+ {
42
+ modelId: "mimo-v2.5-asr",
43
+ contextWindow: 32768,
44
+ input: ["text"],
45
+ description: "小米 MiMo V2.5 语音识别模型,音频转文字。",
46
+ },
47
+ {
48
+ modelId: "mimo-v2.5-pro",
49
+ contextWindow: 32768,
50
+ input: ["text"],
51
+ description: "小米 MiMo V2.5 推理增强版,复杂任务与编程。",
52
+ },
53
+ {
54
+ modelId: "mimo-v2.5-tts",
55
+ contextWindow: 32768,
56
+ input: ["text"],
57
+ description: "小米 MiMo V2.5 文本转语音合成模型。",
58
+ },
59
+ {
60
+ modelId: "mimo-v2.5-tts-voiceclone",
61
+ contextWindow: 32768,
62
+ input: ["text"],
63
+ description: "小米 MiMo V2.5 声音克隆 TTS,参考音频复刻音色。",
64
+ },
65
+ {
66
+ modelId: "mimo-v2.5-tts-voicedesign",
67
+ contextWindow: 32768,
68
+ input: ["text"],
69
+ description: "小米 MiMo V2.5 声音设计 TTS,按描述/特征生成新音色。",
70
+ },
71
+ {
72
+ modelId: "gemma4:e4b",
73
+ contextWindow: 32768,
74
+ input: ["text"],
75
+ description:
76
+ "Google Gemma 4 边缘端模型(4.5B effective / 8B with embeddings),128K 上下文,支持文本+图像+音频。",
77
+ },
78
+ {
79
+ modelId: "qwen3.6:27b",
80
+ contextWindow: 32768,
81
+ input: ["text"],
82
+ description:
83
+ "Qwen3.5 架构的 27.8B dense 本地模型(Q4_K_M 17GB),本地多用途对话与编程。",
84
+ },
85
+ {
86
+ modelId: "qwen3.6-plus",
87
+ contextWindow: 1000000,
88
+ input: ["text", "image"],
89
+ description:
90
+ "阿里 Qwen3.6 旗舰 VL 模型,主打 Agentic Coding、Vibe Coding 与 1M 多模态长上下文。",
91
+ },
92
+ {
93
+ modelId: "qwen3-vl-plus",
94
+ contextWindow: 1000000,
95
+ input: ["text", "image"],
96
+ description:
97
+ "通义千问视觉旗舰,OS World 视觉 Agent SOTA,文档/视频/OCR 全能理解。",
98
+ },
99
+ {
100
+ modelId: "qwen3.5-122b-a10b",
101
+ contextWindow: 262144,
102
+ input: ["text", "image"],
103
+ description:
104
+ "Qwen3.5 开源 MoE 旗舰(122B 总参/10B 激活),262K 上下文,原生视觉语言模型。",
105
+ },
106
+ {
107
+ modelId: "qwen3-coder-plus",
108
+ contextWindow: 1000000,
109
+ input: ["text"],
110
+ description:
111
+ "通义千问编码专项旗舰,1M 仓库级上下文,强工具调用与多文件重构。",
112
+ },
113
+ {
114
+ modelId: "text-embedding-v4",
115
+ contextWindow: 32000,
116
+ input: ["text"],
117
+ description:
118
+ "阿里 Qwen3-Embedding 旗舰,MTEB 多语言榜单领先,支持 8 种向量维度的语义检索首选。",
119
+ },
120
+ {
121
+ modelId: "GLM-5.0",
122
+ contextWindow: 200000,
123
+ input: ["text"],
124
+ description:
125
+ "智谱基座旗舰 744B MoE,编程对齐 Claude Opus 4.5,复杂系统工程与长程 Agent 基座。",
126
+ },
127
+ {
128
+ modelId: "GLM-5.0-anthropic",
129
+ contextWindow: 200000,
130
+ input: ["text"],
131
+ description: "GLM-5.0 的 Anthropic 协议兼容封装。",
132
+ },
133
+ {
134
+ modelId: "glm-5.1",
135
+ contextWindow: 200000,
136
+ input: ["text"],
137
+ description:
138
+ "智谱长程任务旗舰,可单任务持续自主工作 8 小时,编程对齐 Claude Opus 4.6。",
139
+ },
140
+ {
141
+ modelId: "glm-5.1-anthropic",
142
+ contextWindow: 200000,
143
+ input: ["text"],
144
+ description: "glm-5.1 的 Anthropic 协议兼容封装。",
145
+ },
146
+ {
147
+ modelId: "deepseek-v4-pro",
148
+ contextWindow: 1000000,
149
+ input: ["text"],
150
+ description:
151
+ "DeepSeek V4 旗舰 1.6T MoE,开源 SOTA 世界知识与 Agent Coding,支持双思考模式。",
152
+ },
153
+ {
154
+ modelId: "deepseek-v4-pro-anthropic",
155
+ contextWindow: 1000000,
156
+ input: ["text"],
157
+ description: "DeepSeek V4-Pro 的 Anthropic 协议封装。",
158
+ },
159
+ {
160
+ modelId: "deepseek-v4-flash",
161
+ contextWindow: 1000000,
162
+ input: ["text"],
163
+ description:
164
+ "DeepSeek V4 轻量版(284B/13B 激活),1M 上下文,主打高吞吐低成本日常 Agent。",
165
+ },
166
+ {
167
+ modelId: "DeepSeek-V3.2",
168
+ contextWindow: 128000,
169
+ input: ["text"],
170
+ description:
171
+ "DeepSeek V3 末代 DSA 稀疏注意力版本,平衡推理深度与输出长度,适合日常问答与 Agent。",
172
+ },
173
+ {
174
+ modelId: "Doubao-Seed-2.0-Pro",
175
+ contextWindow: 256000,
176
+ input: ["text", "image"],
177
+ description:
178
+ "字节豆包 2.0 旗舰,多模态视觉推理/空间推理 SOTA,全面对标 GPT-5.2 / Gemini 3 Pro。",
179
+ },
180
+ {
181
+ modelId: "Doubao-Seed-2.0-Code",
182
+ contextWindow: 256000,
183
+ input: ["text", "image"],
184
+ description:
185
+ "字节豆包 2.0 编程专版,前端强、支持 UI-to-Code 视觉理解,与 TRAE 集成最佳。",
186
+ },
187
+ {
188
+ modelId: "Kimi-K2.5",
189
+ contextWindow: 256000,
190
+ input: ["text", "image"],
191
+ description:
192
+ "月之暗面原生多模态旗舰,文本+视觉联合优化,Agent Swarm 与视觉编程(UI/视频生成代码)。",
193
+ },
194
+ {
195
+ modelId: "kimi-k2.6",
196
+ contextWindow: 256000,
197
+ input: ["text", "image"],
198
+ description:
199
+ "月之暗面 K2.6,开源 SOTA 长程编码(13 小时不间断)+ Agent Swarm 300 子智能体。",
200
+ },
201
+ {
202
+ modelId: "kimi-k2.6-anthropic",
203
+ contextWindow: 256000,
204
+ input: ["text", "image"],
205
+ description: "kimi-k2.6 的 Anthropic 协议兼容封装。",
206
+ },
207
+ {
208
+ modelId: "minimax-m3",
209
+ contextWindow: 1000000,
210
+ input: ["text", "image"],
211
+ description:
212
+ "MiniMax 2026 旗舰,国内首个前沿 Coding + 1M 上下文 + 原生多模态三合一模型。",
213
+ },
214
+ {
215
+ modelId: "MiniMax-M2.7",
216
+ contextWindow: 204800,
217
+ input: ["text"],
218
+ description:
219
+ "MiniMax M2.7,模型自我进化路径,端到端软件工程交付与 Office 复杂编辑。",
220
+ },
221
+ {
222
+ modelId: "minimax-m2.7",
223
+ contextWindow: 204800,
224
+ input: ["text"],
225
+ description: "MiniMax M2.7 小写别名,与 MiniMax-M2.7 同款。",
226
+ },
227
+ ];
228
+
229
+ /**
230
+ * 在 baseline 字典中查找模型
231
+ * 匹配规则:
232
+ * 1) 精确匹配 (大小写不敏感)
233
+ * 2) 包含匹配 (查询 ID 包含 baseline 的 modelId 或反之)
234
+ * @param {string} modelId
235
+ * @returns {object|null} 命中的 baseline 记录
236
+ */
237
+ export function lookupBaselineModel(modelId) {
238
+ if (!modelId) return null;
239
+ const lower = String(modelId).toLowerCase();
240
+
241
+ // 1) 精确匹配
242
+ const exact = BASELINE_MODELS.find((m) => m.modelId.toLowerCase() === lower);
243
+ if (exact) return exact;
244
+
245
+ // 2) 包含匹配: baseline.modelId ⊆ query.id (适用 qwen3.6 / MiniMax 大小写差异)
246
+ const contains = BASELINE_MODELS.find((m) =>
247
+ lower.includes(m.modelId.toLowerCase()),
248
+ );
249
+ if (contains) return contains;
250
+
251
+ // 3) 反向包含: query.id ⊆ baseline.modelId (适用 minimax-m3 命中 MiniMax-M2.7 等)
252
+ const reverse = BASELINE_MODELS.find((m) =>
253
+ m.modelId.toLowerCase().includes(lower),
254
+ );
255
+ return reverse || null;
256
+ }
257
+
258
+ /**
259
+ * 用 baseline 字典补全 model 字段
260
+ * 命中时, baseline 的 contextWindow / input / description 会覆盖传入值
261
+ * 命不中则原样返回
262
+ * maxTokens / reasoning 不在 baseline 范围, 由调用方自己处理
263
+ * @param {object} model 至少有 { id }
264
+ * @returns {object} 增强后的 model
265
+ */
266
+ export function enrichModelWithBaseline(model) {
267
+ if (!model || !model.id) return model;
268
+ const baseline = lookupBaselineModel(model.id);
269
+ if (!baseline) return model;
270
+
271
+ const enriched = { ...model };
272
+
273
+ if (typeof baseline.contextWindow === "number") {
274
+ enriched.contextWindow = baseline.contextWindow;
275
+ }
276
+
277
+ if (Array.isArray(baseline.input) && baseline.input.length > 0) {
278
+ enriched.input = [...baseline.input];
279
+ }
280
+
281
+ if (baseline.description) {
282
+ enriched.description = baseline.description;
283
+ }
284
+
285
+ return enriched;
286
+ }