@link-assistant/hive-mind 1.33.0 → 1.34.0

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 614c3d9: Add model information display in PR/issue log comments. Shows actual models used (extracted from CLI JSON output) vs requested model. Main model is bolded when it matches the request; a warning appears when it doesn't. Supporting models are listed separately. Uses models.dev API for full model name, provider, and knowledge cutoff. Replaces duplicated tool name mapping with unified getToolDisplayName() helper.
8
+
3
9
  ## 1.33.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -10,26 +10,16 @@ import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTok
10
10
  export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeLogContent }; // Re-export for backward compatibility
11
11
  import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
12
12
  import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
13
+ // Import model info helpers (Issue #1225)
14
+ import { getToolDisplayName, getModelInfoForComment } from './model-info.lib.mjs';
15
+ // Re-export for use by other modules
16
+ export { getToolDisplayName };
13
17
 
14
- /**
15
- * Build cost estimation string for log comments
16
- * Issue #1250: Enhanced to show both public pricing estimate and actual provider cost
17
- *
18
- * @param {number|null} totalCostUSD - Public pricing estimate
19
- * @param {number|null} anthropicTotalCostUSD - Cost calculated by Anthropic (Claude-specific)
20
- * @param {Object|null} pricingInfo - Pricing info from agent tool
21
- * - opencodeCost: Actual billed cost from OpenCode Zen (for agent tool)
22
- * - isOpencodeFreeModel: Whether OpenCode Zen provides this model for free
23
- * - originalProvider: Original provider for pricing reference
24
- * - baseModelName: Base model name if pricing was derived from base model (Issue #1250)
25
- * @returns {string} Formatted cost info string for markdown (empty if no data available)
26
- */
18
+ /** Build cost estimation string for log comments (Issue #1250) */
27
19
  const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
28
- // Issue #1015: Don't show cost section when all values are unknown (clutters output)
29
20
  const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
30
21
  const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
31
22
  const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
32
- // Issue #1250: Check for OpenCode Zen actual cost
33
23
  const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
34
24
  if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
35
25
  let costInfo = '\n\nšŸ’° **Cost estimation:**';
@@ -37,15 +27,10 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
37
27
  costInfo += `\n- Model: ${pricingInfo.modelName}`;
38
28
  if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
39
29
  }
40
- // Issue #1250: Show public pricing estimate based on original provider prices
41
30
  if (hasPublic) {
42
- // Issue #1250: For free models accessed via OpenCode Zen, show pricing based on base model
43
- // Only show as completely free if the base model also has no pricing
44
31
  if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
45
32
  costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
46
33
  } else {
47
- // Show actual public pricing estimate with original provider reference
48
- // Issue #1250: Include base model reference when pricing comes from base model
49
34
  let pricingRef = '';
50
35
  if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
51
36
  pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
@@ -57,7 +42,6 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
57
42
  } else if (hasPricing) {
58
43
  costInfo += '\n- Public pricing estimate: unknown';
59
44
  }
