@iinm/plain-agent 1.4.1 → 1.5.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.
@@ -140,6 +140,16 @@
140
140
  "max_tokens": 32768,
141
141
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
142
142
  }
143
+ },
144
+ "cost": {
145
+ "currency": "USD",
146
+ "unit": "1M",
147
+ "costs": {
148
+ "input_tokens": 1.0,
149
+ "output_tokens": 5.0,
150
+ "cache_read_input_tokens": 0.1,
151
+ "cache_creation_input_tokens": 1.25
152
+ }
143
153
  }
144
154
  },
145
155
  {
@@ -157,6 +167,16 @@
157
167
  "max_tokens": 64000,
158
168
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
159
169
  }
170
+ },
171
+ "cost": {
172
+ "currency": "USD",
173
+ "unit": "1M",
174
+ "costs": {
175
+ "input_tokens": 1.0,
176
+ "output_tokens": 5.0,
177
+ "cache_read_input_tokens": 0.1,
178
+ "cache_creation_input_tokens": 1.25
179
+ }
160
180
  }
161
181
  },
162
182
  {
@@ -174,6 +194,16 @@
174
194
  "max_tokens": 32768,
175
195
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
176
196
  }
197
+ },
198
+ "cost": {
199
+ "currency": "USD",
200
+ "unit": "1M",
201
+ "costs": {
202
+ "input_tokens": 3,
203
+ "output_tokens": 15,
204
+ "cache_read_input_tokens": 0.3,
205
+ "cache_creation_input_tokens": 3.75
206
+ }
177
207
  }
178
208
  },
179
209
  {
@@ -191,6 +221,16 @@
191
221
  "max_tokens": 64000,
192
222
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
193
223
  }
224
+ },
225
+ "cost": {
226
+ "currency": "USD",
227
+ "unit": "1M",
228
+ "costs": {
229
+ "input_tokens": 3,
230
+ "output_tokens": 15,
231
+ "cache_read_input_tokens": 0.3,
232
+ "cache_creation_input_tokens": 3.75
233
+ }
194
234
  }
195
235
  },
196
236
  {
@@ -208,6 +248,16 @@
208
248
  "max_tokens": 32768,
209
249
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
210
250
  }
251
+ },
252
+ "cost": {
253
+ "currency": "USD",
254
+ "unit": "1M",
255
+ "costs": {
256
+ "input_tokens": 5,
257
+ "output_tokens": 25,
258
+ "cache_read_input_tokens": 0.5,
259
+ "cache_creation_input_tokens": 6.25
260
+ }
211
261
  }
212
262
  },
213
263
  {
@@ -225,6 +275,16 @@
225
275
  "max_tokens": 64000,
226
276
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
227
277
  }
278
+ },
279
+ "cost": {
280
+ "currency": "USD",
281
+ "unit": "1M",
282
+ "costs": {
283
+ "input_tokens": 5,
284
+ "output_tokens": 25,
285
+ "cache_read_input_tokens": 0.5,
286
+ "cache_creation_input_tokens": 6.25
287
+ }
228
288
  }
229
289
  },
230
290
 
@@ -242,6 +302,16 @@
242
302
  "max_tokens": 32768,
243
303
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
244
304
  }
305
+ },
306
+ "cost": {
307
+ "currency": "USD",
308
+ "unit": "1M",
309
+ "costs": {
310
+ "input_tokens": 1.0,
311
+ "output_tokens": 5.0,
312
+ "cache_read_input_tokens": 0.1,
313
+ "cache_creation_input_tokens": 1.25
314
+ }
245
315
  }
246
316
  },
247
317
  {
@@ -258,6 +328,16 @@
258
328
  "max_tokens": 64000,
259
329
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
260
330
  }
331
+ },
332
+ "cost": {
333
+ "currency": "USD",
334
+ "unit": "1M",
335
+ "costs": {
336
+ "input_tokens": 1.0,
337
+ "output_tokens": 5.0,
338
+ "cache_read_input_tokens": 0.1,
339
+ "cache_creation_input_tokens": 1.25
340
+ }
261
341
  }
262
342
  },
263
343
  {
@@ -274,6 +354,16 @@
274
354
  "max_tokens": 32768,
275
355
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
276
356
  }
