@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.52.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d5d3762: Fix calculation bugs and format unification for budget stats using decimal.js-light for precision.
|
|
8
|
+
|
|
9
|
+
## 1.52.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 5b24866: Add Claude Opus 4.7 model support with adaptive thinking, model-correct xhigh/max effort mapping, Opus 4.5/Mythos effort detection, and the --show-thinking-content option.
|
|
14
|
+
|
|
3
15
|
## 1.51.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"@sentry/node": "^10.15.0",
|
|
71
71
|
"@sentry/profiling-node": "^10.15.0",
|
|
72
72
|
"dayjs": "^1.11.19",
|
|
73
|
+
"decimal.js-light": "^2.5.1",
|
|
73
74
|
"secretlint": "^11.2.5",
|
|
74
75
|
"semver": "^7.7.3"
|
|
75
76
|
},
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Decimal from 'decimal.js-light';
|
|
4
|
+
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
5
|
+
|
|
6
|
+
export const createTokenFieldAvailability = () => ({
|
|
7
|
+
inputTokens: false,
|
|
8
|
+
outputTokens: false,
|
|
9
|
+
reasoningTokens: false,
|
|
10
|
+
cacheReadTokens: false,
|
|
11
|
+
cacheWriteTokens: false,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const createAgentTokenUsage = () => ({
|
|
15
|
+
inputTokens: 0,
|
|
16
|
+
outputTokens: 0,
|
|
17
|
+
reasoningTokens: 0,
|
|
18
|
+
cacheReadTokens: 0,
|
|
19
|
+
cacheWriteTokens: 0,
|
|
20
|
+
totalCost: 0,
|
|
21
|
+
stepCount: 0,
|
|
22
|
+
requestedModelId: null,
|
|
23
|
+
respondedModelId: null,
|
|
24
|
+
contextLimit: null,
|
|
25
|
+
outputLimit: null,
|
|
26
|
+
peakContextUsage: 0,
|
|
27
|
+
tokenFieldAvailability: createTokenFieldAvailability(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const addObservedTokenValue = (usage, source, sourceFieldName, targetFieldName) => {
|
|
31
|
+
if (!source || !Object.hasOwn(source, sourceFieldName)) return;
|
|
32
|
+
usage.tokenFieldAvailability ||= createTokenFieldAvailability();
|
|
33
|
+
usage.tokenFieldAvailability[targetFieldName] = true;
|
|
34
|
+
const value = source[sourceFieldName];
|
|
35
|
+
if (Number.isFinite(value)) usage[targetFieldName] = (usage[targetFieldName] || 0) + value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getTokenCount = value => (Number.isFinite(value) ? value : 0);
|
|
39
|
+
|
|
40
|
+
export const accumulateAgentStepFinishUsage = (usage, data) => {
|
|
41
|
+
if (!usage || data?.type !== 'step_finish' || !data.part?.tokens) return false;
|
|
42
|
+
|
|
43
|
+
const tokens = data.part.tokens;
|
|
44
|
+
usage.stepCount = (usage.stepCount || 0) + 1;
|
|
45
|
+
usage.tokenFieldAvailability ||= createTokenFieldAvailability();
|
|
46
|
+
|
|
47
|
+
addObservedTokenValue(usage, tokens, 'input', 'inputTokens');
|
|
48
|
+
addObservedTokenValue(usage, tokens, 'output', 'outputTokens');
|
|
49
|
+
addObservedTokenValue(usage, tokens, 'reasoning', 'reasoningTokens');
|
|
50
|
+
if (tokens.cache) {
|
|
51
|
+
addObservedTokenValue(usage, tokens.cache, 'read', 'cacheReadTokens');
|
|
52
|
+
addObservedTokenValue(usage, tokens.cache, 'write', 'cacheWriteTokens');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Number.isFinite(data.part.cost)) {
|
|
56
|
+
usage.totalCost = new Decimal(usage.totalCost || 0).plus(data.part.cost).toNumber();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (data.part.model) {
|
|
60
|
+
if (data.part.model.requestedModelID) usage.requestedModelId = data.part.model.requestedModelID;
|
|
61
|
+
if (data.part.model.respondedModelID) usage.respondedModelId = data.part.model.respondedModelID;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (data.part.context) {
|
|
65
|
+
if (data.part.context.contextLimit) usage.contextLimit = data.part.context.contextLimit;
|
|
66
|
+
if (data.part.context.outputLimit) usage.outputLimit = data.part.context.outputLimit;
|
|
67
|
+
const stepContextUsage = getTokenCount(tokens.input) + getTokenCount(tokens.cache?.read);
|
|
68
|
+
if (stepContextUsage > (usage.peakContextUsage || 0)) {
|
|
69
|
+
usage.peakContextUsage = stepContextUsage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse Agent/OpenCode NDJSON output to extract token usage from step_finish events.
|
|
78
|
+
* @param {string} output - Raw JSONL output from the command
|
|
79
|
+
* @returns {Object} Aggregated token usage and cost data
|
|
80
|
+
*/
|
|
81
|
+
export const parseAgentTokenUsage = output => {
|
|
82
|
+
const usage = createAgentTokenUsage();
|
|
83
|
+
|
|
84
|
+
for (const rawLine of output.split('\n')) {
|
|
85
|
+
const line = rawLine.trim();
|
|
86
|
+
if (!line || !line.startsWith('{')) continue;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
accumulateAgentStepFinishUsage(usage, sanitizeObjectStrings(JSON.parse(line)));
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return usage;
|
|
96
|
+
};
|
package/src/agent.lib.mjs
CHANGED
|
@@ -18,93 +18,17 @@ import { reportError } from './sentry.lib.mjs';
|
|
|
18
18
|
import { timeouts } from './config.lib.mjs';
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
20
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
21
|
+
import Decimal from 'decimal.js-light';
|
|
21
22
|
import { agentModels, defaultModels, freeToBaseModelMap } from './models/index.mjs';
|
|
23
|
+
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage } from './agent-token-usage.lib.mjs';
|
|
24
|
+
|
|
25
|
+
export { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage };
|
|
22
26
|
|
|
23
27
|
// Import pricing functions from claude.lib.mjs
|
|
24
28
|
// We reuse fetchModelInfo and checkModelVisionCapability to get data from models.dev API
|
|
25
29
|
const claudeLib = await import('./claude.lib.mjs');
|
|
26
30
|
const { fetchModelInfo, checkModelVisionCapability } = claudeLib;
|
|
27
31
|
|
|
28
|
-
/**
|
|
29
|
-
* Parse agent JSON output to extract token usage from step_finish events
|
|
30
|
-
* Agent outputs NDJSON (newline-delimited JSON) with step_finish events containing token data
|
|
31
|
-
* @param {string} output - Raw stdout output from agent command
|
|
32
|
-
* @returns {Object} Aggregated token usage and cost data
|
|
33
|
-
*/
|
|
34
|
-
export const parseAgentTokenUsage = output => {
|
|
35
|
-
const usage = {
|
|
36
|
-
inputTokens: 0,
|
|
37
|
-
outputTokens: 0,
|
|
38
|
-
reasoningTokens: 0,
|
|
39
|
-
cacheReadTokens: 0,
|
|
40
|
-
cacheWriteTokens: 0,
|
|
41
|
-
totalCost: 0,
|
|
42
|
-
stepCount: 0,
|
|
43
|
-
// Issue #1526: Track model and context info from step_finish events
|
|
44
|
-
requestedModelId: null,
|
|
45
|
-
respondedModelId: null,
|
|
46
|
-
contextLimit: null,
|
|
47
|
-
outputLimit: null,
|
|
48
|
-
peakContextUsage: 0, // Track peak context usage across steps
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Try to parse each line as JSON (agent outputs NDJSON format)
|
|
52
|
-
const lines = output.split('\n');
|
|
53
|
-
for (const line of lines) {
|
|
54
|
-
const trimmedLine = line.trim();
|
|
55
|
-
if (!trimmedLine || !trimmedLine.startsWith('{')) continue;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const parsed = sanitizeObjectStrings(JSON.parse(trimmedLine));
|
|
59
|
-
|
|
60
|
-
// Look for step_finish events which contain token usage
|
|
61
|
-
if (parsed.type === 'step_finish' && parsed.part?.tokens) {
|
|
62
|
-
const tokens = parsed.part.tokens;
|
|
63
|
-
usage.stepCount++;
|
|
64
|
-
|
|
65
|
-
// Add token counts
|
|
66
|
-
if (tokens.input) usage.inputTokens += tokens.input;
|
|
67
|
-
if (tokens.output) usage.outputTokens += tokens.output;
|
|
68
|
-
if (tokens.reasoning) usage.reasoningTokens += tokens.reasoning;
|
|
69
|
-
|
|
70
|
-
// Handle cache tokens (can be in different formats)
|
|
71
|
-
if (tokens.cache) {
|
|
72
|
-
if (tokens.cache.read) usage.cacheReadTokens += tokens.cache.read;
|
|
73
|
-
if (tokens.cache.write) usage.cacheWriteTokens += tokens.cache.write;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Add cost from step_finish (usually 0 for free models like grok-code)
|
|
77
|
-
if (parsed.part.cost !== undefined) {
|
|
78
|
-
usage.totalCost += parsed.part.cost;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Issue #1526: Extract model info from step_finish events
|
|
82
|
-
if (parsed.part.model) {
|
|
83
|
-
if (parsed.part.model.requestedModelID) usage.requestedModelId = parsed.part.model.requestedModelID;
|
|
84
|
-
if (parsed.part.model.respondedModelID) usage.respondedModelId = parsed.part.model.respondedModelID;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Issue #1526: Extract context limits and track peak context usage
|
|
88
|
-
if (parsed.part.context) {
|
|
89
|
-
if (parsed.part.context.contextLimit) usage.contextLimit = parsed.part.context.contextLimit;
|
|
90
|
-
if (parsed.part.context.outputLimit) usage.outputLimit = parsed.part.context.outputLimit;
|
|
91
|
-
// Track peak context usage: input_tokens (current request) is the context usage for this step
|
|
92
|
-
// The actual context used per request = input tokens + cache_read tokens for that request
|
|
93
|
-
const stepContextUsage = (tokens.input || 0) + (tokens.cache?.read || 0);
|
|
94
|
-
if (stepContextUsage > usage.peakContextUsage) {
|
|
95
|
-
usage.peakContextUsage = stepContextUsage;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// Skip lines that aren't valid JSON
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return usage;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
32
|
/**
|
|
109
33
|
* Helper function to get original provider name from provider identifier
|
|
110
34
|
* Used for calculating public pricing estimates based on original provider prices
|
|
@@ -221,13 +145,29 @@ export const calculateAgentPricing = async (modelId, tokenUsage) => {
|
|
|
221
145
|
// Calculate public pricing estimate based on original provider prices
|
|
222
146
|
// Prices are per 1M tokens, so divide by 1,000,000
|
|
223
147
|
// All priced components from models.dev: input, output, cache_read, cache_write, reasoning
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
148
|
+
const million = new Decimal(1_000_000);
|
|
149
|
+
const inputCost = new Decimal(tokenUsage.inputTokens)
|
|
150
|
+
.mul(cost.input || 0)
|
|
151
|
+
.div(million)
|
|
152
|
+
.toNumber();
|
|
153
|
+
const outputCost = new Decimal(tokenUsage.outputTokens)
|
|
154
|
+
.mul(cost.output || 0)
|
|
155
|
+
.div(million)
|
|
156
|
+
.toNumber();
|
|
157
|
+
const cacheReadCost = new Decimal(tokenUsage.cacheReadTokens)
|
|
158
|
+
.mul(cost.cache_read || 0)
|
|
159
|
+
.div(million)
|
|
160
|
+
.toNumber();
|
|
161
|
+
const cacheWriteCost = new Decimal(tokenUsage.cacheWriteTokens)
|
|
162
|
+
.mul(cost.cache_write || 0)
|
|
163
|
+
.div(million)
|
|
164
|
+
.toNumber();
|
|
165
|
+
const reasoningCost = new Decimal(tokenUsage.reasoningTokens)
|
|
166
|
+
.mul(cost.reasoning || 0)
|
|
167
|
+
.div(million)
|
|
168
|
+
.toNumber();
|
|
169
|
+
|
|
170
|
+
const totalCost = new Decimal(inputCost).plus(outputCost).plus(cacheReadCost).plus(cacheWriteCost).plus(reasoningCost).toNumber();
|
|
231
171
|
|
|
232
172
|
// Determine if this is a free model from OpenCode Zen or Kilo Gateway
|
|
233
173
|
// Models accessed via OpenCode Zen or Kilo Gateway are free, regardless of original provider pricing
|
|
@@ -576,52 +516,8 @@ export const executeAgentCommand = async params => {
|
|
|
576
516
|
let agentCompletedSuccessfully = false;
|
|
577
517
|
// Issue #1250: Accumulate token usage during streaming instead of parsing fullOutput later
|
|
578
518
|
// This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
|
|
579
|
-
const streamingTokenUsage =
|
|
580
|
-
|
|
581
|
-
outputTokens: 0,
|
|
582
|
-
reasoningTokens: 0,
|
|
583
|
-
cacheReadTokens: 0,
|
|
584
|
-
cacheWriteTokens: 0,
|
|
585
|
-
totalCost: 0,
|
|
586
|
-
stepCount: 0,
|
|
587
|
-
// Issue #1526: Track model and context info from step_finish events
|
|
588
|
-
requestedModelId: null,
|
|
589
|
-
respondedModelId: null,
|
|
590
|
-
contextLimit: null,
|
|
591
|
-
outputLimit: null,
|
|
592
|
-
peakContextUsage: 0,
|
|
593
|
-
};
|
|
594
|
-
// Helper to accumulate tokens from step_finish events during streaming
|
|
595
|
-
const accumulateTokenUsage = data => {
|
|
596
|
-
if (data.type === 'step_finish' && data.part?.tokens) {
|
|
597
|
-
const tokens = data.part.tokens;
|
|
598
|
-
streamingTokenUsage.stepCount++;
|
|
599
|
-
if (tokens.input) streamingTokenUsage.inputTokens += tokens.input;
|
|
600
|
-
if (tokens.output) streamingTokenUsage.outputTokens += tokens.output;
|
|
601
|
-
if (tokens.reasoning) streamingTokenUsage.reasoningTokens += tokens.reasoning;
|
|
602
|
-
if (tokens.cache) {
|
|
603
|
-
if (tokens.cache.read) streamingTokenUsage.cacheReadTokens += tokens.cache.read;
|
|
604
|
-
if (tokens.cache.write) streamingTokenUsage.cacheWriteTokens += tokens.cache.write;
|
|
605
|
-
}
|
|
606
|
-
if (data.part.cost !== undefined) {
|
|
607
|
-
streamingTokenUsage.totalCost += data.part.cost;
|
|
608
|
-
}
|
|
609
|
-
// Issue #1526: Extract model info from step_finish events
|
|
610
|
-
if (data.part.model) {
|
|
611
|
-
if (data.part.model.requestedModelID) streamingTokenUsage.requestedModelId = data.part.model.requestedModelID;
|
|
612
|
-
if (data.part.model.respondedModelID) streamingTokenUsage.respondedModelId = data.part.model.respondedModelID;
|
|
613
|
-
}
|
|
614
|
-
// Issue #1526: Extract context limits and track peak context usage
|
|
615
|
-
if (data.part.context) {
|
|
616
|
-
if (data.part.context.contextLimit) streamingTokenUsage.contextLimit = data.part.context.contextLimit;
|
|
617
|
-
if (data.part.context.outputLimit) streamingTokenUsage.outputLimit = data.part.context.outputLimit;
|
|
618
|
-
const stepContextUsage = (tokens.input || 0) + (tokens.cache?.read || 0);
|
|
619
|
-
if (stepContextUsage > streamingTokenUsage.peakContextUsage) {
|
|
620
|
-
streamingTokenUsage.peakContextUsage = stepContextUsage;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
};
|
|
519
|
+
const streamingTokenUsage = createAgentTokenUsage();
|
|
520
|
+
const accumulateTokenUsage = data => accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
625
521
|
|
|
626
522
|
for await (const chunk of execCommand.stream()) {
|
|
627
523
|
if (chunk.type === 'stdout') {
|
|
@@ -980,8 +876,10 @@ export const executeAgentCommand = async params => {
|
|
|
980
876
|
if (tokenUsage.reasoningTokens > 0) {
|
|
981
877
|
await log(` Reasoning tokens: ${tokenUsage.reasoningTokens.toLocaleString()}`);
|
|
982
878
|
}
|
|
983
|
-
if (tokenUsage.cacheReadTokens > 0 || tokenUsage.
|
|
879
|
+
if (tokenUsage.cacheReadTokens > 0 || tokenUsage.tokenFieldAvailability?.cacheReadTokens) {
|
|
984
880
|
await log(` Cache read: ${tokenUsage.cacheReadTokens.toLocaleString()}`);
|
|
881
|
+
}
|
|
882
|
+
if (tokenUsage.cacheWriteTokens > 0 || tokenUsage.tokenFieldAvailability?.cacheWriteTokens) {
|
|
985
883
|
await log(` Cache write: ${tokenUsage.cacheWriteTokens.toLocaleString()}`);
|
|
986
884
|
}
|
|
987
885
|
|
|
@@ -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 Agent
|
|
@@ -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: 'agent', argv });
|
|
63
|
+
if (thinkingPromptInstruction) {
|
|
64
|
+
promptLines.push(thinkingPromptInstruction);
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
// Final instruction
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Extracted from claude.lib.mjs to maintain file line limits
|
|
4
4
|
|
|
5
5
|
import { formatNumber } from './claude.lib.mjs';
|
|
6
|
+
import Decimal from 'decimal.js-light';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Helper: creates a fresh sub-session usage object for tracking tokens between compactification events
|
|
@@ -105,11 +106,11 @@ export const displayModelUsage = async (usage, log) => {
|
|
|
105
106
|
];
|
|
106
107
|
for (const { key, label } of types) {
|
|
107
108
|
if (breakdown[key].tokens > 0) {
|
|
108
|
-
await log(` ${label}: ${formatNumber(breakdown[key].tokens)} tokens × $${breakdown[key].costPerMillion}/M = $${breakdown[key].cost.toFixed(6)}`);
|
|
109
|
+
await log(` ${label}: ${formatNumber(breakdown[key].tokens)} tokens × $${breakdown[key].costPerMillion}/M = $${new Decimal(breakdown[key].cost).toFixed(6)}`);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
await log(' ─────────────────────────────────');
|
|
112
|
-
await log(` Total: $${usage.costUSD.toFixed(6)}`);
|
|
113
|
+
await log(` Total: $${new Decimal(usage.costUSD).toFixed(6)}`);
|
|
113
114
|
} else if (usage.modelInfo === null) {
|
|
114
115
|
await log('');
|
|
115
116
|
await log(' Cost: Not available (could not fetch pricing)');
|
|
@@ -126,18 +127,19 @@ export const displayModelUsage = async (usage, log) => {
|
|
|
126
127
|
export const displayCostComparison = async (publicCost, anthropicCost, log) => {
|
|
127
128
|
const hasPublic = publicCost !== null && publicCost !== undefined;
|
|
128
129
|
const hasAnthropic = anthropicCost !== null && anthropicCost !== undefined;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const publicDec = hasPublic ? new Decimal(publicCost) : null;
|
|
131
|
+
const anthropicDec = hasAnthropic ? new Decimal(anthropicCost) : null;
|
|
132
|
+
if (publicDec && anthropicDec && publicDec.toFixed(6) === anthropicDec.toFixed(6)) {
|
|
133
|
+
await log(`\n 💰 Cost: $${anthropicDec.toFixed(6)}`);
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
136
|
await log('\n 💰 Cost estimation:');
|
|
135
|
-
await log(` Public pricing estimate: ${
|
|
136
|
-
await log(` Calculated by Anthropic: ${
|
|
137
|
-
if (
|
|
138
|
-
const difference =
|
|
139
|
-
const percentDiff =
|
|
140
|
-
await log(` Difference: $${difference.toFixed(6)} (${percentDiff
|
|
137
|
+
await log(` Public pricing estimate: ${publicDec ? `$${publicDec.toFixed(6)}` : 'unknown'}`);
|
|
138
|
+
await log(` Calculated by Anthropic: ${anthropicDec ? `$${anthropicDec.toFixed(6)}` : 'unknown'}`);
|
|
139
|
+
if (publicDec && anthropicDec) {
|
|
140
|
+
const difference = anthropicDec.minus(publicDec);
|
|
141
|
+
const percentDiff = publicDec.gt(0) ? difference.div(publicDec).mul(100) : new Decimal(0);
|
|
142
|
+
await log(` Difference: $${difference.toFixed(6)} (${percentDiff.gt(0) ? '+' : ''}${percentDiff.toFixed(2)}%)`);
|
|
141
143
|
} else {
|
|
142
144
|
await log(' Difference: unknown');
|
|
143
145
|
}
|
|
@@ -169,11 +171,10 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
169
171
|
const peakContext = usage.peakContextUsage || 0;
|
|
170
172
|
|
|
171
173
|
if (hasMultipleSubSessions) {
|
|
174
|
+
// Issue #1600: Unified format — numbered list without "Context window:" prefix
|
|
172
175
|
for (let i = 0; i < subSessions.length; i++) {
|
|
173
176
|
const sub = subSessions[i];
|
|
174
177
|
const subPeak = sub.peakContextUsage || 0;
|
|
175
|
-
// Issue #1539: Only use peak per-request context for context window display.
|
|
176
|
-
// Issue #1547: Percentage before unit label: X / Y (Z%) input tokens
|
|
177
178
|
const parts = [];
|
|
178
179
|
if (contextLimit && subPeak > 0) {
|
|
179
180
|
const pct = ((subPeak / contextLimit) * 100).toFixed(0);
|
|
@@ -184,12 +185,10 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
184
185
|
parts.push(`${formatNumber(sub.outputTokens)} / ${formatNumber(outputLimit)} (${outPct}%) output tokens`);
|
|
185
186
|
}
|
|
186
187
|
if (parts.length > 0) {
|
|
187
|
-
await log(` ${i + 1}.
|
|
188
|
+
await log(` ${i + 1}. ${parts.join(', ')}`);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
} else if (peakContext > 0) {
|
|
191
|
-
// Single sub-session with known peak: single-line format
|
|
192
|
-
// Issue #1547: Percentage before unit label
|
|
193
192
|
const parts = [];
|
|
194
193
|
if (contextLimit) {
|
|
195
194
|
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
@@ -200,11 +199,9 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
200
199
|
parts.push(`${formatNumber(usage.outputTokens)} / ${formatNumber(outputLimit)} (${outPct}%) output tokens`);
|
|
201
200
|
}
|
|
202
201
|
if (parts.length > 0) {
|
|
203
|
-
await log(`
|
|
202
|
+
await log(` - ${parts.join(', ')}`);
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
|
-
// Issue #1539: When peakContextUsage is unknown, skip context window line entirely.
|
|
207
|
-
// Cumulative totals are shown on the Total line below — no duplication needed.
|
|
208
205
|
|
|
209
206
|
// Cumulative totals — single line
|
|
210
207
|
// Issue #1547: Parenthesized cached format and consistent output format
|
|
@@ -303,14 +300,13 @@ const formatTokensCompact = tokens => {
|
|
|
303
300
|
* @returns {string} Formatted sub-sessions string
|
|
304
301
|
*/
|
|
305
302
|
/**
|
|
306
|
-
* Issue #
|
|
307
|
-
* Each sub-session gets: "N.
|
|
303
|
+
* Issue #1600: Format sub-sessions list using numbered single-line format.
|
|
304
|
+
* Each sub-session gets: "N. X / Y (Z%) input tokens, A / B (W%) output tokens"
|
|
308
305
|
*/
|
|
309
306
|
const formatSubSessionsList = (subSessions, contextLimit, outputLimit) => {
|
|
310
307
|
let result = '';
|
|
311
308
|
for (let i = 0; i < subSessions.length; i++) {
|
|
312
309
|
const sub = subSessions[i];
|
|
313
|
-
// Issue #1539: Only use peak per-request context; skip context display when unknown
|
|
314
310
|
const subPeakContext = sub.peakContextUsage || 0;
|
|
315
311
|
result += formatContextOutputLine(subPeakContext, contextLimit, sub.outputTokens, outputLimit, `${i + 1}. `);
|
|
316
312
|
}
|
|
@@ -318,10 +314,7 @@ const formatSubSessionsList = (subSessions, contextLimit, outputLimit) => {
|
|
|
318
314
|
};
|
|
319
315
|
|
|
320
316
|
/**
|
|
321
|
-
* Issue #
|
|
322
|
-
* Issue #1539: Only show context window when peakContext > 0 (per-request peak known).
|
|
323
|
-
* When peakContext is 0 (unknown), context part is omitted to avoid misleading percentages.
|
|
324
|
-
* Format: "- Context window: X / Y input tokens (Z%), A / B output tokens (W%)"
|
|
317
|
+
* Issue #1600: Build a single-line context + output tokens string (unified format, no "Context window:" prefix).
|
|
325
318
|
* @param {number} peakContext - Peak context usage (0 if unknown — context display skipped)
|
|
326
319
|
* @param {number} contextLimit - Context window limit (null if unknown)
|
|
327
320
|
* @param {number} outputTokens - Output tokens used
|
|
@@ -331,22 +324,16 @@ const formatSubSessionsList = (subSessions, contextLimit, outputLimit) => {
|
|
|
331
324
|
*/
|
|
332
325
|
const formatContextOutputLine = (peakContext, contextLimit, outputTokens, outputLimit, prefix = '- ') => {
|
|
333
326
|
const parts = [];
|
|
334
|
-
if (contextLimit) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// skip context display. Cumulative totals across all requests are not valid
|
|
338
|
-
// context window metrics and produce impossible percentages (e.g. 250%).
|
|
339
|
-
if (peakContext > 0) {
|
|
340
|
-
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
341
|
-
parts.push(`${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)} (${pct}%) input tokens`);
|
|
342
|
-
}
|
|
327
|
+
if (contextLimit && peakContext > 0) {
|
|
328
|
+
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
329
|
+
parts.push(`${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)} (${pct}%) input tokens`);
|
|
343
330
|
}
|
|
344
331
|
if (outputLimit) {
|
|
345
332
|
const outPct = ((outputTokens / outputLimit) * 100).toFixed(0);
|
|
346
333
|
parts.push(`${formatTokensCompact(outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`);
|
|
347
334
|
}
|
|
348
335
|
if (parts.length === 0) return '';
|
|
349
|
-
return `\n${prefix}
|
|
336
|
+
return `\n${prefix}${parts.join(', ')}`;
|
|
350
337
|
};
|
|
351
338
|
|
|
352
339
|
/**
|
|
@@ -445,31 +432,37 @@ export const buildBudgetStatsString = (tokenUsage, subAgentCalls = null) => {
|
|
|
445
432
|
|
|
446
433
|
// Issue #1590: Check if this model was used as a sub-agent
|
|
447
434
|
const callCount = getSubAgentCallCount(modelId, subAgentCallCounts);
|
|
435
|
+
const isPrimaryModel = !isMultiModel || modelId === modelIds[0];
|
|
436
|
+
const showSubSessions = hasMultipleSubSessions && isPrimaryModel;
|
|
448
437
|
|
|
449
438
|
if (isMultiModel) {
|
|
450
439
|
// Issue #1590: Show sub-agent call count alongside model name
|
|
440
|
+
// Issue #1600: Show session segment count for primary model
|
|
451
441
|
if (callCount > 1) {
|
|
452
442
|
stats += `\n\n**${modelName}:** (${callCount} sub-agent calls)`;
|
|
443
|
+
} else if (showSubSessions) {
|
|
444
|
+
stats += `\n\n**${modelName}:** (${subSessions.length} session segments)`;
|
|
453
445
|
} else {
|
|
454
446
|
stats += `\n\n**${modelName}:**`;
|
|
455
447
|
}
|
|
448
|
+
} else if (showSubSessions) {
|
|
449
|
+
stats += `\n\n**${modelName}:** (${subSessions.length} session segments)`;
|
|
456
450
|
}
|
|
457
451
|
|
|
458
452
|
const peakContext = usage.peakContextUsage || 0;
|
|
459
453
|
|
|
460
|
-
if (
|
|
461
|
-
// Issue #
|
|
462
|
-
// For single-model sessions, show under that model. For multi-model, under the first model.
|
|
454
|
+
if (showSubSessions) {
|
|
455
|
+
// Issue #1600: Unified format — no "Context window:" prefix, same format as sub-agent calls
|
|
463
456
|
stats += formatSubSessionsList(subSessions, contextLimit, outputLimit);
|
|
464
457
|
} else if (peakContext > 0) {
|
|
465
|
-
// Issue #1526: Single line format for context window + output tokens
|
|
466
458
|
stats += formatContextOutputLine(peakContext, contextLimit, usage.outputTokens, outputLimit, '- ');
|
|
459
|
+
} else if (outputLimit && callCount <= 1) {
|
|
460
|
+
// Issue #1600: Show output-only detalization for sub-agent single sessions
|
|
461
|
+
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
462
|
+
stats += `\n- ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`;
|
|
467
463
|
}
|
|
468
|
-
// Issue #1539: When peakContextUsage is unknown, skip context window line entirely.
|
|
469
|
-
// Cumulative totals are shown on the Total line below — no duplication needed.
|
|
470
464
|
|
|
471
465
|
// Cumulative totals per model: input tokens + cached shown separately
|
|
472
|
-
// Issue #1547: Parenthesized cached format: (X + Y cached) input tokens
|
|
473
466
|
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
474
467
|
const cachedTokens = usage.cacheReadTokens;
|
|
475
468
|
let totalLine;
|
|
@@ -479,36 +472,25 @@ export const buildBudgetStatsString = (tokenUsage, subAgentCalls = null) => {
|
|
|
479
472
|
totalLine = `${formatTokensCompact(totalInputNonCached)} input tokens`;
|
|
480
473
|
}
|
|
481
474
|
|
|
482
|
-
// Issue #
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (peakContext === 0 && outputLimit) {
|
|
486
|
-
if (callCount > 1) {
|
|
487
|
-
// Show total output without percentage (percentage is misleading for aggregated sub-agent calls)
|
|
488
|
-
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
489
|
-
} else {
|
|
490
|
-
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
491
|
-
totalLine += `, ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`;
|
|
492
|
-
}
|
|
475
|
+
// Issue #1600: Output tokens on Total line — skip percentage if already shown above or aggregated
|
|
476
|
+
if (callCount > 1) {
|
|
477
|
+
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
493
478
|
} else {
|
|
494
479
|
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
495
480
|
}
|
|
496
481
|
|
|
497
|
-
// Issue #
|
|
482
|
+
// Issue #1600: Use Decimal for cost display precision
|
|
498
483
|
if (usage.costUSD !== null && usage.costUSD !== undefined) {
|
|
499
|
-
totalLine += `, $${usage.costUSD.toFixed(6)} cost`;
|
|
484
|
+
totalLine += `, $${new Decimal(usage.costUSD).toFixed(6)} cost`;
|
|
500
485
|
}
|
|
501
486
|
|
|
502
487
|
// Issue #1590: Show individual sub-agent call list when multiple calls exist
|
|
503
|
-
// Total line appears AFTER the sub-agent calls list (not before)
|
|
504
488
|
if (callCount > 1) {
|
|
505
489
|
const matchingCalls = getSubAgentCallsForModel(modelId, validSubAgentCalls);
|
|
506
|
-
// Issue #1590: Check if actual per-call usage data is available from parent_tool_use_id tracking
|
|
507
490
|
const hasActualUsage = matchingCalls.some(c => c.usage && (c.usage.inputTokens > 0 || c.usage.outputTokens > 0 || c.usage.cacheReadTokens > 0 || c.usage.cacheCreationTokens > 0));
|
|
508
491
|
|
|
509
492
|
stats += `\n\nSub-agent calls:`;
|
|
510
493
|
if (hasActualUsage) {
|
|
511
|
-
// Show actual per-call usage with limits and percentages (same format as sub-sessions)
|
|
512
494
|
for (let i = 0; i < matchingCalls.length; i++) {
|
|
513
495
|
const call = matchingCalls[i];
|
|
514
496
|
const cu = call.usage || {};
|
|
@@ -530,7 +512,6 @@ export const buildBudgetStatsString = (tokenUsage, subAgentCalls = null) => {
|
|
|
530
512
|
stats += `\n${i + 1}. ${parts.join(', ')}`;
|
|
531
513
|
}
|
|
532
514
|
} else {
|
|
533
|
-
// Fallback: show estimates with limits and percentages when actual per-call data is not available
|
|
534
515
|
const avgInput = Math.round((totalInputNonCached + cachedTokens) / callCount);
|
|
535
516
|
const avgOutput = Math.round(usage.outputTokens / callCount);
|
|
536
517
|
for (let i = 0; i < matchingCalls.length; i++) {
|
|
@@ -549,7 +530,6 @@ export const buildBudgetStatsString = (tokenUsage, subAgentCalls = null) => {
|
|
|
549
530
|
}
|
|
550
531
|
stats += `\n${i + 1}. ${parts.join(', ')}`;
|
|
551
532
|
}
|
|
552
|
-
// Note about estimates only when using fallback
|
|
553
533
|
stats += `\n\n_Per-call values are estimates (total ÷ ${callCount}). Exact per-call breakdown requires [upstream support](https://github.com/anthropics/claude-code/issues/46520)._`;
|
|
554
534
|
}
|
|
555
535
|
}
|