60
- // Issue #1250: Show actual cost from OpenCode Zen for agent tool
61
45
  if (hasOpencodeCost) {
62
46
  if (pricingInfo.isOpencodeFreeModel) {
63
47
  costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
@@ -360,26 +344,7 @@ Thank you! šŸ™`;
360
344
  return false;
361
345
  }
362
346
  };
363
- /**
364
- * Attaches a log file to a GitHub PR or issue as a comment
365
- * @param {Object} options - Configuration options
366
- * @param {string} options.logFile - Path to the log file
367
- * @param {string} options.targetType - 'pr' or 'issue'
368
- * @param {number} options.targetNumber - PR or issue number
369
- * @param {string} options.owner - Repository owner
370
- * @param {string} options.repo - Repository name
371
- * @param {Function} options.$ - Command execution function
372
- * @param {Function} options.log - Logging function
373
- * @param {Function} options.sanitizeLogContent - Function to sanitize log content
374
- * @param {boolean} [options.verbose=false] - Enable verbose logging
375
- * @param {string} [options.errorMessage] - Error message to include in comment (for failure logs)
376
- * @param {string} [options.customTitle] - Custom title for the comment (defaults to "šŸ¤– Solution Draft Log")
377
- * @param {boolean} [options.isUsageLimit] - Whether this is a usage limit error
378
- * @param {string} [options.limitResetTime] - Time when usage limit resets
379
- * @param {string} [options.toolName] - Name of the tool (claude, codex, opencode)
380
- * @param {string} [options.resumeCommand] - Command to resume the session
381
- * @returns {Promise<boolean>} - True if upload succeeded
382
- */
347
+ /** Attaches a log file to a GitHub PR or issue as a comment. Returns true if upload succeeded. */
383
348
  export async function attachLogToGitHub(options) {
384
349
  const fs = (await use('fs')).promises;
385
350
  const {
@@ -413,6 +378,9 @@ export async function attachLogToGitHub(options) {
413
378
  pricingInfo = null,
414
379
  // Issue #1088: Track error_during_execution for "Finished with errors" state
415
380
  errorDuringExecution = false,
381
+ // Issue #1225: Model information for PR comments
382
+ requestedModel = null, // The --model flag value (e.g., "opus", "sonnet")
383
+ tool = null, // The tool used (e.g., "claude", "agent", "codex", "opencode")
416
384
  } = options;
417
385
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
418
386
  const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
@@ -433,6 +401,8 @@ export async function attachLogToGitHub(options) {
433
401
  // Calculate token usage if sessionId and tempDir are provided
434
402
  // For agent tool, publicPricingEstimate is already provided, so we skip Claude-specific calculation
435
403
  let totalCostUSD = publicPricingEstimate;
404
+ // Issue #1225: Collect actual model IDs from Claude session JSON output
405
+ let actualModelIds = null;
436
406
  if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
437
407
  try {
438
408
  const { calculateSessionTokens } = await import('./claude.lib.mjs');
@@ -444,6 +414,13 @@ export async function attachLogToGitHub(options) {
444
414
  await log(` šŸ’° Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
445
415
  }
446
416
  }
