@leo000001/opencode-quota-sidebar 1.13.4 → 1.13.7

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,25 @@ export function guessModelCostDivisor(rates) {
65
79
  : MODEL_COST_DIVISOR_PER_TOKEN;
66
80
  }
67
81
  export function calcEquivalentApiCostForMessage(message, rates) {
82
+ const info = message;
83
+ const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
84
+ ? rates.contextOver200k
85
+ : rates;
86
+ const serviceTier = info.providerMetadata?.openai?.serviceTier ??
87
+ info.providerMetadata?.openai?.service_tier;
88
+ const priorityMultiplier = message.providerID === 'openai' && serviceTier === 'priority'
89
+ ? 2
90
+ : 1;
68
91
  // For providers that expose reasoning tokens separately, they are still
69
92
  // billed as output/completion tokens (same unit price). Our UI also merges
70
93
  // reasoning into the single Output statistic, so API cost should match that.
71
94
  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);
95
+ const rawCost = (message.tokens.input * effectiveRates.input +
96
+ billedOutput * effectiveRates.output +
97
+ message.tokens.cache.read * effectiveRates.cacheRead +
98
+ message.tokens.cache.write * effectiveRates.cacheWrite) *
99
+ priorityMultiplier;
100
+ const divisor = guessModelCostDivisor(effectiveRates);
77
101
  const normalized = rawCost / divisor;
78
102
  return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
79
103
  }
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;
@@ -143,12 +143,34 @@ export function createUsageService(deps) {
143
143
  tokens,
144
144
  };
145
145
  };
146
+ const extractProviderMetadata = (parts) => {
147
+ if (!Array.isArray(parts))
148
+ return undefined;
149
+ for (const part of parts) {
150
+ if (!isRecord(part))
151
+ continue;
152
+ const meta = part.metadata;
153
+ if (isRecord(meta))
154
+ return meta;
155
+ const stateMeta = isRecord(part.state)
156
+ ? part.state?.metadata
157
+ : undefined;
158
+ if (isRecord(stateMeta))
159
+ return stateMeta;
160
+ }
161
+ return undefined;
162
+ };
146
163
  const decodeMessageEntry = (value) => {
147
164
  if (!isRecord(value))
148
165
  return undefined;
149
166
  const decoded = decodeMessageInfo(value.info);
150
167
  if (!decoded)
151
168
  return undefined;
169
+ const metadata = extractProviderMetadata(value.parts);
170
+ if (metadata && decoded.role === 'assistant') {
171
+ const msg = decoded;
172
+ msg.providerMetadata = metadata;
173
+ }
152
174
  return { info: decoded };
153
175
  };
154
176
  const decodeMessageEntries = (value) => {
@@ -279,7 +301,11 @@ export function createUsageService(deps) {
279
301
  for (const childID of descendantIDs) {
280
302
  const cached = deps.state.sessions[childID]?.usage;
281
303
  if (cached && !isDirty(childID) && isUsageBillingCurrent(cached)) {
282
- mergeUsage(merged, fromCachedSessionUsage(cached, 1));
304
+ // Keep measured cost aligned with OpenCode session semantics by only
305
+ // using child sessions for token/API-cost aggregation.
306
+ mergeUsage(merged, fromCachedSessionUsage(cached, 1), {
307
+ includeCost: false,
308
+ });
283
309
  }
284
310
  else {
285
311
  needsFetch.push(childID);
@@ -294,7 +320,7 @@ export function createUsageService(deps) {
294
320
  return child.usage;
295
321
  });
296
322
  for (const childUsage of fetched) {
297
- mergeUsage(merged, childUsage);
323
+ mergeUsage(merged, childUsage, { includeCost: false });
298
324
  }
299
325
  }
300
326
  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.7",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",