357
+ },
358
+ "cost": {
359
+ "currency": "USD",
360
+ "unit": "1M",
361
+ "costs": {
362
+ "input_tokens": 3,
363
+ "output_tokens": 15,
364
+ "cache_read_input_tokens": 0.3,
365
+ "cache_creation_input_tokens": 3.75
366
+ }
277
367
  }
278
368
  },
279
369
  {
@@ -290,6 +380,16 @@
290
380
  "max_tokens": 64000,
291
381
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
292
382
  }
383
+ },
384
+ "cost": {
385
+ "currency": "USD",
386
+ "unit": "1M",
387
+ "costs": {
388
+ "input_tokens": 3,
389
+ "output_tokens": 15,
390
+ "cache_read_input_tokens": 0.3,
391
+ "cache_creation_input_tokens": 3.75
392
+ }
293
393
  }
294
394
  },
295
395
  {
@@ -306,6 +406,16 @@
306
406
  "max_tokens": 32768,
307
407
  "thinking": { "type": "enabled", "budget_tokens": 16384 }
308
408
  }
409
+ },
410
+ "cost": {
411
+ "currency": "USD",
412
+ "unit": "1M",
413
+ "costs": {
414
+ "input_tokens": 5,
415
+ "output_tokens": 25,
416
+ "cache_read_input_tokens": 0.5,
417
+ "cache_creation_input_tokens": 6.25
418
+ }
309
419
  }
310
420
  },
311
421
  {
@@ -322,6 +432,16 @@
322
432
  "max_tokens": 64000,
323
433
  "thinking": { "type": "enabled", "budget_tokens": 32768 }
324
434
  }
435
+ },
436
+ "cost": {
437
+ "currency": "USD",
438
+ "unit": "1M",
439
+ "costs": {
440
+ "input_tokens": 5,
441
+ "output_tokens": 25,
442
+ "cache_read_input_tokens": 0.5,
443
+ "cache_creation_input_tokens": 6.25
444
+ }
325
445
  }
326
446
  },
327
447
 
@@ -349,6 +469,15 @@
349
469
  }
350
470
  }
351
471
  }
472
+ },
473
+ "cost": {
474
+ "currency": "USD",
475
+ "unit": "1M",
476
+ "costs": {
477
+ "promptTokenCount": 0.5,
478
+ "cachedContentTokenCount": -0.45,
479
+ "candidatesTokenCount": 3
480
+ }
352
481
  }
353
482
  },
354
483
  {
@@ -375,6 +504,15 @@
375
504
  }
376
505
  }
377
506
  }
507
+ },
508
+ "cost": {
509
+ "currency": "USD",
510
+ "unit": "1M",
511
+ "costs": {
512
+ "promptTokenCount": 0.5,
513
+ "cachedContentTokenCount": -0.45,
514
+ "candidatesTokenCount": 3
515
+ }
378
516
  }
379
517
  },
380
518
  {
@@ -453,6 +591,15 @@
453
591
  }
454
592
  }
455
593
  }
594
+ },
595
+ "cost": {
596
+ "currency": "USD",
597
+ "unit": "1M",
598
+ "costs": {
599
+ "promptTokenCount": 0.5,
600
+ "cachedContentTokenCount": -0.45,
601
+ "candidatesTokenCount": 3
602
+ }
456
603
  }
457
604
  },
458
605
  {
@@ -478,6 +625,15 @@
478
625
  }
479
626
  }
480
627
  }
628
+ },
629
+ "cost": {
630
+ "currency": "USD",
631
+ "unit": "1M",
632
+ "costs": {
633
+ "promptTokenCount": 0.5,
634
+ "cachedContentTokenCount": -0.45,
635
+ "candidatesTokenCount": 3
636
+ }
481
637
  }
482
638
  },
483
639
  {
@@ -699,6 +855,15 @@
699
855
  "config": {
700
856
  "model": "zai-org/glm-5-maas"
701
857
  }
858
+ },
859
+ "cost": {
860
+ "currency": "USD",
861
+ "unit": "1M",
862
+ "costs": {
863
+ "prompt_tokens": 1,
864
+ "completion_tokens": 3.2,
865
+ "prompt_tokens_details.cached_tokens": -0.9
866
+ }
702
867
  }
