@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 +4 -0
- package/dist/cost.d.ts +3 -2
- package/dist/cost.js +24 -4
- package/dist/usage_service.js +79 -8
- package/package.json +1 -1
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
|
-
|
|
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;
|
package/dist/usage_service.js
CHANGED
|
@@ -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
|
|
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
|
-
?
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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) => {
|