@link-assistant/hive-mind 1.54.1 → 1.54.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +2 -2
- package/src/claude.lib.mjs +2 -52
- package/src/codex.lib.mjs +91 -9
- package/src/github-cost-info.lib.mjs +3 -2
- package/src/github.lib.mjs +4 -4
- package/src/limits.lib.mjs +19 -3
- package/src/model-info.lib.mjs +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.54.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5030b04: Fix Codex pricing display by calculating OpenAI public estimates from models.dev token rates, passing Codex totals into shared budget stats, and avoiding duplicate raw token usage lines when a Total line is already shown.
|
|
8
|
+
|
|
9
|
+
## 1.54.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9d4e473: Cap `/limits` CPU cores display at available CPU count when load average demand exceeds capacity.
|
|
14
|
+
- ea0b9f5: Use Anthropic's native Claude Code installer in Docker images so the CLI binary is installed even when Bun blocks dependency postinstall scripts.
|
|
15
|
+
|
|
3
16
|
## 1.54.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.54.
|
|
3
|
+
"version": "1.54.3",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
16
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
17
17
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
18
18
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
19
19
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
package/src/claude.lib.mjs
CHANGED
|
@@ -21,7 +21,9 @@ import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; //
|
|
|
21
21
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
22
22
|
import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
|
|
23
23
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
24
|
+
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
24
25
|
export { availableModels }; // Re-export for backward compatibility
|
|
26
|
+
export { fetchModelInfo };
|
|
25
27
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
26
28
|
if (!sessionId || !tempDir) return;
|
|
27
29
|
const cmd = buildClaudeResumeCommand({ tempDir, sessionId, claudePath, model });
|
|
@@ -370,58 +372,6 @@ export const executeClaude = async params => {
|
|
|
370
372
|
prNumber,
|
|
371
373
|
});
|
|
372
374
|
};
|
|
373
|
-
/**
|
|
374
|
-
* Fetches model information from pricing API
|
|
375
|
-
* @param {string} modelId - The model ID (e.g., "claude-sonnet-4-5-20250929")
|
|
376
|
-
* @returns {Promise<Object|null>} Model information or null if not found
|
|
377
|
-
*/
|
|
378
|
-
export const fetchModelInfo = async modelId => {
|
|
379
|
-
try {
|
|
380
|
-
const https = (await use('https')).default;
|
|
381
|
-
return new Promise((resolve, reject) => {
|
|
382
|
-
https
|
|
383
|
-
.get('https://models.dev/api.json', res => {
|
|
384
|
-
let data = '';
|
|
385
|
-
res.on('data', chunk => {
|
|
386
|
-
data += chunk;
|
|
387
|
-
});
|
|
388
|
-
res.on('end', () => {
|
|
389
|
-
try {
|
|
390
|
-
const apiData = JSON.parse(data);
|
|
391
|
-
// For public pricing calculation, prefer Anthropic provider for Claude models
|
|
392
|
-
// Check Anthropic provider first
|
|
393
|
-
if (apiData.anthropic?.models?.[modelId]) {
|
|
394
|
-
const modelInfo = apiData.anthropic.models[modelId];
|
|
395
|
-
modelInfo.provider = apiData.anthropic.name || 'Anthropic';
|
|
396
|
-
resolve(modelInfo);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
// Search for the model across all other providers
|
|
400
|
-
for (const provider of Object.values(apiData)) {
|
|
401
|
-
if (provider.models && provider.models[modelId]) {
|
|
402
|
-
const modelInfo = provider.models[modelId];
|
|
403
|
-
// Add provider info
|
|
404
|
-
modelInfo.provider = provider.name || provider.id;
|
|
405
|
-
resolve(modelInfo);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
// Model not found
|
|
410
|
-
resolve(null);
|
|
411
|
-
} catch (parseError) {
|
|
412
|
-
reject(parseError);
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
})
|
|
416
|
-
.on('error', err => {
|
|
417
|
-
reject(err);
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
} catch {
|
|
421
|
-
// If we can't fetch model info, return null and continue without it
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
375
|
/** Check if a model supports vision (image input) using models.dev API @returns {Promise<boolean>} */
|
|
426
376
|
export const checkModelVisionCapability = async modelId => {
|
|
427
377
|
try {
|
package/src/codex.lib.mjs
CHANGED
|
@@ -22,8 +22,11 @@ import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.m
|
|
|
22
22
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
23
23
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
24
24
|
import { getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
25
|
+
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
26
|
+
import Decimal from 'decimal.js-light';
|
|
25
27
|
|
|
26
28
|
const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
|
|
29
|
+
const CODEX_LONG_CONTEXT_PRICE_THRESHOLD = 272000;
|
|
27
30
|
const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
|
|
28
31
|
const CODEX_MODEL_DIAGNOSTIC_PATHS = [
|
|
29
32
|
['model', data => data?.model],
|
|
@@ -77,6 +80,9 @@ export const createCodexTokenUsage = requestedModelId => ({
|
|
|
77
80
|
stepCount: 0,
|
|
78
81
|
requestedModelId: requestedModelId || null,
|
|
79
82
|
respondedModelId: requestedModelId || null,
|
|
83
|
+
contextLimit: null,
|
|
84
|
+
outputLimit: null,
|
|
85
|
+
peakContextUsage: 0,
|
|
80
86
|
tokenFieldAvailability: createCodexTokenFieldAvailability(),
|
|
81
87
|
});
|
|
82
88
|
|
|
@@ -262,6 +268,10 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
262
268
|
nextState.tokenUsage.reasoningTokens += reasoningTokens;
|
|
263
269
|
nextState.tokenUsage.totalTokens = nextState.tokenUsage.inputTokens + nextState.tokenUsage.cacheReadTokens + nextState.tokenUsage.outputTokens + nextState.tokenUsage.cacheWriteTokens;
|
|
264
270
|
nextState.tokenUsage.stepCount += 1;
|
|
271
|
+
const turnContextUsage = inputTokens + cacheWriteTokens;
|
|
272
|
+
if (turnContextUsage > (nextState.tokenUsage.peakContextUsage || 0)) {
|
|
273
|
+
nextState.tokenUsage.peakContextUsage = turnContextUsage;
|
|
274
|
+
}
|
|
265
275
|
|
|
266
276
|
const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => hasOwnPath(data.usage, fieldName));
|
|
267
277
|
if (usageFieldSet.length > 0) nextState.observedUsageFieldSets.push(usageFieldSet);
|
|
@@ -323,12 +333,80 @@ export const buildCodexResultModelUsage = (modelId, tokenUsage, pricingInfo = nu
|
|
|
323
333
|
outputTokens: tokenUsage.outputTokens || 0,
|
|
324
334
|
modelName: pricingInfo?.modelName || modelId,
|
|
325
335
|
modelInfo: pricingInfo?.modelInfo || null,
|
|
326
|
-
peakContextUsage: 0,
|
|
336
|
+
peakContextUsage: tokenUsage.peakContextUsage || 0,
|
|
327
337
|
costUSD: pricingInfo?.totalCostUSD ?? null,
|
|
328
338
|
},
|
|
329
339
|
};
|
|
330
340
|
};
|
|
331
341
|
|
|
342
|
+
const toCost = (tokens, pricePerMillion) => {
|
|
343
|
+
if (!Number.isFinite(tokens) || !Number.isFinite(pricePerMillion)) return 0;
|
|
344
|
+
return new Decimal(tokens).mul(pricePerMillion).div(1_000_000).toNumber();
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const buildCodexPricingFallback = (modelId, tokenUsage, error = null) => ({
|
|
348
|
+
modelId,
|
|
349
|
+
modelName: modelId,
|
|
350
|
+
provider: 'OpenAI',
|
|
351
|
+
tokenUsage,
|
|
352
|
+
modelInfo: null,
|
|
353
|
+
totalCostUSD: null,
|
|
354
|
+
error,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
export const calculateCodexPricingFromModelInfo = (modelId, tokenUsage, modelInfo) => {
|
|
358
|
+
if (!modelId) return null;
|
|
359
|
+
if (!tokenUsage) return buildCodexPricingFallback(modelId, null);
|
|
360
|
+
if (!modelInfo?.cost) return buildCodexPricingFallback(modelId, tokenUsage, 'Model pricing not found in models.dev API');
|
|
361
|
+
|
|
362
|
+
const standardCost = modelInfo.cost;
|
|
363
|
+
const usesLongContextPricing = !!standardCost.context_over_200k && (tokenUsage.peakContextUsage || 0) > CODEX_LONG_CONTEXT_PRICE_THRESHOLD;
|
|
364
|
+
const cost = usesLongContextPricing ? { ...standardCost, ...standardCost.context_over_200k } : standardCost;
|
|
365
|
+
|
|
366
|
+
const pricing = {
|
|
367
|
+
inputPerMillion: cost.input || 0,
|
|
368
|
+
outputPerMillion: cost.output || 0,
|
|
369
|
+
cacheReadPerMillion: cost.cache_read || 0,
|
|
370
|
+
cacheWritePerMillion: cost.cache_write ?? cost.input ?? 0,
|
|
371
|
+
reasoningPerMillion: cost.reasoning || 0,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const breakdown = {
|
|
375
|
+
input: toCost(tokenUsage.inputTokens || 0, pricing.inputPerMillion),
|
|
376
|
+
output: toCost(tokenUsage.outputTokens || 0, pricing.outputPerMillion),
|
|
377
|
+
cacheRead: toCost(tokenUsage.cacheReadTokens || 0, pricing.cacheReadPerMillion),
|
|
378
|
+
cacheWrite: toCost(tokenUsage.cacheWriteTokens || 0, pricing.cacheWritePerMillion),
|
|
379
|
+
reasoning: toCost(tokenUsage.reasoningTokens || 0, pricing.reasoningPerMillion),
|
|
380
|
+
};
|
|
381
|
+
const totalCostUSD = Object.values(breakdown).reduce((sum, value) => new Decimal(sum).plus(value).toNumber(), 0);
|
|
382
|
+
|
|
383
|
+
tokenUsage.contextLimit = tokenUsage.contextLimit || modelInfo.limit?.context || null;
|
|
384
|
+
tokenUsage.outputLimit = tokenUsage.outputLimit || modelInfo.limit?.output || null;
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
modelId,
|
|
388
|
+
modelName: modelInfo.name || modelId,
|
|
389
|
+
provider: modelInfo.provider || 'OpenAI',
|
|
390
|
+
tokenUsage,
|
|
391
|
+
modelInfo,
|
|
392
|
+
pricing,
|
|
393
|
+
breakdown,
|
|
394
|
+
totalCostUSD,
|
|
395
|
+
usesLongContextPricing,
|
|
396
|
+
longContextThreshold: usesLongContextPricing ? CODEX_LONG_CONTEXT_PRICE_THRESHOLD : null,
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const calculateCodexPricing = async (modelId, tokenUsage) => {
|
|
401
|
+
if (!modelId) return null;
|
|
402
|
+
try {
|
|
403
|
+
const modelInfo = await fetchModelInfo(modelId, { preferredProviderIds: ['openai'] });
|
|
404
|
+
return calculateCodexPricingFromModelInfo(modelId, tokenUsage, modelInfo);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return buildCodexPricingFallback(modelId, tokenUsage, error.message);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
332
410
|
// Function to validate Codex CLI connection
|
|
333
411
|
export const validateCodexConnection = async (model = 'gpt-5.4', verbose = false) => {
|
|
334
412
|
// Map model alias to full ID
|
|
@@ -773,14 +851,15 @@ export const executeCodexCommand = async params => {
|
|
|
773
851
|
}
|
|
774
852
|
|
|
775
853
|
const firstActualModelId = mappedModel;
|
|
776
|
-
const pricingInfo = firstActualModelId
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
:
|
|
854
|
+
const pricingInfo = firstActualModelId ? await calculateCodexPricing(firstActualModelId, codexJsonState.tokenUsage.stepCount > 0 ? codexJsonState.tokenUsage : null) : null;
|
|
855
|
+
if (pricingInfo?.totalCostUSD !== null && pricingInfo?.totalCostUSD !== undefined) {
|
|
856
|
+
await log(`💰 Codex public pricing estimate: $${new Decimal(pricingInfo.totalCostUSD).toFixed(6)}`, { verbose: true });
|
|
857
|
+
if (pricingInfo.usesLongContextPricing) {
|
|
858
|
+
await log(` Long-context pricing applied because peak prompt exceeded ${pricingInfo.longContextThreshold.toLocaleString()} input tokens`, { verbose: true });
|
|
859
|
+
}
|
|
860
|
+
} else if (pricingInfo?.error) {
|
|
861
|
+
await log(`⚠️ Codex public pricing estimate unavailable: ${pricingInfo.error}`, { level: 'warning', verbose: true });
|
|
862
|
+
}
|
|
784
863
|
const resultModelUsage = pricingInfo?.tokenUsage ? buildCodexResultModelUsage(firstActualModelId, pricingInfo.tokenUsage, pricingInfo) : null;
|
|
785
864
|
|
|
786
865
|
// Check for authentication errors first - these should never be retried
|
|
@@ -831,6 +910,7 @@ export const executeCodexCommand = async params => {
|
|
|
831
910
|
limitReached,
|
|
832
911
|
limitResetTime,
|
|
833
912
|
pricingInfo,
|
|
913
|
+
publicPricingEstimate: pricingInfo?.totalCostUSD ?? null,
|
|
834
914
|
resultModelUsage,
|
|
835
915
|
subAgentCalls: codexJsonState.subAgentCalls.length > 0 ? codexJsonState.subAgentCalls : null,
|
|
836
916
|
codexJsonDetails: codexJsonState,
|
|
@@ -853,6 +933,7 @@ export const executeCodexCommand = async params => {
|
|
|
853
933
|
limitReached,
|
|
854
934
|
limitResetTime,
|
|
855
935
|
pricingInfo,
|
|
936
|
+
publicPricingEstimate: pricingInfo?.totalCostUSD ?? null,
|
|
856
937
|
resultModelUsage,
|
|
857
938
|
subAgentCalls: codexJsonState.subAgentCalls.length > 0 ? codexJsonState.subAgentCalls : null,
|
|
858
939
|
codexJsonDetails: codexJsonState,
|
|
@@ -882,6 +963,7 @@ export const executeCodexCommand = async params => {
|
|
|
882
963
|
limitReached: false,
|
|
883
964
|
limitResetTime: null,
|
|
884
965
|
pricingInfo: null,
|
|
966
|
+
publicPricingEstimate: null,
|
|
885
967
|
resultSummary: null, // Issue #1263: No result summary available on error
|
|
886
968
|
};
|
|
887
969
|
} finally {
|
|
@@ -21,7 +21,8 @@ const buildTokenUsageString = tokenUsage => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/** Build cost estimation string for log comments (Issue #1250, Issue #1557, Issue #1600: Decimal precision) */
|
|
24
|
-
export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
24
|
+
export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo, options = {}) => {
|
|
25
|
+
const includeTokenUsage = options.includeTokenUsage !== false;
|
|
25
26
|
const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
|
|
26
27
|
const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
|
|
27
28
|
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
@@ -57,7 +58,7 @@ export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricing
|
|
|
57
58
|
costInfo += `\n- Calculated by OpenCode Zen: $${new Decimal(pricingInfo.opencodeCost).toFixed(6)}`;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
if (pricingInfo?.tokenUsage) costInfo += buildTokenUsageString(pricingInfo.tokenUsage);
|
|
61
|
+
if (includeTokenUsage && pricingInfo?.tokenUsage) costInfo += buildTokenUsageString(pricingInfo.tokenUsage);
|
|
61
62
|
if (hasAnthropic) {
|
|
62
63
|
costInfo += `\n- Calculated by Anthropic: $${anthropicDec.toFixed(6)}`;
|
|
63
64
|
if (hasPublic) {
|
package/src/github.lib.mjs
CHANGED
|
@@ -501,7 +501,7 @@ ${logContent}
|
|
|
501
501
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
502
502
|
} else if (errorDuringExecution) {
|
|
503
503
|
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
504
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
504
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
505
505
|
logComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
|
|
506
506
|
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
507
507
|
|
|
@@ -519,7 +519,7 @@ ${logContent}
|
|
|
519
519
|
---
|
|
520
520
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
521
521
|
} else {
|
|
522
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
522
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
523
523
|
// Determine title based on session type (Issue #1152)
|
|
524
524
|
// Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so
|
|
525
525
|
// the filter in checkForAiCreatedComments matches every variant with a
|
|
@@ -684,7 +684,7 @@ ${errorMessage}
|
|
|
684
684
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
685
685
|
} else if (errorDuringExecution) {
|
|
686
686
|
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
687
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
687
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
688
688
|
logUploadComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
|
|
689
689
|
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
690
690
|
|
|
@@ -697,7 +697,7 @@ This log file contains the complete execution trace of the AI ${targetType === '
|
|
|
697
697
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
698
698
|
} else {
|
|
699
699
|
// Success log format - use helper function for cost info
|
|
700
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
700
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
701
701
|
// Determine title based on session type
|
|
702
702
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
703
703
|
// Issue #1625: titles embed SOLUTION_DRAFT_LOG_MARKER (single source).
|
package/src/limits.lib.mjs
CHANGED
|
@@ -297,6 +297,15 @@ function formatBytesRange(usedBytes, totalBytes) {
|
|
|
297
297
|
return `${usedValue.toFixed(decimals)}/${totalValue.toFixed(decimals)} ${sizes[i]} used`;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
function formatRoundedNumber(value, decimals = 2) {
|
|
301
|
+
return parseFloat(value.toFixed(decimals));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getDisplayCpuCoresUsed(loadAvg5, cpuCount) {
|
|
305
|
+
const boundedLoad = Math.min(Math.max(loadAvg5, 0), cpuCount);
|
|
306
|
+
return formatRoundedNumber(boundedLoad);
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
/**
|
|
301
310
|
* Get GitHub API rate limits by calling gh api rate_limit
|
|
302
311
|
* Returns rate limit info for core, search, graphql, and other resources
|
|
@@ -428,6 +437,7 @@ export async function getCpuLoadInfo(verbose = false) {
|
|
|
428
437
|
// Load average of 1.0 per CPU = 100% utilization
|
|
429
438
|
// Using 5m average for consistency with solve queue (see issue #1137)
|
|
430
439
|
const usagePercentage = Math.min(100, Math.round((loadAvg5 / cpuCount) * 100));
|
|
440
|
+
const usedCpuCores = getDisplayCpuCoresUsed(loadAvg5, cpuCount);
|
|
431
441
|
|
|
432
442
|
if (verbose) {
|
|
433
443
|
console.log(`[VERBOSE] /limits CPU load: ${loadAvg1.toFixed(2)} (1m), ${loadAvg5.toFixed(2)} (5m), ${loadAvg15.toFixed(2)} (15m), ${cpuCount} CPUs, ${usagePercentage}% used`);
|
|
@@ -441,6 +451,7 @@ export async function getCpuLoadInfo(verbose = false) {
|
|
|
441
451
|
loadAvg15,
|
|
442
452
|
cpuCount,
|
|
443
453
|
usagePercentage,
|
|
454
|
+
usedCpuCores,
|
|
444
455
|
},
|
|
445
456
|
};
|
|
446
457
|
} catch (error) {
|
|
@@ -1027,9 +1038,14 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
1027
1038
|
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
1028
1039
|
const suffix = cpuLoad.usagePercentage >= DISPLAY_THRESHOLDS.CPU ? ' ⚠️' : ' used';
|
|
1029
1040
|
section += `${usedBar} ${cpuLoad.usagePercentage}%${suffix}\n`;
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1041
|
+
// Linux load average is demand, not bounded CPU time. Keep the cores-used
|
|
1042
|
+
// display within CPU capacity and show raw load average only when saturated.
|
|
1043
|
+
const usedCpuCores = cpuLoad.usedCpuCores ?? getDisplayCpuCoresUsed(cpuLoad.loadAvg5, cpuLoad.cpuCount);
|
|
1044
|
+
let cpuCoresLine = `${formatRoundedNumber(usedCpuCores)}/${cpuLoad.cpuCount} CPU cores used`;
|
|
1045
|
+
if (cpuLoad.loadAvg5 > cpuLoad.cpuCount) {
|
|
1046
|
+
cpuCoresLine += ` (5m load avg ${formatRoundedNumber(cpuLoad.loadAvg5)})`;
|
|
1047
|
+
}
|
|
1048
|
+
section += `${cpuCoresLine}\n`;
|
|
1033
1049
|
sections.push(section);
|
|
1034
1050
|
}
|
|
1035
1051
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
|
|
3
|
+
const buildLookupIds = modelId => (modelId?.includes('/') ? [modelId.split('/').pop(), modelId] : [modelId]);
|
|
4
|
+
|
|
5
|
+
const buildProviderPriority = (modelId, preferredProviderIds = []) => {
|
|
6
|
+
const inferredProviderIds = [];
|
|
7
|
+
if (modelId?.startsWith('claude-')) inferredProviderIds.push('anthropic');
|
|
8
|
+
if (modelId?.startsWith('gpt-') || modelId?.startsWith('chatgpt-')) inferredProviderIds.push('openai');
|
|
9
|
+
return [...new Set([...preferredProviderIds, ...inferredProviderIds])];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const findProviderModel = (apiData, providerIds, lookupIds) => {
|
|
13
|
+
for (const providerId of providerIds) {
|
|
14
|
+
const provider = apiData[providerId];
|
|
15
|
+
if (!provider?.models) continue;
|
|
16
|
+
for (const lookupId of lookupIds) {
|
|
17
|
+
if (provider.models[lookupId]) {
|
|
18
|
+
return {
|
|
19
|
+
...provider.models[lookupId],
|
|
20
|
+
provider: provider.name || providerId,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fetchModelsDevApi = () =>
|
|
29
|
+
new Promise((resolve, reject) => {
|
|
30
|
+
https
|
|
31
|
+
.get('https://models.dev/api.json', res => {
|
|
32
|
+
let data = '';
|
|
33
|
+
res.on('data', chunk => {
|
|
34
|
+
data += chunk;
|
|
35
|
+
});
|
|
36
|
+
res.on('end', () => {
|
|
37
|
+
try {
|
|
38
|
+
resolve(JSON.parse(data));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
})
|
|
44
|
+
.on('error', reject);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetches model information from models.dev.
|
|
49
|
+
* @param {string} modelId - The model ID (e.g., "claude-sonnet-4-5-20250929")
|
|
50
|
+
* @param {Object} [options]
|
|
51
|
+
* @param {string[]} [options.preferredProviderIds] Provider IDs to check before the default search order.
|
|
52
|
+
* @returns {Promise<Object|null>} Model information or null if not found
|
|
53
|
+
*/
|
|
54
|
+
export const fetchModelInfo = async (modelId, options = {}) => {
|
|
55
|
+
if (!modelId) return null;
|
|
56
|
+
try {
|
|
57
|
+
const apiData = await fetchModelsDevApi();
|
|
58
|
+
const lookupIds = buildLookupIds(modelId);
|
|
59
|
+
const preferredProviderIds = Array.isArray(options.preferredProviderIds) ? options.preferredProviderIds : [];
|
|
60
|
+
const providerPriority = buildProviderPriority(modelId, preferredProviderIds);
|
|
61
|
+
return findProviderModel(apiData, providerPriority, lookupIds) || findProviderModel(apiData, Object.keys(apiData), lookupIds);
|
|
62
|
+
} catch {
|
|
63
|
+
// If we can't fetch model info, return null and continue without it.
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
};
|