417
+ // Extract actual model IDs from session data (Issue #1225)
418
+ if (tokenUsage.modelUsage && Object.keys(tokenUsage.modelUsage).length > 0) {
419
+ actualModelIds = Object.keys(tokenUsage.modelUsage);
420
+ if (verbose) {
421
+ await log(` šŸ¤– Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
422
+ }
423
+ }
447
424
  }
448
425
  } catch (tokenError) {
449
426
  // Don't fail the entire upload if token calculation fails
@@ -452,6 +429,25 @@ export async function attachLogToGitHub(options) {
452
429
  }
453
430
  }
454
431
  }
432
+ // For agent tool, extract actual model ID from pricingInfo (Issue #1225)
433
+ if (!actualModelIds && pricingInfo?.modelId) {
434
+ actualModelIds = [pricingInfo.modelId];
435
+ }
436
+ // Issue #1225: Fetch model information for comment using actual models from CLI output
437
+ let modelInfoString = '';
438
+ if (requestedModel || tool || actualModelIds) {
439
+ try {
440
+ modelInfoString = await getModelInfoForComment({ requestedModel, tool, pricingInfo, actualModelIds });
441
+ if (verbose && modelInfoString) {
442
+ await log(' šŸ¤– Model info fetched for comment', { verbose: true });
443
+ }
444
+ } catch (modelInfoError) {
445
+ // Non-critical: continue without model info
446
+ if (verbose) {
447
+ await log(` āš ļø Could not fetch model info: ${modelInfoError.message}`, { verbose: true });
448
+ }
449
+ }
450
+ }
455
451
  // Read and sanitize log content
456
452
  const rawLogContent = await fs.readFile(logFile, 'utf8');
457
453
  if (verbose) {
@@ -523,7 +519,7 @@ ${resumeCommand}
523
519
  }
524
520
  }
525
521
 
526
- logComment += `
522
+ logComment += `${modelInfoString}
527
523
 
528
524
  <details>
529
525
  <summary>Click to expand execution log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -542,7 +538,7 @@ ${logContent}
542
538
  The automated solution draft encountered an error:
543
539
  \`\`\`
544
540
  ${errorMessage}
545
- \`\`\`
541
+ \`\`\`${modelInfoString}
546
542
 
547
543
  <details>
548
544
  <summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -559,7 +555,7 @@ ${logContent}
559
555
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
560
556
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
561
557
  logComment = `## āš ļø Solution Draft Finished with Errors
562
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
558
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
563
559
 
564
560
  **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
565
561
 
@@ -592,7 +588,7 @@ ${logContent}
592
588
  sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
593
589
  }
594
590
  logComment = `## ${title}
595
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${sessionNote}
591
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}${sessionNote}
596
592
 
597
593
  <details>
598
594
  <summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -716,7 +712,7 @@ ${resumeCommand}
716
712
  }
717
713
  }
718
714
 
719
- logUploadComment += `
715
+ logUploadComment += `${modelInfoString}
720
716
 
721
717
  šŸ“Ž **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
722
718
  šŸ”— [View complete execution log](${logUrl})
@@ -729,7 +725,7 @@ ${resumeCommand}
729
725
  The automated solution draft encountered an error:
730
726
  \`\`\`
731
727
  ${errorMessage}
732
- \`\`\`
728
+ \`\`\`${modelInfoString}
733
729
  šŸ“Ž **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
734
730
  šŸ”— [View complete failure log](${logUrl})
735
731
  ---
@@ -738,7 +734,7 @@ ${errorMessage}
738
734
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
739
735
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
740
736
  logUploadComment = `## āš ļø Solution Draft Finished with Errors
741
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
737
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
742
738
 
743
739
  **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
744
740
 
@@ -764,7 +760,7 @@ This log file contains the complete execution trace of the AI ${targetType === '
764
760
  sessionNote = '\n**Note**: This session was manually resumed using the --resume flag.\n';
765
761
  }
766
762
  logUploadComment = `## ${title}
767
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
763
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
768
764
  ${sessionNote}šŸ“Ž **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
769
765
  šŸ”— [View complete solution draft log](${logUrl})
770
766
  ---
@@ -1482,6 +1478,7 @@ export default {
1482
1478
  checkGitHubPermissions,
1483
1479
  checkRepositoryWritePermission,
1484
1480
  attachLogToGitHub,
1481
+ getToolDisplayName,
1485
1482
  uploadLogWithGhUploadLog,
1486
1483
  fetchAllIssuesWithPagination,
1487
1484
  fetchProjectIssues,
@@ -0,0 +1,360 @@
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
+ const mainModelProvider = mainModelMeta?.provider || null;
205
+ const mainModelKnowledge = mainModelMeta?.knowledge || null;
206
+
207
+ if (mainMatches) {
208
+ info += `\n- **Main model: ${mainModelName}** (ID: \`${mainModelId}\`${mainModelProvider ? `, ${mainModelProvider}` : ''}${mainModelKnowledge ? `, cutoff: ${mainModelKnowledge}` : ''})`;
209
+ } else {
210
+ // Main model doesn't match requested - show warning
211
+ info += `\n- **Main model: ${mainModelName}** (ID: \`${mainModelId}\`${mainModelProvider ? `, ${mainModelProvider}` : ''}${mainModelKnowledge ? `, cutoff: ${mainModelKnowledge}` : ''})`;
212
+ if (hasRequested) {
213
+ info += `\n- āš ļø **Warning**: Main model \`${mainModelId}\` does not match requested model \`${requestedModel}\``;
214
+ }
215
+ }
216
+
217
+ // Display supporting models
218
+ if (supportingEntries.length > 0) {
219
+ info += '\n- Supporting models:';
220
+ for (const entry of supportingEntries) {
221
+ const name = entry.modelInfo?.name || entry.modelId;
222
+ const provider = entry.modelInfo?.provider || null;
223
+ info += `\n - ${name} (\`${entry.modelId}\`${provider ? `, ${provider}` : ''})`;
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-5-20251101',
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
+ };
@@ -790,6 +790,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
790
790
  anthropicTotalCostUSD: latestAnthropicCost,
791
791
  publicPricingEstimate: toolResult.publicPricingEstimate,
792
792
  pricingInfo: toolResult.pricingInfo,
793
+ // Issue #1225: Pass model and tool info for PR comments
794
+ requestedModel: argv.model,
795
+ tool: argv.tool || 'claude',
793
796
  });
794
797
  await log(formatAligned('', 'āœ… Session log uploaded to PR', '', 2));
795
798
  }
@@ -54,6 +54,9 @@ export const handleFailure = async options => {
54
54
  sanitizeLogContent,
55
55
  verbose: argv.verbose,
56
56
  errorMessage: cleanErrorMessage(error),
57
+ // Issue #1225: Pass model and tool info for PR comments
58
+ requestedModel: argv.model,
59
+ tool: argv.tool || 'claude',
57
60
  });
