@link-assistant/hive-mind 1.35.9 → 1.35.10
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent.lib.mjs +7 -47
- package/src/claude.lib.mjs +2 -2
- package/src/claude.prompts.lib.mjs +2 -1
- package/src/github.lib.mjs +1 -1
- package/src/hive.config.lib.mjs +2 -1
- package/src/hive.mjs +1 -1
- package/src/models/index.mjs +871 -0
- package/src/opencode.lib.mjs +4 -15
- package/src/review.mjs +4 -3
- package/src/solve.config.lib.mjs +8 -19
- package/src/solve.mjs +1 -1
- package/src/task.mjs +4 -3
- package/src/telegram-bot.mjs +2 -2
- package/src/model-info.lib.mjs +0 -360
- package/src/model-mapping.lib.mjs +0 -176
- package/src/model-validation.lib.mjs +0 -427
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified models module for hive-mind
|
|
5
|
+
* Single source of truth for all model data, mapping, validation, and info.
|
|
6
|
+
*
|
|
7
|
+
* Consolidates the former:
|
|
8
|
+
* - model-mapping.lib.mjs (model data, maps, tool-model functions)
|
|
9
|
+
* - model-validation.lib.mjs (validation, fuzzy matching, 1M context)
|
|
10
|
+
* - model-info.lib.mjs (display names, models.dev API, PR comment helpers)
|
|
11
|
+
*
|
|
12
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1473
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Check if use is already defined (when imported from solve.mjs)
|
|
16
|
+
// If not, fetch it (when running standalone)
|
|
17
|
+
if (typeof globalThis.use === 'undefined') {
|
|
18
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import { log } from '../lib.mjs';
|
|
22
|
+
|
|
23
|
+
// ─── MODEL DATA ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
// Claude models (Anthropic API)
|
|
26
|
+
// Updated for Opus 4.5/4.6 and Sonnet 4.6 support (Issue #1221, Issue #1238, Issue #1329, Issue #1433)
|
|
27
|
+
export const claudeModels = {
|
|
28
|
+
sonnet: 'claude-sonnet-4-6', // Sonnet 4.6 (default, Issue #1329)
|
|
29
|
+
opus: 'claude-opus-4-6', // Opus 4.6 (Issue #1433)
|
|
30
|
+
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
31
|
+
'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
|
|
32
|
+
'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
|
|
33
|
+
// Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
|
|
34
|
+
'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
|
|
35
|
+
'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias
|
|
36
|
+
'opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5 short alias
|
|
37
|
+
'sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 short alias (backward compatibility)
|
|
38
|
+
'haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5 short alias
|
|
39
|
+
// Version aliases for backward compatibility (Issue #1221, Issue #1329)
|
|
40
|
+
'claude-sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
|
|
41
|
+
'claude-opus-4-6': 'claude-opus-4-6', // Opus 4.6
|
|
42
|
+
'claude-opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5
|
|
43
|
+
'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 (backward compatibility)
|
|
44
|
+
'claude-haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Agent models (OpenCode API and Kilo Gateway via agent CLI)
|
|
48
|
+
// Issue #1300: Updated free models to match agent PR #191
|
|
49
|
+
export const agentModels = {
|
|
50
|
+
// OpenCode Zen free models (current)
|
|
51
|
+
grok: 'opencode/grok-code',
|
|
52
|
+
'grok-code': 'opencode/grok-code',
|
|
53
|
+
'grok-code-fast-1': 'opencode/grok-code',
|
|
54
|
+
'big-pickle': 'opencode/big-pickle',
|
|
55
|
+
'gpt-5-nano': 'opencode/gpt-5-nano',
|
|
56
|
+
'minimax-m2.5-free': 'opencode/minimax-m2.5-free', // New: upgraded from M2.1 (Issue #1391: now default)
|
|
57
|
+
// Kilo Gateway free models (Issue #1282, updated in #1300)
|
|
58
|
+
// Short names for Kilo-exclusive models (Issue #1300)
|
|
59
|
+
'glm-5-free': 'kilo/glm-5-free', // Kilo-exclusive
|
|
60
|
+
'glm-4.5-air-free': 'kilo/glm-4.5-air-free', // Kilo-exclusive: agent-centric model
|
|
61
|
+
'deepseek-r1-free': 'kilo/deepseek-r1-free', // Kilo-exclusive: reasoning model
|
|
62
|
+
'giga-potato-free': 'kilo/giga-potato-free', // Kilo-exclusive
|
|
63
|
+
'trinity-large-preview': 'kilo/trinity-large-preview', // Kilo-exclusive
|
|
64
|
+
// Full names with kilo/ prefix
|
|
65
|
+
'kilo/glm-5-free': 'kilo/glm-5-free',
|
|
66
|
+
'kilo/glm-4.5-air-free': 'kilo/glm-4.5-air-free',
|
|
67
|
+
'kilo/minimax-m2.5-free': 'kilo/minimax-m2.5-free', // Also on OpenCode Zen
|
|
68
|
+
'kilo/deepseek-r1-free': 'kilo/deepseek-r1-free',
|
|
69
|
+
'kilo/giga-potato-free': 'kilo/giga-potato-free',
|
|
70
|
+
'kilo/trinity-large-preview': 'kilo/trinity-large-preview',
|
|
71
|
+
// Deprecated free models (kept for backward compatibility)
|
|
72
|
+
'kimi-k2.5-free': 'opencode/kimi-k2.5-free', // Deprecated: not supported (Issue #1391)
|
|
73
|
+
'glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated: no longer free
|
|
74
|
+
'minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
75
|
+
'kilo/glm-4.7-free': 'kilo/glm-4.7-free', // Deprecated: replaced by glm-4.5-air-free
|
|
76
|
+
'kilo/kimi-k2.5-free': 'kilo/kimi-k2.5-free', // Deprecated: not recommended
|
|
77
|
+
'kilo/minimax-m2.1-free': 'kilo/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
78
|
+
// Premium models
|
|
79
|
+
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
80
|
+
haiku: 'anthropic/claude-3-5-haiku',
|
|
81
|
+
opus: 'anthropic/claude-3-opus',
|
|
82
|
+
'gemini-3-pro': 'google/gemini-3-pro',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// OpenCode models (OpenCode API)
|
|
86
|
+
export const opencodeModels = {
|
|
87
|
+
gpt4: 'openai/gpt-4',
|
|
88
|
+
gpt4o: 'openai/gpt-4o',
|
|
89
|
+
claude: 'anthropic/claude-3-5-sonnet',
|
|
90
|
+
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
91
|
+
opus: 'anthropic/claude-3-opus',
|
|
92
|
+
gemini: 'google/gemini-pro',
|
|
93
|
+
grok: 'opencode/grok-code',
|
|
94
|
+
'grok-code': 'opencode/grok-code',
|
|
95
|
+
'grok-code-fast-1': 'opencode/grok-code',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Codex models (OpenAI API)
|
|
99
|
+
export const codexModels = {
|
|
100
|
+
gpt5: 'gpt-5',
|
|
101
|
+
'gpt5-codex': 'gpt-5-codex',
|
|
102
|
+
o3: 'o3',
|
|
103
|
+
'o3-mini': 'o3-mini',
|
|
104
|
+
gpt4: 'gpt-4',
|
|
105
|
+
gpt4o: 'gpt-4o',
|
|
106
|
+
claude: 'claude-3-5-sonnet',
|
|
107
|
+
sonnet: 'claude-3-5-sonnet',
|
|
108
|
+
opus: 'claude-3-opus',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Default model for each tool (Issue #1473: centralized to avoid scattered hardcoded defaults)
|
|
112
|
+
export const defaultModels = {
|
|
113
|
+
claude: 'sonnet',
|
|
114
|
+
agent: 'minimax-m2.5-free',
|
|
115
|
+
opencode: 'grok-code-fast-1',
|
|
116
|
+
codex: 'gpt-5',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Models that support 1M token context window via [1m] suffix (Issue #1221, Issue #1238, Issue #1329)
|
|
120
|
+
// See: https://code.claude.com/docs/en/model-config
|
|
121
|
+
export const MODELS_SUPPORTING_1M_CONTEXT = [
|
|
122
|
+
'claude-opus-4-6',
|
|
123
|
+
'claude-opus-4-5-20251101',
|
|
124
|
+
'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
|
|
125
|
+
'claude-sonnet-4-5-20250929',
|
|
126
|
+
'claude-sonnet-4-5',
|
|
127
|
+
'sonnet', // Now maps to Sonnet 4.6 (Issue #1329)
|
|
128
|
+
'sonnet-4-6', // Short alias (Issue #1329)
|
|
129
|
+
'opus',
|
|
130
|
+
'opus-4-6', // Short alias (Issue #1221 - PR comment feedback)
|
|
131
|
+
'opus-4-5', // Short alias (Issue #1238)
|
|
132
|
+
'sonnet-4-5', // Short alias (Issue #1221 - PR comment feedback)
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Free model to base model mapping for pricing lookup (Issue #1250, Issue #1473)
|
|
136
|
+
// Free models like "kimi-k2.5-free" should use pricing from base model "kimi-k2.5"
|
|
137
|
+
export const freeToBaseModelMap = {
|
|
138
|
+
'kimi-k2.5-free': 'kimi-k2.5',
|
|
139
|
+
'glm-4.7-free': 'glm-4.7',
|
|
140
|
+
'minimax-m2.1-free': 'minimax-m2.1',
|
|
141
|
+
'minimax-m2.5-free': 'minimax-m2.5',
|
|
142
|
+
'glm-5-free': 'glm-5',
|
|
143
|
+
'glm-4.5-air-free': 'glm-4.5-air',
|
|
144
|
+
'deepseek-r1-free': 'deepseek-r1',
|
|
145
|
+
'giga-potato-free': 'giga-potato',
|
|
146
|
+
'trinity-large-preview-free': 'trinity-large-preview',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ─── VALIDATION-EXTENDED MODEL MAPS ──────────────────────────────────────────
|
|
150
|
+
// These extend the base maps with full model ID identity entries for validation
|
|
151
|
+
// (e.g., 'claude-sonnet-4-5-20250929' → 'claude-sonnet-4-5-20250929')
|
|
152
|
+
// so that full model IDs are also accepted as valid inputs
|
|
153
|
+
|
|
154
|
+
export const CLAUDE_MODELS = {
|
|
155
|
+
...claudeModels,
|
|
156
|
+
'claude-sonnet-4-5-20250929': 'claude-sonnet-4-5-20250929',
|
|
157
|
+
'claude-opus-4-5-20251101': 'claude-opus-4-5-20251101',
|
|
158
|
+
'claude-haiku-4-5-20251001': 'claude-haiku-4-5-20251001',
|
|
159
|
+
'claude-3-5-haiku-20241022': 'claude-3-5-haiku-20241022',
|
|
160
|
+
'claude-3-haiku-20240307': 'claude-3-haiku-20240307',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const OPENCODE_MODELS = {
|
|
164
|
+
...opencodeModels,
|
|
165
|
+
'openai/gpt-4': 'openai/gpt-4',
|
|
166
|
+
'openai/gpt-4o': 'openai/gpt-4o',
|
|
167
|
+
'anthropic/claude-3-5-sonnet': 'anthropic/claude-3-5-sonnet',
|
|
168
|
+
'anthropic/claude-3-opus': 'anthropic/claude-3-opus',
|
|
169
|
+
'google/gemini-pro': 'google/gemini-pro',
|
|
170
|
+
'opencode/grok-code': 'opencode/grok-code',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const CODEX_MODELS = {
|
|
174
|
+
...codexModels,
|
|
175
|
+
'gpt-5': 'gpt-5',
|
|
176
|
+
'gpt-5-codex': 'gpt-5-codex',
|
|
177
|
+
'gpt-4': 'gpt-4',
|
|
178
|
+
'gpt-4o': 'gpt-4o',
|
|
179
|
+
'claude-3-5-sonnet': 'claude-3-5-sonnet',
|
|
180
|
+
'claude-3-opus': 'claude-3-opus',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const AGENT_MODELS = {
|
|
184
|
+
...agentModels,
|
|
185
|
+
'opencode/grok-code': 'opencode/grok-code',
|
|
186
|
+
'opencode/big-pickle': 'opencode/big-pickle',
|
|
187
|
+
'opencode/gpt-5-nano': 'opencode/gpt-5-nano',
|
|
188
|
+
'opencode/minimax-m2.5-free': 'opencode/minimax-m2.5-free',
|
|
189
|
+
'opencode/kimi-k2.5-free': 'opencode/kimi-k2.5-free', // Deprecated
|
|
190
|
+
'opencode/glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated
|
|
191
|
+
'opencode/minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated
|
|
192
|
+
'anthropic/claude-3-5-sonnet': 'anthropic/claude-3-5-sonnet',
|
|
193
|
+
'anthropic/claude-3-5-haiku': 'anthropic/claude-3-5-haiku',
|
|
194
|
+
'anthropic/claude-3-opus': 'anthropic/claude-3-opus',
|
|
195
|
+
'google/gemini-3-pro': 'google/gemini-3-pro',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ─── MODEL MAPPING FUNCTIONS ─────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get the model map object for a given tool
|
|
202
|
+
* @param {string} tool - The tool name (claude, agent, opencode, codex)
|
|
203
|
+
* @returns {Object} The model mapping for the tool
|
|
204
|
+
*/
|
|
205
|
+
export const getModelMapForTool = tool => {
|
|
206
|
+
switch (tool) {
|
|
207
|
+
case 'claude':
|
|
208
|
+
return claudeModels;
|
|
209
|
+
case 'agent':
|
|
210
|
+
return agentModels;
|
|
211
|
+
case 'opencode':
|
|
212
|
+
return opencodeModels;
|
|
213
|
+
case 'codex':
|
|
214
|
+
return codexModels;
|
|
215
|
+
default:
|
|
216
|
+
return claudeModels;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the default model for a given tool
|
|
222
|
+
* @param {string} tool - The tool name (claude, agent, opencode, codex)
|
|
223
|
+
* @returns {string} The default model alias for the tool
|
|
224
|
+
*/
|
|
225
|
+
export const getDefaultModelForTool = tool => {
|
|
226
|
+
return defaultModels[tool] || defaultModels.claude;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Map model name to full model ID for a specific tool
|
|
231
|
+
* @param {string} tool - The tool name (claude, agent, opencode, codex)
|
|
232
|
+
* @param {string} model - The model name or alias
|
|
233
|
+
* @returns {string} The full model ID
|
|
234
|
+
*/
|
|
235
|
+
export const mapModelForTool = (tool, model) => {
|
|
236
|
+
switch (tool) {
|
|
237
|
+
case 'claude':
|
|
238
|
+
return claudeModels[model] || model;
|
|
239
|
+
case 'agent':
|
|
240
|
+
return agentModels[model] || model;
|
|
241
|
+
case 'opencode':
|
|
242
|
+
return opencodeModels[model] || model;
|
|
243
|
+
case 'codex':
|
|
244
|
+
return codexModels[model] || model;
|
|
245
|
+
default:
|
|
246
|
+
return model;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validate if a model is compatible with a tool
|
|
252
|
+
* @param {string} tool - The tool name (claude, agent, opencode, codex)
|
|
253
|
+
* @param {string} model - The model name or alias
|
|
254
|
+
* @returns {boolean} True if the model is compatible with the tool
|
|
255
|
+
*/
|
|
256
|
+
export const isModelCompatibleWithTool = (tool, model) => {
|
|
257
|
+
const mappedModel = mapModelForTool(tool, model);
|
|
258
|
+
|
|
259
|
+
switch (tool) {
|
|
260
|
+
case 'claude':
|
|
261
|
+
return mappedModel.startsWith('claude-');
|
|
262
|
+
case 'agent':
|
|
263
|
+
return mappedModel.includes('/') || Object.keys(agentModels).includes(model);
|
|
264
|
+
case 'opencode':
|
|
265
|
+
return mappedModel.includes('/') || Object.keys(opencodeModels).includes(model);
|
|
266
|
+
case 'codex':
|
|
267
|
+
return Object.keys(codexModels).includes(model) || mappedModel.startsWith('gpt-') || mappedModel.startsWith('o3') || mappedModel.startsWith('claude-');
|
|
268
|
+
default:
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get a list of valid model names for a tool
|
|
275
|
+
* @param {string} tool - The tool name
|
|
276
|
+
* @returns {string[]} Array of valid model names
|
|
277
|
+
*/
|
|
278
|
+
export const getValidModelsForTool = tool => {
|
|
279
|
+
switch (tool) {
|
|
280
|
+
case 'claude':
|
|
281
|
+
return Object.keys(claudeModels);
|
|
282
|
+
case 'agent':
|
|
283
|
+
return Object.keys(agentModels);
|
|
284
|
+
case 'opencode':
|
|
285
|
+
return Object.keys(opencodeModels);
|
|
286
|
+
case 'codex':
|
|
287
|
+
return Object.keys(codexModels);
|
|
288
|
+
default:
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Primary (non-alias, non-deprecated) short names shown in CLI help descriptions
|
|
294
|
+
// These are the recommended model names users should see in --model help text
|
|
295
|
+
export const primaryModelNames = {
|
|
296
|
+
claude: ['opus', 'sonnet', 'haiku'],
|
|
297
|
+
opencode: ['grok', 'gpt4o'],
|
|
298
|
+
codex: ['gpt5', 'gpt5-codex', 'o3'],
|
|
299
|
+
agent: ['minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free'],
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build the --model CLI option description string dynamically from centralized model data.
|
|
304
|
+
* @returns {string} Description like "Model to use (for claude: opus, sonnet, ...; for agent: ...)"
|
|
305
|
+
*/
|
|
306
|
+
export const buildModelOptionDescription = () => {
|
|
307
|
+
const parts = Object.entries(primaryModelNames).map(([tool, names]) => `for ${tool}: ${names.join(', ')}`);
|
|
308
|
+
return `Model to use (${parts.join('; ')})`;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get the primary choices for Claude model selection (used in review.mjs and task.mjs).
|
|
313
|
+
* Returns short aliases plus key full model IDs for backward compatibility.
|
|
314
|
+
* @returns {string[]}
|
|
315
|
+
*/
|
|
316
|
+
export const getClaudeModelChoices = () => {
|
|
317
|
+
return Object.keys(claudeModels);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Validate tool-model compatibility and throw descriptive error if invalid
|
|
322
|
+
* @param {string} tool - The tool name
|
|
323
|
+
* @param {string} model - The model name
|
|
324
|
+
* @throws {Error} If the model is not compatible with the tool
|
|
325
|
+
*/
|
|
326
|
+
export const validateToolModelCompatibility = (tool, model) => {
|
|
327
|
+
if (!isModelCompatibleWithTool(tool, model)) {
|
|
328
|
+
const validModels = getValidModelsForTool(tool);
|
|
329
|
+
const mappedModel = mapModelForTool(tool, model);
|
|
330
|
+
|
|
331
|
+
throw new Error(`Model '${model}' (mapped to '${mappedModel}') is not compatible with --tool ${tool}.\n` + `Valid models for ${tool}: ${validModels.join(', ')}\n` + 'Hint: Different tools use different model APIs and naming conventions.');
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// ─── MODEL VALIDATION FUNCTIONS ──────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get the model map for a given tool (validation-extended version with full ID entries)
|
|
339
|
+
* @param {string} tool - The tool name ('claude', 'opencode', 'codex', 'agent')
|
|
340
|
+
* @returns {Object} The model mapping for the tool
|
|
341
|
+
*/
|
|
342
|
+
const getValidationModelMapForTool = tool => {
|
|
343
|
+
switch (tool) {
|
|
344
|
+
case 'opencode':
|
|
345
|
+
return OPENCODE_MODELS;
|
|
346
|
+
case 'codex':
|
|
347
|
+
return CODEX_MODELS;
|
|
348
|
+
case 'agent':
|
|
349
|
+
return AGENT_MODELS;
|
|
350
|
+
case 'claude':
|
|
351
|
+
default:
|
|
352
|
+
return CLAUDE_MODELS;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the list of available model names for a tool (for display in help/error messages)
|
|
358
|
+
* @param {string} tool - The tool name ('claude', 'opencode', 'codex', 'agent')
|
|
359
|
+
* @returns {string[]} Array of available model short names
|
|
360
|
+
*/
|
|
361
|
+
export const getAvailableModelNames = tool => {
|
|
362
|
+
const modelMap = getValidationModelMapForTool(tool);
|
|
363
|
+
// Get unique short names (aliases) - exclude full model IDs that contain '/' or long claude- prefixed IDs
|
|
364
|
+
const aliases = Object.keys(modelMap).filter(key => {
|
|
365
|
+
// Keep short aliases only - exclude:
|
|
366
|
+
// - Full model IDs with slashes (e.g., 'openai/gpt-4')
|
|
367
|
+
// - Long claude-prefixed model IDs (e.g., 'claude-sonnet-4-5-20250929')
|
|
368
|
+
// - Full gpt- prefixed IDs that are ONLY version numbers (e.g., 'gpt-4', 'gpt-4o', 'gpt-5')
|
|
369
|
+
// But keep descriptive aliases like 'gpt-5-nano', 'gpt-5-codex', 'o3', 'o3-mini', 'gpt5', etc.
|
|
370
|
+
// Issue #1185: Updated regex to not filter out gpt-5-nano (a valid short alias)
|
|
371
|
+
if (key.includes('/')) return false;
|
|
372
|
+
if (key.match(/^claude-.*-\d{8}$/)) return false; // Full claude model IDs with date
|
|
373
|
+
if (key.match(/^gpt-\d+[a-z]?$/)) return false; // Full gpt-N or gpt-No model IDs only (e.g., gpt-4, gpt-4o, gpt-5)
|
|
374
|
+
return true;
|
|
375
|
+
});
|
|
376
|
+
return [...new Set(aliases)];
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Calculate Levenshtein distance between two strings (case-insensitive)
|
|
381
|
+
* @param {string} a - First string
|
|
382
|
+
* @param {string} b - Second string
|
|
383
|
+
* @returns {number} The edit distance between the strings
|
|
384
|
+
*/
|
|
385
|
+
export const levenshteinDistance = (a, b) => {
|
|
386
|
+
const aLower = a.toLowerCase();
|
|
387
|
+
const bLower = b.toLowerCase();
|
|
388
|
+
|
|
389
|
+
if (aLower === bLower) return 0;
|
|
390
|
+
if (aLower.length === 0) return bLower.length;
|
|
391
|
+
if (bLower.length === 0) return aLower.length;
|
|
392
|
+
|
|
393
|
+
const matrix = [];
|
|
394
|
+
|
|
395
|
+
for (let i = 0; i <= bLower.length; i++) {
|
|
396
|
+
matrix[i] = [i];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (let j = 0; j <= aLower.length; j++) {
|
|
400
|
+
matrix[0][j] = j;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (let i = 1; i <= bLower.length; i++) {
|
|
404
|
+
for (let j = 1; j <= aLower.length; j++) {
|
|
405
|
+
if (bLower.charAt(i - 1) === aLower.charAt(j - 1)) {
|
|
406
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
407
|
+
} else {
|
|
408
|
+
matrix[i][j] = Math.min(
|
|
409
|
+
matrix[i - 1][j - 1] + 1, // substitution
|
|
410
|
+
matrix[i][j - 1] + 1, // insertion
|
|
411
|
+
matrix[i - 1][j] + 1 // deletion
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return matrix[bLower.length][aLower.length];
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Find the closest matching model names using fuzzy matching
|
|
422
|
+
* @param {string} input - The user-provided model name
|
|
423
|
+
* @param {string[]} validModels - Array of valid model names
|
|
424
|
+
* @param {number} maxSuggestions - Maximum number of suggestions to return
|
|
425
|
+
* @param {number} maxDistance - Maximum Levenshtein distance to consider
|
|
426
|
+
* @returns {string[]} Array of suggested model names
|
|
427
|
+
*/
|
|
428
|
+
export const findSimilarModels = (input, validModels, maxSuggestions = 3, maxDistance = 3) => {
|
|
429
|
+
const suggestions = validModels
|
|
430
|
+
.map(model => ({
|
|
431
|
+
model,
|
|
432
|
+
distance: levenshteinDistance(input, model),
|
|
433
|
+
}))
|
|
434
|
+
.filter(({ distance }) => distance <= maxDistance)
|
|
435
|
+
.sort((a, b) => a.distance - b.distance)
|
|
436
|
+
.slice(0, maxSuggestions)
|
|
437
|
+
.map(({ model }) => model);
|
|
438
|
+
|
|
439
|
+
return suggestions;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Parse model name to extract base model and optional [1m] suffix
|
|
444
|
+
* @param {string} model - The model name (e.g., "opus[1m]", "claude-opus-4-6[1m]")
|
|
445
|
+
* @returns {{ baseModel: string, has1mSuffix: boolean }}
|
|
446
|
+
*/
|
|
447
|
+
export const parseModelWith1mSuffix = model => {
|
|
448
|
+
if (!model || typeof model !== 'string') {
|
|
449
|
+
return { baseModel: model, has1mSuffix: false };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const match = model.match(/^(.+?)\[1m\]$/i);
|
|
453
|
+
if (match) {
|
|
454
|
+
return { baseModel: match[1], has1mSuffix: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { baseModel: model, has1mSuffix: false };
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Check if a model supports the [1m] context window
|
|
462
|
+
* @param {string} model - The base model name (without [1m] suffix)
|
|
463
|
+
* @param {string} tool - The tool name
|
|
464
|
+
* @returns {boolean} True if the model supports 1M context
|
|
465
|
+
*/
|
|
466
|
+
export const supports1mContext = (model, tool = 'claude') => {
|
|
467
|
+
if (tool !== 'claude') {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const normalizedModel = model.toLowerCase();
|
|
472
|
+
|
|
473
|
+
for (const supportedModel of MODELS_SUPPORTING_1M_CONTEXT) {
|
|
474
|
+
if (supportedModel.toLowerCase() === normalizedModel) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const modelMap = getValidationModelMapForTool(tool);
|
|
480
|
+
const matchedKey = Object.keys(modelMap).find(key => key.toLowerCase() === normalizedModel);
|
|
481
|
+
if (matchedKey) {
|
|
482
|
+
const mappedModel = modelMap[matchedKey];
|
|
483
|
+
for (const supportedModel of MODELS_SUPPORTING_1M_CONTEXT) {
|
|
484
|
+
if (supportedModel.toLowerCase() === mappedModel.toLowerCase()) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Validate a model name against the available models for a tool
|
|
495
|
+
* Supports [1m] suffix for 1 million token context (Issue #1221)
|
|
496
|
+
* @param {string} model - The model name to validate (e.g., "opus", "opus[1m]", "claude-opus-4-6[1m]")
|
|
497
|
+
* @param {string} tool - The tool name ('claude', 'opencode', 'codex')
|
|
498
|
+
* @returns {{ valid: boolean, message?: string, suggestions?: string[], mappedModel?: string, has1mSuffix?: boolean }}
|
|
499
|
+
*/
|
|
500
|
+
export const validateModelName = (model, tool = 'claude') => {
|
|
501
|
+
if (!model || typeof model !== 'string') {
|
|
502
|
+
return {
|
|
503
|
+
valid: false,
|
|
504
|
+
message: 'Model name is required',
|
|
505
|
+
suggestions: [],
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const { baseModel, has1mSuffix } = parseModelWith1mSuffix(model);
|
|
510
|
+
|
|
511
|
+
const modelMap = getValidationModelMapForTool(tool);
|
|
512
|
+
const availableNames = Object.keys(modelMap);
|
|
513
|
+
|
|
514
|
+
const normalizedModel = baseModel.toLowerCase();
|
|
515
|
+
const matchedKey = availableNames.find(key => key.toLowerCase() === normalizedModel);
|
|
516
|
+
|
|
517
|
+
if (matchedKey) {
|
|
518
|
+
const mappedModel = modelMap[matchedKey];
|
|
519
|
+
|
|
520
|
+
if (has1mSuffix) {
|
|
521
|
+
if (!supports1mContext(baseModel, tool)) {
|
|
522
|
+
const supportedModels = MODELS_SUPPORTING_1M_CONTEXT.filter(m => !m.includes('-')).join(', ');
|
|
523
|
+
return {
|
|
524
|
+
valid: false,
|
|
525
|
+
message: `Model "${baseModel}" does not support [1m] context window.\n Models supporting 1M context: ${supportedModels}`,
|
|
526
|
+
suggestions: [],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
valid: true,
|
|
531
|
+
mappedModel: `${mappedModel}[1m]`,
|
|
532
|
+
has1mSuffix: true,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
valid: true,
|
|
538
|
+
mappedModel,
|
|
539
|
+
has1mSuffix: false,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Model not found - provide helpful error with suggestions
|
|
544
|
+
const shortNames = getAvailableModelNames(tool);
|
|
545
|
+
const suggestions = findSimilarModels(baseModel, shortNames);
|
|
546
|
+
|
|
547
|
+
let message = `Unrecognized model: "${model}"`;
|
|
548
|
+
|
|
549
|
+
if (suggestions.length > 0) {
|
|
550
|
+
message += `\n Did you mean: ${suggestions.map(s => `"${s}"`).join(', ')}?`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
message += `\n Available models for ${tool}: ${shortNames.join(', ')}`;
|
|
554
|
+
|
|
555
|
+
if (tool === 'claude') {
|
|
556
|
+
message += `\n Tip: Use [1m] suffix for 1M context (e.g., opus[1m], sonnet[1m])`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
valid: false,
|
|
561
|
+
message,
|
|
562
|
+
suggestions,
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Validate model name and exit with error if invalid
|
|
568
|
+
* This is the main entry point for model validation in solve.mjs, hive.mjs, etc.
|
|
569
|
+
* @param {string} model - The model name to validate
|
|
570
|
+
* @param {string} tool - The tool name ('claude', 'opencode', 'codex')
|
|
571
|
+
* @param {Function} exitFn - Function to call for exiting (default: process.exit)
|
|
572
|
+
* @returns {Promise<boolean>} True if valid, exits process if invalid
|
|
573
|
+
*/
|
|
574
|
+
export const validateAndExitOnInvalidModel = async (model, tool = 'claude', exitFn = null) => {
|
|
575
|
+
const result = validateModelName(model, tool);
|
|
576
|
+
|
|
577
|
+
if (!result.valid) {
|
|
578
|
+
await log(`\u274C ${result.message}`, { level: 'error' });
|
|
579
|
+
|
|
580
|
+
if (exitFn) {
|
|
581
|
+
await exitFn(1, 'Invalid model name');
|
|
582
|
+
} else {
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return true;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Format the list of available models for help text
|
|
593
|
+
* @param {string} tool - The tool name
|
|
594
|
+
* @returns {string} Formatted list of available models
|
|
595
|
+
*/
|
|
596
|
+
export const formatAvailableModelsForHelp = (tool = 'claude') => {
|
|
597
|
+
const names = getAvailableModelNames(tool);
|
|
598
|
+
return names.join(', ');
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// ─── MODEL INFO FUNCTIONS ────────────────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Map tool identifier to user-friendly display name.
|
|
605
|
+
* @param {string|null} tool - The tool identifier (claude, codex, opencode, agent)
|
|
606
|
+
* @returns {string} User-friendly display name
|
|
607
|
+
*/
|
|
608
|
+
export const getToolDisplayName = tool => {
|
|
609
|
+
const name = (tool || '').toString().toLowerCase();
|
|
610
|
+
switch (name) {
|
|
611
|
+
case 'claude':
|
|
612
|
+
return 'Anthropic Claude Code';
|
|
613
|
+
case 'codex':
|
|
614
|
+
return 'OpenAI Codex';
|
|
615
|
+
case 'opencode':
|
|
616
|
+
return 'OpenCode';
|
|
617
|
+
case 'agent':
|
|
618
|
+
return 'Agent CLI';
|
|
619
|
+
default:
|
|
620
|
+
return 'AI tool';
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Cached models.dev API response to avoid repeated network requests.
|
|
626
|
+
*/
|
|
627
|
+
let modelsDevCache = null;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Fetch the full models.dev API data with caching.
|
|
631
|
+
* @returns {Promise<Object|null>} The full API response or null on failure
|
|
632
|
+
*/
|
|
633
|
+
const fetchModelsDevApi = async () => {
|
|
634
|
+
if (modelsDevCache) return modelsDevCache;
|
|
635
|
+
try {
|
|
636
|
+
const https = (await globalThis.use('https')).default;
|
|
637
|
+
return new Promise((resolve, reject) => {
|
|
638
|
+
https
|
|
639
|
+
.get('https://models.dev/api.json', res => {
|
|
640
|
+
let data = '';
|
|
641
|
+
res.on('data', chunk => {
|
|
642
|
+
data += chunk;
|
|
643
|
+
});
|
|
644
|
+
res.on('end', () => {
|
|
645
|
+
try {
|
|
646
|
+
modelsDevCache = JSON.parse(data);
|
|
647
|
+
resolve(modelsDevCache);
|
|
648
|
+
} catch (parseError) {
|
|
649
|
+
reject(parseError);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
})
|
|
653
|
+
.on('error', err => {
|
|
654
|
+
reject(err);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Fetch model metadata from models.dev API.
|
|
664
|
+
* @param {string} modelId - The model ID (e.g., "claude-opus-4-6", "opencode/grok-code")
|
|
665
|
+
* @returns {Promise<Object|null>} Model metadata or null if not found
|
|
666
|
+
*/
|
|
667
|
+
export const fetchModelInfoForComment = async modelId => {
|
|
668
|
+
if (!modelId) return null;
|
|
669
|
+
try {
|
|
670
|
+
const apiData = await fetchModelsDevApi();
|
|
671
|
+
if (!apiData) return null;
|
|
672
|
+
|
|
673
|
+
const lookupId = modelId.includes('/') ? modelId.split('/').pop() : modelId;
|
|
674
|
+
|
|
675
|
+
if (apiData.anthropic?.models?.[lookupId]) {
|
|
676
|
+
const modelInfo = { ...apiData.anthropic.models[lookupId] };
|
|
677
|
+
modelInfo.provider = apiData.anthropic.name || 'Anthropic';
|
|
678
|
+
return modelInfo;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const provider of Object.values(apiData)) {
|
|
682
|
+
if (provider.models && provider.models[lookupId]) {
|
|
683
|
+
const modelInfo = { ...provider.models[lookupId] };
|
|
684
|
+
modelInfo.provider = provider.name || provider.id;
|
|
685
|
+
return modelInfo;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (lookupId !== modelId) {
|
|
690
|
+
for (const provider of Object.values(apiData)) {
|
|
691
|
+
if (provider.models && provider.models[modelId]) {
|
|
692
|
+
const modelInfo = { ...provider.models[modelId] };
|
|
693
|
+
modelInfo.provider = provider.name || provider.id;
|
|
694
|
+
return modelInfo;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null;
|
|
700
|
+
} catch {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Normalize model ID for comparison purposes (strip suffixes, lowercase).
|
|
707
|
+
* @param {string} modelId - A model ID or alias
|
|
708
|
+
* @returns {string} Normalized ID
|
|
709
|
+
*/
|
|
710
|
+
const normalizeForComparison = modelId => {
|
|
711
|
+
if (!modelId) return '';
|
|
712
|
+
return modelId
|
|
713
|
+
.toLowerCase()
|
|
714
|
+
.replace(/\[1m\]$/i, '')
|
|
715
|
+
.trim();
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Check if a requested model alias matches an actual model ID.
|
|
720
|
+
* @param {string} requestedModel - The --model flag value (alias or full ID)
|
|
721
|
+
* @param {string} actualModelId - The actual model ID from CLI output
|
|
722
|
+
* @param {string|null} tool - The tool being used
|
|
723
|
+
* @returns {boolean}
|
|
724
|
+
*/
|
|
725
|
+
const doesRequestedMatchActual = (requestedModel, actualModelId, tool) => {
|
|
726
|
+
if (!requestedModel || !actualModelId) return false;
|
|
727
|
+
const resolvedRequested = resolveModelId(requestedModel, tool);
|
|
728
|
+
const normResolved = normalizeForComparison(resolvedRequested);
|
|
729
|
+
const normActual = normalizeForComparison(actualModelId);
|
|
730
|
+
if (normResolved === normActual) return true;
|
|
731
|
+
if (normActual.startsWith(normResolved) || normResolved.startsWith(normActual)) return true;
|
|
732
|
+
return false;
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Build model information string for PR/issue comments.
|
|
737
|
+
*
|
|
738
|
+
* @param {Object} options - Model info options
|
|
739
|
+
* @param {string|null} options.requestedModel - The model requested via --model flag
|
|
740
|
+
* @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
|
|
741
|
+
* @param {Object|null} options.pricingInfo - Pricing info from tool result
|
|
742
|
+
* @param {Object|null} options.modelInfo - Pre-fetched model metadata from models.dev
|
|
743
|
+
* @param {Array<{modelId: string, modelInfo: Object|null}>|null} options.modelsUsed - Actual models used from CLI JSON output
|
|
744
|
+
* @returns {string} Formatted markdown string for model info section
|
|
745
|
+
*/
|
|
746
|
+
export const buildModelInfoString = ({ requestedModel = null, tool = null, pricingInfo = null, modelInfo = null, modelsUsed = null } = {}) => {
|
|
747
|
+
const hasRequested = requestedModel !== null && requestedModel !== undefined;
|
|
748
|
+
const hasModelsUsed = Array.isArray(modelsUsed) && modelsUsed.length > 0;
|
|
749
|
+
const hasModelInfo = modelInfo !== null;
|
|
750
|
+
const hasPricingModel = pricingInfo?.modelId || pricingInfo?.modelName;
|
|
751
|
+
|
|
752
|
+
if (!hasRequested && !hasModelsUsed && !hasModelInfo && !hasPricingModel) return '';
|
|
753
|
+
|
|
754
|
+
let info = '\n\n### \uD83E\uDD16 **Models used:**';
|
|
755
|
+
|
|
756
|
+
if (tool) {
|
|
757
|
+
info += `\n- Tool: ${getToolDisplayName(tool)}`;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (hasRequested) {
|
|
761
|
+
info += `\n- Requested: \`${requestedModel}\``;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (hasModelsUsed) {
|
|
765
|
+
const [mainEntry, ...supportingEntries] = modelsUsed;
|
|
766
|
+
const mainModelId = mainEntry.modelId;
|
|
767
|
+
const mainModelMeta = mainEntry.modelInfo;
|
|
768
|
+
|
|
769
|
+
const mainMatches = hasRequested ? doesRequestedMatchActual(requestedModel, mainModelId, tool) : true;
|
|
770
|
+
|
|
771
|
+
const mainModelName = mainModelMeta?.name || mainModelId;
|
|
772
|
+
const modelLabel = supportingEntries.length > 0 ? 'Main model' : 'Model';
|
|
773
|
+
|
|
774
|
+
if (mainMatches) {
|
|
775
|
+
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
776
|
+
} else {
|
|
777
|
+
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
778
|
+
if (hasRequested) {
|
|
779
|
+
info += `\n- \u26A0\uFE0F **Warning**: Main model \`${mainModelId}\` does not match requested model \`${requestedModel}\``;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (supportingEntries.length > 0) {
|
|
784
|
+
info += '\n- **Additional models:**';
|
|
785
|
+
for (const entry of supportingEntries) {
|
|
786
|
+
const name = entry.modelInfo?.name || entry.modelId;
|
|
787
|
+
info += `\n * **${name}** (\`${entry.modelId}\`)`;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} else if (hasModelInfo) {
|
|
791
|
+
const mainModelName = modelInfo.name || (pricingInfo?.modelId ? pricingInfo.modelId : null) || 'Unknown';
|
|
792
|
+
info += `\n- Model: ${mainModelName}`;
|
|
793
|
+
if (modelInfo.id) info += ` (ID: \`${modelInfo.id}\`)`;
|
|
794
|
+
if (modelInfo.provider) info += `\n- Provider: ${modelInfo.provider}`;
|
|
795
|
+
if (modelInfo.knowledge) info += `\n- Knowledge cutoff: ${modelInfo.knowledge}`;
|
|
796
|
+
} else if (hasPricingModel) {
|
|
797
|
+
const modelId = pricingInfo.modelId || null;
|
|
798
|
+
const modelName = pricingInfo.modelName || modelId || 'Unknown';
|
|
799
|
+
if (modelId && modelId !== modelName) {
|
|
800
|
+
info += `\n- Model: ${modelName} (ID: \`${modelId}\`)`;
|
|
801
|
+
} else {
|
|
802
|
+
info += `\n- Model: ${modelName}`;
|
|
803
|
+
}
|
|
804
|
+
if (pricingInfo.provider) info += `\n- Provider: ${pricingInfo.provider}`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return info;
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Resolve the full model ID from a user-provided alias using the model mapping.
|
|
812
|
+
* @param {string|null} requestedModel - The model alias (e.g., "opus", "sonnet")
|
|
813
|
+
* @param {string|null} tool - The tool being used
|
|
814
|
+
* @returns {string|null} The full model ID or null
|
|
815
|
+
*/
|
|
816
|
+
export const resolveModelId = (requestedModel, tool) => {
|
|
817
|
+
if (!requestedModel) return null;
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const toolName = (tool || 'claude').toString().toLowerCase();
|
|
821
|
+
const cleanModel = requestedModel.replace(/\[1m\]$/i, '');
|
|
822
|
+
return mapModelForTool(toolName, cleanModel);
|
|
823
|
+
} catch {
|
|
824
|
+
return requestedModel;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Fetch model info and build the complete model information string for PR comments.
|
|
830
|
+
* Uses actual models from CLI JSON output when available.
|
|
831
|
+
*
|
|
832
|
+
* @param {Object} options
|
|
833
|
+
* @param {string|null} options.requestedModel - The --model flag value
|
|
834
|
+
* @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
|
|
835
|
+
* @param {Object|null} options.pricingInfo - Pricing info from tool result
|
|
836
|
+
* @param {Array<string>|null} options.actualModelIds - Actual model IDs from CLI JSON output
|
|
837
|
+
* @returns {Promise<string>} Formatted markdown model info section
|
|
838
|
+
*/
|
|
839
|
+
export const getModelInfoForComment = async ({ requestedModel = null, tool = null, pricingInfo = null, actualModelIds = null } = {}) => {
|
|
840
|
+
let modelIds = [];
|
|
841
|
+
|
|
842
|
+
if (Array.isArray(actualModelIds) && actualModelIds.length > 0) {
|
|
843
|
+
modelIds = actualModelIds;
|
|
844
|
+
} else if (pricingInfo?.modelId) {
|
|
845
|
+
modelIds = [pricingInfo.modelId];
|
|
846
|
+
} else if (requestedModel) {
|
|
847
|
+
const resolved = resolveModelId(requestedModel, tool);
|
|
848
|
+
if (resolved) modelIds = [resolved];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const modelsUsed = [];
|
|
852
|
+
for (const modelId of modelIds) {
|
|
853
|
+
let meta = null;
|
|
854
|
+
try {
|
|
855
|
+
meta = await fetchModelInfoForComment(modelId);
|
|
856
|
+
} catch {
|
|
857
|
+
await log(' \u26A0\uFE0F Could not fetch model info from models.dev', { verbose: true });
|
|
858
|
+
}
|
|
859
|
+
modelsUsed.push({ modelId, modelInfo: meta });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const firstModelInfo = modelsUsed.length > 0 ? modelsUsed[0].modelInfo : null;
|
|
863
|
+
|
|
864
|
+
return buildModelInfoString({
|
|
865
|
+
requestedModel,
|
|
866
|
+
tool,
|
|
867
|
+
pricingInfo,
|
|
868
|
+
modelInfo: modelsUsed.length === 0 ? firstModelInfo : null,
|
|
869
|
+
modelsUsed: modelsUsed.length > 0 ? modelsUsed : null,
|
|
870
|
+
});
|
|
871
|
+
};
|