703
868
  },
704
869
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/agent.d.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  } from "./model";
10
10
  import type { Tool, ToolUseApprover } from "./tool";
11
11
  import type { AgentRole } from "./context/loadAgentRoles.mjs";
12
+ import type { CostSummary, CostConfig } from "./costTracker.mjs";
12
13
 
13
14
  export type Agent = {
14
15
  userEventEmitter: UserEventEmitter;
@@ -19,6 +20,7 @@ export type Agent = {
19
20
  export type AgentCommands = {
20
21
  dumpMessages: () => Promise<void>;
21
22
  loadMessages: () => Promise<void>;
23
+ getCostSummary: () => CostSummary;
22
24
  };
23
25
 
24
26
  type UserEventMap = {
@@ -45,4 +47,5 @@ export type AgentConfig = {
45
47
  tools: Tool[];
46
48
  toolUseApprover: ToolUseApprover;
47
49
  agentRoles: Map<string, AgentRole>;
50
+ modelCostConfig?: CostConfig;
48
51
  };
package/src/agent.mjs CHANGED
@@ -9,6 +9,7 @@ import { EventEmitter } from "node:events";
9
9
  import fs from "node:fs/promises";
10
10
  import { createAgentLoop } from "./agentLoop.mjs";
11
11
  import { createStateManager } from "./agentState.mjs";
12
+ import { createCostTracker } from "./costTracker.mjs";
12
13
  import { MESSAGES_DUMP_FILE_PATH } from "./env.mjs";
13
14
  import { createSubagentManager } from "./subagent.mjs";
14
15
  import { createToolExecutor } from "./toolExecutor.mjs";
@@ -25,12 +26,19 @@ export function createAgent({
25
26
  tools,
26
27
  toolUseApprover,
27
28
  agentRoles,
29
+ modelCostConfig,
28
30
  }) {
29
31
  /** @type {UserEventEmitter} */
30
32
  const userEventEmitter = new EventEmitter();
31
33
  /** @type {AgentEventEmitter} */
32
34
  const agentEventEmitter = new EventEmitter();
33
35
 
36
+ const costTracker = createCostTracker(modelCostConfig);
37
+
38
+ agentEventEmitter.on("providerTokenUsage", (usage) => {
39
+ costTracker.recordUsage(usage);
40
+ });
41
+
34
42
  const stateManager = createStateManager(
35
43
  [
36
44
  {
@@ -154,6 +162,7 @@ export function createAgent({
154
162
  agentCommands: {
155
163
  dumpMessages,
156
164
  loadMessages,
165
+ getCostSummary: () => costTracker.calculateCost(),
157
166
  },
158
167
  };
159
168
  }
package/src/cliBatch.mjs CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * @import { UserEventEmitter, AgentEventEmitter } from "./agent"
2
+ * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
3
3
  */
4
4
 
5
+ import { formatCostForBatch } from "./cliFormatter.mjs";
6
+
5
7
  /**
6
8
  * @typedef {object} BatchSessionOptions
7
9
  * @property {UserEventEmitter} userEventEmitter
8
10
  * @property {AgentEventEmitter} agentEventEmitter
11
+ * @property {AgentCommands} agentCommands
9
12
  * @property {string} task - Task instruction to execute
10
13
  * @property {string} sessionId
11
14
  * @property {string} modelName
@@ -23,6 +26,7 @@
23
26
  export async function startBatchSession({
24
27
  userEventEmitter,
25
28
  agentEventEmitter,
29
+ agentCommands,
26
30
  task,
27
31
  sessionId,
28
32
  modelName,
@@ -35,9 +39,12 @@ export async function startBatchSession({
35
39
 
36
40
  await new Promise((/** @type {(value?: void) => void} */ resolve) => {
37
41
  agentEventEmitter.on("turnEnd", async () => {
42
+ const costSummary = agentCommands.getCostSummary();
43
+
38
44
  outputEvent({
39
45
  type: "session_end",
40
46
  timestamp: new Date().toISOString(),
47
+ cost: formatCostForBatch(costSummary),
41
48
  });
42
49
  await onStop();
43
50
  resolve();
@@ -206,3 +206,70 @@ export function formatProviderTokenUsage(usage) {
206
206
 
207
207
  return styleText("gray", outputLines.join("\n"));
208
208
  }
209
+
210
+ /**
211
+ * Format cost summary for interactive display
212
+ * @param {import("./costTracker.mjs").CostSummary} summary
213
+ * @returns {string}
214
+ */
215
+ export function formatCostSummary(summary) {
216
+ if (!summary || Object.keys(summary.breakdown).length === 0) {
217
+ return styleText("gray", "No token usage recorded yet.");
218
+ }
219
+
220
+ const lines = [];
221
+
222
+ // Header
223
+ lines.push(styleText("bold", "\nSession Cost Summary\n"));
224
+
225
+ // Tokens
226
+ lines.push(styleText("bold", "Tokens:"));
227
+
228
+ for (const [key, { tokens, cost }] of Object.entries(summary.breakdown)) {
229
+ const tokenStr = `${key}: ${tokens.toLocaleString()}`;
230
+
231
+ if (cost !== undefined) {
232
+ const costStr = `${cost.toFixed(4)} ${summary.currency}`;
233
+ lines.push(` ${tokenStr.padEnd(30)} ${styleText("cyan", costStr)}`);
234
+ } else {
235
+ lines.push(` ${tokenStr.padEnd(30)} ${styleText("gray", "N/A")}`);
236
+ }
237
+ }
238
+
239
+ // Total
240
+ lines.push("");
241
+ if (summary.totalCost !== undefined) {
242
+ lines.push(
243
+ styleText(
244
+ "bold",
245
+ `Total: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
246
+ ),
247
+ );
248
+ } else {
249
+ lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
250
+ }
251
+
252
+ return lines.join("\n");
253
+ }
254
+
255
+ /**
256
+ * Format cost for batch mode JSON output
257
+ * @param {import("./costTracker.mjs").CostSummary} summary
258
+ */
259
+ export function formatCostForBatch(summary) {
260
+ if (!summary || Object.keys(summary.breakdown).length === 0) {
261
+ return undefined;
262
+ }
263
+
264
+ return {
265
+ total: summary.totalCost,
266
+ currency: summary.currency,
267
+ unit: summary.unit,
268
+ breakdown: Object.fromEntries(
269
+ Object.entries(summary.breakdown).map(([key, { tokens, cost }]) => [
270
+ key,
271
+ { tokens, cost },
272
+ ]),
273
+ ),
274
+ };
275
+ }
@@ -8,6 +8,7 @@ import { execFileSync } from "node:child_process";
8
8
  import readline from "node:readline";
9
9
  import { styleText } from "node:util";
10
10
  import {
11
+ formatCostSummary,
11
12
  formatProviderTokenUsage,
12
13
  formatToolResult,
13
14
  formatToolUse,
@@ -47,6 +48,7 @@ const SLASH_COMMANDS = [
47
48
  },
48
49
  { name: "/dump", description: "Save current messages to a JSON file" },
49
50
  { name: "/load", description: "Load messages from a JSON file" },
51
+ { name: "/cost", description: "Display session cost and token usage" },
50
52
  ];
51
53
 
52
54
  /**
@@ -237,7 +239,6 @@ export function startInteractiveSession({
237
239
  console.log(styleText("gray", "</agent>"));
238
240
 
239
241
  userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
240
- state.turn = false;
241
242
  }
242
243
 
243
244
  /**
@@ -252,6 +253,7 @@ export function startInteractiveSession({
252
253
 
253
254
  if (!prompt) {
254
255
  console.log(styleText("red", `\nPrompt not found: ${id}`));
256
+ state.turn = true;
255
257
  cli.prompt();
256
258
  return;
257
259
  }
@@ -266,7 +268,6 @@ export function startInteractiveSession({
266
268
  console.log(styleText("gray", "</prompt>"));
267
269
 
268
270
  userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
269
- state.turn = false;
270
271
  }
271
272
 
272
273
  const getCliPrompt = (subagentName = "") =>
@@ -388,9 +389,13 @@ export function startInteractiveSession({
388
389
  * @returns {Promise<void>}
389
390
  */
390
391
  async function processInput(input) {
392
+ // Prevent concurrent input processing from multi-line paste
393
+ state.turn = false;
394
+
391
395
  const inputTrimmed = input.trim();
392
396
 
393
397
  if (inputTrimmed.length === 0) {
398
+ state.turn = true;
394
399
  cli.prompt();
395
400
  return;
396
401
  }
@@ -400,6 +405,7 @@ export function startInteractiveSession({
400
405
 
401
406
  if (["/help", "help"].includes(inputTrimmed.toLowerCase())) {
402
407
  console.log(`\n${HELP_MESSAGE}`);
408
+ state.turn = true;
403
409
  cli.prompt();
404
410
  return;
405
411
  }
@@ -408,6 +414,7 @@ export function startInteractiveSession({
408
414
  const fileRange = parseFileRange(inputTrimmed.slice(1));
409
415
  if (fileRange instanceof Error) {
410
416
  console.log(styleText("red", `\n${fileRange.message}`));
417
+ state.turn = true;
411
418
  cli.prompt();
412
419
  return;
413
420
  }
@@ -415,6 +422,7 @@ export function startInteractiveSession({
415
422
  const fileContent = await readFileRange(fileRange);
416
423
  if (fileContent instanceof Error) {
417
424
  console.log(styleText("red", `\n${fileContent.message}`));
425
+ state.turn = true;
418
426
  cli.prompt();
419
427
  return;
420
428
  }
@@ -426,18 +434,27 @@ export function startInteractiveSession({
426
434
  const messageWithContext = await loadUserMessageContext(fileContent);
427
435
 
428
436
  userEventEmitter.emit("userInput", messageWithContext);
429
- state.turn = false;
430
437
  return;
431
438
  }
432
439
 
433
440
  if (inputTrimmed.toLowerCase() === "/dump") {
434
441
  await agentCommands.dumpMessages();
442
+ state.turn = true;
435
443
  cli.prompt();
436
444
  return;
437
445
  }
438
446
 
439
447
  if (inputTrimmed.toLowerCase() === "/load") {
440
448
  await agentCommands.loadMessages();
449
+ state.turn = true;
450
+ cli.prompt();
451
+ return;
452
+ }
453
+
454
+ if (inputTrimmed.toLowerCase() === "/cost") {
455
+ const summary = agentCommands.getCostSummary();
456
+ console.log(formatCostSummary(summary));
457
+ state.turn = true;
441
458
  cli.prompt();
442
459
  return;
443
460
  }
@@ -457,6 +474,7 @@ export function startInteractiveSession({
457
474
  );
458
475
  }
459
476
  }
477
+ state.turn = true;
460
478
  cli.prompt();
461
479
  return;
462
480
  }
@@ -477,6 +495,7 @@ export function startInteractiveSession({
477
495
  );
478
496
  }
479
497
  }
498
+ state.turn = true;
480
499
  cli.prompt();
481
500
  return;
482
501
  }
@@ -485,6 +504,7 @@ export function startInteractiveSession({
485
504
  const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/);
486
505
  if (!match) {
487
506
  console.log(styleText("red", "\nInvalid prompt invocation format."));
507
+ state.turn = true;
488
508
  cli.prompt();
489
509
  return;
490
510
  }
@@ -497,6 +517,7 @@ export function startInteractiveSession({
497
517
  const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/);
498
518
  if (!match) {
499
519
  console.log(styleText("red", "\nInvalid agent invocation format."));
520
+ state.turn = true;
500
521
  cli.prompt();
501
522
  return;
502
523
  }
@@ -521,6 +542,7 @@ export function startInteractiveSession({
521
542
  `\nUnsupported platform for /paste: ${process.platform}`,
522
543
  ),
523
544
  );
545
+ state.turn = true;
524
546
  cli.prompt();
525
547
  return;
526
548
  }
@@ -532,6 +554,7 @@ export function startInteractiveSession({
532
554
  `\nFailed to get clipboard content: ${errorMessage}`,
533
555
  ),
534
556
  );
557
+ state.turn = true;
535
558
  cli.prompt();
536
559
  return;
537
560
  }
@@ -544,7 +567,6 @@ export function startInteractiveSession({
544
567
 
545
568
  const messageWithContext = await loadUserMessageContext(combinedInput);
546
569
  userEventEmitter.emit("userInput", messageWithContext);
547
- state.turn = false;
548
570
  return;
549
571
  }
550
572
 
@@ -565,7 +587,6 @@ export function startInteractiveSession({
565
587
 
566
588
  const messageWithContext = await loadUserMessageContext(inputTrimmed);
567
589
  userEventEmitter.emit("userInput", messageWithContext);
568
- state.turn = false;
569
590
  }
570
591
 
571
592
  cli.on("line", async (lineInput) => {
@@ -627,10 +648,6 @@ export function startInteractiveSession({
627
648
  });
628
649
 
629
650
  agentEventEmitter.on("message", (message) => {
630
- // Skip user message
631
- if (state.turn) {
632
- return;
633
- }
634
651
  printMessage(message);
635
652
  });
636
653
 
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @import { ProviderTokenUsage } from "./model"
3
+ */
4
+
5
+ /**
6
+ * @typedef {Object} TokenBreakdown
7
+ * @property {number} tokens - Token count
8
+ * @property {number | undefined} cost - Cost (undefined if no pricing)
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} CostSummary
13
+ * @property {string} currency - Currency code (e.g., "USD")
14
+ * @property {string} unit - Unit size (e.g., "1M")
15
+ * @property {Record<string, TokenBreakdown>} breakdown - Token breakdown
16
+ * @property {number | undefined} totalCost - Total cost (undefined if no pricing)
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} CostConfig
21
+ * @property {string} currency
22
+ * @property {string} unit
23
+ * @property {Record<string, number>} costs
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} CostTracker
28
+ * @property {(usage: ProviderTokenUsage) => void} recordUsage - Record token usage
29
+ * @property {() => Record<string, number>} getAggregatedUsage - Get aggregated usage
30
+ * @property {() => CostSummary} calculateCost - Calculate cost summary
31
+ * @property {() => boolean} hasUsage - Check if any usage recorded
32
+ */
33
+
34
+ /**
35
+ * Create a cost tracker for session token usage
36
+ * @param {CostConfig} [costConfig] - Optional cost configuration
37
+ * @returns {CostTracker}
38
+ */
39
+ export function createCostTracker(costConfig) {
40
+ /** @type {ProviderTokenUsage[]} */
41
+ const usageHistory = [];
42
+
43
+ /**
44
+ * Record token usage from a provider
45
+ * @param {ProviderTokenUsage} usage
46
+ */
47
+ function recordUsage(usage) {
48
+ if (typeof usage === "object" && usage !== null) {
49
+ usageHistory.push(usage);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get aggregated token usage
55
+ * @returns {Record<string, number>}
56
+ */
57
+ function getAggregatedUsage() {
58
+ return aggregateTokens(usageHistory);
59
+ }
60
+
61
+ /**
62
+ * Calculate cost summary
63
+ * @returns {CostSummary}
64
+ */
65
+ function calculateCost() {
66
+ const aggregated = aggregateTokens(usageHistory);
67
+ return calculateCostFromConfig(aggregated, costConfig);
68
+ }
69
+
70
+ /**
71
+ * Check if any usage recorded
72
+ * @returns {boolean}
73
+ */
74
+ function hasUsage() {
75
+ return usageHistory.length > 0;
76
+ }
77
+
78
+ return {
79
+ recordUsage,
80
+ getAggregatedUsage,
81
+ calculateCost,
82
+ hasUsage,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Aggregate token usage history by key
88
+ * @param {ProviderTokenUsage[]} usageHistory
89
+ * @returns {Record<string, number>}
90
+ */
91
+ function aggregateTokens(usageHistory) {
92
+ /** @type {Record<string, number>} */
93
+ const aggregated = {};
94
+
95
+ for (const usage of usageHistory) {
96
+ recursivelySumValues(usage, [], aggregated);
97
+ }
98
+
99
+ return aggregated;
100
+ }
101
+
102
+ /**
103
+ * Recursively sum numeric values in token usage
104
+ * @param {ProviderTokenUsage} obj
105
+ * @param {string[]} path
106
+ * @param {Record<string, number>} result
107
+ */
108
+ function recursivelySumValues(obj, path, result) {
109
+ for (const [key, value] of Object.entries(obj)) {
110
+ const currentPath = [...path, key];
111
+ const pathStr = currentPath.join(".");
112
+
113
+ if (typeof value === "number") {
114
+ result[pathStr] = (result[pathStr] || 0) + value;
115
+ } else if (
116
+ typeof value === "object" &&
117
+ value !== null &&
118
+ !Array.isArray(value)
119
+ ) {
120
+ recursivelySumValues(value, currentPath, result);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Calculate cost from aggregated tokens and config
127
+ * @param {Record<string, number>} aggregated
128
+ * @param {CostConfig | undefined} config
129
+ * @returns {CostSummary}
130
+ */
131
+ function calculateCostFromConfig(aggregated, config) {
132
+ /** @type {Record<string, TokenBreakdown>} */
133
+ const breakdown = {};
134
+ let totalCost = 0;
135
+ const hasPricing = config?.costs;
136
+
137
+ for (const [key, tokens] of Object.entries(aggregated)) {
138
+ breakdown[key] = { tokens, cost: undefined };
139
+
140
+ if (!hasPricing || !config.costs[key]) {
141
+ continue;
142
+ }
143
+
144
+ const costValue = config.costs[key];
145
+ const unitSize = parseUnit(config.unit);
146
+
147
+ if (typeof costValue === "number") {
148
+ const cost = (tokens * costValue) / unitSize;
149
+ breakdown[key].cost = cost;
150
+ totalCost += cost;
151
+ }
152
+ }
153
+
154
+ return {
155
+ currency: config?.currency || "USD",
156
+ unit: config?.unit || "1M",
157
+ breakdown,
158
+ totalCost: hasPricing ? totalCost : undefined,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Parse unit string to number
164
+ * @param {string} unit
165
+ * @returns {number}
166
+ */
167
+ function parseUnit(unit) {
168
+ if (unit === "1M") return 1_000_000;
169
+ if (unit === "1K") return 1_000;
170
+ return 1;
171
+ }
package/src/main.mjs CHANGED
@@ -223,6 +223,7 @@ if (cliArgs.subcommand.type === "install-claude-code-plugins") {
223
223
  tools: [...builtinTools, ...mcpTools],
224
224
  toolUseApprover,
225
225
  agentRoles,
226
+ modelCostConfig: modelDef.cost,
226
227
  });
227
228
 
228
229
  const sessionOptions = {
@@ -9,6 +9,7 @@ export type ModelDefinition = {
9
9
  variant: string;
10
10
  platform: PlatformConfig;
11
11
  model: ModelConfig;
12
+ cost?: CostConfig;
12
13
  };
13
14
 
14
15
  export type PlatformConfig =
@@ -76,3 +77,9 @@ export type ModelConfig =
76
77
  format: "bedrock-converse";
77
78
  config: BedrockConverseModelConfig;
78
79
  };
80
+
81
+ export type CostConfig = {
82
+ currency: string;
83
+ unit: string;
84
+ costs: Record<string, number>;
85
+ };
@@ -204,13 +204,15 @@ export function createCacheEnabledGeminiModelCaller(
204
204
 
205
205
  /** @type {ProviderTokenUsage} */
206
206
  const tokenUsage = {
207
- input:
208
- content.usageMetadata.promptTokenCount -
209
- (content.usageMetadata.cachedContentTokenCount ?? 0),
210
- cached: content.usageMetadata.cachedContentTokenCount ?? 0,
211
- output: content.usageMetadata.candidatesTokenCount ?? 0,
212
- thought: content.usageMetadata.thoughtsTokenCount ?? 0,
213
- total: content.usageMetadata.totalTokenCount,
207
+ promptTokenCount: content.usageMetadata.promptTokenCount,
208
+ // nonCachedPromptTokenCount:
209
+ // content.usageMetadata.promptTokenCount -
210
+ // (content.usageMetadata.cachedContentTokenCount ?? 0),
211
+ cachedContentTokenCount:
212
+ content.usageMetadata.cachedContentTokenCount ?? 0,
213
+ candidatesTokenCount: content.usageMetadata.candidatesTokenCount ?? 0,
214
+ thoughtsTokenCount: content.usageMetadata.thoughtsTokenCount ?? 0,
215
+ totalTokenCount: content.usageMetadata.totalTokenCount,
214
216
  };
215
217
 
216
218
  const message = convertGeminiAssistantMessageToGenericFormat(content);