@link-assistant/hive-mind 1.35.9 → 1.35.11

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.
@@ -1,176 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Unified model mapping module
5
- * Provides a single source of truth for model name mapping across all tools
6
- */
7
-
8
- // Claude models (Anthropic API)
9
- // Updated for Opus 4.5/4.6 and Sonnet 4.6 support (Issue #1221, Issue #1238, Issue #1329, Issue #1433)
10
- export const claudeModels = {
11
- sonnet: 'claude-sonnet-4-6', // Sonnet 4.6 (default, Issue #1329)
12
- opus: 'claude-opus-4-6', // Opus 4.6 (Issue #1433)
13
- haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
14
- 'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
15
- 'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
16
- // Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
17
- 'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
18
- 'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias
19
- 'opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5 short alias
20
- 'sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 short alias (backward compatibility)
21
- 'haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5 short alias
22
- // Version aliases for backward compatibility (Issue #1221, Issue #1329)
23
- 'claude-sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
24
- 'claude-opus-4-6': 'claude-opus-4-6', // Opus 4.6
25
- 'claude-opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5
26
- 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 (backward compatibility)
27
- 'claude-haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5
28
- };
29
-
30
- // Agent models (OpenCode API and Kilo Gateway via agent CLI)
31
- // Issue #1300: Updated free models to match agent PR #191
32
- export const agentModels = {
33
- // OpenCode Zen free models (current)
34
- grok: 'opencode/grok-code',
35
- 'grok-code': 'opencode/grok-code',
36
- 'grok-code-fast-1': 'opencode/grok-code',
37
- 'big-pickle': 'opencode/big-pickle',
38
- 'gpt-5-nano': 'opencode/gpt-5-nano',
39
- 'minimax-m2.5-free': 'opencode/minimax-m2.5-free', // New: upgraded from M2.1 (Issue #1391: now default)
40
- // Kilo Gateway free models (Issue #1282, updated in #1300)
41
- // Short names for Kilo-exclusive models (Issue #1300)
42
- 'glm-5-free': 'kilo/glm-5-free', // Kilo-exclusive
43
- 'glm-4.5-air-free': 'kilo/glm-4.5-air-free', // Kilo-exclusive: agent-centric model
44
- 'deepseek-r1-free': 'kilo/deepseek-r1-free', // Kilo-exclusive: reasoning model
45
- 'giga-potato-free': 'kilo/giga-potato-free', // Kilo-exclusive
46
- 'trinity-large-preview': 'kilo/trinity-large-preview', // Kilo-exclusive
47
- // Full names with kilo/ prefix
48
- 'kilo/glm-5-free': 'kilo/glm-5-free',
49
- 'kilo/glm-4.5-air-free': 'kilo/glm-4.5-air-free',
50
- 'kilo/minimax-m2.5-free': 'kilo/minimax-m2.5-free', // Also on OpenCode Zen
51
- 'kilo/deepseek-r1-free': 'kilo/deepseek-r1-free',
52
- 'kilo/giga-potato-free': 'kilo/giga-potato-free',
53
- 'kilo/trinity-large-preview': 'kilo/trinity-large-preview',
54
- // Deprecated free models (kept for backward compatibility)
55
- 'kimi-k2.5-free': 'opencode/kimi-k2.5-free', // Deprecated: not supported (Issue #1391)
56
- 'glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated: no longer free
57
- 'minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated: replaced by m2.5
58
- 'kilo/glm-4.7-free': 'kilo/glm-4.7-free', // Deprecated: replaced by glm-4.5-air-free
59
- 'kilo/kimi-k2.5-free': 'kilo/kimi-k2.5-free', // Deprecated: not recommended
60
- 'kilo/minimax-m2.1-free': 'kilo/minimax-m2.1-free', // Deprecated: replaced by m2.5
61
- // Premium models
62
- sonnet: 'anthropic/claude-3-5-sonnet',
63
- haiku: 'anthropic/claude-3-5-haiku',
64
- opus: 'anthropic/claude-3-opus',
65
- 'gemini-3-pro': 'google/gemini-3-pro',
66
- };
67
-
68
- // OpenCode models (OpenCode API)
69
- export const opencodeModels = {
70
- gpt4: 'openai/gpt-4',
71
- gpt4o: 'openai/gpt-4o',
72
- claude: 'anthropic/claude-3-5-sonnet',
73
- sonnet: 'anthropic/claude-3-5-sonnet',
74
- opus: 'anthropic/claude-3-opus',
75
- gemini: 'google/gemini-pro',
76
- grok: 'opencode/grok-code',
77
- 'grok-code': 'opencode/grok-code',
78
- 'grok-code-fast-1': 'opencode/grok-code',
79
- };
80
-
81
- // Codex models (OpenAI API)
82
- export const codexModels = {
83
- gpt5: 'gpt-5',
84
- 'gpt5-codex': 'gpt-5-codex',
85
- o3: 'o3',
86
- 'o3-mini': 'o3-mini',
87
- gpt4: 'gpt-4',
88
- gpt4o: 'gpt-4o',
89
- claude: 'claude-3-5-sonnet',
90
- sonnet: 'claude-3-5-sonnet',
91
- opus: 'claude-3-opus',
92
- };
93
-
94
- /**
95
- * Map model name to full model ID for a specific tool
96
- * @param {string} tool - The tool name (claude, agent, opencode, codex)
97
- * @param {string} model - The model name or alias
98
- * @returns {string} The full model ID
99
- */
100
- export const mapModelForTool = (tool, model) => {
101
- switch (tool) {
102
- case 'claude':
103
- return claudeModels[model] || model;
104
- case 'agent':
105
- return agentModels[model] || model;
106
- case 'opencode':
107
- return opencodeModels[model] || model;
108
- case 'codex':
109
- return codexModels[model] || model;
110
- default:
111
- return model;
112
- }
113
- };
114
-
115
- /**
116
- * Validate if a model is compatible with a tool
117
- * @param {string} tool - The tool name (claude, agent, opencode, codex)
118
- * @param {string} model - The model name or alias
119
- * @returns {boolean} True if the model is compatible with the tool
120
- */
121
- export const isModelCompatibleWithTool = (tool, model) => {
122
- const mappedModel = mapModelForTool(tool, model);
123
-
124
- switch (tool) {
125
- case 'claude':
126
- // Claude only accepts models in the claude- namespace
127
- return mappedModel.startsWith('claude-');
128
- case 'agent':
129
- // Agent accepts any model with provider prefix (opencode/, anthropic/, etc.)
130
- // or models in the agentModels list
131
- return mappedModel.includes('/') || Object.keys(agentModels).includes(model);
132
- case 'opencode':
133
- // OpenCode accepts models with provider prefix
134
- return mappedModel.includes('/') || Object.keys(opencodeModels).includes(model);
135
- case 'codex':
136
- // Codex accepts OpenAI and some Claude models
137
- return Object.keys(codexModels).includes(model) || mappedModel.startsWith('gpt-') || mappedModel.startsWith('o3') || mappedModel.startsWith('claude-');
138
- default:
139
- return true;
140
- }
141
- };
142
-
143
- /**
144
- * Get a list of valid model names for a tool
145
- * @param {string} tool - The tool name
146
- * @returns {string[]} Array of valid model names
147
- */
148
- export const getValidModelsForTool = tool => {
149
- switch (tool) {
150
- case 'claude':
151
- return Object.keys(claudeModels);
152
- case 'agent':
153
- return Object.keys(agentModels);
154
- case 'opencode':
155
- return Object.keys(opencodeModels);
156
- case 'codex':
157
- return Object.keys(codexModels);
158
- default:
159
- return [];
160
- }
161
- };
162
-
163
- /**
164
- * Validate tool-model compatibility and throw descriptive error if invalid
165
- * @param {string} tool - The tool name
166
- * @param {string} model - The model name
167
- * @throws {Error} If the model is not compatible with the tool
168
- */
169
- export const validateToolModelCompatibility = (tool, model) => {
170
- if (!isModelCompatibleWithTool(tool, model)) {
171
- const validModels = getValidModelsForTool(tool);
172
- const mappedModel = mapModelForTool(tool, model);
173
-
174
- 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.');
175
- }
176
- };
@@ -1,427 +0,0 @@
1
- #!/usr/bin/env node
2
- // Model validation library for hive-mind
3
- // Provides model name validation with exact matching and fuzzy suggestions
4
-
5
- // Check if use is already defined (when imported from solve.mjs)
6
- // If not, fetch it (when running standalone)
7
- if (typeof globalThis.use === 'undefined') {
8
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
9
- }
10
-
11
- import { log } from './lib.mjs';
12
-
13
- // Available models for each tool
14
- // These are the "known good" model names that we accept
15
- export const CLAUDE_MODELS = {
16
- // Short aliases (single word)
17
- sonnet: 'claude-sonnet-4-6', // Sonnet 4.6 (default, Issue #1329)
18
- opus: 'claude-opus-4-6', // Opus 4.6 (Issue #1433)
19
- haiku: 'claude-haiku-4-5-20251001',
20
- 'haiku-3-5': 'claude-3-5-haiku-20241022',
21
- 'haiku-3': 'claude-3-haiku-20240307',
22
- // Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
23
- 'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
24
- 'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias
25
- 'opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5 short alias
26
- 'sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 short alias (backward compatibility)
27
- 'haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5 short alias
28
- // Sonnet version aliases (Issue #1329)
29
- 'claude-sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6
30
- 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 (backward compatibility)
31
- // Opus version aliases (Issue #1221)
32
- 'claude-opus-4-6': 'claude-opus-4-6', // Opus 4.6
33
- 'claude-opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5
34
- // Haiku version aliases
35
- 'claude-haiku-4-5': 'claude-haiku-4-5-20251001',
36
- // Full model IDs (also valid inputs)
37
- 'claude-sonnet-4-5-20250929': 'claude-sonnet-4-5-20250929',
38
- 'claude-opus-4-5-20251101': 'claude-opus-4-5-20251101',
39
- 'claude-haiku-4-5-20251001': 'claude-haiku-4-5-20251001',
40
- 'claude-3-5-haiku-20241022': 'claude-3-5-haiku-20241022',
41
- 'claude-3-haiku-20240307': 'claude-3-haiku-20240307',
42
- };
43
-
44
- // Models that support 1M token context window via [1m] suffix (Issue #1221, Issue #1238, Issue #1329)
45
- // See: https://code.claude.com/docs/en/model-config
46
- export const MODELS_SUPPORTING_1M_CONTEXT = [
47
- 'claude-opus-4-6',
48
- 'claude-opus-4-5-20251101',
49
- 'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
50
- 'claude-sonnet-4-5-20250929',
51
- 'claude-sonnet-4-5',
52
- 'sonnet', // Now maps to Sonnet 4.6 (Issue #1329)
53
- 'sonnet-4-6', // Short alias (Issue #1329)
54
- 'opus',
55
- 'opus-4-6', // Short alias (Issue #1221 - PR comment feedback)
56
- 'opus-4-5', // Short alias (Issue #1238)
57
- 'sonnet-4-5', // Short alias (Issue #1221 - PR comment feedback)
58
- ];
59
-
60
- export const OPENCODE_MODELS = {
61
- gpt4: 'openai/gpt-4',
62
- gpt4o: 'openai/gpt-4o',
63
- claude: 'anthropic/claude-3-5-sonnet',
64
- sonnet: 'anthropic/claude-3-5-sonnet',
65
- opus: 'anthropic/claude-3-opus',
66
- gemini: 'google/gemini-pro',
67
- grok: 'opencode/grok-code',
68
- 'grok-code': 'opencode/grok-code',
69
- 'grok-code-fast-1': 'opencode/grok-code',
70
- // Full model IDs
71
- 'openai/gpt-4': 'openai/gpt-4',
72
- 'openai/gpt-4o': 'openai/gpt-4o',
73
- 'anthropic/claude-3-5-sonnet': 'anthropic/claude-3-5-sonnet',
74
- 'anthropic/claude-3-opus': 'anthropic/claude-3-opus',
75
- 'google/gemini-pro': 'google/gemini-pro',
76
- 'opencode/grok-code': 'opencode/grok-code',
77
- };
78
-
79
- export const CODEX_MODELS = {
80
- gpt5: 'gpt-5',
81
- 'gpt-5': 'gpt-5',
82
- 'gpt5-codex': 'gpt-5-codex',
83
- 'gpt-5-codex': 'gpt-5-codex',
84
- o3: 'o3',
85
- 'o3-mini': 'o3-mini',
86
- gpt4: 'gpt-4',
87
- gpt4o: 'gpt-4o',
88
- claude: 'claude-3-5-sonnet',
89
- sonnet: 'claude-3-5-sonnet',
90
- opus: 'claude-3-opus',
91
- // Full model IDs
92
- 'gpt-4': 'gpt-4',
93
- 'gpt-4o': 'gpt-4o',
94
- 'claude-3-5-sonnet': 'claude-3-5-sonnet',
95
- 'claude-3-opus': 'claude-3-opus',
96
- };
97
-
98
- export const AGENT_MODELS = {
99
- // Free models (via OpenCode Zen)
100
- // Issue #1185: Model IDs must use opencode/ prefix for OpenCode Zen models
101
- // Issue #1300: Updated free models - minimax-m2.5-free replaces m2.1, glm-4.7-free removed
102
- grok: 'opencode/grok-code',
103
- 'grok-code': 'opencode/grok-code',
104
- 'grok-code-fast-1': 'opencode/grok-code',
105
- 'big-pickle': 'opencode/big-pickle',
106
- 'gpt-5-nano': 'opencode/gpt-5-nano',
107
- 'minimax-m2.5-free': 'opencode/minimax-m2.5-free', // Upgraded from M2.1 (Issue #1300); default as of Issue #1391
108
- // Free models (via Kilo Gateway)
109
- // Issue #1282: Kilo provider adds access to 500+ models including free tier
110
- // Issue #1300: Updated Kilo free models with new offerings
111
- // See: https://kilo.ai/docs/advanced-usage/free-and-budget-models
112
- // Short names for Kilo-exclusive models (Issue #1300)
113
- 'glm-5-free': 'kilo/glm-5-free', // Kilo-exclusive: Z.AI flagship model
114
- 'glm-4.5-air-free': 'kilo/glm-4.5-air-free', // Kilo-exclusive: Z.AI agent-centric model
115
- 'deepseek-r1-free': 'kilo/deepseek-r1-free', // Kilo-exclusive: DeepSeek reasoning model
116
- 'giga-potato-free': 'kilo/giga-potato-free', // Kilo-exclusive: Evaluation model
117
- 'trinity-large-preview': 'kilo/trinity-large-preview', // Kilo-exclusive: Arcee AI preview
118
- // Full names with kilo/ prefix
119
- 'kilo/glm-5-free': 'kilo/glm-5-free',
120
- 'kilo/glm-4.5-air-free': 'kilo/glm-4.5-air-free',
121
- 'kilo/minimax-m2.5-free': 'kilo/minimax-m2.5-free', // Also on OpenCode Zen
122
- 'kilo/deepseek-r1-free': 'kilo/deepseek-r1-free',
123
- 'kilo/giga-potato-free': 'kilo/giga-potato-free',
124
- 'kilo/trinity-large-preview': 'kilo/trinity-large-preview',
125
- // Deprecated free models (kept for backward compatibility)
126
- // These models are no longer the recommended options but may still work
127
- 'kimi-k2.5-free': 'opencode/kimi-k2.5-free', // Deprecated: not supported by OpenCode Zen (Issue #1391)
128
- 'glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated: no longer free on OpenCode Zen
129
- 'minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated: replaced by m2.5
130
- 'kilo/glm-4.7-free': 'kilo/glm-4.7-free', // Deprecated: replaced by glm-4.5-air-free
131
- 'kilo/kimi-k2.5-free': 'kilo/kimi-k2.5-free', // Deprecated: not recommended
132
- 'kilo/minimax-m2.1-free': 'kilo/minimax-m2.1-free', // Deprecated: replaced by m2.5
133
- // Premium models (requires OpenCode Zen subscription)
134
- sonnet: 'anthropic/claude-3-5-sonnet',
135
- haiku: 'anthropic/claude-3-5-haiku',
136
- opus: 'anthropic/claude-3-opus',
137
- 'gemini-3-pro': 'google/gemini-3-pro',
138
- // Full model IDs with provider prefix (OpenCode Zen)
139
- 'opencode/grok-code': 'opencode/grok-code',
140
- 'opencode/big-pickle': 'opencode/big-pickle',
141
- 'opencode/gpt-5-nano': 'opencode/gpt-5-nano',
142
- 'opencode/minimax-m2.5-free': 'opencode/minimax-m2.5-free', // New (Issue #1300)
143
- // Deprecated OpenCode Zen models (kept for backward compatibility)
144
- 'opencode/kimi-k2.5-free': 'opencode/kimi-k2.5-free', // Deprecated: not supported (Issue #1391)
145
- 'opencode/glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated
146
- 'opencode/minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated
147
- // Premium models with provider prefix
148
- 'anthropic/claude-3-5-sonnet': 'anthropic/claude-3-5-sonnet',
149
- 'anthropic/claude-3-5-haiku': 'anthropic/claude-3-5-haiku',
150
- 'anthropic/claude-3-opus': 'anthropic/claude-3-opus',
151
- 'google/gemini-3-pro': 'google/gemini-3-pro',
152
- };
153
-
154
- /**
155
- * Get the model map for a given tool
156
- * @param {string} tool - The tool name ('claude', 'opencode', 'codex', 'agent')
157
- * @returns {Object} The model mapping for the tool
158
- */
159
- export const getModelMapForTool = tool => {
160
- switch (tool) {
161
- case 'opencode':
162
- return OPENCODE_MODELS;
163
- case 'codex':
164
- return CODEX_MODELS;
165
- case 'agent':
166
- return AGENT_MODELS;
167
- case 'claude':
168
- default:
169
- return CLAUDE_MODELS;
170
- }
171
- };
172
-
173
- /**
174
- * Get the list of available model names for a tool (for display in help/error messages)
175
- * @param {string} tool - The tool name ('claude', 'opencode', 'codex', 'agent')
176
- * @returns {string[]} Array of available model short names
177
- */
178
- export const getAvailableModelNames = tool => {
179
- const modelMap = getModelMapForTool(tool);
180
- // Get unique short names (aliases) - exclude full model IDs that contain '/' or long claude- prefixed IDs
181
- const aliases = Object.keys(modelMap).filter(key => {
182
- // Keep short aliases only - exclude:
183
- // - Full model IDs with slashes (e.g., 'openai/gpt-4')
184
- // - Long claude-prefixed model IDs (e.g., 'claude-sonnet-4-5-20250929')
185
- // - Full gpt- prefixed IDs that are ONLY version numbers (e.g., 'gpt-4', 'gpt-4o', 'gpt-5')
186
- // But keep descriptive aliases like 'gpt-5-nano', 'gpt-5-codex', 'o3', 'o3-mini', 'gpt5', etc.
187
- // Issue #1185: Updated regex to not filter out gpt-5-nano (a valid short alias)
188
- if (key.includes('/')) return false;
189
- if (key.match(/^claude-.*-\d{8}$/)) return false; // Full claude model IDs with date
190
- 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)
191
- return true;
192
- });
193
- return [...new Set(aliases)];
194
- };
195
-
196
- /**
197
- * Calculate Levenshtein distance between two strings (case-insensitive)
198
- * @param {string} a - First string
199
- * @param {string} b - Second string
200
- * @returns {number} The edit distance between the strings
201
- */
202
- export const levenshteinDistance = (a, b) => {
203
- const aLower = a.toLowerCase();
204
- const bLower = b.toLowerCase();
205
-
206
- if (aLower === bLower) return 0;
207
- if (aLower.length === 0) return bLower.length;
208
- if (bLower.length === 0) return aLower.length;
209
-
210
- const matrix = [];
211
-
212
- // Initialize first column
213
- for (let i = 0; i <= bLower.length; i++) {
214
- matrix[i] = [i];
215
- }
216
-
217
- // Initialize first row
218
- for (let j = 0; j <= aLower.length; j++) {
219
- matrix[0][j] = j;
220
- }
221
-
222
- // Fill in the rest of the matrix
223
- for (let i = 1; i <= bLower.length; i++) {
224
- for (let j = 1; j <= aLower.length; j++) {
225
- if (bLower.charAt(i - 1) === aLower.charAt(j - 1)) {
226
- matrix[i][j] = matrix[i - 1][j - 1];
227
- } else {
228
- matrix[i][j] = Math.min(
229
- matrix[i - 1][j - 1] + 1, // substitution
230
- matrix[i][j - 1] + 1, // insertion
231
- matrix[i - 1][j] + 1 // deletion
232
- );
233
- }
234
- }
235
- }
236
-
237
- return matrix[bLower.length][aLower.length];
238
- };
239
-
240
- /**
241
- * Find the closest matching model names using fuzzy matching
242
- * @param {string} input - The user-provided model name
243
- * @param {string[]} validModels - Array of valid model names
244
- * @param {number} maxSuggestions - Maximum number of suggestions to return
245
- * @param {number} maxDistance - Maximum Levenshtein distance to consider
246
- * @returns {string[]} Array of suggested model names
247
- */
248
- export const findSimilarModels = (input, validModels, maxSuggestions = 3, maxDistance = 3) => {
249
- const suggestions = validModels
250
- .map(model => ({
251
- model,
252
- distance: levenshteinDistance(input, model),
253
- }))
254
- .filter(({ distance }) => distance <= maxDistance)
255
- .sort((a, b) => a.distance - b.distance)
256
- .slice(0, maxSuggestions)
257
- .map(({ model }) => model);
258
-
259
- return suggestions;
260
- };
261
-
262
- /**
263
- * Parse model name to extract base model and optional [1m] suffix
264
- * @param {string} model - The model name (e.g., "opus[1m]", "claude-opus-4-6[1m]")
265
- * @returns {{ baseModel: string, has1mSuffix: boolean }}
266
- */
267
- export const parseModelWith1mSuffix = model => {
268
- if (!model || typeof model !== 'string') {
269
- return { baseModel: model, has1mSuffix: false };
270
- }
271
-
272
- // Check for [1m] suffix (case-insensitive)
273
- const match = model.match(/^(.+?)\[1m\]$/i);
274
- if (match) {
275
- return { baseModel: match[1], has1mSuffix: true };
276
- }
277
-
278
- return { baseModel: model, has1mSuffix: false };
279
- };
280
-
281
- /**
282
- * Check if a model supports the [1m] context window
283
- * @param {string} model - The base model name (without [1m] suffix)
284
- * @param {string} tool - The tool name
285
- * @returns {boolean} True if the model supports 1M context
286
- */
287
- export const supports1mContext = (model, tool = 'claude') => {
288
- if (tool !== 'claude') {
289
- return false;
290
- }
291
-
292
- const normalizedModel = model.toLowerCase();
293
-
294
- // Check if the model or its mapped version supports 1M context
295
- for (const supportedModel of MODELS_SUPPORTING_1M_CONTEXT) {
296
- if (supportedModel.toLowerCase() === normalizedModel) {
297
- return true;
298
- }
299
- }
300
-
301
- // Also check if the mapped model supports 1M context
302
- const modelMap = getModelMapForTool(tool);
303
- const matchedKey = Object.keys(modelMap).find(key => key.toLowerCase() === normalizedModel);
304
- if (matchedKey) {
305
- const mappedModel = modelMap[matchedKey];
306
- for (const supportedModel of MODELS_SUPPORTING_1M_CONTEXT) {
307
- if (supportedModel.toLowerCase() === mappedModel.toLowerCase()) {
308
- return true;
309
- }
310
- }
311
- }
312
-
313
- return false;
314
- };
315
-
316
- /**
317
- * Validate a model name against the available models for a tool
318
- * Supports [1m] suffix for 1 million token context (Issue #1221)
319
- * @param {string} model - The model name to validate (e.g., "opus", "opus[1m]", "claude-opus-4-6[1m]")
320
- * @param {string} tool - The tool name ('claude', 'opencode', 'codex')
321
- * @returns {{ valid: boolean, message?: string, suggestions?: string[], mappedModel?: string, has1mSuffix?: boolean }}
322
- */
323
- export const validateModelName = (model, tool = 'claude') => {
324
- if (!model || typeof model !== 'string') {
325
- return {
326
- valid: false,
327
- message: 'Model name is required',
328
- suggestions: [],
329
- };
330
- }
331
-
332
- // Parse [1m] suffix (Issue #1221)
333
- const { baseModel, has1mSuffix } = parseModelWith1mSuffix(model);
334
-
335
- const modelMap = getModelMapForTool(tool);
336
- const availableNames = Object.keys(modelMap);
337
-
338
- // Case-insensitive exact match
339
- const normalizedModel = baseModel.toLowerCase();
340
- const matchedKey = availableNames.find(key => key.toLowerCase() === normalizedModel);
341
-
342
- if (matchedKey) {
343
- const mappedModel = modelMap[matchedKey];
344
-
345
- // If [1m] suffix is present, validate it's supported
346
- if (has1mSuffix) {
347
- if (!supports1mContext(baseModel, tool)) {
348
- const supportedModels = MODELS_SUPPORTING_1M_CONTEXT.filter(m => !m.includes('-')).join(', ');
349
- return {
350
- valid: false,
351
- message: `Model "${baseModel}" does not support [1m] context window.\n Models supporting 1M context: ${supportedModels}`,
352
- suggestions: [],
353
- };
354
- }
355
- // Return the mapped model with [1m] suffix appended
356
- return {
357
- valid: true,
358
- mappedModel: `${mappedModel}[1m]`,
359
- has1mSuffix: true,
360
- };
361
- }
362
-
363
- return {
364
- valid: true,
365
- mappedModel,
366
- has1mSuffix: false,
367
- };
368
- }
369
-
370
- // Model not found - provide helpful error with suggestions
371
- const shortNames = getAvailableModelNames(tool);
372
- const suggestions = findSimilarModels(baseModel, shortNames);
373
-
374
- let message = `Unrecognized model: "${model}"`;
375
-
376
- if (suggestions.length > 0) {
377
- message += `\n Did you mean: ${suggestions.map(s => `"${s}"`).join(', ')}?`;
378
- }
379
-
380
- message += `\n Available models for ${tool}: ${shortNames.join(', ')}`;
381
-
382
- // Add hint about [1m] suffix if available
383
- if (tool === 'claude') {
384
- message += `\n Tip: Use [1m] suffix for 1M context (e.g., opus[1m], sonnet[1m])`;
385
- }
386
-
387
- return {
388
- valid: false,
389
- message,
390
- suggestions,
391
- };
392
- };
393
-
394
- /**
395
- * Validate model name and exit with error if invalid
396
- * This is the main entry point for model validation in solve.mjs, hive.mjs, etc.
397
- * @param {string} model - The model name to validate
398
- * @param {string} tool - The tool name ('claude', 'opencode', 'codex')
399
- * @param {Function} exitFn - Function to call for exiting (default: process.exit)
400
- * @returns {Promise<boolean>} True if valid, exits process if invalid
401
- */
402
- export const validateAndExitOnInvalidModel = async (model, tool = 'claude', exitFn = null) => {
403
- const result = validateModelName(model, tool);
404
-
405
- if (!result.valid) {
406
- await log(`❌ ${result.message}`, { level: 'error' });
407
-
408
- if (exitFn) {
409
- await exitFn(1, 'Invalid model name');
410
- } else {
411
- process.exit(1);
412
- }
413
- return false;
414
- }
415
-
416
- return true;
417
- };
418
-
419
- /**
420
- * Format the list of available models for help text
421
- * @param {string} tool - The tool name
422
- * @returns {string} Formatted list of available models
423
- */
424
- export const formatAvailableModelsForHelp = (tool = 'claude') => {
425
- const names = getAvailableModelNames(tool);
426
- return names.join(', ');
427
- };