@dudousxd/nestjs-agent-opencode 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -53,7 +53,8 @@ export class AppModule {}
53
53
  | OpenCode | The library's protocol and store |
54
54
  | --- | --- |
55
55
  | `session.text.delta` / `session.reasoning.delta` | `text` / `reasoning` |
56
- | a step (`session.step.ended`) | `step-start` … `step-finish` with usage; usage recorded per step |
56
+ | a step (`session.step.ended`) | `step-start` … `step-finish` with usage and cost; a `chat` usage row per step (see [Cost and usage](#cost-and-usage)) |
57
+ | `session.usage.recorded` (the title, a compaction) | a `title` / `history_summary` usage row |
57
58
  | tools (`session.tool.*`) | `tool-input-*` / `tool-output*`, code-mode inner calls nested by `parentId` |
58
59
  | `permission.asked` | an `action` call + `approval-requested`, recorded `pending_approval`; `approve` / `reject` → `permission.reply` |
59
60
  | `form.created` | `elicitation`; `answer` / `skip` → `session.form.reply` / `cancel` |
@@ -64,6 +65,33 @@ export class AppModule {}
64
65
  Sessions: one per thread, kept by an `OpenCodeSessionStore` (in memory by default; persist it for
65
66
  several replicas) and recreated when the server's `bootId` changes, told the conversation so far.
66
67
 
68
+ ## Cost and usage
69
+
70
+ Every model call OpenCode makes for a turn — each step, the title it gives the session, a
71
+ compaction — goes to the same token-usage ledger the library's own loop writes (`recordUsage`:
72
+ `agent_token_usage` with `cost_usd` and `cost_source`), so the ledger quota (`quota: { limits }`),
73
+ the dashboard and `GET /quota` see an OpenCode turn like any other.
74
+
75
+ - **Tokens.** OpenCode reports the uncached input in `tokens.input` and the cache beside it; the row
76
+ counts the whole input side in `inputTokens`, with `cacheReadTokens` / `cacheWriteTokens` as
77
+ subsets (the library's `MessageUsage`). The row's model is the one OpenCode names for the step
78
+ (`<providerID>/<model id>`, e.g. `amazon-bedrock/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0`).
79
+ - **Cost.** OpenCode prices each call itself, off its model catalog. A call it priced (`cost > 0`) is
80
+ recorded at that figure, `cost_source: 'provider'`. A call it priced at 0 — no price for the model
81
+ in its catalog — is estimated from the library's price for the model, `cost_source: 'estimate'`:
82
+ the pricing store's row, else (seeded into the store once, as at boot) `priceCatalog.prices`, the
83
+ built-in GovCloud Bedrock table (`priceCatalog.region` / `AWS_REGION`, or a `us-gov.` id) and
84
+ models.dev. A model the library cannot price keeps OpenCode's 0.
85
+ - `openCode({ cost: 'estimate' })` prefers the library's price whenever it has one — for models
86
+ OpenCode would price at a list price that is not yours (models.dev lists commercial Bedrock prices,
87
+ so Bedrock in GovCloud is priced too low unless OpenCode's config declares the model's `cost`).
88
+ - **The run's total** (`OpenCodeRunResult.usage` for `beforeSettle` / `onSettled`, and the usage on
89
+ the turn's last message) is added up from the turn's own calls, not read back from the store: each
90
+ milestone carries what was spent since the last one, and a durable run journals it with the
91
+ milestone, so a run resumed in another process (an approval answered after a restart) still reports
92
+ the whole run, cache tokens included. A process that dies while OpenCode works loses the calls it
93
+ saw from that total (their ledger rows are already written).
94
+
67
95
  ## Durable turns
68
96
 
69
97
  ```ts
@@ -249,6 +249,41 @@ function toFormAnswer(form, answers = {}) {
249
249
  __name(toFormAnswer, "toFormAnswer");
250
250
 
251
251
  // src/turn.ts
252
+ function emptyUsage() {
253
+ return {
254
+ inputTokens: 0,
255
+ outputTokens: 0,
256
+ cacheReadTokens: 0,
257
+ cacheWriteTokens: 0,
258
+ reasoningTokens: 0,
259
+ costUsd: 0
260
+ };
261
+ }
262
+ __name(emptyUsage, "emptyUsage");
263
+ function addUsage(a, b) {
264
+ const x = a ?? emptyUsage();
265
+ const y = b ?? emptyUsage();
266
+ return {
267
+ inputTokens: x.inputTokens + (y.inputTokens ?? 0),
268
+ outputTokens: x.outputTokens + (y.outputTokens ?? 0),
269
+ cacheReadTokens: (x.cacheReadTokens ?? 0) + (y.cacheReadTokens ?? 0),
270
+ cacheWriteTokens: (x.cacheWriteTokens ?? 0) + (y.cacheWriteTokens ?? 0),
271
+ reasoningTokens: (x.reasoningTokens ?? 0) + (y.reasoningTokens ?? 0),
272
+ costUsd: x.costUsd + (y.costUsd ?? 0)
273
+ };
274
+ }
275
+ __name(addUsage, "addUsage");
276
+ function subUsage(a, b) {
277
+ return {
278
+ inputTokens: a.inputTokens - b.inputTokens,
279
+ outputTokens: a.outputTokens - b.outputTokens,
280
+ cacheReadTokens: a.cacheReadTokens - b.cacheReadTokens,
281
+ cacheWriteTokens: a.cacheWriteTokens - b.cacheWriteTokens,
282
+ reasoningTokens: a.reasoningTokens - b.reasoningTokens,
283
+ costUsd: a.costUsd - b.costUsd
284
+ };
285
+ }
286
+ __name(subUsage, "subUsage");
252
287
  var ASK_TOOL_NAME = "ask";
253
288
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
254
289
  "error",
@@ -314,11 +349,11 @@ var OpenCodeTurn = class {
314
349
  hidden = /* @__PURE__ */ new Set();
315
350
  milestones = [];
316
351
  waiting;
317
- usage = {
318
- inputTokens: 0,
319
- outputTokens: 0
320
- };
321
- costUsd;
352
+ /** What this turn object saw spent, and how much of it a milestone already reported. */
353
+ spent = emptyUsage();
354
+ reported = emptyUsage();
355
+ priced = false;
356
+ model;
322
357
  stepOpen = false;
323
358
  sawText = false;
324
359
  separator = false;
@@ -328,6 +363,11 @@ var OpenCodeTurn = class {
328
363
  stepError;
329
364
  constructor(a) {
330
365
  this.a = a;
366
+ this.model = modelOfLabel(a.input.model);
367
+ }
368
+ /** What the run spent so far: carried from before this turn object, plus what it saw. */
369
+ totalUsage() {
370
+ return addUsage(this.a.carried, this.spent);
331
371
  }
332
372
  get sessionId() {
333
373
  return this.a.sessionId;
@@ -410,7 +450,15 @@ var OpenCodeTurn = class {
410
450
  this.chain = this.chain.then(() => ask.kind === "approval" ? this.decided(ask, reply, tell) : this.answered(ask, reply, tell)).then(resolve, reject);
411
451
  });
412
452
  }
413
- reach(milestone) {
453
+ reach(reached) {
454
+ const usage = subUsage(this.spent, this.reported);
455
+ this.reported = {
456
+ ...this.spent
457
+ };
458
+ const milestone = {
459
+ ...reached,
460
+ usage
461
+ };
414
462
  const waiting = this.waiting;
415
463
  if (waiting !== void 0) {
416
464
  this.waiting = void 0;
@@ -566,9 +614,22 @@ var OpenCodeTurn = class {
566
614
  }
567
615
  }
568
616
  async onEvent(e) {
569
- if (this.ended) return;
570
617
  const d = e.data ?? {};
618
+ if (e.type === "session.usage.recorded") {
619
+ await this.usageRecorded(d);
620
+ return;
621
+ }
622
+ if (this.ended) return;
571
623
  switch (e.type) {
624
+ case "session.step.started":
625
+ case "session.model.selected": {
626
+ const model = d.model;
627
+ if (typeof model?.id === "string" && typeof model.providerID === "string") this.model = {
628
+ providerID: model.providerID,
629
+ id: model.id
630
+ };
631
+ return;
632
+ }
572
633
  case "session.text.delta": {
573
634
  const delta = typeof d.delta === "string" ? d.delta : "";
574
635
  if (!delta) return;
@@ -722,41 +783,63 @@ ${delta}` : delta;
722
783
  });
