@link-assistant/hive-mind 1.52.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 +6 -0
- package/package.json +2 -1
- package/src/agent-token-usage.lib.mjs +96 -0
- package/src/agent.lib.mjs +32 -134
- package/src/claude.budget-stats.lib.mjs +41 -61
- package/src/claude.lib.mjs +11 -13
- package/src/codex.lib.mjs +52 -5
- package/src/github-cost-info.lib.mjs +70 -0
- package/src/github.lib.mjs +2 -54
- package/src/opencode.lib.mjs +47 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.52.
|
|
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
|
|
|
@@ -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
|
}
|
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
|
|
@@ -429,51 +430,48 @@ export const checkModelVisionCapability = async modelId => {
|
|
|
429
430
|
return false;
|
|
430
431
|
}
|
|
431
432
|
};
|
|
432
|
-
/** 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) */
|
|
433
434
|
export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) => {
|
|
434
435
|
if (!modelInfo || !modelInfo.cost) {
|
|
435
436
|
return includeBreakdown ? { total: 0, breakdown: null } : 0;
|
|
436
437
|
}
|
|
437
438
|
const cost = modelInfo.cost;
|
|
439
|
+
const million = new Decimal(1000000);
|
|
438
440
|
const breakdown = {
|
|
439
441
|
input: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
440
442
|
cacheWrite: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
441
443
|
cacheRead: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
442
444
|
output: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
443
445
|
};
|
|
444
|
-
// Input tokens cost (per million tokens)
|
|
445
446
|
if (usage.inputTokens && cost.input) {
|
|
446
447
|
breakdown.input = {
|
|
447
448
|
tokens: usage.inputTokens,
|
|
448
449
|
costPerMillion: cost.input,
|
|
449
|
-
cost: (usage.inputTokens
|
|
450
|
+
cost: new Decimal(usage.inputTokens).div(million).mul(new Decimal(cost.input)).toNumber(),
|
|
450
451
|
};
|
|
451
452
|
}
|
|
452
|
-
// Cache creation tokens cost
|
|
453
453
|
if (usage.cacheCreationTokens && cost.cache_write) {
|
|
454
454
|
breakdown.cacheWrite = {
|
|
455
455
|
tokens: usage.cacheCreationTokens,
|
|
456
456
|
costPerMillion: cost.cache_write,
|
|
457
|
-
cost: (usage.cacheCreationTokens
|
|
457
|
+
cost: new Decimal(usage.cacheCreationTokens).div(million).mul(new Decimal(cost.cache_write)).toNumber(),
|
|
458
458
|
};
|
|
459
459
|
}
|
|
460
|
-
// Cache read tokens cost
|
|
461
460
|
if (usage.cacheReadTokens && cost.cache_read) {
|
|
462
461
|
breakdown.cacheRead = {
|
|
463
462
|
tokens: usage.cacheReadTokens,
|
|
464
463
|
costPerMillion: cost.cache_read,
|
|
465
|
-
cost: (usage.cacheReadTokens
|
|
464
|
+
cost: new Decimal(usage.cacheReadTokens).div(million).mul(new Decimal(cost.cache_read)).toNumber(),
|
|
466
465
|
};
|
|
467
466
|
}
|
|
468
|
-
// Output tokens cost
|
|
469
467
|
if (usage.outputTokens && cost.output) {
|
|
470
468
|
breakdown.output = {
|
|
471
469
|
tokens: usage.outputTokens,
|
|
472
470
|
costPerMillion: cost.output,
|
|
473
|
-
cost: (usage.outputTokens
|
|
471
|
+
cost: new Decimal(usage.outputTokens).div(million).mul(new Decimal(cost.output)).toNumber(),
|
|
474
472
|
};
|
|
475
473
|
}
|
|
476
|
-
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();
|
|
477
475
|
if (includeBreakdown) {
|
|
478
476
|
return {
|
|
479
477
|
total: totalCost,
|
|
@@ -619,7 +617,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
619
617
|
let totalCacheCreationTokens = 0;
|
|
620
618
|
let totalCacheReadTokens = 0;
|
|
621
619
|
let totalOutputTokens = 0;
|
|
622
|
-
let
|
|
620
|
+
let totalCostDecimal = new Decimal(0);
|
|
623
621
|
let hasCostData = false;
|
|
624
622
|
for (const usage of Object.values(modelUsage)) {
|
|
625
623
|
totalInputTokens += usage.inputTokens;
|
|
@@ -627,7 +625,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
627
625
|
totalCacheReadTokens += usage.cacheReadTokens;
|
|
628
626
|
totalOutputTokens += usage.outputTokens;
|
|
629
627
|
if (usage.costUSD !== null) {
|
|
630
|
-
|
|
628
|
+
totalCostDecimal = totalCostDecimal.plus(new Decimal(usage.costUSD));
|
|
631
629
|
hasCostData = true;
|
|
632
630
|
}
|
|
633
631
|
}
|
|
@@ -642,7 +640,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
642
640
|
cacheReadTokens: totalCacheReadTokens,
|
|
643
641
|
outputTokens: totalOutputTokens,
|
|
644
642
|
totalTokens,
|
|
645
|
-
totalCostUSD: hasCostData ?
|
|
643
|
+
totalCostUSD: hasCostData ? totalCostDecimal.toNumber() : null,
|
|
646
644
|
// Issue #1501: Peak context usage (max single-request fill) and dedup stats
|
|
647
645
|
peakContextUsage: globalPeakContext,
|
|
648
646
|
duplicateEntriesSkipped: duplicateCount,
|
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
|
|
|
@@ -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
|
+
};
|
package/src/github.lib.mjs
CHANGED
|
@@ -14,60 +14,8 @@ import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: htt
|
|
|
14
14
|
import { getToolDisplayName, getModelInfoForComment } from './models/index.mjs';
|
|
15
15
|
export { getToolDisplayName }; // Re-export for use by other modules
|
|
16
16
|
import { buildBudgetStatsString } from './claude.budget-stats.lib.mjs';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
20
|
-
const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
|
|
21
|
-
const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
|
|
22
|
-
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
23
|
-
const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
|
|
24
|
-
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
25
|
-
// Issue #1557: Simplified display when public and Anthropic costs match
|
|
26
|
-
if (hasPublic && hasAnthropic && totalCostUSD.toFixed(6) === anthropicTotalCostUSD.toFixed(6)) return `\n\n### š° Cost: **$${anthropicTotalCostUSD.toFixed(6)}**`;
|
|
27
|
-
let costInfo = '\n\n### š° **Cost estimation:**';
|
|
28
|
-
if (pricingInfo?.modelName) {
|
|
29
|
-
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
|
30
|
-
if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
|
|
31
|
-
}
|
|
32
|
-
if (hasPublic) {
|
|
33
|
-
if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
|
|
34
|
-
costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
|
|
35
|
-
} else {
|
|
36
|
-
let pricingRef = '';
|
|
37
|
-
if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
|
|
38
|
-
pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
|
|
39
|
-
} else if (pricingInfo?.originalProvider) {
|
|
40
|
-
pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
|
|
41
|
-
}
|
|
42
|
-
costInfo += `\n- Public pricing estimate: $${totalCostUSD.toFixed(6)}${pricingRef}`;
|
|
43
|
-
}
|
|
44
|
-
} else if (hasPricing) {
|
|
45
|
-
costInfo += '\n- Public pricing estimate: unknown';
|
|
46
|
-
}
|
|
47
|
-
if (hasOpencodeCost) {
|
|
48
|
-
if (pricingInfo.isOpencodeFreeModel) {
|
|
49
|
-
costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
|
|
50
|
-
} else {
|
|
51
|
-
costInfo += `\n- Calculated by OpenCode Zen: $${pricingInfo.opencodeCost.toFixed(6)}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (pricingInfo?.tokenUsage) {
|
|
55
|
-
const u = pricingInfo.tokenUsage;
|
|
56
|
-
let tokenInfo = `\n- Token usage: ${u.inputTokens?.toLocaleString() || 0} input, ${u.outputTokens?.toLocaleString() || 0} output`;
|
|
57
|
-
if (u.reasoningTokens > 0) tokenInfo += `, ${u.reasoningTokens.toLocaleString()} reasoning`;
|
|
58
|
-
if (u.cacheReadTokens > 0 || u.cacheWriteTokens > 0) tokenInfo += `, ${u.cacheReadTokens?.toLocaleString() || 0} cache read, ${u.cacheWriteTokens?.toLocaleString() || 0} cache write`;
|
|
59
|
-
costInfo += tokenInfo;
|
|
60
|
-
}
|
|
61
|
-
if (hasAnthropic) {
|
|
62
|
-
costInfo += `\n- Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)}`;
|
|
63
|
-
if (hasPublic) {
|
|
64
|
-
const diff = anthropicTotalCostUSD - totalCostUSD;
|
|
65
|
-
const pct = totalCostUSD > 0 ? (diff / totalCostUSD) * 100 : 0;
|
|
66
|
-
costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct > 0 ? '+' : ''}${pct.toFixed(2)}%)`;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return costInfo;
|
|
70
|
-
};
|
|
17
|
+
import { buildCostInfoString } from './github-cost-info.lib.mjs';
|
|
18
|
+
export { buildCostInfoString };
|
|
71
19
|
export const maskGitHubToken = maskToken; // Alias for backward compatibility
|
|
72
20
|
export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
|
|
73
21
|
export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -19,6 +19,10 @@ 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
21
|
import { opencodeModels, defaultModels } from './models/index.mjs';
|
|
22
|
+
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
|
|
23
|
+
import { calculateAgentPricing } from './agent.lib.mjs';
|
|
24
|
+
|
|
25
|
+
export { parseOpenCodeTokenUsage };
|
|
22
26
|
|
|
23
27
|
// Model mapping to translate aliases to full model IDs for OpenCode
|
|
24
28
|
// Issue #1473: Uses centralized opencodeModels from models/index.mjs (single source of truth)
|
|
@@ -242,6 +246,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
242
246
|
|
|
243
247
|
// Map model alias to full ID
|
|
244
248
|
const mappedModel = mapModelToId(argv.model);
|
|
249
|
+
const streamingTokenUsage = createAgentTokenUsage();
|
|
245
250
|
|
|
246
251
|
// Build opencode command arguments
|
|
247
252
|
let opencodeArgs = `run --format json --model ${mappedModel}`;
|
|
@@ -268,6 +273,15 @@ export const executeOpenCodeCommand = async params => {
|
|
|
268
273
|
await log(`${fullCommand}`);
|
|
269
274
|
await log('');
|
|
270
275
|
|
|
276
|
+
const buildPricingInfo = async () => {
|
|
277
|
+
const tokenUsage = streamingTokenUsage;
|
|
278
|
+
if (tokenUsage.stepCount === 0) {
|
|
279
|
+
return { tokenUsage, pricingInfo: null, publicPricingEstimate: null };
|
|
280
|
+
}
|
|
281
|
+
const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
|
|
282
|
+
return { tokenUsage, pricingInfo, publicPricingEstimate: pricingInfo?.totalCostUSD ?? null };
|
|
283
|
+
};
|
|
284
|
+
|
|
271
285
|
try {
|
|
272
286
|
// Pipe the prompt file to opencode via stdin
|
|
273
287
|
if (argv.resume) {
|
|
@@ -313,6 +327,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
313
327
|
for (const line of lines) {
|
|
314
328
|
if (!line.trim()) continue;
|
|
315
329
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
330
|
+
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
316
331
|
// Track text content for result summary
|
|
317
332
|
// OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
|
|
318
333
|
if (data.type === 'text' && data.text) {
|
|
@@ -355,6 +370,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
355
370
|
for (const line of lines) {
|
|
356
371
|
if (!line.trim()) continue;
|
|
357
372
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
373
|
+
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
358
374
|
if (data.type === 'text' && data.text) {
|
|
359
375
|
lastTextContent = data.text;
|
|
360
376
|
} else if (data.type === 'assistant' && data.message?.content) {
|
|
@@ -427,12 +443,14 @@ export const executeOpenCodeCommand = async params => {
|
|
|
427
443
|
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
428
444
|
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
429
445
|
|
|
446
|
+
const pricingResult = await buildPricingInfo();
|
|
430
447
|
return {
|
|
431
448
|
success: false,
|
|
432
449
|
sessionId,
|
|
433
450
|
limitReached: false,
|
|
434
451
|
limitResetTime: null,
|
|
435
452
|
permissionPromptDetected: true,
|
|
453
|
+
...pricingResult,
|
|
436
454
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
437
455
|
};
|
|
438
456
|
}
|
|
@@ -466,17 +484,41 @@ export const executeOpenCodeCommand = async params => {
|
|
|
466
484
|
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
467
485
|
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
468
486
|
|
|
487
|
+
const pricingResult = await buildPricingInfo();
|
|
469
488
|
return {
|
|
470
489
|
success: false,
|
|
471
490
|
sessionId,
|
|
472
491
|
limitReached,
|
|
473
492
|
limitResetTime,
|
|
493
|
+
...pricingResult,
|
|
474
494
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
475
495
|
};
|
|
476
496
|
}
|
|
477
497
|
|
|
478
498
|
await log('\n\nā
OpenCode command completed');
|
|
479
499
|
|
|
500
|
+
const pricingResult = await buildPricingInfo();
|
|
501
|
+
if (pricingResult.tokenUsage.stepCount > 0) {
|
|
502
|
+
await log('\nš° Token Usage Summary:');
|
|
503
|
+
await log(` š ${pricingResult.pricingInfo?.modelName || mappedModel} (${pricingResult.tokenUsage.stepCount} steps):`);
|
|
504
|
+
await log(` Input tokens: ${pricingResult.tokenUsage.inputTokens.toLocaleString()}`);
|
|
505
|
+
await log(` Output tokens: ${pricingResult.tokenUsage.outputTokens.toLocaleString()}`);
|
|
506
|
+
if (pricingResult.tokenUsage.reasoningTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.reasoningTokens) {
|
|
507
|
+
await log(` Reasoning tokens: ${pricingResult.tokenUsage.reasoningTokens.toLocaleString()}`);
|
|
508
|
+
}
|
|
509
|
+
if (pricingResult.tokenUsage.cacheReadTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheReadTokens) {
|
|
510
|
+
await log(` Cache read: ${pricingResult.tokenUsage.cacheReadTokens.toLocaleString()}`);
|
|
511
|
+
}
|
|
512
|
+
if (pricingResult.tokenUsage.cacheWriteTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheWriteTokens) {
|
|
513
|
+
await log(` Cache write: ${pricingResult.tokenUsage.cacheWriteTokens.toLocaleString()}`);
|
|
514
|
+
}
|
|
515
|
+
if (pricingResult.pricingInfo?.totalCostUSD !== null && pricingResult.pricingInfo?.totalCostUSD !== undefined) {
|
|
516
|
+
await log(` Public pricing estimate: $${pricingResult.pricingInfo.totalCostUSD.toFixed(6)}`);
|
|
517
|
+
} else {
|
|
518
|
+
await log(' Cost: Not available (could not fetch pricing)');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
480
522
|
// Issue #1263: Log if result summary was captured
|
|
481
523
|
if (lastTextContent) {
|
|
482
524
|
await log('š Captured result summary from OpenCode output', { verbose: true });
|
|
@@ -487,6 +529,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
487
529
|
sessionId,
|
|
488
530
|
limitReached,
|
|
489
531
|
limitResetTime,
|
|
532
|
+
...pricingResult,
|
|
490
533
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
491
534
|
};
|
|
492
535
|
} catch (error) {
|
|
@@ -510,6 +553,9 @@ export const executeOpenCodeCommand = async params => {
|
|
|
510
553
|
sessionId: null,
|
|
511
554
|
limitReached: false,
|
|
512
555
|
limitResetTime: null,
|
|
556
|
+
tokenUsage: streamingTokenUsage.stepCount > 0 ? streamingTokenUsage : null,
|
|
557
|
+
pricingInfo: null,
|
|
558
|
+
publicPricingEstimate: null,
|
|
513
559
|
resultSummary: null, // Issue #1263: No result summary available on error
|
|
514
560
|
};
|
|
515
561
|
}
|
|
@@ -610,4 +656,5 @@ export default {
|
|
|
610
656
|
executeOpenCode,
|
|
611
657
|
executeOpenCodeCommand,
|
|
612
658
|
checkForUncommittedChanges,
|
|
659
|
+
parseOpenCodeTokenUsage,
|
|
613
660
|
};
|