@leo000001/opencode-quota-sidebar 1.13.7 → 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,16 +96,11 @@ 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) {
82
- const info = message;
99
+ export function calcEquivalentApiCostForMessage(message, rates, canonicalProviderID = message.providerID, serviceTier = openAIServiceTierFromMessage(message)) {
83
100
  const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
84
101
  ? rates.contextOver200k
85
102
  : 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;
103
+ const priorityMultiplier = canonicalProviderID === 'openai' && serviceTier === 'priority' ? 2 : 1;
91
104
  // For providers that expose reasoning tokens separately, they are still
92
105
  // billed as output/completion tokens (same unit price). Our UI also merges
93
106
  // reasoning into the single Output statistic, so API cost should match that.
@@ -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) => {
60
+ if (!isRecord(provider))
61
+ return acc;
62
+ const providerID = typeof provider.id === 'string'
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) => {
49
93
  if (!isRecord(provider))
50
94
  return acc;
51
95
  const providerID = typeof provider.id === 'string'
52
- ? canonicalApiCostProviderID(provider.id)
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.13.7",
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",