@link-assistant/hive-mind 1.51.0 → 1.52.1
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 +12 -0
- package/package.json +2 -1
- package/src/agent-token-usage.lib.mjs +96 -0
- package/src/agent.lib.mjs +32 -134
- package/src/agent.prompts.lib.mjs +4 -9
- package/src/claude.budget-stats.lib.mjs +41 -61
- package/src/claude.lib.mjs +14 -14
- package/src/claude.prompts.lib.mjs +5 -13
- package/src/codex.lib.mjs +52 -5
- package/src/codex.options.lib.mjs +1 -0
- package/src/codex.prompts.lib.mjs +4 -9
- package/src/config.lib.mjs +133 -31
- package/src/github-cost-info.lib.mjs +70 -0
- package/src/github.lib.mjs +2 -54
- package/src/models/index.mjs +11 -6
- package/src/opencode.lib.mjs +47 -0
- package/src/opencode.prompts.lib.mjs +4 -9
- package/src/solve.config.lib.mjs +7 -2
- package/src/telegram-bot.mjs +1 -1
- package/src/thinking-prompt.lib.mjs +61 -0
package/src/claude.lib.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs
|
|
|
13
13
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
14
14
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
15
15
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
16
|
+
import Decimal from 'decimal.js-light';
|
|
16
17
|
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage } from './claude.budget-stats.lib.mjs';
|
|
17
18
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
18
19
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
@@ -301,6 +302,7 @@ export const executeClaude = async params => {
|
|
|
301
302
|
owner,
|
|
302
303
|
repo,
|
|
303
304
|
argv,
|
|
305
|
+
claudeVersion: getClaudeVersion(),
|
|
304
306
|
});
|
|
305
307
|
// Build the system prompt
|
|
306
308
|
const systemPrompt = buildSystemPrompt({
|
|
@@ -428,51 +430,48 @@ export const checkModelVisionCapability = async modelId => {
|
|
|
428
430
|
return false;
|
|
429
431
|
}
|
|
430
432
|
};
|
|
431
|
-
/** Calculate USD cost for a model's usage with detailed breakdown */
|
|
433
|
+
/** Calculate USD cost for a model's usage with detailed breakdown (Issue #1600: uses Decimal for precision) */
|
|
432
434
|
export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) => {
|
|
433
435
|
if (!modelInfo || !modelInfo.cost) {
|
|
434
436
|
return includeBreakdown ? { total: 0, breakdown: null } : 0;
|
|
435
437
|
}
|
|
436
438
|
const cost = modelInfo.cost;
|
|
439
|
+
const million = new Decimal(1000000);
|
|
437
440
|
const breakdown = {
|
|
438
441
|
input: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
439
442
|
cacheWrite: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
440
443
|
cacheRead: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
441
444
|
output: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
442
445
|
};
|
|
443
|
-
// Input tokens cost (per million tokens)
|
|
444
446
|
if (usage.inputTokens && cost.input) {
|
|
445
447
|
breakdown.input = {
|
|
446
448
|
tokens: usage.inputTokens,
|
|
447
449
|
costPerMillion: cost.input,
|
|
448
|
-
cost: (usage.inputTokens
|
|
450
|
+
cost: new Decimal(usage.inputTokens).div(million).mul(new Decimal(cost.input)).toNumber(),
|
|
449
451
|
};
|
|
450
452
|
}
|
|
451
|
-
// Cache creation tokens cost
|
|
452
453
|
if (usage.cacheCreationTokens && cost.cache_write) {
|
|
453
454
|
breakdown.cacheWrite = {
|
|
454
455
|
tokens: usage.cacheCreationTokens,
|
|
455
456
|
costPerMillion: cost.cache_write,
|
|
456
|
-
cost: (usage.cacheCreationTokens
|
|
457
|
+
cost: new Decimal(usage.cacheCreationTokens).div(million).mul(new Decimal(cost.cache_write)).toNumber(),
|
|
457
458
|
};
|
|
458
459
|
}
|
|
459
|
-
// Cache read tokens cost
|
|
460
460
|
if (usage.cacheReadTokens && cost.cache_read) {
|
|
461
461
|
breakdown.cacheRead = {
|
|
462
462
|
tokens: usage.cacheReadTokens,
|
|
463
463
|
costPerMillion: cost.cache_read,
|
|
464
|
-
cost: (usage.cacheReadTokens
|
|
464
|
+
cost: new Decimal(usage.cacheReadTokens).div(million).mul(new Decimal(cost.cache_read)).toNumber(),
|
|
465
465
|
};
|
|
466
466
|
}
|
|
467
|
-
// Output tokens cost
|
|
468
467
|
if (usage.outputTokens && cost.output) {
|
|
469
468
|
breakdown.output = {
|
|
470
469
|
tokens: usage.outputTokens,
|
|
471
470
|
costPerMillion: cost.output,
|
|
472
|
-
cost: (usage.outputTokens
|
|
471
|
+
cost: new Decimal(usage.outputTokens).div(million).mul(new Decimal(cost.output)).toNumber(),
|
|
473
472
|
};
|
|
474
473
|
}
|
|
475
|
-
const totalCost = breakdown.input.cost
|
|
474
|
+
const totalCost = new Decimal(breakdown.input.cost).plus(breakdown.cacheWrite.cost).plus(breakdown.cacheRead.cost).plus(breakdown.output.cost).toNumber();
|
|
476
475
|
if (includeBreakdown) {
|
|
477
476
|
return {
|
|
478
477
|
total: totalCost,
|
|
@@ -618,7 +617,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
618
617
|
let totalCacheCreationTokens = 0;
|
|
619
618
|
let totalCacheReadTokens = 0;
|
|
620
619
|
let totalOutputTokens = 0;
|
|
621
|
-
let
|
|
620
|
+
let totalCostDecimal = new Decimal(0);
|
|
622
621
|
let hasCostData = false;
|
|
623
622
|
for (const usage of Object.values(modelUsage)) {
|
|
624
623
|
totalInputTokens += usage.inputTokens;
|
|
@@ -626,7 +625,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
626
625
|
totalCacheReadTokens += usage.cacheReadTokens;
|
|
627
626
|
totalOutputTokens += usage.outputTokens;
|
|
628
627
|
if (usage.costUSD !== null) {
|
|
629
|
-
|
|
628
|
+
totalCostDecimal = totalCostDecimal.plus(new Decimal(usage.costUSD));
|
|
630
629
|
hasCostData = true;
|
|
631
630
|
}
|
|
632
631
|
}
|
|
@@ -641,7 +640,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
641
640
|
cacheReadTokens: totalCacheReadTokens,
|
|
642
641
|
outputTokens: totalOutputTokens,
|
|
643
642
|
totalTokens,
|
|
644
|
-
totalCostUSD: hasCostData ?
|
|
643
|
+
totalCostUSD: hasCostData ? totalCostDecimal.toNumber() : null,
|
|
645
644
|
// Issue #1501: Peak context usage (max single-request fill) and dedup stats
|
|
646
645
|
peakContextUsage: globalPeakContext,
|
|
647
646
|
duplicateEntriesSkipped: duplicateCount,
|
|
@@ -784,7 +783,7 @@ export const executeClaudeCommand = async params => {
|
|
|
784
783
|
}
|
|
785
784
|
try {
|
|
786
785
|
const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
|
|
787
|
-
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel });
|
|
786
|
+
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel, showThinkingContent: argv.showThinkingContent });
|
|
788
787
|
if (argv.verbose) claudeEnv.ANTHROPIC_LOG = 'debug';
|
|
789
788
|
const modelMaxOutputTokens = getMaxOutputTokensForModel(effectiveModel);
|
|
790
789
|
if (argv.verbose) {
|
|
@@ -792,6 +791,7 @@ export const executeClaudeCommand = async params => {
|
|
|
792
791
|
if (resolvedPlanModel) await log(`📊 opusplan: plan=${resolvedPlanModel}, exec=${resolvedExecutionModel}`, { verbose: true });
|
|
793
792
|
if (resolvedThinkingBudget !== undefined) await log(`📊 MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
|
|
794
793
|
if (claudeEnv.CLAUDE_CODE_EFFORT_LEVEL) await log(`📊 CLAUDE_CODE_EFFORT_LEVEL: ${claudeEnv.CLAUDE_CODE_EFFORT_LEVEL}`, { verbose: true });
|
|
794
|
+
if (claudeEnv.CLAUDE_CODE_SHOW_THINKING) await log(`📊 CLAUDE_CODE_SHOW_THINKING: ${claudeEnv.CLAUDE_CODE_SHOW_THINKING}`, { verbose: true });
|
|
795
795
|
if (!isNewVersion && thinkLevel) await log(`📊 Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
|
|
796
796
|
}
|
|
797
797
|
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mjs';
|
|
7
7
|
import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.lib.mjs';
|
|
8
8
|
import { primaryModelNames } from './models/index.mjs';
|
|
9
|
+
import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Build the user prompt for Claude
|
|
@@ -13,7 +14,7 @@ import { primaryModelNames } from './models/index.mjs';
|
|
|
13
14
|
* @returns {string} The formatted user prompt
|
|
14
15
|
*/
|
|
15
16
|
export const buildUserPrompt = params => {
|
|
16
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, owner, repo, argv, contributingGuidelines } = params;
|
|
17
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, owner, repo, argv, contributingGuidelines, claudeVersion } = params;
|
|
17
18
|
|
|
18
19
|
const promptLines = [];
|
|
19
20
|
|
|
@@ -65,18 +66,9 @@ export const buildUserPrompt = params => {
|
|
|
65
66
|
promptLines.push('');
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Keeping keywords for backward compatibility with older Claude Code versions
|
|
72
|
-
if (argv && argv.think) {
|
|
73
|
-
const thinkMessages = {
|
|
74
|
-
low: 'Think.',
|
|
75
|
-
medium: 'Think hard.',
|
|
76
|
-
high: 'Think harder.',
|
|
77
|
-
max: 'Ultrathink.',
|
|
78
|
-
};
|
|
79
|
-
promptLines.push(thinkMessages[argv.think]);
|
|
69
|
+
const thinkingPromptInstruction = getThinkingPromptInstruction({ tool: 'claude', argv, claudeVersion });
|
|
70
|
+
if (thinkingPromptInstruction) {
|
|
71
|
+
promptLines.push(thinkingPromptInstruction);
|
|
80
72
|
}
|
|
81
73
|
|
|
82
74
|
// Final instruction
|
package/src/codex.lib.mjs
CHANGED
|
@@ -22,7 +22,7 @@ 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
|
|
|
25
|
-
const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens'];
|
|
25
|
+
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'];
|
|
26
26
|
const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
|
|
27
27
|
const CODEX_MODEL_DIAGNOSTIC_PATHS = [
|
|
28
28
|
['model', data => data?.model],
|
|
@@ -32,6 +32,40 @@ const CODEX_MODEL_DIAGNOSTIC_PATHS = [
|
|
|
32
32
|
['message.model', data => data?.message?.model],
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
const createCodexTokenFieldAvailability = () => ({
|
|
36
|
+
inputTokens: false,
|
|
37
|
+
outputTokens: false,
|
|
38
|
+
reasoningTokens: false,
|
|
39
|
+
cacheReadTokens: false,
|
|
40
|
+
cacheWriteTokens: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const hasOwnPath = (object, pathName) => {
|
|
44
|
+
let cursor = object;
|
|
45
|
+
for (const part of pathName.split('.')) {
|
|
46
|
+
if (!cursor || typeof cursor !== 'object' || !Object.hasOwn(cursor, part)) return false;
|
|
47
|
+
cursor = cursor[part];
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getPathValue = (object, pathName) => pathName.split('.').reduce((cursor, part) => cursor?.[part], object);
|
|
53
|
+
|
|
54
|
+
const getFirstObservedNumber = (object, pathNames) => {
|
|
55
|
+
for (const pathName of pathNames) {
|
|
56
|
+
if (!hasOwnPath(object, pathName)) continue;
|
|
57
|
+
const value = getPathValue(object, pathName);
|
|
58
|
+
return Number.isFinite(value) ? value : 0;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const hasAnyObservedPath = (object, pathNames) => pathNames.some(pathName => hasOwnPath(object, pathName));
|
|
64
|
+
|
|
65
|
+
const CODEX_CACHE_READ_USAGE_PATHS = ['cached_input_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens'];
|
|
66
|
+
const CODEX_CACHE_WRITE_USAGE_PATHS = ['cache_write_tokens', 'cache_creation_input_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens'];
|
|
67
|
+
const CODEX_REASONING_USAGE_PATHS = ['reasoning_tokens', 'output_tokens_details.reasoning_tokens'];
|
|
68
|
+
|
|
35
69
|
export const createCodexTokenUsage = requestedModelId => ({
|
|
36
70
|
inputTokens: 0,
|
|
37
71
|
outputTokens: 0,
|
|
@@ -42,6 +76,7 @@ export const createCodexTokenUsage = requestedModelId => ({
|
|
|
42
76
|
stepCount: 0,
|
|
43
77
|
requestedModelId: requestedModelId || null,
|
|
44
78
|
respondedModelId: requestedModelId || null,
|
|
79
|
+
tokenFieldAvailability: createCodexTokenFieldAvailability(),
|
|
45
80
|
});
|
|
46
81
|
|
|
47
82
|
const createEmptyCodexItemUsage = () => ({
|
|
@@ -162,6 +197,7 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
162
197
|
observedModelDiagnosticPaths: state.observedModelDiagnosticPaths || [],
|
|
163
198
|
};
|
|
164
199
|
|
|
200
|
+
nextState.tokenUsage.tokenFieldAvailability ||= createCodexTokenFieldAvailability();
|
|
165
201
|
const observedModelPaths = new Set(nextState.observedModelDiagnosticPaths);
|
|
166
202
|
|
|
167
203
|
for (const rawLine of output.split('\n')) {
|
|
@@ -205,17 +241,28 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
205
241
|
}
|
|
206
242
|
|
|
207
243
|
if (eventType === 'turn.completed' && data.usage && typeof data.usage === 'object') {
|
|
208
|
-
const inputTokens =
|
|
209
|
-
const cachedInputTokens =
|
|
210
|
-
const
|
|
244
|
+
const inputTokens = getFirstObservedNumber(data.usage, ['input_tokens']);
|
|
245
|
+
const cachedInputTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_READ_USAGE_PATHS);
|
|
246
|
+
const cacheWriteTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS);
|
|
247
|
+
const outputTokens = getFirstObservedNumber(data.usage, ['output_tokens']);
|
|
248
|
+
const reasoningTokens = getFirstObservedNumber(data.usage, CODEX_REASONING_USAGE_PATHS);
|
|
249
|
+
|
|
250
|
+
if (hasOwnPath(data.usage, 'input_tokens')) nextState.tokenUsage.tokenFieldAvailability.inputTokens = true;
|
|
251
|
+
if (hasAnyObservedPath(data.usage, CODEX_CACHE_READ_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheReadTokens = true;
|
|
252
|
+
if (hasAnyObservedPath(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheWriteTokens = true;
|
|
253
|
+
if (hasOwnPath(data.usage, 'output_tokens')) nextState.tokenUsage.tokenFieldAvailability.outputTokens = true;
|
|
254
|
+
if (hasAnyObservedPath(data.usage, CODEX_REASONING_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.reasoningTokens = true;
|
|
255
|
+
|
|
211
256
|
const nonCachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
212
257
|
nextState.tokenUsage.inputTokens += nonCachedInputTokens;
|
|
213
258
|
nextState.tokenUsage.cacheReadTokens += cachedInputTokens;
|
|
259
|
+
nextState.tokenUsage.cacheWriteTokens += cacheWriteTokens;
|
|
214
260
|
nextState.tokenUsage.outputTokens += outputTokens;
|
|
261
|
+
nextState.tokenUsage.reasoningTokens += reasoningTokens;
|
|
215
262
|
nextState.tokenUsage.totalTokens = nextState.tokenUsage.inputTokens + nextState.tokenUsage.cacheReadTokens + nextState.tokenUsage.outputTokens + nextState.tokenUsage.cacheWriteTokens;
|
|
216
263
|
nextState.tokenUsage.stepCount += 1;
|
|
217
264
|
|
|
218
|
-
const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName =>
|
|
265
|
+
const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => hasOwnPath(data.usage, fieldName));
|
|
219
266
|
if (usageFieldSet.length > 0) nextState.observedUsageFieldSets.push(usageFieldSet);
|
|
220
267
|
}
|
|
221
268
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mjs';
|
|
7
7
|
import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.lib.mjs';
|
|
8
|
+
import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Build the user prompt for Codex
|
|
@@ -58,15 +59,9 @@ export const buildUserPrompt = params => {
|
|
|
58
59
|
promptLines.push('');
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
low: 'Think.',
|
|
65
|
-
medium: 'Think hard.',
|
|
66
|
-
high: 'Think harder.',
|
|
67
|
-
max: 'Ultrathink.',
|
|
68
|
-
};
|
|
69
|
-
promptLines.push(thinkMessages[argv.think]);
|
|
62
|
+
const thinkingPromptInstruction = getThinkingPromptInstruction({ tool: 'codex', argv });
|
|
63
|
+
if (thinkingPromptInstruction) {
|
|
64
|
+
promptLines.push(thinkingPromptInstruction);
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
// Final instruction
|
package/src/config.lib.mjs
CHANGED
|
@@ -178,11 +178,82 @@ export const isOpus46OrLater = model => {
|
|
|
178
178
|
if (!model) return false;
|
|
179
179
|
const normalizedModel = model.toLowerCase();
|
|
180
180
|
// Check for explicit opus-4-6 or later versions, or opusplan (Issue #1223)
|
|
181
|
-
// Note: The 'opus' alias now maps to Opus 4.
|
|
181
|
+
// Note: The 'opus' alias now maps to Opus 4.7 (Issue #1620), so we also check for the alias directly
|
|
182
182
|
// opusplan uses Opus for planning, so it should get Opus-level settings
|
|
183
183
|
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-5');
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
+
const isOpus47 = model => {
|
|
187
|
+
if (!model) return false;
|
|
188
|
+
const normalizedModel = model.toLowerCase();
|
|
189
|
+
// 'opus' alias now maps to Opus 4.7 (Issue #1620)
|
|
190
|
+
// opusplan uses Opus for planning, so it gets Opus-level settings
|
|
191
|
+
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-7');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if a model is Opus 4.7 or later (Issue #1620)
|
|
196
|
+
* These models use Opus 4.7+ adaptive thinking behavior.
|
|
197
|
+
* @param {string} model - The model name or ID
|
|
198
|
+
* @returns {boolean} True if the model is Opus 4.7 or later
|
|
199
|
+
*/
|
|
200
|
+
export const isOpus47OrLater = model => {
|
|
201
|
+
if (!model) return false;
|
|
202
|
+
const normalizedModel = model.toLowerCase();
|
|
203
|
+
return isOpus47(model) || normalizedModel.includes('opus-5');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const isOpus45 = model => {
|
|
207
|
+
if (!model) return false;
|
|
208
|
+
const m = model.toLowerCase();
|
|
209
|
+
return m === 'opus-4-5' || m.includes('opus-4-5');
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const isOpus46 = model => {
|
|
213
|
+
if (!model) return false;
|
|
214
|
+
const m = model.toLowerCase();
|
|
215
|
+
return m === 'opus-4-6' || m.includes('opus-4-6');
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const isSonnet46OrLater = model => {
|
|
219
|
+
if (!model) return false;
|
|
220
|
+
const m = model.toLowerCase();
|
|
221
|
+
return m === 'sonnet' || m === 'sonnet-4-6' || m.includes('sonnet-4-6') || m.includes('sonnet-5');
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const isMythosPreview = model => {
|
|
225
|
+
if (!model) return false;
|
|
226
|
+
return model.toLowerCase().includes('mythos');
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if a model supports CLAUDE_CODE_EFFORT_LEVEL (Issue #1238, Issue #1620)
|
|
231
|
+
* Official effort support: Claude Mythos Preview, Opus 4.7, Opus 4.6, Sonnet 4.6, and Opus 4.5.
|
|
232
|
+
* Haiku 4.5 and older models use MAX_THINKING_TOKENS only.
|
|
233
|
+
* @param {string} model - The model name or ID
|
|
234
|
+
* @returns {boolean} True if the model supports effort levels
|
|
235
|
+
*/
|
|
236
|
+
export const supportsEffortLevel = model => {
|
|
237
|
+
if (!model) return false;
|
|
238
|
+
return isMythosPreview(model) || isOpus47OrLater(model) || isOpus46(model) || isSonnet46OrLater(model) || isOpus45(model);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if a model supports the xhigh effort level.
|
|
243
|
+
* Official docs list xhigh only for Claude Opus 4.7.
|
|
244
|
+
* @param {string} model - The model name or ID
|
|
245
|
+
* @returns {boolean} True if the model supports xhigh effort
|
|
246
|
+
*/
|
|
247
|
+
export const supportsXHighEffortLevel = model => isOpus47(model);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if a model supports the max effort level.
|
|
251
|
+
* Official docs list max for Claude Mythos Preview, Opus 4.7, Opus 4.6, and Sonnet 4.6.
|
|
252
|
+
* @param {string} model - The model name or ID
|
|
253
|
+
* @returns {boolean} True if the model supports max effort
|
|
254
|
+
*/
|
|
255
|
+
export const supportsMaxEffortLevel = model => isMythosPreview(model) || isOpus47OrLater(model) || isOpus46(model) || isSonnet46OrLater(model);
|
|
256
|
+
|
|
186
257
|
/**
|
|
187
258
|
* Get the max output tokens for a specific model (Issue #1221)
|
|
188
259
|
* @param {string} model - The model name or ID
|
|
@@ -218,6 +289,7 @@ export const getThinkingLevelToTokens = (maxBudget = DEFAULT_MAX_THINKING_BUDGET
|
|
|
218
289
|
low: Math.floor(maxBudget / 4), // ~8000 for default 31999
|
|
219
290
|
medium: Math.floor(maxBudget / 2), // ~16000 for default 31999
|
|
220
291
|
high: Math.floor((maxBudget * 3) / 4), // ~24000 for default 31999
|
|
292
|
+
xhigh: maxBudget, // same as max when represented as MAX_THINKING_TOKENS
|
|
221
293
|
max: maxBudget, // 31999 by default
|
|
222
294
|
});
|
|
223
295
|
|
|
@@ -250,56 +322,73 @@ export const getTokensToThinkingLevel = (maxBudget = DEFAULT_MAX_THINKING_BUDGET
|
|
|
250
322
|
export const tokensToThinkingLevel = getTokensToThinkingLevel(DEFAULT_MAX_THINKING_BUDGET);
|
|
251
323
|
|
|
252
324
|
/**
|
|
253
|
-
* Valid effort levels for Opus 4.6 (Issue #1238)
|
|
254
|
-
*
|
|
325
|
+
* Valid effort levels for Opus 4.6 and Sonnet 4.6 (Issue #1238, Issue #1620)
|
|
326
|
+
* These models use CLAUDE_CODE_EFFORT_LEVEL for thinking depth control
|
|
327
|
+
* @type {string[]}
|
|
328
|
+
*/
|
|
329
|
+
export const OPUS_46_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Valid effort levels for Opus 4.7 (Issue #1620)
|
|
333
|
+
* Opus 4.7 supports the additional 'xhigh' level.
|
|
334
|
+
* See: https://platform.claude.com/docs/en/build-with-claude/effort
|
|
255
335
|
* @type {string[]}
|
|
256
336
|
*/
|
|
257
|
-
export const
|
|
337
|
+
export const OPUS_47_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
258
338
|
|
|
259
339
|
/**
|
|
260
|
-
* Convert thinking level to
|
|
261
|
-
* Opus 4.
|
|
262
|
-
*
|
|
263
|
-
* @
|
|
340
|
+
* Convert thinking level to effort level (Issue #1238, Issue #1620)
|
|
341
|
+
* Models with max support keep max as max. Opus 4.7 keeps xhigh as xhigh.
|
|
342
|
+
* Models with effort but without max support use high for max/xhigh.
|
|
343
|
+
* @param {string|undefined} thinkLevel - The thinking level (off/low/medium/high/xhigh/max)
|
|
344
|
+
* @param {Object} [options] - Options
|
|
345
|
+
* @param {boolean} [options.isOpus47] - Backward-compatible shorthand for supportsXHigh
|
|
346
|
+
* @param {boolean} [options.supportsXHigh] - Whether the model supports xhigh effort
|
|
347
|
+
* @param {boolean} [options.supportsMax] - Whether the model supports max effort
|
|
348
|
+
* @returns {string|undefined} The effort level or undefined if thinking is off
|
|
264
349
|
*/
|
|
265
|
-
export const thinkLevelToEffortLevel = thinkLevel => {
|
|
350
|
+
export const thinkLevelToEffortLevel = (thinkLevel, options = {}) => {
|
|
266
351
|
if (!thinkLevel || thinkLevel === 'off') {
|
|
267
|
-
// No effort level when thinking is disabled
|
|
268
352
|
return undefined;
|
|
269
353
|
}
|
|
270
354
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
355
|
+
const supportsXHigh = options.supportsXHigh ?? options.isOpus47 ?? false;
|
|
356
|
+
const supportsMax = options.supportsMax ?? true;
|
|
357
|
+
|
|
274
358
|
switch (thinkLevel) {
|
|
275
359
|
case 'low':
|
|
276
360
|
return 'low';
|
|
277
361
|
case 'medium':
|
|
278
362
|
return 'medium';
|
|
279
363
|
case 'high':
|
|
280
|
-
case 'max':
|
|
281
364
|
return 'high';
|
|
365
|
+
case 'xhigh':
|
|
366
|
+
return supportsXHigh ? 'xhigh' : supportsMax ? 'max' : 'high';
|
|
367
|
+
case 'max':
|
|
368
|
+
return supportsMax ? 'max' : 'high';
|
|
282
369
|
default:
|
|
283
370
|
return undefined;
|
|
284
371
|
}
|
|
285
372
|
};
|
|
286
373
|
|
|
287
374
|
/**
|
|
288
|
-
* Convert thinking budget (tokens) to
|
|
375
|
+
* Convert thinking budget (tokens) to effort level (Issue #1238, Issue #1620)
|
|
289
376
|
* Uses token thresholds to determine the appropriate effort level
|
|
290
377
|
* @param {number|undefined} thinkingBudget - The thinking budget in tokens
|
|
291
378
|
* @param {number} maxBudget - Maximum thinking budget (default: 31999)
|
|
292
|
-
* @
|
|
379
|
+
* @param {Object} [options] - Options
|
|
380
|
+
* @param {boolean} [options.isOpus47] - Backward-compatible shorthand for supportsXHigh
|
|
381
|
+
* @param {boolean} [options.supportsXHigh] - Whether the model supports xhigh effort
|
|
382
|
+
* @param {boolean} [options.supportsMax] - Whether the model supports max effort
|
|
383
|
+
* @returns {string|undefined} The effort level or undefined if thinking is off
|
|
293
384
|
*/
|
|
294
|
-
export const thinkingBudgetToEffortLevel = (thinkingBudget, maxBudget = DEFAULT_MAX_THINKING_BUDGET) => {
|
|
385
|
+
export const thinkingBudgetToEffortLevel = (thinkingBudget, maxBudget = DEFAULT_MAX_THINKING_BUDGET, options = {}) => {
|
|
295
386
|
if (thinkingBudget === undefined || thinkingBudget === 0) {
|
|
296
|
-
// No effort level when thinking is disabled
|
|
297
387
|
return undefined;
|
|
298
388
|
}
|
|
299
389
|
|
|
300
|
-
// Convert tokens to thinking level, then to effort level
|
|
301
390
|
const thinkLevel = getTokensToThinkingLevel(maxBudget)(thinkingBudget);
|
|
302
|
-
return thinkLevelToEffortLevel(thinkLevel);
|
|
391
|
+
return thinkLevelToEffortLevel(thinkLevel, options);
|
|
303
392
|
};
|
|
304
393
|
|
|
305
394
|
// Check if a version supports thinking budget (>= minimum version)
|
|
@@ -339,27 +428,40 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
339
428
|
MCP_TOOL_TIMEOUT: String(claudeCode.mcpToolTimeout),
|
|
340
429
|
};
|
|
341
430
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
431
|
+
// Opus 4.7+ always uses adaptive thinking — MAX_THINKING_TOKENS has no effect (Issue #1620)
|
|
432
|
+
// For Opus 4.6 and earlier, MAX_THINKING_TOKENS controls extended thinking (Claude Code >= 2.1.12)
|
|
433
|
+
// Default is 0 (thinking disabled) per Issue #1238.
|
|
434
|
+
const opus47 = options.model && isOpus47OrLater(options.model);
|
|
435
|
+
if (opus47) {
|
|
436
|
+
// Remove any inherited MAX_THINKING_TOKENS from process.env — Opus 4.7 ignores it
|
|
437
|
+
delete env.MAX_THINKING_TOKENS;
|
|
438
|
+
} else {
|
|
439
|
+
env.MAX_THINKING_TOKENS = String(options.thinkingBudget ?? 0);
|
|
440
|
+
}
|
|
346
441
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
442
|
+
// Set CLAUDE_CODE_EFFORT_LEVEL for models that support it (Issue #1238, Issue #1620)
|
|
443
|
+
if (options.model && supportsEffortLevel(options.model)) {
|
|
444
|
+
const effortOptions = {
|
|
445
|
+
supportsXHigh: supportsXHighEffortLevel(options.model),
|
|
446
|
+
supportsMax: supportsMaxEffortLevel(options.model),
|
|
447
|
+
};
|
|
352
448
|
let effortLevel;
|
|
353
449
|
if (options.thinkLevel) {
|
|
354
|
-
effortLevel = thinkLevelToEffortLevel(options.thinkLevel);
|
|
450
|
+
effortLevel = thinkLevelToEffortLevel(options.thinkLevel, effortOptions);
|
|
355
451
|
} else if (options.thinkingBudget !== undefined && options.thinkingBudget > 0) {
|
|
356
|
-
effortLevel = thinkingBudgetToEffortLevel(options.thinkingBudget, options.maxBudget);
|
|
452
|
+
effortLevel = thinkingBudgetToEffortLevel(options.thinkingBudget, options.maxBudget, effortOptions);
|
|
357
453
|
}
|
|
358
454
|
|
|
359
455
|
if (effortLevel) {
|
|
360
456
|
env.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
|
|
361
457
|
}
|
|
362
458
|
}
|
|
459
|
+
|
|
460
|
+
// Opus 4.7 omits thinking content by default; opt in with --show-thinking-content (Issue #1620)
|
|
461
|
+
// Sets CLAUDE_CODE_SHOW_THINKING=1 which Claude Code uses to request display: "summarized"
|
|
462
|
+
if (options.showThinkingContent) {
|
|
463
|
+
env.CLAUDE_CODE_SHOW_THINKING = '1';
|
|
464
|
+
}
|
|
363
465
|
// Set ANTHROPIC_DEFAULT_OPUS_MODEL when planModel is specified (Issue #1223)
|
|
364
466
|
// This tells Claude Code which model to use during plan mode in opusplan
|
|
365
467
|
if (options.planModel) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Decimal from 'decimal.js-light';
|
|
4
|
+
|
|
5
|
+
const formatTokenCount = value => (Number.isFinite(value) ? value : 0).toLocaleString();
|
|
6
|
+
|
|
7
|
+
const isObservedTokenField = (usage, fieldName) => {
|
|
8
|
+
const value = usage?.[fieldName];
|
|
9
|
+
if (Number.isFinite(value) && value > 0) return true;
|
|
10
|
+
if (usage?.tokenFieldAvailability?.[fieldName] === true) return true;
|
|
11
|
+
if (Array.isArray(usage?.availableTokenFields) && usage.availableTokenFields.includes(fieldName)) return true;
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const buildTokenUsageString = tokenUsage => {
|
|
16
|
+
const parts = [`${formatTokenCount(tokenUsage.inputTokens)} input`, `${formatTokenCount(tokenUsage.outputTokens)} output`];
|
|
17
|
+
if (isObservedTokenField(tokenUsage, 'reasoningTokens')) parts.push(`${formatTokenCount(tokenUsage.reasoningTokens)} reasoning`);
|
|
18
|
+
if (isObservedTokenField(tokenUsage, 'cacheReadTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheReadTokens)} cache read`);
|
|
19
|
+
if (isObservedTokenField(tokenUsage, 'cacheWriteTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheWriteTokens)} cache write`);
|
|
20
|
+
return `\n- Token usage: ${parts.join(', ')}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Build cost estimation string for log comments (Issue #1250, Issue #1557, Issue #1600: Decimal precision) */
|
|
24
|
+
export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
25
|
+
const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
|
|
26
|
+
const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
|
|
27
|
+
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
28
|
+
const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
|
|
29
|
+
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
30
|
+
const publicDec = hasPublic ? new Decimal(totalCostUSD) : null;
|
|
31
|
+
const anthropicDec = hasAnthropic ? new Decimal(anthropicTotalCostUSD) : null;
|
|
32
|
+
if (publicDec && anthropicDec && publicDec.toFixed(6) === anthropicDec.toFixed(6)) return `\n\n### 💰 Cost: **$${anthropicDec.toFixed(6)}**`;
|
|
33
|
+
let costInfo = '\n\n### 💰 **Cost estimation:**';
|
|
34
|
+
if (pricingInfo?.modelName) {
|
|
35
|
+
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
|
36
|
+
if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
|
|
37
|
+
}
|
|
38
|
+
if (hasPublic) {
|
|
39
|
+
if (pricingInfo?.isFreeModel && publicDec.eq(0) && !pricingInfo?.baseModelName) {
|
|
40
|
+
costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
|
|
41
|
+
} else {
|
|
42
|
+
let pricingRef = '';
|
|
43
|
+
if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
|
|
44
|
+
pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
|
|
45
|
+
} else if (pricingInfo?.originalProvider) {
|
|
46
|
+
pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
|
|
47
|
+
}
|
|
48
|
+
costInfo += `\n- Public pricing estimate: $${publicDec.toFixed(6)}${pricingRef}`;
|
|
49
|
+
}
|
|
50
|
+
} else if (hasPricing) {
|
|
51
|
+
costInfo += '\n- Public pricing estimate: unknown';
|
|
52
|
+
}
|
|
53
|
+
if (hasOpencodeCost) {
|
|
54
|
+
if (pricingInfo.isOpencodeFreeModel) {
|
|
55
|
+
costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
|
|
56
|
+
} else {
|
|
57
|
+
costInfo += `\n- Calculated by OpenCode Zen: $${new Decimal(pricingInfo.opencodeCost).toFixed(6)}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (pricingInfo?.tokenUsage) costInfo += buildTokenUsageString(pricingInfo.tokenUsage);
|
|
61
|
+
if (hasAnthropic) {
|
|
62
|
+
costInfo += `\n- Calculated by Anthropic: $${anthropicDec.toFixed(6)}`;
|
|
63
|
+
if (hasPublic) {
|
|
64
|
+
const diff = anthropicDec.minus(publicDec);
|
|
65
|
+
const pct = publicDec.gt(0) ? diff.div(publicDec).mul(100) : new Decimal(0);
|
|
66
|
+
costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct.gt(0) ? '+' : ''}${pct.toFixed(2)}%)`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return costInfo;
|
|
70
|
+
};
|