58
61
  if (logUploadSuccess) {
59
62
  await log('šŸ“Ž Failure log attached to Pull Request');
@@ -194,6 +194,9 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
194
194
  sanitizeLogContent,
195
195
  verbose: argv.verbose || false,
196
196
  errorMessage: cleanErrorMessage(error),
197
+ // Issue #1225: Pass model and tool info for PR comments
198
+ requestedModel: argv.model,
199
+ tool: argv.tool || 'claude',
197
200
  });
198
201
 
199
202
  if (logUploadSuccess) {
@@ -59,6 +59,9 @@ export const createInterruptWrapper = ({ cleanupContext, checkForUncommittedChan
59
59
  sanitizeLogContent,
60
60
  verbose: ctx.argv.verbose || false,
61
61
  errorMessage: 'Session interrupted by user (CTRL+C)',
62
+ // Issue #1225: Pass model and tool info for PR comments
63
+ requestedModel: ctx.argv.model,
64
+ tool: ctx.argv.tool || 'claude',
62
65
  });
63
66
  } catch (uploadError) {
64
67
  await log(`āš ļø Could not upload logs on interrupt: ${uploadError.message}`, {
package/src/solve.mjs CHANGED
@@ -50,7 +50,7 @@ const memoryCheck = await import('./memory-check.mjs');
50
50
  const lib = await import('./lib.mjs');
51
51
  const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo } = lib;
52
52
  const githubLib = await import('./github.lib.mjs');
53
- const { sanitizeLogContent, attachLogToGitHub } = githubLib;
53
+ const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
54
54
  const validation = await import('./solve.validation.lib.mjs');
55
55
  const { validateGitHubUrl, showAttachLogsWarning, initializeLogFile, validateUrlRequirement, validateContinueOnlyOnFeedback, performSystemChecks } = validation;
56
56
  const autoContinue = await import('./solve.auto-continue.lib.mjs');
@@ -79,26 +79,15 @@ const exitHandler = await import('./exit-handler.lib.mjs');
79
79
  const { initializeExitHandler, installGlobalExitHandlers, safeExit, logActiveHandles } = exitHandler;
80
80
  const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
81
81
  const getResourceSnapshot = memoryCheck.getResourceSnapshot;
82
-
83
- // Import new modular components
84
- const autoPrLib = await import('./solve.auto-pr.lib.mjs');
85
- const { handleAutoPrCreation } = autoPrLib;
86
- const repoSetupLib = await import('./solve.repo-setup.lib.mjs');
87
- const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = repoSetupLib;
88
- const branchLib = await import('./solve.branch.lib.mjs');
89
- const { createOrCheckoutBranch } = branchLib;
90
- const sessionLib = await import('./solve.session.lib.mjs');
91
- const { startWorkSession, endWorkSession, SESSION_TYPES } = sessionLib;
92
- const preparationLib = await import('./solve.preparation.lib.mjs');
93
- const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = preparationLib;
94
-
95
- // Import model validation library
96
- const modelValidation = await import('./model-validation.lib.mjs');
97
- const { validateAndExitOnInvalidModel } = modelValidation;
98
- const acceptInviteLib = await import('./solve.accept-invite.lib.mjs');
99
- const { autoAcceptInviteForRepo } = acceptInviteLib;
100
-
101
- // Initialize log file EARLY (use cwd initially, will be updated after argv parsing)
82
+ const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
83
+ const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
84
+ const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
85
+ const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
86
+ const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
87
+ const { validateAndExitOnInvalidModel } = await import('./model-validation.lib.mjs');
88
+ const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
89
+
90
+ // Initialize log file early (before argument parsing) to capture all output
102
91
  const logFile = await initializeLogFile(null);
103
92
 
104
93
  // Log version and raw command IMMEDIATELY after log file initialization (ensures they appear even if parsing fails)
@@ -944,9 +933,11 @@ try {
944
933
  // Mark this as a usage limit case for proper formatting
945
934
  isUsageLimit: true,
946
935
  limitResetTime: global.limitResetTime,
947
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
936
+ toolName: getToolDisplayName(argv.tool),
948
937
  resumeCommand,
949
938
  sessionId,
939
+ requestedModel: argv.model,
940
+ tool: argv.tool || 'claude',
950
941
  });
951
942
 
952
943
  if (logUploadSuccess) {
@@ -1004,13 +995,15 @@ try {
1004
995
  // Mark this as a usage limit case for proper formatting
1005
996
  isUsageLimit: true,
1006
997
  limitResetTime: global.limitResetTime,
1007
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
998
+ toolName: getToolDisplayName(argv.tool),
1008
999
  resumeCommand,
1009
1000
  sessionId,
1010
1001
  // Tell attachLogToGitHub that auto-resume is enabled to suppress CLI commands in the comment
1011
1002
  // See: https://github.com/link-assistant/hive-mind/issues/1152
1012
1003
  isAutoResumeEnabled: true,
1013
1004
  autoResumeMode: limitContinueMode,
1005
+ requestedModel: argv.model,
1006
+ tool: argv.tool || 'claude',
1014
1007
  });
1015
1008
 
1016
1009
  if (logUploadSuccess) {
@@ -1099,12 +1092,14 @@ try {
1099
1092
  // For usage limit, use a dedicated comment format to make it clear and actionable
1100
1093
  isUsageLimit: !!limitReached,
1101
1094
  limitResetTime: limitReached ? toolResult.limitResetTime : null,
1102
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
1095
+ toolName: getToolDisplayName(argv.tool),
1103
1096
  resumeCommand,
1104
1097
  // Include sessionId so the PR comment can present it
1105
1098
  sessionId,
1106
1099
  // If not a usage limit case, fall back to generic failure format
1107
1100
  errorMessage: limitReached ? undefined : `${argv.tool.toUpperCase()} execution failed`,
1101
+ requestedModel: argv.model,
1102
+ tool: argv.tool || 'claude',
1108
1103
  });
1109
1104
 
1110
1105
  if (logUploadSuccess) {
@@ -1356,6 +1351,8 @@ try {
1356
1351
  sessionId,
1357
1352
  tempDir,
1358
1353
  anthropicTotalCostUSD,
1354
+ requestedModel: argv.model,
1355
+ tool: argv.tool || 'claude',
1359
1356
  });
1360
1357
 
1361
1358
  if (logUploadSuccess) {
@@ -675,6 +675,9 @@ Fixes ${issueRef}
675
675
  errorDuringExecution,
676
676
  // Issue #1152: Pass sessionType for differentiated log comments
677
677
  sessionType,
678
+ // Issue #1225: Pass model and tool info for PR comments
679
+ requestedModel: argv.model,
680
+ tool: argv.tool || 'claude',
678
681
  });
