teapot-coding-agent 0.19.4 → 0.19.6

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.
@@ -78,6 +78,8 @@ export class Agent {
78
78
  outputTokens: 0,
79
79
  compactions: 0,
80
80
  startedAt: null,
81
+ /** running cost estimate in USD (pricing known only) */
82
+ costUsd: 0,
81
83
  };
82
84
  opts;
83
85
  messages = [];
@@ -105,6 +107,9 @@ export class Agent {
105
107
  lastProgressAt = Date.now();
106
108
  /** the provider's own prompt_tokens from the last completed turn */
107
109
  lastUsage;
110
+ /** USD per token for the CURRENT model (from provider /models metadata);
111
+ undefined = pricing unknown → cost estimate stays 0 and hidden */
112
+ modelPricing;
108
113
  /** messages.length right after the last successful compaction */
109
114
  compactedAtLen = 0;
110
115
  /** live compaction phase for UI progress ("summarizing"/"harvesting") */
@@ -194,6 +199,35 @@ export class Agent {
194
199
  this.messages = msgs;
195
200
  this.compactedAtLen = 0;
196
201
  }
202
+ /**
203
+ * Attach USD-per-token pricing for the CURRENT model (provider /models
204
+ * metadata). Late resolution is normal: the metadata lookup races boot.
205
+ * Recomputes the session cost from the log so a restart with pricing now
206
+ * known still shows the full number instead of starting from zero.
207
+ */
208
+ async setModelPricing(p) {
209
+ const wasUndefined = !this.modelPricing;
210
+ this.modelPricing = p;
211
+ if (!p || !wasUndefined)
212
+ return; // already priced, or nothing to price
213
+ // recompute from the usage events (cheap: file is mtime-cached)
214
+ try {
215
+ const events = await readEvents(this.log.filePath);
216
+ let cost = 0;
217
+ for (const e of events) {
218
+ if (e.type !== "usage")
219
+ continue;
220
+ const u = e.data;
221
+ const inTok = u.inputTokens ?? 0;
222
+ const cached = Math.min(u.cachedInputTokens ?? 0, inTok);
223
+ cost += (inTok - cached) * p.prompt + (u.outputTokens ?? 0) * p.completion + cached * p.prompt * 0.1;
224
+ }
225
+ this.stats.costUsd = cost;
226
+ }
227
+ catch {
228
+ /* no log yet — live accumulation takes over */
229
+ }
230
+ }
197
231
  get workspace() {
198
232
  return this.opts.workspace;
199
233
  }
@@ -420,6 +454,15 @@ export class Agent {
420
454
  this.stats.inputTokens += u.inputTokens ?? 0;
421
455
  this.stats.outputTokens += u.outputTokens ?? 0;
422
456
  this.stats.cachedInputTokens += u.cachedInputTokens ?? 0;
457
+ if (this.modelPricing) {
458
+ const p = this.modelPricing;
459
+ const inTok = u.inputTokens ?? 0;
460
+ const cached = Math.min(u.cachedInputTokens ?? 0, inTok);
461
+ // same blended-cache model as the live path (cached ≈ 10% of prompt)
462
+ this.stats.costUsd =
463
+ (this.stats.costUsd ?? 0) +
464
+ ((inTok - cached) * p.prompt + (u.outputTokens ?? 0) * p.completion + cached * p.prompt * 0.1);
465
+ }
423
466
  }
424
467
  else if (e.type === "system_note") {
425
468
  const d = e.data;
@@ -666,7 +709,7 @@ export class Agent {
666
709
  ...(this.goal.audit ? { audit: this.goal.audit } : {}),
667
710
  },
668
711
  latestProgress: this.latestProgress,
669
- stats: { ...this.stats },
712
+ stats: { ...this.stats, costUsd: this.modelPricing ? this.stats.costUsd : undefined },
670
713
  model: this.opts.llm.model,
671
714
  provider: this.opts.provider,
672
715
  sessionDir: this.opts.sessionDir,
@@ -1029,9 +1072,22 @@ export class Agent {
1029
1072
  throw err;
1030
1073
  }
1031
1074
  if (res.usage) {
1032
- this.stats.inputTokens += res.usage.inputTokens ?? 0;
1033
- this.stats.cachedInputTokens += res.usage.cachedInputTokens ?? 0;
1075
+ const inTok = res.usage.inputTokens ?? 0;
1076
+ const cached = res.usage.cachedInputTokens ?? 0;
1077
+ this.stats.inputTokens += inTok;
1078
+ this.stats.cachedInputTokens += cached;
1034
1079
  this.stats.outputTokens += res.usage.outputTokens ?? 0;
1080
+ // cost estimate: uncached input at prompt rate, cache hits usually
1081
+ // bill at a fraction (OpenRouter reports the blended prompt price, so
1082
+ // cached tokens are charged separately at ~10% — a common provider
1083
+ // convention; without per-provider detail this is the honest guess)
1084
+ if (this.modelPricing) {
1085
+ const p = this.modelPricing;
1086
+ const uncached = Math.max(0, inTok - cached);
1087
+ this.stats.costUsd =
1088
+ (this.stats.costUsd ?? 0) +
1089
+ (uncached * p.prompt + (res.usage.outputTokens ?? 0) * p.completion + cached * p.prompt * 0.1);
1090
+ }
1035
1091
  this.lastUsage = {
1036
1092
  input: res.usage.inputTokens ?? 0,
1037
1093
  output: res.usage.outputTokens ?? 0,
package/dist/master.js CHANGED
@@ -397,6 +397,11 @@ export class Master {
397
397
  ...(ac.parent ? { parent: ac.parent } : {}),
398
398
  ...(ac.chatFn ? { chatFn: ac.chatFn } : {}),
399
399
  });
400
+ // pricing metadata (USD/token) for the runtime cost estimate — best
401
+ // effort: providers without pricing in /models simply show no cost
402
+ void fetchModelList(llm.baseUrl, llm.apiKey)
403
+ .then((list) => agent.setModelPricing(list.find((m) => m.id === llm.model)?.pricing))
404
+ .catch(() => { });
400
405
  // console line + broadcast: the web UI only refreshes on bus traffic, so
401
406
  // every appended event must reach it (otherwise messages sit invisible
402
407
  // until an unrelated status change happens to fire)
@@ -812,6 +817,12 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
812
817
  if (!agent.compactBudgetIsManual)
813
818
  opts.opts.contextTokenBudget = Math.round(win * 0.75);
814
819
  }
820
+ // re-price for the NEW model (cost estimate follows the model switch)
821
+ try {
822
+ const list = await fetchModelList(llm.baseUrl, llm.apiKey);
823
+ await agent.setModelPricing(list.find((m) => m.id === llm.model)?.pricing);
824
+ }
825
+ catch { /* pricing stays stale — cosmetic only */ }
815
826
  this.saveConfig();
816
827
  void agent.log.append("system_note", agent.currentSession, agent.currentBranch, {
817
828
  event: "model-changed",
@@ -30,6 +30,14 @@ export async function fetchModelList(baseUrl, apiKey) {
30
30
  .map((m) => ({
31
31
  id: typeof m.id === "string" ? m.id : "",
32
32
  contextLength: typeof m.context_length === "number" ? m.context_length : undefined,
33
+ // OpenRouter-style USD per token (e.g. "0.000003") — powers the
34
+ // runtime cost estimate. Providers without pricing stay undefined.
35
+ pricing: m.pricing && (m.pricing.prompt !== undefined || m.pricing.completion !== undefined)
36
+ ? {
37
+ prompt: Number(m.pricing.prompt ?? 0),
38
+ completion: Number(m.pricing.completion ?? 0),
39
+ }
40
+ : undefined,
33
41
  }))
34
42
  .filter((m) => m.id);
35
43
  cache.set(root, { at: Date.now(), list });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",