@iinm/plain-agent 1.4.1 → 1.5.0

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.0",
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
  /**
@@ -214,11 +216,12 @@ export function startInteractiveSession({
214
216
  onStop,
215
217
  claudeCodePlugins,
216
218
  }) {
217
- /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string }} */
219
+ /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string, skipNextUserMessage: boolean }} */
218
220
  const state = {
219
221
  turn: true,
220
222
  multiLineBuffer: null,
221
223
  subagentName: "",
224
+ skipNextUserMessage: false,
222
225
  };
223
226
 
224
227
  /**
@@ -236,8 +239,8 @@ export function startInteractiveSession({
236
239
  console.log(message);
237
240
  console.log(styleText("gray", "</agent>"));
238
241
 
242
+ state.skipNextUserMessage = true;
239
243
  userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
240
- state.turn = false;
241
244
  }
242
245
 
243
246
  /**
@@ -252,6 +255,7 @@ export function startInteractiveSession({
252
255
 
253
256
  if (!prompt) {
254
257
  console.log(styleText("red", `\nPrompt not found: ${id}`));
258
+ state.turn = true;
255
259
  cli.prompt();
256
260
  return;
257
261
  }
@@ -265,8 +269,8 @@ export function startInteractiveSession({
265
269
  console.log(message);
266
270
  console.log(styleText("gray", "</prompt>"));
267
271
 
272
+ state.skipNextUserMessage = true;
268
273
  userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
269
- state.turn = false;
270
274
  }
271
275
 
272
276
  const getCliPrompt = (subagentName = "") =>
@@ -388,9 +392,13 @@ export function startInteractiveSession({
388
392
  * @returns {Promise<void>}
389
393
  */
390
394
  async function processInput(input) {
395
+ // Prevent concurrent input processing from multi-line paste
396
+ state.turn = false;
397
+
391
398
  const inputTrimmed = input.trim();
392
399
 
393
400
  if (inputTrimmed.length === 0) {
401
+ state.turn = true;
394
402
  cli.prompt();
395
403
  return;
396
404
  }
@@ -400,6 +408,7 @@ export function startInteractiveSession({
400
408
 
401
409
  if (["/help", "help"].includes(inputTrimmed.toLowerCase())) {
402
410
  console.log(`\n${HELP_MESSAGE}`);
411
+ state.turn = true;
403
412
  cli.prompt();
404
413
  return;
405
414
  }
@@ -408,6 +417,7 @@ export function startInteractiveSession({
408
417
  const fileRange = parseFileRange(inputTrimmed.slice(1));
409
418
  if (fileRange instanceof Error) {
410
419
  console.log(styleText("red", `\n${fileRange.message}`));
420
+ state.turn = true;
411
421
  cli.prompt();
412
422
  return;
413
423
  }
@@ -415,6 +425,7 @@ export function startInteractiveSession({
415
425
  const fileContent = await readFileRange(fileRange);
416
426
  if (fileContent instanceof Error) {
417
427
  console.log(styleText("red", `\n${fileContent.message}`));
428
+ state.turn = true;
418
429
  cli.prompt();
419
430
  return;
420
431
  }
@@ -425,19 +436,29 @@ export function startInteractiveSession({
425
436
 
426
437
  const messageWithContext = await loadUserMessageContext(fileContent);
427
438
 
439
+ state.skipNextUserMessage = true;
428
440
  userEventEmitter.emit("userInput", messageWithContext);
429
- state.turn = false;
430
441
  return;
431
442
  }
432
443
 
433
444
  if (inputTrimmed.toLowerCase() === "/dump") {
434
445
  await agentCommands.dumpMessages();
446
+ state.turn = true;
435
447
  cli.prompt();
436
448
  return;
437
449
  }
438
450
 
439
451
  if (inputTrimmed.toLowerCase() === "/load") {
440
452
  await agentCommands.loadMessages();
453
+ state.turn = true;
454
+ cli.prompt();
455
+ return;
456
+ }
457
+
458
+ if (inputTrimmed.toLowerCase() === "/cost") {
459
+ const summary = agentCommands.getCostSummary();
460
+ console.log(formatCostSummary(summary));
461
+ state.turn = true;
441
462
  cli.prompt();
442
463
  return;
443
464
  }
@@ -457,6 +478,7 @@ export function startInteractiveSession({
457
478
  );
458
479
  }
459
480
  }
481
+ state.turn = true;
460
482
  cli.prompt();
461
483
  return;
462
484
  }
@@ -477,6 +499,7 @@ export function startInteractiveSession({
477
499
  );
478
500
  }
479
501
  }
502
+ state.turn = true;
480
503
  cli.prompt();
481
504
  return;
482
505
  }
@@ -485,6 +508,7 @@ export function startInteractiveSession({
485
508
  const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/);
486
509
  if (!match) {
487
510
  console.log(styleText("red", "\nInvalid prompt invocation format."));
511
+ state.turn = true;
488
512
  cli.prompt();
489
513
  return;
490
514
  }
@@ -497,6 +521,7 @@ export function startInteractiveSession({
497
521
  const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/);
498
522
  if (!match) {
499
523
  console.log(styleText("red", "\nInvalid agent invocation format."));
524
+ state.turn = true;
500
525
  cli.prompt();
501
526
  return;
502
527
  }
@@ -521,6 +546,7 @@ export function startInteractiveSession({
521
546
  `\nUnsupported platform for /paste: ${process.platform}`,
522
547
  ),
523
548
  );
549
+ state.turn = true;
524
550
  cli.prompt();
525
551
  return;
526
552
  }
@@ -532,6 +558,7 @@ export function startInteractiveSession({
532
558
  `\nFailed to get clipboard content: ${errorMessage}`,
533
559
  ),
534
560
  );
561
+ state.turn = true;
535
562
  cli.prompt();
536
563
  return;
537
564
  }
@@ -543,8 +570,8 @@ export function startInteractiveSession({
543
570
  console.log(styleText("gray", "</paste>"));
544
571
 
545
572
  const messageWithContext = await loadUserMessageContext(combinedInput);
573
+ state.skipNextUserMessage = true;
546
574
  userEventEmitter.emit("userInput", messageWithContext);
547
- state.turn = false;
548
575
  return;
549
576
  }
550
577
 
@@ -564,8 +591,8 @@ export function startInteractiveSession({
564
591
  }
565
592
 
566
593
  const messageWithContext = await loadUserMessageContext(inputTrimmed);
594
+ state.skipNextUserMessage = true;
567
595
  userEventEmitter.emit("userInput", messageWithContext);
568
- state.turn = false;
569
596
  }
570
597
 
571
598
  cli.on("line", async (lineInput) => {
@@ -627,8 +654,9 @@ export function startInteractiveSession({
627
654
  });
628
655
 
629
656
  agentEventEmitter.on("message", (message) => {
630
- // Skip user message
631
- if (state.turn) {
657
+ // Skip user input message (echoing back what the user just sent)
658
+ if (state.skipNextUserMessage) {
659
+ state.skipNextUserMessage = false;
632
660
  return;
633
661
  }
634
662
  printMessage(message);
@@ -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);