723
784
  }
724
785
  }
725
- async stepEnded(d) {
726
- const usage = {
727
- inputTokens: Number(d.tokens?.input) || 0,
728
- outputTokens: Number(d.tokens?.output) || 0,
729
- ...Number(d.tokens?.cache?.read) ? {
730
- cacheReadTokens: Number(d.tokens.cache.read)
731
- } : {},
732
- ...Number(d.tokens?.cache?.write) ? {
733
- cacheWriteTokens: Number(d.tokens.cache.write)
734
- } : {}
735
- };
736
- const costUsd = typeof d.cost === "number" ? d.cost : void 0;
737
- this.usage.inputTokens += usage.inputTokens;
738
- this.usage.outputTokens += usage.outputTokens;
739
- if (costUsd !== void 0) this.costUsd = (this.costUsd ?? 0) + costUsd;
786
+ modelId() {
787
+ return this.model !== void 0 ? `${this.model.providerID}/${this.model.id}` : this.a.modelLabel;
788
+ }
789
+ /** One model call's usage and cost, into the ledger and the run's total. */
790
+ async spend(d, purpose) {
791
+ const usage = usageOf(d.tokens);
792
+ const reportedCostUsd = typeof d.cost === "number" && Number.isFinite(d.cost) && d.cost >= 0 ? d.cost : void 0;
793
+ const fallback = reportedCostUsd !== void 0 ? {
794
+ costUsd: reportedCostUsd,
795
+ costSource: "provider"
796
+ } : {};
797
+ let cost = fallback;
798
+ if (this.a.priceStep !== void 0) {
799
+ cost = await this.a.priceStep({
800
+ model: this.model,
801
+ usage,
802
+ reportedCostUsd
803
+ }).catch(() => fallback);
804
+ }
805
+ this.spent = addUsage(this.spent, {
806
+ inputTokens: usage.inputTokens,
807
+ outputTokens: usage.outputTokens,
808
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
809
+ cacheWriteTokens: usage.cacheWriteTokens ?? 0,
810
+ reasoningTokens: usage.reasoningTokens ?? 0,
811
+ costUsd: cost.costUsd ?? 0
812
+ });
813
+ if (cost.costUsd !== void 0) this.priced = true;
740
814
  await this.a.store.recordUsage({
741
815
  threadId: this.a.input.threadId,
742
816
  actorRef: this.a.input.actor.id,
743
- modelId: this.a.modelLabel,
744
- purpose: "chat",
817
+ modelId: this.modelId(),
818
+ purpose,
745
819
  usage,
746
- ...costUsd !== void 0 ? {
747
- costUsd
748
- } : {}
820
+ ...cost
749
821
  });
822
+ return {
823
+ usage,
824
+ cost
825
+ };
826
+ }
827
+ /** `session.usage.recorded`: a call OpenCode made besides the steps (the title, a compaction). */
828
+ async usageRecorded(d) {
829
+ await this.spend(d, d.source === "title" ? "title" : "history_summary");
830
+ }
831
+ async stepEnded(d) {
832
+ const { usage, cost } = await this.spend(d, "chat");
750
833
  if (!this.stepOpen) return;
751
834
  this.stepOpen = false;
752
835
  await this.write({
753
836
  kind: "step-finish",
754
837
  usage,
755
- costUsd: costUsd ?? null,
838
+ costUsd: cost.costUsd ?? null,
756
839
  ...this.segment.reasoningMs > 0 ? {
757
840
  reasoningMs: this.segment.reasoningMs
758
841
  } : {},
759
- model: this.a.modelLabel
842
+ model: this.modelId()
760
843
  });
761
844
  }
762
845
  /**
@@ -775,12 +858,7 @@ ${delta}` : delta;
775
858
  content: s.text,
776
859
  runId: this.a.runId,
777
860
  ...final ? {
778
- usage: {
779
- ...this.usage,
780
- ...this.costUsd !== void 0 ? {
781
- costUsd: this.costUsd
782
- } : {}
783
- }
861
+ usage: this.messageUsage()
784
862
  } : {},
785
863
  ...this.a.input.agentName !== void 0 ? {
786
864
  agentName: this.a.input.agentName
@@ -808,6 +886,26 @@ ${delta}` : delta;
808
886
  ]);
809
887
  return message.id;
810
888
  }
889
+ /** The run's usage as its last message carries it: the whole run, cache tokens included. */
890
+ messageUsage() {
891
+ const total = this.totalUsage();
892
+ return {
893
+ inputTokens: total.inputTokens,
894
+ outputTokens: total.outputTokens,
895
+ ...total.cacheReadTokens > 0 ? {
896
+ cacheReadTokens: total.cacheReadTokens
897
+ } : {},
898
+ ...total.cacheWriteTokens > 0 ? {
899
+ cacheWriteTokens: total.cacheWriteTokens
900
+ } : {},
901
+ ...total.reasoningTokens > 0 ? {
902
+ reasoningTokens: total.reasoningTokens
903
+ } : {},
904
+ ...this.priced || (this.a.carried?.costUsd ?? 0) > 0 ? {
905
+ costUsd: total.costUsd
906
+ } : {}
907
+ };
908
+ }
811
909
  /** A request recorded by an earlier process (a turn resumed after a restart). */
812
910
  async alreadyRecorded(id) {
813
911
  return (await this.a.store.toolCallApproval?.(id) ?? null) !== null;
@@ -1177,6 +1275,35 @@ ${delta}` : delta;
1177
1275
  });
1178
1276
  }
1179
1277
  };
1278
+ function modelOfLabel(label) {
1279
+ const slash = label?.indexOf("/") ?? -1;
1280
+ if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
1281
+ return {
1282
+ providerID: label.slice(0, slash),
1283
+ id: label.slice(slash + 1)
1284
+ };
1285
+ }
1286
+ __name(modelOfLabel, "modelOfLabel");
1287
+ function usageOf(tokens) {
1288
+ const n = /* @__PURE__ */ __name((v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0, "n");
1289
+ const read = n(tokens?.cache?.read);
1290
+ const write = n(tokens?.cache?.write);
1291
+ const reasoning = n(tokens?.reasoning);
1292
+ return {
1293
+ inputTokens: n(tokens?.input) + read + write,
1294
+ outputTokens: n(tokens?.output),
1295
+ ...read > 0 ? {
1296
+ cacheReadTokens: read
1297
+ } : {},
1298
+ ...write > 0 ? {
1299
+ cacheWriteTokens: write
1300
+ } : {},
1301
+ ...reasoning > 0 ? {
1302
+ reasoningTokens: reasoning
1303
+ } : {}
1304
+ };
1305
+ }
1306
+ __name(usageOf, "usageOf");
1180
1307
 
1181
1308
  // src/mcp.ts
1182
1309
  var LOOP_SERVED_KINDS = /* @__PURE__ */ new Set([
@@ -1608,7 +1735,7 @@ var OpenCodeTurns = class {
1608
1735
  spent = /* @__PURE__ */ new Set();
1609
1736
  /** When the tools endpoint was last registered for a location (`server|boot|directory|actor`). */
1610
1737
  toolsIssued = /* @__PURE__ */ new Map();
1611
- constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2) {
1738
+ constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2, pricingStore, agentOptions) {
1612
1739
  this.host = host;
1613
1740
  this.sessions = sessions;
1614
1741
  this.settings = settings;
@@ -1620,6 +1747,30 @@ var OpenCodeTurns = class {
1620
1747
  this.memory = memory;
1621
1748
  this.queue = queue;
1622
1749
  this.toolsTokens = toolsTokens2;
1750
+ this.prices = new import_nestjs_agent_core4.ModelPriceResolver({
1751
+ pricingStore,
1752
+ catalog: agentOptions?.priceCatalog,
1753
+ log: {
1754
+ info: /* @__PURE__ */ __name((message) => this.logger.log(message), "info"),
1755
+ warn: /* @__PURE__ */ __name((message) => this.logger.warn(message), "warn")
1756
+ }
1757
+ });
1758
+ }
1759
+ prices;
1760
+ /**
1761
+ * What a model call OpenCode made cost, by the same rule the library's loop records its own steps
1762
+ * at: a reported figure wins, else an estimate off the model's price — see
1763
+ * {@link OpenCodeEngineSettings.cost}.
1764
+ */
1765
+ async priceStep(step) {
1766
+ const { model, usage, reportedCostUsd } = step;
1767
+ const tokens = usage.inputTokens + usage.outputTokens;
1768
+ const reported = reportedCostUsd !== void 0 && (reportedCostUsd > 0 || tokens === 0);
1769
+ if (reported && this.settings.cost !== "estimate") {
1770
+ return (0, import_nestjs_agent_core4.resolveUsageCost)(usage, reportedCostUsd, void 0);
1771
+ }
1772
+ const price = model !== void 0 && tokens > 0 ? await this.prices.priceFor(priceRef(model)) : void 0;
1773
+ return (0, import_nestjs_agent_core4.resolveUsageCost)(usage, price === void 0 ? reportedCostUsd : void 0, price);
1623
1774
  }
1624
1775
  /** The tools server's name in OpenCode. */
1625
1776
  get toolsServer() {
@@ -2209,8 +2360,8 @@ ${body}
2209
2360
  }
2210
2361
  // ---- prompt / observe / reply --------------------------------------------------------------
2211
2362
  /** Start listening to the session (so nothing it says is missed), then send the user message. */
2212
- async prompt(runId, input, handle) {
2213
- const live = await this.ensureLive(runId, input, handle, false);
2363
+ async prompt(runId, input, handle, spent) {
2364
+ const live = await this.ensureLive(runId, input, handle, false, spent);
2214
2365
  try {
2215
2366
  const prompt = await this.host.promptFor?.({
2216
2367
  input,
@@ -2240,9 +2391,12 @@ ${body}
2240
2391
  * Wait for the turn's next milestone. In a process that was not following the turn (a resumed
2241
2392
  * durable run), the turn is rebuilt and first catches up on requests OpenCode raised meanwhile;
2242
2393
  * `session.wait` is the safety net for a terminal event that was missed.
2394
+ *
2395
+ * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2396
+ * rebuilt here starts from it, so the answer it persists carries the whole run's usage.
2243
2397
  */
2244
- async observe(runId, input, handle) {
2245
- const live = await this.ensureLive(runId, input, handle, true);
2398
+ async observe(runId, input, handle, spent) {
2399
+ const live = await this.ensureLive(runId, input, handle, true, spent);
2246
2400
  const { turn, client } = live;
2247
2401
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2248
2402
  let done = false;
@@ -2281,23 +2435,23 @@ ${body}
2281
2435
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2282
2436
  * to go on.
2283
2437
  */
2284
- async reply(runId, input, handle, ask, reply) {
2438
+ async reply(runId, input, handle, ask, reply, spent) {
2285
2439
  const server = await this.host.server(input.actor);
2286
2440
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2287
- return this.continueAfterRestart(runId, input, server, ask, reply);
2441
+ return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2288
2442
  }
2289
- const live = await this.ensureLive(runId, input, handle, false);
2443
+ const live = await this.ensureLive(runId, input, handle, false, spent);
2290
2444
  await live.turn.decide(ask, reply);
2291
2445
  return handle;
2292
2446
  }
2293
- async continueAfterRestart(runId, input, server, ask, reply) {
2447
+ async continueAfterRestart(runId, input, server, ask, reply, spent) {
2294
2448
  const approved = "approved" in reply && reply.approved === true;
2295
2449
  const note = ask.kind === "approval" ? approved ? `The user approved the action ${ask.action} you asked permission for. Continue the task.` : `The user did not approve the action ${ask.action}. Continue without it and explain.` : `The user answered your question: ${JSON.stringify("answers" in reply ? reply.answers : {})}. Continue.`;
2296
2450
  const thread = await this.store.getThread(input.threadId);
2297
2451
  const handle = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2298
2452
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2299
2453
  this.drop(runId);
2300
- const live = await this.ensureLive(runId, input, handle, false);
2454
+ const live = await this.ensureLive(runId, input, handle, false, spent);
2301
2455
  await live.turn.decide(ask, reply, {
2302
2456
  tellOpenCode: false
2303
2457
  });
@@ -2317,7 +2471,7 @@ ${body}
2317
2471
  sessionID: known.sessionId
2318
2472
  });
2319
2473
  }
2320
- async ensureLive(runId, input, handle, catchUp) {
2474
+ async ensureLive(runId, input, handle, catchUp, spent) {
2321
2475
  const current = this.live.get(runId);
2322
2476
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2323
2477
  current?.stop();
@@ -2343,6 +2497,10 @@ ${body}
2343
2497
  } : {}
2344
2498
  }), "approvalFor"),
2345
2499
  modelLabel: input.model ?? "opencode",
2500
+ priceStep: /* @__PURE__ */ __name((step) => this.priceStep(step), "priceStep"),
2501
+ ...spent !== void 0 ? {
2502
+ carried: spent
2503
+ } : {},
2346
2504
  logger: this.logger,
2347
2505
  onGranted: /* @__PURE__ */ __name((action, id) => {
2348
2506
  const grants = this.grants.get(runId) ?? [];
@@ -2389,20 +2547,14 @@ ${body}
2389
2547
  }
2390
2548
  return milestone;
2391
2549
  }
2392
- /** What the run produced, as the host's settle hooks see it. */
2393
- async runResult(runId, input, outcome, durationMs) {
2550
+ /**
2551
+ * What the run produced, as the host's settle hooks see it. `usage` is what the run spent — the
2552
+ * runner's sum of its milestones' usage, which a resumed run reads back from its journal — else
2553
+ * what the turn this process follows saw.
2554
+ */
2555
+ async runResult(runId, input, outcome, durationMs, usage) {
2394
2556
  const thread = await this.store.getThread(input.threadId);
2395
2557
  const messages = (thread?.messages ?? []).filter((m) => m.role === "assistant" && m.runId === runId);
2396
- const usage = {
2397
- inputTokens: 0,
2398
- outputTokens: 0,
2399
- costUsd: 0
2400
- };
2401
- for (const m of messages) {
2402
- usage.inputTokens += m.usage?.inputTokens ?? 0;
2403
- usage.outputTokens += m.usage?.outputTokens ?? 0;
2404
- usage.costUsd += m.usage?.costUsd ?? 0;
2405
- }
2406
2558
  return {
2407
2559
  runId,
2408
2560
  input,
@@ -2447,15 +2599,31 @@ ${body}
2447
2599
  this.logger.warn(`onSettled failed: ${errorText(error, "error")}`);
2448
2600
  }
2449
2601
  }
2450
- /** End the run's stream and bookkeeping for how its execution ended. */
2451
- async settle(runId, input, outcome, durationMs) {
2602
+ /**
2603
+ * What the run spent: the runner's sum of its milestones, or what the turn this process follows
2604
+ * saw (which starts from that sum) when it saw more — a run that failed or was cancelled between
2605
+ * two milestones.
2606
+ */
2607
+ spentBy(runId, spent) {
2608
+ const seen = this.live.get(runId)?.turn.totalUsage();
2609
+ if (spent === void 0) return seen ?? emptyUsage();
2610
+ if (seen === void 0) return spent;
2611
+ const tokens = /* @__PURE__ */ __name((u) => u.inputTokens + u.outputTokens, "tokens");
2612
+ return tokens(seen) > tokens(spent) ? seen : spent;
2613
+ }
2614
+ /**
2615
+ * End the run's stream and bookkeeping for how its execution ended. `spent` is what the run spent
2616
+ * (the sum of its milestones' `usage`); omit → what the turn this process follows saw.
2617
+ */
2618
+ async settle(runId, input, outcome, durationMs, spent) {
2452
2619
  if (outcome.status === "failed") {
2453
- await this.settleFailed(runId, input, outcome.error, durationMs);
2620
+ await this.settleFailed(runId, input, outcome.error, durationMs, spent);
2454
2621
  return;
2455
2622
  }
2623
+ const usage = this.spentBy(runId, spent);
2456
2624
  this.forget(runId);
2457
2625
  const writer = await this.sink.open(runId);
2458
- const result = await this.runResult(runId, input, outcome, durationMs);
2626
+ const result = await this.runResult(runId, input, outcome, durationMs, usage);
2459
2627
  await this.amend(writer, result);
2460
2628
  if (outcome.status === "interrupted") {
2461
2629
  this.logger.log(`agent run ${runId} cancelled`);
@@ -2483,13 +2651,14 @@ ${body}
2483
2651
  }
2484
2652
  await this.settled(result);
2485
2653
  }
2486
- async settleFailed(runId, input, error, durationMs = 0) {
2654
+ async settleFailed(runId, input, error, durationMs = 0, spent) {
2655
+ const usage = this.spentBy(runId, spent);
2487
2656
  this.forget(runId);
2488
2657
  const writer = await this.sink.open(runId);
2489
2658
  const result = await this.runResult(runId, input, {
2490
2659
  status: "failed",
2491
2660
  error
2492
- }, durationMs);
2661
+ }, durationMs, usage);
2493
2662
  const worded = (await this.amend(writer, result)).error;
2494
2663
  const message = worded ?? (0, import_nestjs_agent_core4.streamFailure)(new Error(error)).message;
2495
2664
  this.logger.error(`agent run ${runId} failed: ${error}`);
@@ -2559,6 +2728,10 @@ OpenCodeTurns = _ts_decorate2([
2559
2728
  _ts_param2(9, (0, import_common4.Inject)(import_nestjs_agent.AGENT_CHAT_QUEUE)),
2560
2729
  _ts_param2(10, (0, import_common4.Optional)()),
2561
2730
  _ts_param2(10, (0, import_common4.Inject)(OPENCODE_TOOLS_TOKENS)),
2731
+ _ts_param2(11, (0, import_common4.Optional)()),
2732
+ _ts_param2(11, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_PRICING_STORE)),
2733
+ _ts_param2(12, (0, import_common4.Optional)()),
2734
+ _ts_param2(12, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_OPTIONS)),
2562
2735
  _ts_metadata2("design:type", Function),
2563
2736
  _ts_metadata2("design:paramtypes", [
2564
2737
  typeof OpenCodeHost === "undefined" ? Object : OpenCodeHost,
@@ -2571,6 +2744,8 @@ OpenCodeTurns = _ts_decorate2([
2571
2744
  typeof SkillsConfig === "undefined" ? Object : SkillsConfig,
2572
2745
  typeof MemoryConfig === "undefined" ? Object : MemoryConfig,
2573
2746
  typeof ChatQueueService === "undefined" ? Object : ChatQueueService,
2747
+ Object,
2748
+ typeof AgentPricingStore === "undefined" ? Object : AgentPricingStore,
2574
2749
  Object
2575
2750
  ])
2576
2751
  ], OpenCodeTurns);
@@ -2613,6 +2788,19 @@ function uiEmitter(scope, push) {
2613
2788
  };
2614
2789
  }
2615
2790
  __name(uiEmitter, "uiEmitter");
2791
+ function priceRef(model) {
2792
+ const aliases = [
2793
+ model.id
2794
+ ];
2795
+ const tail = model.id.slice(model.id.lastIndexOf("/") + 1);
2796
+ if (tail !== model.id && (0, import_nestjs_agent_core4.parseBedrockModelId)(tail) !== void 0) aliases.push(tail);
2797
+ return {
2798
+ modelId: `${model.providerID}/${model.id}`,
2799
+ provider: model.providerID,
2800
+ aliases
2801
+ };
2802
+ }
2803
+ __name(priceRef, "priceRef");
2616
2804
 
2617
2805
  // src/runner.ts
2618
2806
  function _ts_decorate3(decorators, target, key, desc) {
@@ -2714,18 +2902,20 @@ var OpenCodeAgentRunner = class {
2714
2902
  async run(runId, input) {
2715
2903
  const started = Date.now();
2716
2904
  this.inputs.set(runId, input);
2905
+ let spent = emptyUsage();
2717
2906
  try {
2718
2907
  let handle = await this.turns.begin(runId, input);
2719
2908
  if (this.cancelled.has(runId)) throw new import_nestjs_agent_core5.RunCancelledError();
2720
- await this.turns.prompt(runId, input, handle);
2909
+ await this.turns.prompt(runId, input, handle, spent);
2721
2910
  for (; ; ) {
2722
- const milestone = await this.turns.observe(runId, input, handle);
2911
+ const milestone = await this.turns.observe(runId, input, handle, spent);
2912
+ spent = addUsage(spent, milestone.usage);
2723
2913
  if (milestone.kind === "finished") {
2724
- await this.turns.settle(runId, input, milestone.outcome, Date.now() - started);
2914
+ await this.turns.settle(runId, input, milestone.outcome, Date.now() - started, spent);
2725
2915
  return;
2726
2916
  }
2727
2917
  const reply = await this.park(runId, milestone.ask.id, milestone.ask.kind === "approval" ? "approval" : "answers", milestone.timeoutMs);
2728
- handle = await this.turns.reply(runId, input, handle, milestone.ask, reply);
2918
+ handle = await this.turns.reply(runId, input, handle, milestone.ask, reply, spent);
2729
2919
  }
2730
2920
  } catch (error) {
2731
2921
  if (error instanceof import_nestjs_agent_core5.RunCancelledError) {
@@ -2735,7 +2925,7 @@ var OpenCodeAgentRunner = class {
2735
2925
  }, Date.now() - started);
2736
2926
  return;
2737
2927
  }
2738
- await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"));
2928
+ await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"), Date.now() - started);
2739
2929
  } finally {
2740
2930
  this.inputs.delete(runId);
2741
2931
  }
@@ -2907,6 +3097,7 @@ var OpenCodeRunWorkflow = class {
2907
3097
  this.runs = runs;
2908
3098
  }
2909
3099
  async run(ctx, input) {
3100
+ let spent = emptyUsage();
2910
3101
  try {
2911
3102
  const begun = await ctx.localStep("begin", async () => ({
2912
3103
  handle: await this.turns.begin(ctx.runId, input),
@@ -2914,15 +3105,18 @@ var OpenCodeRunWorkflow = class {
2914
3105
  }));
2915
3106
  let handle = begun.handle;
2916
3107
  await ctx.localStep("prompt", async () => {
2917
- await this.turns.prompt(ctx.runId, input, handle);
3108
+ await this.turns.prompt(ctx.runId, input, handle, spent);
2918
3109
  return true;
2919
3110
  });
2920
3111
  for (let n = 0; ; n += 1) {
2921
- const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle));
3112
+ const before = spent;
3113
+ const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before));
3114
+ spent = addUsage(spent, milestone.usage);
3115
+ const total = spent;
2922
3116
  if (milestone.kind === "finished") {
2923
3117
  await ctx.localStep("finish", async () => {
2924
3118
  if (await this.cancelled(ctx.runId)) this.turns.drop(ctx.runId);
2925
- else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt);
3119
+ else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
2926
3120
  return true;
2927
3121
  });
2928
3122
  return {
@@ -2942,12 +3136,12 @@ var OpenCodeRunWorkflow = class {
2942
3136
  };
2943
3137
  }
2944
3138
  const ask = milestone.ask;
2945
- handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply));
3139
+ handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total));
2946
3140
  }
2947
3141
  } catch (error) {
2948
3142
  if ((0, import_nestjs_durable_core.isWorkflowControlFlowSignal)(error)) throw error;
2949
3143
  await ctx.localStep("fail", async () => {
2950
- await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"));
3144
+ await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent);
2951
3145
  return true;
2952
3146
  });
2953
3147
  return {