@leo000001/opencode-quota-sidebar 1.13.4 → 1.13.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  - Add Buzz API balance support for OpenAI-compatible providers that use a Buzz `baseURL`.
6
6
  - Document Buzz configuration, rendering, and outbound billing endpoints.
7
+ - Keep session measured cost aligned with OpenCode root-session `message.cost` while still including descendant subagent usage in API-equivalent cost.
8
+ - Support OpenCode long-context pricing tiers via `context_over_200k` when estimating API-equivalent cost.
9
+ - Bump the usage billing cache version so `/qday`, `/qweek`, and `/qmonth` recompute historical API cost with the updated rules.
10
+ - Document API-cost estimation, billing-cache behavior, and child-session aggregation semantics in the README.
7
11
 
8
12
  ## 1.13.2
9
13
 
package/README.md CHANGED
@@ -290,8 +290,11 @@ Other defaults:
290
290
  - `sidebar.childrenConcurrency` controls parallel fetches for descendant session messages (default: `5`, clamped 1–10).
291
291
  - `output` includes reasoning tokens (`output = tokens.output + tokens.reasoning`). Reasoning is not rendered as a separate line.
292
292
  - API cost bills reasoning tokens at the output rate (same as completion tokens).
293
+ - API cost is computed from OpenCode model pricing metadata, not from `message.cost`. This keeps subscription-backed providers such as OpenAI OAuth usable for API-equivalent cost estimation even when OpenCode's measured cost is `0`.
294
+ - When OpenCode exposes a long-context tier like `context_over_200k`, the plugin uses that premium rate for the whole request once `input > 200000`, matching OpenCode's current pricing schema.
293
295
  - `quota.providers` is the extensible per-adapter switch map.
294
296
  - If API Cost is `$0.00`, it usually means the model/provider has no pricing mapping in OpenCode at the moment, so equivalent API cost cannot be estimated.
297
+ - Usage chunks cache both measured `cost` and computed `apiCost`. `quota_summary` (`/qday`, `/qweek`, `/qmonth`) usually reads those cached aggregates first, but a billing-cache version bump or missing/legacy API-cost data will trigger a rescan and persist refreshed values.
295
298
 
296
299
  ### Buzz provider example
297
300
 
@@ -424,6 +427,13 @@ Mixed with Buzz balance:
424
427
 
425
428
  `quota_summary` also supports an optional `includeChildren` flag (only effective for `period=session`) to override the config per call. For `day`/`week`/`month` periods, children are never merged — each session is counted independently.
426
429
 
430
+ ## Billing cache behavior
431
+
432
+ - Cached per-session usage stores token totals, measured `cost`, computed `apiCost`, provider breakdowns, and the incremental cursor.
433
+ - Session-scoped sidebar aggregation can merge descendant subagents when `sidebar.includeChildren=true` (default). Measured `cost` stays aligned with the root session's OpenCode `message.cost`, while API-equivalent cost still includes descendant usage.
434
+ - Range tools such as `/qday`, `/qweek`, and `/qmonth` do not merge children. They aggregate each session independently across the selected time window.
435
+ - When API-cost logic changes, the plugin bumps an internal billing-cache version so historical range reports are recomputed with the new rules the next time they are queried.
436
+
427
437
  ## Debug logging
428
438
 
429
439
  Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
package/dist/cost.d.ts CHANGED
@@ -6,6 +6,12 @@ export type ModelCostRates = {
6
6
  output: number;
7
7
  cacheRead: number;
8
8
  cacheWrite: number;
9
+ contextOver200k?: {
10
+ input: number;
11
+ output: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ };
9
15
  };
10
16
  export declare function modelCostKey(providerID: string, modelID: string): string;
11
17
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
package/dist/cost.js CHANGED
@@ -42,14 +42,28 @@ export function parseModelCostRates(value) {
42
42
  const output = readRate(value.output ?? value.completion);
43
43
  const cacheRead = readRate(value.cache_read ?? cache?.read);
44
44
  const cacheWrite = readRate(value.cache_write ?? cache?.write);
45
+ const contextOver200k = isRecord(value.context_over_200k)
46
+ ? {
47
+ input: readRate(value.context_over_200k.input),
48
+ output: readRate(value.context_over_200k.output),
49
+ cacheRead: readRate(value.context_over_200k.cache_read),
50
+ cacheWrite: readRate(value.context_over_200k.cache_write),
51
+ }
52
+ : undefined;
45
53
  if (input <= 0 && output <= 0 && cacheRead <= 0 && cacheWrite <= 0) {
46
54
  return undefined;
47
55
  }
56
+ const hasContextTier = !!contextOver200k &&
57
+ (contextOver200k.input > 0 ||
58
+ contextOver200k.output > 0 ||
59
+ contextOver200k.cacheRead > 0 ||
60
+ contextOver200k.cacheWrite > 0);
48
61
  return {
49
62
  input,
50
63
  output,
51
64
  cacheRead,
52
65
  cacheWrite,
66
+ contextOver200k: hasContextTier ? contextOver200k : undefined,
53
67
  };
54
68
  }
55
69
  const MODEL_COST_DIVISOR_PER_TOKEN = 1;
