@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.
@@ -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 / 1000000) * cost.input,
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 / 1000000) * cost.cache_write,
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 / 1000000) * cost.cache_read,
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 / 1000000) * cost.output,
471
+ cost: new Decimal(usage.outputTokens).div(million).mul(new Decimal(cost.output)).toNumber(),
473
472
  };
474
473
  }
475
- const totalCost = breakdown.input.cost + breakdown.cacheWrite.cost + breakdown.cacheRead.cost + breakdown.output.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 totalCostUSD = 0;
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
- totalCostUSD += usage.costUSD;
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 ? totalCostUSD : null,
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
- // Note: --think keywords are deprecated for Claude Code >= 2.1.12
69
- // Thinking is now enabled by default with 31,999 token budget
70
- // Use --thinking-budget to control MAX_THINKING_TOKENS instead
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 = Number.isFinite(data.usage.input_tokens) ? data.usage.input_tokens : 0;
209
- const cachedInputTokens = Number.isFinite(data.usage.cached_input_tokens) ? data.usage.cached_input_tokens : 0;
210
- const outputTokens = Number.isFinite(data.usage.output_tokens) ? data.usage.output_tokens : 0;
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 => Object.hasOwn(data.usage, 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
 
@@ -9,6 +9,7 @@ const THINK_LEVEL_TO_CODEX_REASONING = {
9
9
  low: 'low',
10
10
  medium: 'medium',
11
11
  high: 'high',
12
+ xhigh: 'xhigh',
12
13
  max: 'xhigh',
13
14
  };
14
15
 
@@ -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
- // Add thinking instruction based on --think level
62
- if (argv && argv.think) {
63
- const thinkMessages = {
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
@@ -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.6 (Issue #1433), so we also check for the alias directly
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
- * Opus 4.6 uses CLAUDE_CODE_EFFORT_LEVEL for thinking depth control
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 OPUS_46_EFFORT_LEVELS = ['low', 'medium', 'high'];
337
+ export const OPUS_47_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
258
338
 
259
339
  /**
260
- * Convert thinking level to Opus 4.6 effort level (Issue #1238)
261
- * Opus 4.6 uses CLAUDE_CODE_EFFORT_LEVEL (low/medium/high) instead of MAX_THINKING_TOKENS
262
- * @param {string|undefined} thinkLevel - The thinking level (off/low/medium/high/max)
263
- * @returns {string|undefined} The effort level (low/medium/high) or undefined if thinking is off
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
- // Map hive-mind thinking levels to Opus 4.6 effort levels
272
- // Note: Opus 4.6 only supports low/medium/high, not 'max'
273
- // We map 'max' to 'high' as it's the highest available level
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 Opus 4.6 effort level (Issue #1238)
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
- * @returns {string|undefined} The effort level (low/medium/high) or undefined if thinking is off
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
- // Set MAX_THINKING_TOKENS to control Claude Code's extended thinking feature (Claude Code >= 2.1.12)
343
- // Default is 0 (thinking disabled) per Issue #1238. Set to 0 to disable thinking.
344
- // Users can explicitly enable thinking via --think or --thinking-budget options.
345
- env.MAX_THINKING_TOKENS = String(options.thinkingBudget ?? 0);
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
- // For Opus 4.6+, also set CLAUDE_CODE_EFFORT_LEVEL to control thinking depth (Issue #1238)
348
- // Opus 4.6 uses effort level (low/medium/high) instead of MAX_THINKING_TOKENS for thinking depth.
349
- // MAX_THINKING_TOKENS is only used to disable thinking (when set to 0).
350
- if (options.model && isOpus46OrLater(options.model)) {
351
- // Convert thinkLevel or thinkingBudget to effort level
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
+ };