troxy-cli 1.29.1 → 1.29.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.29.1",
3
+ "version": "1.29.2",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
@@ -42,6 +42,11 @@ export function extractUsage(entry) {
42
42
  // dollar math server-side from the model id; this file only sums what
43
43
  // the transcript says was used.
44
44
  tokens: inputTokens + outputTokens + cacheCreation + cacheRead,
45
+ // Reply-only portion of the total above - sent alongside actual_tokens,
46
+ // never in place of it (server still bills off the combined total; this
47
+ // is purely so the activity log can show "X tokens (Y reply)" instead
48
+ // of one opaque combined number - Gilad, 2026-09-04).
49
+ outputTokens,
45
50
  // Lives on the transcript ENTRY itself, not inside message.usage -
46
51
  // confirmed live 2026-09-03 against a real session file (values seen:
47
52
  // 'high', 'max'). Whatever string Claude Code actually used, passed
@@ -128,7 +133,11 @@ export function usageForCurrentTurn(transcriptPath) {
128
133
  if (seen.size === 0) return null;
129
134
 
130
135
  let totalTokens = 0;
131
- for (const u of seen.values()) totalTokens += u.tokens;
136
+ let totalOutputTokens = 0;
137
+ for (const u of seen.values()) {
138
+ totalTokens += u.tokens;
139
+ totalOutputTokens += u.outputTokens || 0;
140
+ }
132
141
 
133
142
  // turn_key: dedups a hook that fires twice for the same turn (a retry,
134
143
  // a Claude Code internal replay) against a repeat POST for the same
@@ -151,8 +160,8 @@ export function usageForCurrentTurn(transcriptPath) {
151
160
  const contentExcerpt = replyText ? replyText.slice(0, MAX_EXCERPT_LEN) : null;
152
161
 
153
162
  return {
154
- tokens: totalTokens, model: lastModel, effort: lastEffort, toolUseBlocksSoFar,
155
- turnKey, contentExcerpt,
163
+ tokens: totalTokens, outputTokens: totalOutputTokens, model: lastModel,
164
+ effort: lastEffort, toolUseBlocksSoFar, turnKey, contentExcerpt,
156
165
  };
157
166
  }
158
167
 
@@ -202,6 +211,10 @@ export async function runHookReport() {
202
211
  const turnKey = sessionId ? `${sessionId}:${usage.turnKey}` : null;
203
212
 
204
213
  const body = { model: usage.model, actual_tokens: usage.tokens, turn_key: turnKey };
214
+ // Additive only - the backend already treats a missing/invalid value as
215
+ // "no breakdown available" and falls back to the plain total, so it's
216
+ // safe to just omit this rather than validate it client-side too.
217
+ if (usage.outputTokens > 0) body.actual_output_tokens = usage.outputTokens;
205
218
  // content_excerpt is optional and additive - the backend classifies it
206
219
  // if present, samples/rate-limits on its own, and silently skips when
207
220
  // it isn't. Sending it is not required for the usage report itself to
@@ -59,8 +59,9 @@ describe('extractUsage', () => {
59
59
  },
60
60
  };
61
61
  const result = extractUsage(entry);
62
- assert.deepEqual(Object.keys(result).sort(), ['effort', 'messageId', 'model', 'tokens']);
62
+ assert.deepEqual(Object.keys(result).sort(), ['effort', 'messageId', 'model', 'outputTokens', 'tokens']);
63
63
  assert.equal(result.effort, 'high');
64
+ assert.equal(result.outputTokens, 5);
64
65
  assert.ok(!JSON.stringify(result).includes('actual conversation'));
65
66
  });
66
67
 
@@ -108,6 +109,20 @@ describe('usageForCurrentTurn', () => {
108
109
  assert.equal(result.tokens, 10 + 20 + 5 + 8);
109
110
  });
110
111
 
112
+ it('sums outputTokens (reply-only) alongside tokens, same dedup-by-message-id rule', () => {
113
+ // Same fixture as the two-real-model-calls test above - outputTokens must
114
+ // dedup identically (20 + 8, not 20+20+8) since it comes off the same
115
+ // per-message.id `seen` map as the combined total.
116
+ writeLines([
117
+ { type: 'user', message: { content: [{ type: 'text', text: 'hi' }] } },
118
+ assistantLine('msg_1', { input_tokens: 10, output_tokens: 20 }),
119
+ assistantLine('msg_1', { input_tokens: 10, output_tokens: 20 }),
120
+ assistantLine('msg_2', { input_tokens: 5, output_tokens: 8 }),
121
+ ]);
122
+ const result = usageForCurrentTurn(transcriptPath());
123
+ assert.equal(result.outputTokens, 20 + 8, 'reply-only total did not dedup by message.id like tokens does');
124
+ });
125
+
111
126
  it('only looks back to the last user-authored line, not the whole session', () => {
112
127
  writeLines([
113
128
  { type: 'user', message: { content: [{ type: 'text', text: 'first turn' }] } },