@@ -65,15 +79,18 @@ export function guessModelCostDivisor(rates) {
65
79
  : MODEL_COST_DIVISOR_PER_TOKEN;
66
80
  }
67
81
  export function calcEquivalentApiCostForMessage(message, rates) {
82
+ const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
83
+ ? rates.contextOver200k
84
+ : rates;
68
85
  // For providers that expose reasoning tokens separately, they are still
69
86
  // billed as output/completion tokens (same unit price). Our UI also merges
70
87
  // reasoning into the single Output statistic, so API cost should match that.
71
88
  const billedOutput = message.tokens.output + message.tokens.reasoning;
72
- const rawCost = message.tokens.input * rates.input +
73
- billedOutput * rates.output +
74
- message.tokens.cache.read * rates.cacheRead +
75
- message.tokens.cache.write * rates.cacheWrite;
76
- const divisor = guessModelCostDivisor(rates);
89
+ const rawCost = message.tokens.input * effectiveRates.input +
90
+ billedOutput * effectiveRates.output +
91
+ message.tokens.cache.read * effectiveRates.cacheRead +
92
+ message.tokens.cache.write * effectiveRates.cacheWrite;
93
+ const divisor = guessModelCostDivisor(effectiveRates);
77
94
  const normalized = rawCost / divisor;
78
95
  return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
79
96
  }
package/dist/usage.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage, Message } from '@opencode-ai/sdk';
2
2
  import type { CachedSessionUsage, IncrementalCursor } from './types.js';
3
- export declare const USAGE_BILLING_CACHE_VERSION = 1;
3
+ export declare const USAGE_BILLING_CACHE_VERSION = 2;
4
4
  export type ProviderUsage = {
5
5
  providerID: string;
6
6
  input: number;
@@ -47,6 +47,8 @@ export declare function summarizeMessagesIncremental(entries: Array<{
47
47
  usage: UsageSummary;
48
48
  cursor: IncrementalCursor;
49
49
  };
50
- export declare function mergeUsage(target: UsageSummary, source: UsageSummary): UsageSummary;
50
+ export declare function mergeUsage(target: UsageSummary, source: UsageSummary, options?: {
51
+ includeCost?: boolean;
52
+ }): UsageSummary;
51
53
  export declare function toCachedSessionUsage(summary: UsageSummary): CachedSessionUsage;
52
54
  export declare function fromCachedSessionUsage(cached: CachedSessionUsage, sessionCount?: number): UsageSummary;
package/dist/usage.js CHANGED
@@ -1,4 +1,4 @@
1
- export const USAGE_BILLING_CACHE_VERSION = 1;
1
+ export const USAGE_BILLING_CACHE_VERSION = 2;
2
2
  export function emptyUsageSummary() {
3
3
  return {
4
4
  input: 0,
@@ -263,13 +263,16 @@ function findLastCompletedAssistant(entries) {
263
263
  }
264
264
  return best;
265
265
  }
266
- export function mergeUsage(target, source) {
266
+ export function mergeUsage(target, source, options) {
267
+ const includeCost = options?.includeCost !== false;
267
268
  target.input += source.input;
268
269
  target.output += source.output;
269
270
  target.cacheRead += source.cacheRead;
270
271
  target.cacheWrite += source.cacheWrite;
271
272
  target.total += source.total;
272
- target.cost += source.cost;
273
+ if (includeCost) {
274
+ target.cost += source.cost;
275
+ }
273
276
  target.apiCost += source.apiCost;
274
277
  target.assistantMessages += source.assistantMessages;
275
278
  target.sessionCount += source.sessionCount;
@@ -281,7 +284,9 @@ export function mergeUsage(target, source) {
281
284
  existing.cacheRead += provider.cacheRead;
282
285
  existing.cacheWrite += provider.cacheWrite;
283
286
  existing.total += provider.total;
284
- existing.cost += provider.cost;
287
+ if (includeCost) {
288
+ existing.cost += provider.cost;
289
+ }
285
290
  existing.apiCost += provider.apiCost;
286
291
  existing.assistantMessages += provider.assistantMessages;
287
292
  target.providers[provider.providerID] = existing;
@@ -279,7 +279,11 @@ export function createUsageService(deps) {
279
279
  for (const childID of descendantIDs) {
280
280
  const cached = deps.state.sessions[childID]?.usage;
281
281
  if (cached && !isDirty(childID) && isUsageBillingCurrent(cached)) {
282
- mergeUsage(merged, fromCachedSessionUsage(cached, 1));
282
+ // Keep measured cost aligned with OpenCode session semantics by only
283
+ // using child sessions for token/API-cost aggregation.
284
+ mergeUsage(merged, fromCachedSessionUsage(cached, 1), {
285
+ includeCost: false,
286
+ });
283
287
  }
284
288
  else {
285
289
  needsFetch.push(childID);
@@ -294,7 +298,7 @@ export function createUsageService(deps) {
294
298
  return child.usage;
295
299
  });
296
300
  for (const childUsage of fetched) {
297
- mergeUsage(merged, childUsage);
301
+ mergeUsage(merged, childUsage, { includeCost: false });
298
302
  }
299
303
  }
300
304
  return merged;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.13.4",
3
+ "version": "1.13.6",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",