@leo000001/opencode-quota-sidebar 1.13.6 → 1.13.8

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
@@ -63,6 +63,10 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
63
63
  - RightCode daily quota shows `$remaining/$dailyTotal` + expiry (e.g. `RC Daily $105/$60 Exp 02-27`, without trailing percent) and also shows balance on the next indented line when available; `Exp` remains date-only
64
64
  - Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
65
65
  - Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
66
+ - OpenAI priority API-cost detection uses two fallbacks in order:
67
+ - message metadata: `openai.serviceTier` / `openai.service_tier`
68
+ - model defaults from `provider.list`: `models[modelID].options.serviceTier`
69
+ - provider aliases with `npm: "@ai-sdk/openai"` are treated as OpenAI for this billing rule
66
70
  - Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
67
71
  - Custom tools:
68
72
  - `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
package/dist/cost.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage } from '@opencode-ai/sdk';
2
2
  export declare const SUBSCRIPTION_API_COST_PROVIDERS: Set<string>;
3
- export declare function canonicalApiCostProviderID(providerID: string): string;
3
+ export declare function canonicalApiCostProviderID(providerID: string, npmPackage?: string): string;
4
4
  export type ModelCostRates = {
5
5
  input: number;
6
6
  output: number;
@@ -13,7 +13,8 @@ export type ModelCostRates = {
13
13
  cacheWrite: number;
14
14
  };
15
15
  };
16
+ export declare function openAIServiceTierFromMessage(message: AssistantMessage): string | undefined;
16
17
  export declare function modelCostKey(providerID: string, modelID: string): string;
17
18
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
18
19
  export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
19
- export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates): number;
20
+ export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates, canonicalProviderID?: string, serviceTier?: string | undefined): number;
package/dist/cost.js CHANGED
@@ -5,7 +5,20 @@ function normalizeKnownProviderID(providerID) {
5
5
  return 'github-copilot';
6
6
  return providerID;
7
7
  }
8
- export function canonicalApiCostProviderID(providerID) {
8
+ function canonicalProviderByNpmPackage(npmPackage) {
9
+ const normalized = npmPackage.trim().toLowerCase();
10
+ if (normalized === '@ai-sdk/openai')
11
+ return 'openai';
12
+ if (normalized === '@ai-sdk/anthropic')
13
+ return 'anthropic';
14
+ return undefined;
15
+ }
16
+ export function canonicalApiCostProviderID(providerID, npmPackage) {
17
+ const byPackage = typeof npmPackage === 'string'
18
+ ? canonicalProviderByNpmPackage(npmPackage)
19
+ : undefined;
20
+ if (byPackage)
21
+ return byPackage;
9
22
  const normalized = normalizeKnownProviderID(providerID);
10
23
  if (SUBSCRIPTION_API_COST_PROVIDERS.has(normalized))
11
24
  return normalized;
@@ -19,6 +32,11 @@ export function canonicalApiCostProviderID(providerID) {
19
32
  }
20
33
  return normalized;
21
34
  }
35
+ export function openAIServiceTierFromMessage(message) {
36
+ const info = message;
37
+ return (info.providerMetadata?.openai?.serviceTier ??
38
+ info.providerMetadata?.openai?.service_tier);
39
+ }
22
40
  export function modelCostKey(providerID, modelID) {
23
41
  return `${providerID}:${modelID}`;
24
42
  }
@@ -78,18 +96,20 @@ export function guessModelCostDivisor(rates) {
78
96
  ? MODEL_COST_DIVISOR_PER_MILLION
79
97
  : MODEL_COST_DIVISOR_PER_TOKEN;
80
98
  }
81
- export function calcEquivalentApiCostForMessage(message, rates) {
99
+ export function calcEquivalentApiCostForMessage(message, rates, canonicalProviderID = message.providerID, serviceTier = openAIServiceTierFromMessage(message)) {
82
100
  const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
83
101
  ? rates.contextOver200k
84
102
  : rates;
103
+ const priorityMultiplier = canonicalProviderID === 'openai' && serviceTier === 'priority' ? 2 : 1;
85
104
  // For providers that expose reasoning tokens separately, they are still
86
105
  // billed as output/completion tokens (same unit price). Our UI also merges
87
106
  // reasoning into the single Output statistic, so API cost should match that.
88
107
  const billedOutput = message.tokens.output + message.tokens.reasoning;
89
- const rawCost = message.tokens.input * effectiveRates.input +
108
+ const rawCost = (message.tokens.input * effectiveRates.input +
90
109
  billedOutput * effectiveRates.output +
91
110
  message.tokens.cache.read * effectiveRates.cacheRead +
92
- message.tokens.cache.write * effectiveRates.cacheWrite;
111
+ message.tokens.cache.write * effectiveRates.cacheWrite) *
112
+ priorityMultiplier;
93
113
  const divisor = guessModelCostDivisor(effectiveRates);
94
114
  const normalized = rawCost / divisor;
95
115
  return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
@@ -1,5 +1,5 @@
1
1
  import { TtlValueCache } from './cache.js';
2
- import { calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
2
+ import { calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, openAIServiceTierFromMessage, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
3
3
  import { dateKeyFromTimestamp, scanSessionsByCreatedRange, updateSessionsInDayChunks, } from './storage.js';
4
4
  import { periodStart } from './period.js';
5
5
  import { debug, isRecord, mapConcurrent, swallow } from './helpers.js';
@@ -30,7 +30,7 @@ export function createUsageService(deps) {
30
30
  return cached;
31
31
  const providerClient = deps.client;
32
32
  if (!providerClient.provider?.list) {
33
- return modelCostCache.set({}, 30_000);
33
+ return modelCostCache.set({ rates: {}, providerAliases: {}, modelServiceTiers: {} }, 30_000);
34
34
  }
35
35
  const response = await providerClient.provider
36
36
  .list({
@@ -45,11 +45,56 @@ export function createUsageService(deps) {
45
45
  Array.isArray(response.data.all)
46
46
  ? response.data.all
47
47
  : [];
48
- const map = all.reduce((acc, provider) => {
48
+ const providerAliases = all.reduce((acc, provider) => {
49
+ if (!isRecord(provider))
50
+ return acc;
51
+ if (typeof provider.id !== 'string')
52
+ return acc;
53
+ const canonical = canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined);
54
+ if (!SUBSCRIPTION_API_COST_PROVIDERS.has(canonical))
55
+ return acc;
56
+ acc[provider.id] = canonical;
57
+ return acc;
58
+ }, {});
59
+ const modelServiceTiers = all.reduce((acc, provider) => {
49
60
  if (!isRecord(provider))
50
61
  return acc;
51
62
  const providerID = typeof provider.id === 'string'
52
- ? canonicalApiCostProviderID(provider.id)
63
+ ? (providerAliases[provider.id] ??
64
+ canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
65
+ : undefined;
66
+ if (!providerID)
67
+ return acc;
68
+ if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
69
+ return acc;
70
+ const models = provider.models;
71
+ if (!isRecord(models))
72
+ return acc;
73
+ for (const [modelKey, modelValue] of Object.entries(models)) {
74
+ if (!isRecord(modelValue))
75
+ continue;
76
+ const options = isRecord(modelValue.options)
77
+ ? modelValue.options
78
+ : undefined;
79
+ const serviceTier = typeof options?.serviceTier === 'string'
80
+ ? options.serviceTier
81
+ : undefined;
82
+ if (!serviceTier)
83
+ continue;
84
+ const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
85
+ acc[modelCostKey(providerID, modelID)] = serviceTier;
86
+ if (modelKey !== modelID) {
87
+ acc[modelCostKey(providerID, modelKey)] = serviceTier;
88
+ }
89
+ }
90
+ return acc;
91
+ }, {});
92
+ const rates = all.reduce((acc, provider) => {
93
+ if (!isRecord(provider))
94
+ return acc;
95
+ const providerID = typeof provider.id === 'string'
96
+ ? (providerAliases[provider.id] ??
97
+ canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
53
98
  : undefined;
54
99
  if (!providerID)
55
100
  return acc;
@@ -72,13 +117,15 @@ export function createUsageService(deps) {
72
117
  }
73
118
  return acc;
74
119
  }, {});
75
- return modelCostCache.set(map, Math.max(30_000, deps.config.quota.refreshMs));
120
+ return modelCostCache.set({ rates, providerAliases, modelServiceTiers }, Math.max(30_000, deps.config.quota.refreshMs));
76
121
  };
77
122
  const calcEquivalentApiCost = (message, modelCostMap) => {
78
- const providerID = canonicalApiCostProviderID(message.providerID);
123
+ const providerID = typeof modelCostMap.providerAliases[message.providerID] === 'string'
124
+ ? modelCostMap.providerAliases[message.providerID]
125
+ : canonicalApiCostProviderID(message.providerID);
79
126
  if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
80
127
  return 0;
81
- const rates = modelCostMap[modelCostKey(providerID, message.modelID)];
128
+ const rates = modelCostMap.rates[modelCostKey(providerID, message.modelID)];
82
129
  if (!rates) {
83
130
  const key = modelCostKey(providerID, message.modelID);
84
131
  if (!missingApiCostRateKeys.has(key)) {
@@ -87,7 +134,9 @@ export function createUsageService(deps) {
87
134
  }
88
135
  return 0;
89
136
  }
90
- return calcEquivalentApiCostForMessage(message, rates);
137
+ const serviceTier = openAIServiceTierFromMessage(message) ??
138
+ modelCostMap.modelServiceTiers[modelCostKey(providerID, message.modelID)];
139
+ return calcEquivalentApiCostForMessage(message, rates, providerID, serviceTier);
91
140
  };
92
141
  const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
93
142
  const decodeTokens = (value) => {
@@ -143,12 +192,34 @@ export function createUsageService(deps) {
143
192
  tokens,
144
193
  };
145
194
  };
195
+ const extractProviderMetadata = (parts) => {
196
+ if (!Array.isArray(parts))
197
+ return undefined;
198
+ for (const part of parts) {
199
+ if (!isRecord(part))
200
+ continue;
201
+ const meta = part.metadata;
202
+ if (isRecord(meta))
203
+ return meta;
204
+ const stateMeta = isRecord(part.state)
205
+ ? part.state?.metadata
206
+ : undefined;
207
+ if (isRecord(stateMeta))
208
+ return stateMeta;
209
+ }
210
+ return undefined;
211
+ };
146
212
  const decodeMessageEntry = (value) => {
147
213
  if (!isRecord(value))
148
214
  return undefined;
149
215
  const decoded = decodeMessageInfo(value.info);
150
216
  if (!decoded)
151
217
  return undefined;
218
+ const metadata = extractProviderMetadata(value.parts);
219
+ if (metadata && decoded.role === 'assistant') {
220
+ const msg = decoded;
221
+ msg.providerMetadata = metadata;
222
+ }
152
223
  return { info: decoded };
153
224
  };
154
225
  const decodeMessageEntries = (value) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.13.6",
3
+ "version": "1.13.8",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",