679
682
  }
680
683
 
@@ -754,6 +757,9 @@ Fixes ${issueRef}
754
757
  errorDuringExecution,
755
758
  // Issue #1152: Pass sessionType for differentiated log comments
756
759
  sessionType,
760
+ // Issue #1225: Pass model and tool info for issue comments
761
+ requestedModel: argv.model,
762
+ tool: argv.tool || 'claude',
757
763
  });
758
764
  }
759
765
 
@@ -838,6 +844,9 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
838
844
  sanitizeLogContent,
839
845
  verbose: argv.verbose || false,
840
846
  errorMessage: cleanErrorMessage(error),
847
+ // Issue #1225: Pass model and tool info for PR comments
848
+ requestedModel: argv.model,
849
+ tool: argv.tool || 'claude',
841
850
  });
842
851
 
843
852
  if (logUploadSuccess) {
@@ -297,6 +297,9 @@ export const watchForFeedback = async params => {
297
297
  // Mark if this was a usage limit failure
298
298
  isUsageLimit: toolResult.limitReached,
299
299
  limitResetTime: toolResult.limitResetTime,
300
+ // Issue #1225: Pass model and tool info for PR comments
301
+ requestedModel: argv.model,
302
+ tool: argv.tool || 'claude',
300
303
  });
301
304
 
302
305
  if (logUploadSuccess) {
@@ -363,6 +366,9 @@ export const watchForFeedback = async params => {
363
366
  // Pass agent tool pricing data when available
364
367
  publicPricingEstimate: toolResult.publicPricingEstimate,
365
368
  pricingInfo: toolResult.pricingInfo,
369
+ // Issue #1225: Pass model and tool info for PR comments
370
+ requestedModel: argv.model,
371
+ tool: argv.tool || 'claude',
366
372
  });
367
373
 
368
374
  if (logUploadSuccess) {