@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.
@@ -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
- const modelMap = {
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 = 'grok-code-fast-1') => {
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: 'Model to use (opus, sonnet, or full model ID like claude-sonnet-4-5-20250929)',
74
+ description: buildModelOptionDescription(),
74
75
  alias: 'm',
75
- default: 'opus',
76
- choices: ['opus', 'sonnet', 'claude-sonnet-4-5-20250929', 'claude-opus-4-6', 'claude-opus-4-5-20251101'],
76
+ default: defaultModels.claude,
77
+ choices: getClaudeModelChoices(),
77
78
  })
78
79
  .option('focus', {
79
80
  type: 'string',
@@ -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: 'Model to use (for claude: opus, sonnet, haiku, haiku-3-5, haiku-3; for opencode: grok, gpt4o; for codex: gpt5, gpt5-codex, o3; for agent: minimax-m2.5-free, big-pickle, gpt-5-nano, glm-5-free, deepseek-r1-free)',
432
+ description: buildModelOptionDescription(),
432
433
  alias: 'm',
433
434
  default: currentParsedArgs => {
434
- // Dynamic default based on tool selection
435
- if (currentParsedArgs?.tool === 'opencode') {
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 === 'opencode' && !modelExplicitlyProvided) {
573
- // User did not explicitly provide --model, so use the correct default for opencode
574
- argv.model = 'grok-code-fast-1';
575
- } else if (argv.tool === 'codex' && !modelExplicitlyProvided) {
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('./model-validation.lib.mjs');
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: 'Model to use (opus, sonnet, or full model ID like claude-sonnet-4-5-20250929)',
110
+ description: buildModelOptionDescription(),
110
111
  alias: 'm',
111
- default: 'sonnet',
112
- choices: ['opus', 'sonnet', 'claude-sonnet-4-5-20250929', 'claude-opus-4-6', 'claude-opus-4-5-20251101'],
112
+ default: defaultModels.claude,
113
+ choices: Object.keys(claudeModels),
113
114
  })
114
115
  .option('verbose', {
115
116
  type: 'boolean',
@@ -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('./model-validation.lib.mjs');
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 += '• `--model <model>` or `-m` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
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';
@@ -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 'Anthropic Claude Code';
30
- case 'codex':
31
- return 'OpenAI Codex';
32
- case 'opencode':
33
- return 'OpenCode';
34
- case 'agent':
35
- return 'Agent CLI';
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
- };