@link-assistant/hive-mind 1.35.8 → 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 +22 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent.lib.mjs +8 -48
- package/src/claude.lib.mjs +3 -3
- package/src/claude.prompts.lib.mjs +2 -1
- package/src/codex.lib.mjs +1 -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
package/src/opencode.lib.mjs
CHANGED
|
@@ -18,27 +18,16 @@ import { reportError } from './sentry.lib.mjs';
|
|
|
18
18
|
import { timeouts } from './config.lib.mjs';
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
20
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
21
|
+
import { opencodeModels, defaultModels } from './models/index.mjs';
|
|
21
22
|
|
|
22
23
|
// Model mapping to translate aliases to full model IDs for OpenCode
|
|
24
|
+
// Issue #1473: Uses centralized opencodeModels from models/index.mjs (single source of truth)
|
|
23
25
|
export const mapModelToId = model => {
|
|
24
|
-
|
|
25
|
-
gpt4: 'openai/gpt-4',
|
|
26
|
-
gpt4o: 'openai/gpt-4o',
|
|
27
|
-
claude: 'anthropic/claude-3-5-sonnet',
|
|
28
|
-
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
29
|
-
opus: 'anthropic/claude-3-opus',
|
|
30
|
-
gemini: 'google/gemini-pro',
|
|
31
|
-
grok: 'opencode/grok-code',
|
|
32
|
-
'grok-code': 'opencode/grok-code',
|
|
33
|
-
'grok-code-fast-1': 'opencode/grok-code',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Return mapped model ID if it's an alias, otherwise return as-is
|
|
37
|
-
return modelMap[model] || model;
|
|
26
|
+
return opencodeModels[model] || model;
|
|
38
27
|
};
|
|
39
28
|
|
|
40
29
|
// Function to validate OpenCode connection
|
|
41
|
-
export const validateOpenCodeConnection = async (model =
|
|
30
|
+
export const validateOpenCodeConnection = async (model = defaultModels.opencode) => {
|
|
42
31
|
// Map model alias to full ID
|
|
43
32
|
const mappedModel = mapModelToId(model);
|
|
44
33
|
|
package/src/review.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import * as memoryCheck from './memory-check.mjs';
|
|
|
49
49
|
|
|
50
50
|
// Import Claude execution functions
|
|
51
51
|
import { executeClaudeCommand } from './claude.lib.mjs';
|
|
52
|
+
import { defaultModels, getClaudeModelChoices, buildModelOptionDescription } from './models/index.mjs';
|
|
52
53
|
|
|
53
54
|
// Configure command line arguments - GitHub PR URL as positional argument
|
|
54
55
|
// Use yargs().parse(args) instead of yargs(args).argv to ensure .strict() mode works
|
|
@@ -70,10 +71,10 @@ const argv = yargs()
|
|
|
70
71
|
})
|
|
71
72
|
.option('model', {
|
|
72
73
|
type: 'string',
|
|
73
|
-
description:
|
|
74
|
+
description: buildModelOptionDescription(),
|
|
74
75
|
alias: 'm',
|
|
75
|
-
default:
|
|
76
|
-
choices:
|
|
76
|
+
default: defaultModels.claude,
|
|
77
|
+
choices: getClaudeModelChoices(),
|
|
77
78
|
})
|
|
78
79
|
.option('focus', {
|
|
79
80
|
type: 'string',
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// This approach was adopted per issue #482 feedback to minimize custom code maintenance
|
|
9
9
|
|
|
10
10
|
import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.lib.mjs';
|
|
11
|
+
import { defaultModels, buildModelOptionDescription } from './models/index.mjs';
|
|
11
12
|
|
|
12
13
|
// Re-export for use by telegram-bot.mjs (avoids extra import lines there)
|
|
13
14
|
export { detectMalformedFlags };
|
|
@@ -428,18 +429,11 @@ export const createYargsConfig = yargsInstance => {
|
|
|
428
429
|
config = config
|
|
429
430
|
.option('model', {
|
|
430
431
|
type: 'string',
|
|
431
|
-
description:
|
|
432
|
+
description: buildModelOptionDescription(),
|
|
432
433
|
alias: 'm',
|
|
433
434
|
default: currentParsedArgs => {
|
|
434
|
-
// Dynamic default based on tool selection
|
|
435
|
-
|
|
436
|
-
return 'grok-code-fast-1';
|
|
437
|
-
} else if (currentParsedArgs?.tool === 'codex') {
|
|
438
|
-
return 'gpt-5';
|
|
439
|
-
} else if (currentParsedArgs?.tool === 'agent') {
|
|
440
|
-
return 'minimax-m2.5-free';
|
|
441
|
-
}
|
|
442
|
-
return 'sonnet';
|
|
435
|
+
// Dynamic default based on tool selection (Issue #1473: centralized in models/index.mjs)
|
|
436
|
+
return defaultModels[currentParsedArgs?.tool] || defaultModels.claude;
|
|
443
437
|
},
|
|
444
438
|
})
|
|
445
439
|
.parserConfiguration({
|
|
@@ -569,15 +563,10 @@ export const parseArguments = async (yargs, hideBin) => {
|
|
|
569
563
|
}
|
|
570
564
|
}
|
|
571
565
|
|
|
572
|
-
if (argv.tool
|
|
573
|
-
// User did not explicitly provide --model, so use the correct default for
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// User did not explicitly provide --model, so use the correct default for codex
|
|
577
|
-
argv.model = 'gpt-5';
|
|
578
|
-
} else if (argv.tool === 'agent' && !modelExplicitlyProvided) {
|
|
579
|
-
// User did not explicitly provide --model, so use the correct default for agent
|
|
580
|
-
argv.model = 'minimax-m2.5-free';
|
|
566
|
+
if (argv.tool && !modelExplicitlyProvided && defaultModels[argv.tool]) {
|
|
567
|
+
// User did not explicitly provide --model, so use the correct default for the tool
|
|
568
|
+
// (Issue #1473: centralized in models/index.mjs)
|
|
569
|
+
argv.model = defaultModels[argv.tool];
|
|
581
570
|
}
|
|
582
571
|
|
|
583
572
|
// Validate mutual exclusivity of --claude-file and --gitkeep-file
|
package/src/solve.mjs
CHANGED
|
@@ -84,7 +84,7 @@ const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('
|
|
|
84
84
|
const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
|
|
85
85
|
const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
|
|
86
86
|
const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
|
|
87
|
-
const { validateAndExitOnInvalidModel } = await import('./
|
|
87
|
+
const { validateAndExitOnInvalidModel } = await import('./models/index.mjs');
|
|
88
88
|
const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
|
|
89
89
|
|
|
90
90
|
// Initialize log file early (before argument parsing) to capture all output
|
package/src/task.mjs
CHANGED
|
@@ -41,6 +41,7 @@ const { spawn } = (await use('child_process')).default;
|
|
|
41
41
|
|
|
42
42
|
// Import Claude execution functions
|
|
43
43
|
import { mapModelToId } from './claude.lib.mjs';
|
|
44
|
+
import { claudeModels, defaultModels, buildModelOptionDescription } from './models/index.mjs';
|
|
44
45
|
|
|
45
46
|
// Global log file reference
|
|
46
47
|
let logFile = null;
|
|
@@ -106,10 +107,10 @@ const argv = yargs()
|
|
|
106
107
|
})
|
|
107
108
|
.option('model', {
|
|
108
109
|
type: 'string',
|
|
109
|
-
description:
|
|
110
|
+
description: buildModelOptionDescription(),
|
|
110
111
|
alias: 'm',
|
|
111
|
-
default:
|
|
112
|
-
choices:
|
|
112
|
+
default: defaultModels.claude,
|
|
113
|
+
choices: Object.keys(claudeModels),
|
|
113
114
|
})
|
|
114
115
|
.option('verbose', {
|
|
115
116
|
type: 'boolean',
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -47,7 +47,7 @@ const hideBin = _helpersBot.hideBin || (argv => argv.slice(2));
|
|
|
47
47
|
const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
|
|
48
48
|
const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
|
|
49
49
|
const { parseGitHubUrl } = await import('./github.lib.mjs');
|
|
50
|
-
const { validateModelName } = await import('./
|
|
50
|
+
const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
|
|
51
51
|
const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
|
|
52
52
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
53
53
|
const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
|
|
@@ -659,7 +659,7 @@ bot.command('help', async ctx => {
|
|
|
659
659
|
message += '*/help* - Show this help message\n\n';
|
|
660
660
|
message += '⚠️ *Note:* /solve, /hive, /solve\\_queue, /limits, /version, /accept\\_invites and /merge commands only work in group chats.\n\n';
|
|
661
661
|
message += '🔧 *Common Options:*\n';
|
|
662
|
-
message +=
|
|
662
|
+
message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
|
|
663
663
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
664
664
|
message += '• `--think <level>` - Thinking level (off/low/medium/high/max) | `--thinking-budget <num>` - Token budget (0-63999)\n';
|
|
665
665
|
message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
|
package/src/model-info.lib.mjs
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Model information library for hive-mind
|
|
5
|
-
* Provides unified model display, verification, and metadata fetching
|
|
6
|
-
* for all tools (Claude, Agent, OpenCode, Codex).
|
|
7
|
-
*
|
|
8
|
-
* @see https://github.com/link-assistant/hive-mind/issues/1225
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// Check if use is already defined (when imported from solve.mjs)
|
|
12
|
-
// If not, fetch it (when running standalone)
|
|
13
|
-
if (typeof globalThis.use === 'undefined') {
|
|
14
|
-
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
import { log } from './lib.mjs';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Map tool identifier to user-friendly display name.
|
|
21
|
-
* Replaces duplicated ternary chains across the codebase.
|
|
22
|
-
* @param {string|null} tool - The tool identifier (claude, codex, opencode, agent)
|
|
23
|
-
* @returns {string} User-friendly display name
|
|
24
|
-
*/
|
|
25
|
-
export const getToolDisplayName = tool => {
|
|
26
|
-
const name = (tool || '').toString().toLowerCase();
|
|
27
|
-
switch (name) {
|
|
28
|
-
case 'claude':
|
|
29
|
-
return 'Claude';
|
|
30
|
-
case 'codex':
|
|
31
|
-
return 'Codex';
|
|
32
|
-
case 'opencode':
|
|
33
|
-
return 'OpenCode';
|
|
34
|
-
case 'agent':
|
|
35
|
-
return 'Agent';
|
|
36
|
-
default:
|
|
37
|
-
return 'AI tool';
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Cached models.dev API response to avoid repeated network requests.
|
|
43
|
-
* The cache is per-process and cleared when the process exits.
|
|
44
|
-
*/
|
|
45
|
-
let modelsDevCache = null;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Fetch the full models.dev API data with caching.
|
|
49
|
-
* @returns {Promise<Object|null>} The full API response or null on failure
|
|
50
|
-
*/
|
|
51
|
-
const fetchModelsDevApi = async () => {
|
|
52
|
-
if (modelsDevCache) return modelsDevCache;
|
|
53
|
-
try {
|
|
54
|
-
const https = (await globalThis.use('https')).default;
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
https
|
|
57
|
-
.get('https://models.dev/api.json', res => {
|
|
58
|
-
let data = '';
|
|
59
|
-
res.on('data', chunk => {
|
|
60
|
-
data += chunk;
|
|
61
|
-
});
|
|
62
|
-
res.on('end', () => {
|
|
63
|
-
try {
|
|
64
|
-
modelsDevCache = JSON.parse(data);
|
|
65
|
-
resolve(modelsDevCache);
|
|
66
|
-
} catch (parseError) {
|
|
67
|
-
reject(parseError);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
})
|
|
71
|
-
.on('error', err => {
|
|
72
|
-
reject(err);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Fetch model metadata from models.dev API.
|
|
82
|
-
* Returns enriched model information including name, provider, version, and knowledge cutoff.
|
|
83
|
-
* @param {string} modelId - The model ID (e.g., "claude-opus-4-6", "opencode/grok-code")
|
|
84
|
-
* @returns {Promise<Object|null>} Model metadata or null if not found
|
|
85
|
-
*/
|
|
86
|
-
export const fetchModelInfoForComment = async modelId => {
|
|
87
|
-
if (!modelId) return null;
|
|
88
|
-
try {
|
|
89
|
-
const apiData = await fetchModelsDevApi();
|
|
90
|
-
if (!apiData) return null;
|
|
91
|
-
|
|
92
|
-
// Normalize model ID: strip provider prefix for lookup (e.g., "anthropic/claude-3-5-sonnet" -> "claude-3-5-sonnet")
|
|
93
|
-
const lookupId = modelId.includes('/') ? modelId.split('/').pop() : modelId;
|
|
94
|
-
|
|
95
|
-
// Check Anthropic provider first (most common for Claude tools)
|
|
96
|
-
if (apiData.anthropic?.models?.[lookupId]) {
|
|
97
|
-
const modelInfo = { ...apiData.anthropic.models[lookupId] };
|
|
98
|
-
modelInfo.provider = apiData.anthropic.name || 'Anthropic';
|
|
99
|
-
return modelInfo;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Search across all providers
|
|
103
|
-
for (const provider of Object.values(apiData)) {
|
|
104
|
-
if (provider.models && provider.models[lookupId]) {
|
|
105
|
-
const modelInfo = { ...provider.models[lookupId] };
|
|
106
|
-
modelInfo.provider = provider.name || provider.id;
|
|
107
|
-
return modelInfo;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Try the full modelId (with provider prefix) as well
|
|
112
|
-
if (lookupId !== modelId) {
|
|
113
|
-
for (const provider of Object.values(apiData)) {
|
|
114
|
-
if (provider.models && provider.models[modelId]) {
|
|
115
|
-
const modelInfo = { ...provider.models[modelId] };
|
|
116
|
-
modelInfo.provider = provider.name || provider.id;
|
|
117
|
-
return modelInfo;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return null;
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Normalize model ID for comparison purposes (strip suffixes, lowercase).
|
|
130
|
-
* @param {string} modelId - A model ID or alias
|
|
131
|
-
* @returns {string} Normalized ID
|
|
132
|
-
*/
|
|
133
|
-
const normalizeForComparison = modelId => {
|
|
134
|
-
if (!modelId) return '';
|
|
135
|
-
return modelId
|
|
136
|
-
.toLowerCase()
|
|
137
|
-
.replace(/\[1m\]$/i, '')
|
|
138
|
-
.trim();
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check if a requested model alias matches an actual model ID.
|
|
143
|
-
* @param {string} requestedModel - The --model flag value (alias or full ID)
|
|
144
|
-
* @param {string} actualModelId - The actual model ID from CLI output
|
|
145
|
-
* @param {string|null} tool - The tool being used
|
|
146
|
-
* @returns {boolean}
|
|
147
|
-
*/
|
|
148
|
-
const doesRequestedMatchActual = (requestedModel, actualModelId, tool) => {
|
|
149
|
-
if (!requestedModel || !actualModelId) return false;
|
|
150
|
-
const resolvedRequested = resolveModelId(requestedModel, tool);
|
|
151
|
-
const normResolved = normalizeForComparison(resolvedRequested);
|
|
152
|
-
const normActual = normalizeForComparison(actualModelId);
|
|
153
|
-
// Direct match
|
|
154
|
-
if (normResolved === normActual) return true;
|
|
155
|
-
// Partial match: resolved starts with actual or vice versa (for date-suffixed IDs)
|
|
156
|
-
if (normActual.startsWith(normResolved) || normResolved.startsWith(normActual)) return true;
|
|
157
|
-
return false;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Build model information string for PR/issue comments.
|
|
162
|
-
* Displays the requested model vs actual models used from CLI JSON output.
|
|
163
|
-
* The main model is bolded if it matches the requested model.
|
|
164
|
-
* A warning is shown if the main model doesn't match the requested model.
|
|
165
|
-
*
|
|
166
|
-
* @param {Object} options - Model info options
|
|
167
|
-
* @param {string|null} options.requestedModel - The model requested via --model flag (e.g., "opus")
|
|
168
|
-
* @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
|
|
169
|
-
* @param {Object|null} options.pricingInfo - Pricing info from tool result (agent tool provides modelId)
|
|
170
|
-
* @param {Object|null} options.modelInfo - Pre-fetched model metadata from models.dev (for first actual model)
|
|
171
|
-
* @param {Array<{modelId: string, modelInfo: Object|null}>|null} options.modelsUsed - Actual models used from CLI JSON output
|
|
172
|
-
* @returns {string} Formatted markdown string for model info section (empty if no data available)
|
|
173
|
-
*/
|
|
174
|
-
export const buildModelInfoString = ({ requestedModel = null, tool = null, pricingInfo = null, modelInfo = null, modelsUsed = null } = {}) => {
|
|
175
|
-
const hasRequested = requestedModel !== null && requestedModel !== undefined;
|
|
176
|
-
const hasModelsUsed = Array.isArray(modelsUsed) && modelsUsed.length > 0;
|
|
177
|
-
const hasModelInfo = modelInfo !== null;
|
|
178
|
-
const hasPricingModel = pricingInfo?.modelId || pricingInfo?.modelName;
|
|
179
|
-
|
|
180
|
-
if (!hasRequested && !hasModelsUsed && !hasModelInfo && !hasPricingModel) return '';
|
|
181
|
-
|
|
182
|
-
let info = '\n\n### 🤖 **Models used:**';
|
|
183
|
-
|
|
184
|
-
// Display tool name
|
|
185
|
-
if (tool) {
|
|
186
|
-
info += `\n- Tool: ${getToolDisplayName(tool)}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Display requested model (--model flag value)
|
|
190
|
-
if (hasRequested) {
|
|
191
|
-
info += `\n- Requested: \`${requestedModel}\``;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (hasModelsUsed) {
|
|
195
|
-
// The first model is considered the "main" model
|
|
196
|
-
const [mainEntry, ...supportingEntries] = modelsUsed;
|
|
197
|
-
const mainModelId = mainEntry.modelId;
|
|
198
|
-
const mainModelMeta = mainEntry.modelInfo;
|
|
199
|
-
|
|
200
|
-
const mainMatches = hasRequested ? doesRequestedMatchActual(requestedModel, mainModelId, tool) : true;
|
|
201
|
-
|
|
202
|
-
// Build main model line
|
|
203
|
-
const mainModelName = mainModelMeta?.name || mainModelId;
|
|
204
|
-
|
|
205
|
-
// Use "Model" label when only one model, "Main model" when multiple
|
|
206
|
-
const modelLabel = supportingEntries.length > 0 ? 'Main model' : 'Model';
|
|
207
|
-
|
|
208
|
-
if (mainMatches) {
|
|
209
|
-
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
210
|
-
} else {
|
|
211
|
-
// Main model doesn't match requested - show warning
|
|
212
|
-
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
213
|
-
if (hasRequested) {
|
|
214
|
-
info += `\n- ⚠️ **Warning**: Main model \`${mainModelId}\` does not match requested model \`${requestedModel}\``;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Display additional models
|
|
219
|
-
if (supportingEntries.length > 0) {
|
|
220
|
-
info += '\n- **Additional models:**';
|
|
221
|
-
for (const entry of supportingEntries) {
|
|
222
|
-
const name = entry.modelInfo?.name || entry.modelId;
|
|
223
|
-
info += `\n * **${name}** (\`${entry.modelId}\`)`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
} else if (hasModelInfo) {
|
|
227
|
-
// Fallback: single model info from models.dev (no actual CLI output data)
|
|
228
|
-
const mainModelName = modelInfo.name || (pricingInfo?.modelId ? pricingInfo.modelId : null) || 'Unknown';
|
|
229
|
-
info += `\n- Model: ${mainModelName}`;
|
|
230
|
-
if (modelInfo.id) info += ` (ID: \`${modelInfo.id}\`)`;
|
|
231
|
-
if (modelInfo.provider) info += `\n- Provider: ${modelInfo.provider}`;
|
|
232
|
-
if (modelInfo.knowledge) info += `\n- Knowledge cutoff: ${modelInfo.knowledge}`;
|
|
233
|
-
} else if (hasPricingModel) {
|
|
234
|
-
// Fallback to pricingInfo when no models.dev data
|
|
235
|
-
const modelId = pricingInfo.modelId || null;
|
|
236
|
-
const modelName = pricingInfo.modelName || modelId || 'Unknown';
|
|
237
|
-
if (modelId && modelId !== modelName) {
|
|
238
|
-
info += `\n- Model: ${modelName} (ID: \`${modelId}\`)`;
|
|
239
|
-
} else {
|
|
240
|
-
info += `\n- Model: ${modelName}`;
|
|
241
|
-
}
|
|
242
|
-
if (pricingInfo.provider) info += `\n- Provider: ${pricingInfo.provider}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return info;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Resolve the full model ID from a user-provided alias using the model mapping.
|
|
250
|
-
* @param {string|null} requestedModel - The model alias (e.g., "opus", "sonnet")
|
|
251
|
-
* @param {string|null} tool - The tool being used
|
|
252
|
-
* @returns {string|null} The full model ID or null
|
|
253
|
-
*/
|
|
254
|
-
export const resolveModelId = (requestedModel, tool) => {
|
|
255
|
-
if (!requestedModel) return null;
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
// Use model-mapping.lib.mjs mappings as authoritative source
|
|
259
|
-
const modelMaps = {
|
|
260
|
-
claude: {
|
|
261
|
-
sonnet: 'claude-sonnet-4-6',
|
|
262
|
-
opus: 'claude-opus-4-6',
|
|
263
|
-
haiku: 'claude-haiku-4-5-20251001',
|
|
264
|
-
'opus-4-6': 'claude-opus-4-6',
|
|
265
|
-
'opus-4-5': 'claude-opus-4-5-20251101',
|
|
266
|
-
'sonnet-4-6': 'claude-sonnet-4-6',
|
|
267
|
-
'sonnet-4-5': 'claude-sonnet-4-5-20250929',
|
|
268
|
-
'haiku-4-5': 'claude-haiku-4-5-20251001',
|
|
269
|
-
},
|
|
270
|
-
agent: {
|
|
271
|
-
grok: 'opencode/grok-code',
|
|
272
|
-
'grok-code': 'opencode/grok-code',
|
|
273
|
-
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
274
|
-
opus: 'anthropic/claude-3-opus',
|
|
275
|
-
haiku: 'anthropic/claude-3-5-haiku',
|
|
276
|
-
},
|
|
277
|
-
opencode: {
|
|
278
|
-
gpt4: 'openai/gpt-4',
|
|
279
|
-
gpt4o: 'openai/gpt-4o',
|
|
280
|
-
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
281
|
-
opus: 'anthropic/claude-3-opus',
|
|
282
|
-
grok: 'opencode/grok-code',
|
|
283
|
-
},
|
|
284
|
-
codex: {
|
|
285
|
-
gpt5: 'gpt-5',
|
|
286
|
-
'gpt-5': 'gpt-5',
|
|
287
|
-
o3: 'o3',
|
|
288
|
-
gpt4: 'gpt-4',
|
|
289
|
-
gpt4o: 'gpt-4o',
|
|
290
|
-
sonnet: 'claude-3-5-sonnet',
|
|
291
|
-
opus: 'claude-3-opus',
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const toolName = (tool || 'claude').toString().toLowerCase();
|
|
296
|
-
const map = modelMaps[toolName];
|
|
297
|
-
if (map) {
|
|
298
|
-
// Strip [1m] suffix if present (1M context window flag)
|
|
299
|
-
const cleanModel = requestedModel.replace(/\[1m\]$/i, '');
|
|
300
|
-
return map[cleanModel.toLowerCase()] || cleanModel;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return requestedModel;
|
|
304
|
-
} catch {
|
|
305
|
-
return requestedModel;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Fetch model info and build the complete model information string for PR comments.
|
|
311
|
-
* Uses actual models from CLI JSON output when available.
|
|
312
|
-
*
|
|
313
|
-
* @param {Object} options
|
|
314
|
-
* @param {string|null} options.requestedModel - The --model flag value
|
|
315
|
-
* @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
|
|
316
|
-
* @param {Object|null} options.pricingInfo - Pricing info from tool result
|
|
317
|
-
* @param {Array<string>|null} options.actualModelIds - Actual model IDs from CLI JSON output
|
|
318
|
-
* For Claude: from tokenUsage.modelUsage keys (model IDs used in session)
|
|
319
|
-
* For Agent: from pricingInfo.modelId
|
|
320
|
-
* @returns {Promise<string>} Formatted markdown model info section
|
|
321
|
-
*/
|
|
322
|
-
export const getModelInfoForComment = async ({ requestedModel = null, tool = null, pricingInfo = null, actualModelIds = null } = {}) => {
|
|
323
|
-
// Determine the list of actual model IDs to display
|
|
324
|
-
// Priority: explicit actualModelIds > pricingInfo.modelId > resolve from requestedModel
|
|
325
|
-
let modelIds = [];
|
|
326
|
-
|
|
327
|
-
if (Array.isArray(actualModelIds) && actualModelIds.length > 0) {
|
|
328
|
-
modelIds = actualModelIds;
|
|
329
|
-
} else if (pricingInfo?.modelId) {
|
|
330
|
-
// Agent tool provides pricingInfo.modelId as the actual model used
|
|
331
|
-
modelIds = [pricingInfo.modelId];
|
|
332
|
-
} else if (requestedModel) {
|
|
333
|
-
// Fallback: resolve from requested model alias
|
|
334
|
-
const resolved = resolveModelId(requestedModel, tool);
|
|
335
|
-
if (resolved) modelIds = [resolved];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Fetch model metadata from models.dev for each model ID
|
|
339
|
-
const modelsUsed = [];
|
|
340
|
-
for (const modelId of modelIds) {
|
|
341
|
-
let meta = null;
|
|
342
|
-
try {
|
|
343
|
-
meta = await fetchModelInfoForComment(modelId);
|
|
344
|
-
} catch {
|
|
345
|
-
await log(' ⚠️ Could not fetch model info from models.dev', { verbose: true });
|
|
346
|
-
}
|
|
347
|
-
modelsUsed.push({ modelId, modelInfo: meta });
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Determine which modelInfo to pass for legacy fallback (first model's metadata)
|
|
351
|
-
const firstModelInfo = modelsUsed.length > 0 ? modelsUsed[0].modelInfo : null;
|
|
352
|
-
|
|
353
|
-
return buildModelInfoString({
|
|
354
|
-
requestedModel,
|
|
355
|
-
tool,
|
|
356
|
-
pricingInfo,
|
|
357
|
-
modelInfo: modelsUsed.length === 0 ? firstModelInfo : null, // only used as fallback when no modelsUsed
|
|
358
|
-
modelsUsed: modelsUsed.length > 0 ? modelsUsed : null,
|
|
359
|
-
});
|
|
360
|
-
};
|