@leo000001/opencode-quota-sidebar 2.0.14 → 2.0.19

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/dist/cost.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { AssistantMessage } from '@opencode-ai/sdk';
2
2
  import type { CacheCoverageMode } from './types.js';
3
3
  export declare const API_COST_ENABLED_PROVIDERS: Set<string>;
4
+ export type CanonicalPriceSource = 'official-doc' | 'runtime';
5
+ export declare function canonicalPricingProviderID(providerID: string): string;
4
6
  export declare function canonicalApiCostProviderID(providerID: string): string;
5
7
  export type ModelCostRates = {
6
8
  input: number;
@@ -14,8 +16,38 @@ export type ModelCostRates = {
14
16
  cacheWrite: number;
15
17
  };
16
18
  };
19
+ export type CanonicalPriceEntry = {
20
+ provider: string;
21
+ model: string;
22
+ rates: ModelCostRates;
23
+ source: CanonicalPriceSource;
24
+ sourceURL?: string;
25
+ updatedAt?: string;
26
+ };
17
27
  export declare function modelCostKey(providerID: string, modelID: string): string;
18
28
  export declare function modelCostLookupKeys(providerID: string, modelID: string): string[];
29
+ export declare function getBundledModelCostMap(): {
30
+ [x: string]: ModelCostRates;
31
+ };
32
+ export declare function getBundledCanonicalPriceEntries(): {
33
+ rates: {
34
+ contextOver200k: {
35
+ input: number;
36
+ output: number;
37
+ cacheRead: number;
38
+ cacheWrite: number;
39
+ } | undefined;
40
+ input: number;
41
+ output: number;
42
+ cacheRead: number;
43
+ cacheWrite: number;
44
+ };
45
+ provider: string;
46
+ model: string;
47
+ source: CanonicalPriceSource;
48
+ sourceURL?: string;
49
+ updatedAt?: string;
50
+ }[];
19
51
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
20
52
  export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
21
53
  export declare function cacheCoverageModeFromRates(rates: ModelCostRates | undefined): CacheCoverageMode;
package/dist/cost.js CHANGED
@@ -3,11 +3,73 @@ export const API_COST_ENABLED_PROVIDERS = new Set([
3
3
  'openai',
4
4
  'anthropic',
5
5
  'kimi-for-coding',
6
+ 'zhipu',
6
7
  ]);
7
8
  const MODEL_COST_RATE_ALIASES = {
8
- 'kimi-for-coding:k2p5': ['moonshotai-cn:kimi-k2.5'],
9
- 'kimi-for-coding:kimi-k2-thinking': ['moonshotai-cn:kimi-k2-thinking'],
9
+ 'zhipuai-coding-plan:glm-5.1': ['zhipu:glm-5'],
10
+ 'zhipuai-coding-plan:glm-5.1-thinking': ['zhipu:glm-5'],
11
+ 'zhipu:glm-5.1': ['zhipu:glm-5'],
12
+ 'zhipu:glm-5.1-thinking': ['zhipu:glm-5'],
10
13
  };
14
+ function moonshotCanonicalModelID(modelID) {
15
+ const stripped = modelID.replace(/^moonshotai[/:]/i, '');
16
+ switch (stripped) {
17
+ case 'k2p5':
18
+ case 'kimi-k2-5':
19
+ return 'kimi-k2.5';
20
+ default:
21
+ return stripped;
22
+ }
23
+ }
24
+ function moonshotModelAliases(modelID, options) {
25
+ const aliases = [];
26
+ const push = (value) => {
27
+ if (!value)
28
+ return;
29
+ if (!aliases.includes(value))
30
+ aliases.push(value);
31
+ };
32
+ const stripped = modelID.replace(/^moonshotai[/:]/i, '');
33
+ const canonical = moonshotCanonicalModelID(modelID);
34
+ if (!options?.canonicalProviderKeys)
35
+ push(modelID);
36
+ if (stripped !== modelID)
37
+ push(stripped);
38
+ push(canonical);
39
+ return aliases;
40
+ }
41
+ function zhipuModelAliases(modelID) {
42
+ const aliases = [];
43
+ const queue = [];
44
+ const push = (value) => {
45
+ if (!value)
46
+ return;
47
+ if (!aliases.includes(value)) {
48
+ aliases.push(value);
49
+ queue.push(value);
50
+ }
51
+ };
52
+ push(modelID);
53
+ for (let index = 0; index < queue.length; index++) {
54
+ const stem = queue[index];
55
+ const withoutProviderPrefix = stem.replace(/^(?:zhipu|z-ai|bigmodel|zhipuai-coding-plan)[/:]/, '');
56
+ push(withoutProviderPrefix);
57
+ push(`zhipu/${withoutProviderPrefix}`);
58
+ const withoutBillingSuffix = withoutProviderPrefix.replace(/-billing$/, '');
59
+ push(withoutBillingSuffix);
60
+ push(`zhipu/${withoutBillingSuffix}`);
61
+ const withoutThinkingSuffix = withoutBillingSuffix.replace(/-thinking$/, '');
62
+ push(withoutThinkingSuffix);
63
+ push(`zhipu/${withoutThinkingSuffix}`);
64
+ const dotted = withoutThinkingSuffix.replace(/(\d)-(\d)(?=-|$)/g, '$1.$2');
65
+ push(dotted);
66
+ push(`zhipu/${dotted}`);
67
+ const hyphenated = withoutThinkingSuffix.replace(/(\d)\.(\d)(?=-|$)/g, '$1-$2');
68
+ push(hyphenated);
69
+ push(`zhipu/${hyphenated}`);
70
+ }
71
+ return aliases;
72
+ }
11
73
  function anthropicModelAliases(modelID) {
12
74
  const aliases = [];
13
75
  const queue = [];
@@ -61,6 +123,29 @@ function normalizeKnownProviderID(providerID) {
61
123
  return 'github-copilot';
62
124
  return providerID;
63
125
  }
126
+ function isCanonicalZhipuProviderID(providerID) {
127
+ return (providerID === 'zhipu' ||
128
+ providerID === 'bigmodel' ||
129
+ providerID === 'z-ai' ||
130
+ providerID === 'zhipuai-coding-plan');
131
+ }
132
+ export function canonicalPricingProviderID(providerID) {
133
+ const normalized = normalizeKnownProviderID(providerID);
134
+ const lowered = normalized.toLowerCase();
135
+ if (isCanonicalZhipuProviderID(lowered)) {
136
+ return 'zhipu';
137
+ }
138
+ if (lowered === 'kimi-for-coding')
139
+ return 'moonshotai';
140
+ if (lowered.includes('anthropic') || lowered.includes('claude')) {
141
+ return 'anthropic';
142
+ }
143
+ if (lowered.includes('openai') || lowered.endsWith('-oai'))
144
+ return 'openai';
145
+ if (lowered.includes('copilot'))
146
+ return 'github-copilot';
147
+ return normalized;
148
+ }
64
149
  export function canonicalApiCostProviderID(providerID) {
65
150
  const normalized = normalizeKnownProviderID(providerID);
66
151
  if (API_COST_ENABLED_PROVIDERS.has(normalized))
@@ -73,24 +158,262 @@ export function canonicalApiCostProviderID(providerID) {
73
158
  if (lowered.includes('anthropic') || lowered.includes('claude')) {
74
159
  return 'anthropic';
75
160
  }
161
+ if (isCanonicalZhipuProviderID(lowered)) {
162
+ return 'zhipu';
163
+ }
76
164
  return normalized;
77
165
  }
166
+ function anthropicPricing(input, output, options) {
167
+ // OpenCode currently reports zero Anthropic model prices in runtime metadata,
168
+ // so keep a bundled fallback sourced from Anthropic's pricing docs.
169
+ return {
170
+ input,
171
+ output,
172
+ cacheRead: input * 0.1,
173
+ // OpenCode only exposes aggregate cache.write tokens, so use Anthropic's
174
+ // default 5-minute prompt-caching write rate.
175
+ cacheWrite: input * 1.25,
176
+ contextOver200k: options?.longContextInput !== undefined &&
177
+ options?.longContextOutput !== undefined
178
+ ? {
179
+ input: options.longContextInput,
180
+ output: options.longContextOutput,
181
+ cacheRead: options.longContextInput * 0.1,
182
+ cacheWrite: options.longContextInput * 1.25,
183
+ }
184
+ : undefined,
185
+ };
186
+ }
187
+ function zhipuPricing(input, output, cacheRead) {
188
+ return {
189
+ input,
190
+ output,
191
+ cacheRead,
192
+ cacheWrite: 0,
193
+ };
194
+ }
195
+ function moonshotPricing(input, output, cacheRead) {
196
+ return {
197
+ input,
198
+ output,
199
+ cacheRead,
200
+ cacheWrite: 0,
201
+ };
202
+ }
203
+ const BUNDLED_CANONICAL_PRICE_ENTRIES = [
204
+ {
205
+ provider: 'anthropic',
206
+ model: 'claude-opus-4-6',
207
+ rates: anthropicPricing(5, 25),
208
+ source: 'official-doc',
209
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
210
+ },
211
+ {
212
+ provider: 'anthropic',
213
+ model: 'claude-opus-4-5',
214
+ rates: anthropicPricing(5, 25),
215
+ source: 'official-doc',
216
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
217
+ },
218
+ {
219
+ provider: 'anthropic',
220
+ model: 'claude-opus-4-1',
221
+ rates: anthropicPricing(15, 75),
222
+ source: 'official-doc',
223
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
224
+ },
225
+ {
226
+ provider: 'anthropic',
227
+ model: 'claude-opus-4',
228
+ rates: anthropicPricing(15, 75),
229
+ source: 'official-doc',
230
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
231
+ },
232
+ {
233
+ provider: 'anthropic',
234
+ model: 'claude-sonnet-4-6',
235
+ rates: anthropicPricing(3, 15),
236
+ source: 'official-doc',
237
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
238
+ },
239
+ {
240
+ provider: 'anthropic',
241
+ model: 'claude-sonnet-4-5',
242
+ rates: anthropicPricing(3, 15, {
243
+ longContextInput: 6,
244
+ longContextOutput: 22.5,
245
+ }),
246
+ source: 'official-doc',
247
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
248
+ },
249
+ {
250
+ provider: 'anthropic',
251
+ model: 'claude-sonnet-4',
252
+ rates: anthropicPricing(3, 15, {
253
+ longContextInput: 6,
254
+ longContextOutput: 22.5,
255
+ }),
256
+ source: 'official-doc',
257
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
258
+ },
259
+ {
260
+ provider: 'anthropic',
261
+ model: 'claude-3-7-sonnet',
262
+ rates: anthropicPricing(3, 15),
263
+ source: 'official-doc',
264
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
265
+ },
266
+ {
267
+ provider: 'anthropic',
268
+ model: 'claude-3-5-sonnet',
269
+ rates: anthropicPricing(3, 15),
270
+ source: 'official-doc',
271
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
272
+ },
273
+ {
274
+ provider: 'anthropic',
275
+ model: 'claude-haiku-4-5',
276
+ rates: anthropicPricing(1, 5),
277
+ source: 'official-doc',
278
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
279
+ },
280
+ {
281
+ provider: 'anthropic',
282
+ model: 'claude-3-5-haiku',
283
+ rates: anthropicPricing(0.8, 4),
284
+ source: 'official-doc',
285
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
286
+ },
287
+ {
288
+ provider: 'anthropic',
289
+ model: 'claude-3-opus',
290
+ rates: anthropicPricing(15, 75),
291
+ source: 'official-doc',
292
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
293
+ },
294
+ {
295
+ provider: 'anthropic',
296
+ model: 'claude-3-haiku',
297
+ rates: anthropicPricing(0.25, 1.25),
298
+ source: 'official-doc',
299
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
300
+ },
301
+ {
302
+ provider: 'zhipu',
303
+ model: 'glm-5',
304
+ rates: zhipuPricing(1, 3.2, 0.2),
305
+ source: 'official-doc',
306
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
307
+ },
308
+ {
309
+ provider: 'zhipu',
310
+ model: 'glm-4.7',
311
+ rates: zhipuPricing(0.6, 2.2, 0.11),
312
+ source: 'official-doc',
313
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
314
+ },
315
+ {
316
+ provider: 'zhipu',
317
+ model: 'glm-4.6',
318
+ rates: zhipuPricing(0.6, 2.2, 0.11),
319
+ source: 'official-doc',
320
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
321
+ },
322
+ {
323
+ provider: 'zhipu',
324
+ model: 'glm-4.6v',
325
+ rates: zhipuPricing(0.3, 0.9, 0.05),
326
+ source: 'official-doc',
327
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
328
+ },
329
+ {
330
+ provider: 'zhipu',
331
+ model: 'glm-4.5',
332
+ rates: zhipuPricing(0.6, 2.2, 0.11),
333
+ source: 'official-doc',
334
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
335
+ },
336
+ {
337
+ provider: 'zhipu',
338
+ model: 'glm-4.5-air',
339
+ rates: zhipuPricing(0.2, 1.1, 0.03),
340
+ source: 'official-doc',
341
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
342
+ },
343
+ {
344
+ provider: 'zhipu',
345
+ model: 'glm-4.5v',
346
+ rates: zhipuPricing(0.6, 1.8, 0.11),
347
+ source: 'official-doc',
348
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
349
+ },
350
+ {
351
+ provider: 'moonshotai',
352
+ model: 'kimi-k2.5',
353
+ rates: moonshotPricing(0.6, 3, 0.1),
354
+ source: 'official-doc',
355
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
356
+ },
357
+ {
358
+ provider: 'moonshotai',
359
+ model: 'kimi-k2-thinking',
360
+ rates: moonshotPricing(0.6, 2.5, 0.15),
361
+ source: 'official-doc',
362
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
363
+ },
364
+ {
365
+ provider: 'moonshotai',
366
+ model: 'kimi-k2-0711-preview',
367
+ rates: moonshotPricing(0.6, 2.5, 0.15),
368
+ source: 'official-doc',
369
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
370
+ },
371
+ {
372
+ provider: 'moonshotai',
373
+ model: 'kimi-k2-0905-preview',
374
+ rates: moonshotPricing(0.6, 2.5, 0.15),
375
+ source: 'official-doc',
376
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
377
+ },
378
+ {
379
+ provider: 'moonshotai',
380
+ model: 'kimi-k2-turbo-preview',
381
+ rates: moonshotPricing(2.4, 10, 0.6),
382
+ source: 'official-doc',
383
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
384
+ },
385
+ {
386
+ provider: 'moonshotai',
387
+ model: 'kimi-k2-thinking-turbo',
388
+ rates: moonshotPricing(1.15, 8, 0.15),
389
+ source: 'official-doc',
390
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
391
+ },
392
+ ];
78
393
  export function modelCostKey(providerID, modelID) {
79
394
  return `${providerID}:${modelID}`;
80
395
  }
81
396
  export function modelCostLookupKeys(providerID, modelID) {
82
397
  const keys = [];
83
- const canonicalProviderID = canonicalApiCostProviderID(providerID);
398
+ const canonicalProviderID = canonicalPricingProviderID(providerID);
84
399
  const push = (key) => {
85
400
  if (!keys.includes(key))
86
401
  keys.push(key);
87
402
  };
88
- const modelIDs = canonicalProviderID === 'anthropic'
403
+ const modelIDsFor = (options) => canonicalProviderID === 'anthropic'
89
404
  ? anthropicModelAliases(modelID)
90
- : [modelID];
91
- for (const candidateModelID of modelIDs) {
405
+ : canonicalProviderID === 'zhipu'
406
+ ? zhipuModelAliases(modelID)
407
+ : canonicalProviderID === 'moonshotai'
408
+ ? moonshotModelAliases(modelID, options)
409
+ : [modelID];
410
+ for (const candidateModelID of modelIDsFor()) {
92
411
  push(modelCostKey(providerID, candidateModelID));
93
- if (canonicalProviderID !== providerID) {
412
+ }
413
+ if (canonicalProviderID !== providerID) {
414
+ for (const candidateModelID of modelIDsFor({
415
+ canonicalProviderKeys: true,
416
+ })) {
94
417
  push(modelCostKey(canonicalProviderID, candidateModelID));
95
418
  }
96
419
  }
@@ -101,6 +424,30 @@ export function modelCostLookupKeys(providerID, modelID) {
101
424
  }
102
425
  return keys;
103
426
  }
427
+ function createBundledModelCostMap() {
428
+ const map = {};
429
+ for (const entry of BUNDLED_CANONICAL_PRICE_ENTRIES) {
430
+ for (const key of modelCostLookupKeys(entry.provider, entry.model)) {
431
+ map[key] = entry.rates;
432
+ }
433
+ }
434
+ return map;
435
+ }
436
+ const BUNDLED_MODEL_COST_MAP = createBundledModelCostMap();
437
+ export function getBundledModelCostMap() {
438
+ return { ...BUNDLED_MODEL_COST_MAP };
439
+ }
440
+ export function getBundledCanonicalPriceEntries() {
441
+ return BUNDLED_CANONICAL_PRICE_ENTRIES.map((entry) => ({
442
+ ...entry,
443
+ rates: {
444
+ ...entry.rates,
445
+ contextOver200k: entry.rates.contextOver200k
446
+ ? { ...entry.rates.contextOver200k }
447
+ : undefined,
448
+ },
449
+ }));
450
+ }
104
451
  export function parseModelCostRates(value) {
105
452
  if (!isRecord(value))
106
453
  return undefined;
package/dist/format.js CHANGED
@@ -204,6 +204,8 @@ function compactProviderLabel(quota) {
204
204
  return 'Ant';
205
205
  if (canonical === 'kimi-for-coding')
206
206
  return 'Kimi';
207
+ if (canonical === 'zhipuai-coding-plan')
208
+ return 'Zhipu';
207
209
  if (canonical === 'rightcode')
208
210
  return 'RC';
209
211
  if (canonical === 'xyai-vibe')
@@ -257,26 +259,28 @@ function compactQuotaWindowText(win) {
257
259
  const resetToken = reset
258
260
  ? `${compactQuotaResetToken(win.resetLabel)}${reset}`
259
261
  : undefined;
262
+ const note = sanitizeLine(win.note || '');
260
263
  if (win.showPercent === false) {
261
264
  const safe = sanitizeLine(win.label || '');
262
265
  const daily = safe ? safe.replace(/^Daily\s+/i, 'D') : '';
263
- return [daily, resetToken].filter(Boolean).join(' ');
266
+ return [daily, resetToken, note].filter(Boolean).join(' ');
264
267
  }
265
268
  const percentToken = compactQuotaPercentToken(win.label, win.remainingPercent);
266
- return [percentToken, resetToken].filter(Boolean).join(' ');
269
+ return [percentToken, resetToken, note].filter(Boolean).join(' ');
267
270
  }
268
271
  function compactQuotaWindowTokens(win) {
269
272
  const reset = compactReset(win.resetAt, win.resetLabel, win.label);
270
273
  const resetToken = reset
271
274
  ? `${compactQuotaResetToken(win.resetLabel)}${reset}`
272
275
  : undefined;
276
+ const note = sanitizeLine(win.note || '');
273
277
  if (win.showPercent === false) {
274
278
  const safe = sanitizeLine(win.label || '');
275
279
  const daily = safe ? safe.replace(/^Daily\s+/i, 'D') : '';
276
- return [daily, resetToken].filter((value) => Boolean(value));
280
+ return [daily, resetToken, note].filter((value) => Boolean(value));
277
281
  }
278
282
  const percentToken = compactQuotaPercentToken(win.label, win.remainingPercent);
279
- return [percentToken, resetToken].filter((value) => Boolean(value));
283
+ return [percentToken, resetToken, note].filter((value) => Boolean(value));
280
284
  }
281
285
  function compactQuotaBalanceText(balance) {
282
286
  return `B${compactDesktopCurrencyValue(balance.amount, balance.currency)}`;
@@ -611,6 +615,8 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
611
615
  if (reset) {
612
616
  parts.push(`${sanitizeLine(win.resetLabel || 'Rst')} ${reset}`);
613
617
  }
618
+ if (win.note)
619
+ parts.push(sanitizeLine(win.note));
614
620
  return parts.join(' ');
615
621
  };
616
622
  // Multi-window rendering
@@ -849,7 +855,9 @@ export function renderMarkdownReport(period, usage, quotas, options) {
849
855
  // Multi-window detail
850
856
  if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
851
857
  const windowLines = quota.windows.map((win) => {
852
- const extraNote = win === quota.windows?.[0] && quota.note ? ` | ${quota.note}` : '';
858
+ const extraNote = win.note || (win === quota.windows?.[0] && quota.note)
859
+ ? ` | ${win.note || quota.note}`
860
+ : '';
853
861
  if (win.showPercent === false) {
854
862
  const winLabel = win.label ? ` (${win.label})` : '';
855
863
  return mdCell(`- ${displayLabel}${winLabel}: ${quota.status} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}${extraNote}`);
@@ -964,7 +972,13 @@ export function renderToastMessage(period, usage, quotas, options) {
964
972
  }
965
973
  else {
966
974
  const hasAnyUsage = Object.keys(usage.providers).length > 0;
967
- lines.push(fitLine(hasAnyUsage ? ' N/A (Copilot)' : ' -', width));
975
+ const hasOnlyCopilotUsage = hasAnyUsage &&
976
+ Object.values(usage.providers).every((provider) => canonicalProviderID(provider.providerID) === 'github-copilot');
977
+ lines.push(fitLine(hasOnlyCopilotUsage
978
+ ? ' N/A (Copilot)'
979
+ : hasAnyUsage
980
+ ? ' N/A'
981
+ : ' -', width));
968
982
  }
969
983
  }
970
984
  const providerCachePairs = Object.values(usage.providers)
@@ -995,6 +1009,8 @@ export function renderToastMessage(period, usage, quotas, options) {
995
1009
  parts.push(pct);
996
1010
  if (reset)
997
1011
  parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
1012
+ if (win.note)
1013
+ parts.push(win.note);
998
1014
  return {
999
1015
  label: idx === 0 ? quotaDisplayLabel(item) : '',
1000
1016
  value: parts.filter(Boolean).join(' '),
@@ -0,0 +1,2 @@
1
+ import type { QuotaProviderAdapter } from '../types.js';
2
+ export declare const zhipuCodingPlanAdapter: QuotaProviderAdapter;
@@ -0,0 +1,217 @@
1
+ import { isRecord, swallow } from '../../helpers.js';
2
+ import { asNumber, configuredProviderEnabled, fetchWithTimeout, sanitizeBaseURL, toIso, } from '../common.js';
3
+ const ZHIPU_QUOTA_URL = 'https://bigmodel.cn/api/monitor/usage/quota/limit';
4
+ const ZHIPU_INTL_QUOTA_URL = 'https://api.z.ai/api/monitor/usage/quota/limit';
5
+ function resolveApiKey(auth, providerOptions) {
6
+ const optionKey = providerOptions?.apiKey;
7
+ if (typeof optionKey === 'string' && optionKey)
8
+ return optionKey;
9
+ if (!auth)
10
+ return undefined;
11
+ if (auth.type === 'api' && typeof auth.key === 'string' && auth.key) {
12
+ return auth.key;
13
+ }
14
+ if (auth.type === 'wellknown') {
15
+ if (typeof auth.key === 'string' && auth.key)
16
+ return auth.key;
17
+ if (typeof auth.token === 'string' && auth.token)
18
+ return auth.token;
19
+ }
20
+ if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
21
+ return auth.access;
22
+ }
23
+ return undefined;
24
+ }
25
+ function parseBaseURL(value) {
26
+ const normalized = sanitizeBaseURL(value);
27
+ if (!normalized)
28
+ return undefined;
29
+ try {
30
+ return new URL(normalized);
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ function isZhipuCodingBaseURL(value) {
37
+ const parsed = parseBaseURL(value);
38
+ if (!parsed || parsed.protocol !== 'https:')
39
+ return false;
40
+ const pathname = parsed.pathname.replace(/\/+$/, '');
41
+ const isKnownHost = parsed.host === 'open.bigmodel.cn' || parsed.host === 'api.z.ai';
42
+ if (!isKnownHost)
43
+ return false;
44
+ return pathname === '/api/anthropic' || pathname === '/api/coding/paas/v4';
45
+ }
46
+ function quotaUrl(baseURL) {
47
+ const parsed = parseBaseURL(baseURL);
48
+ if (parsed?.host === 'api.z.ai')
49
+ return ZHIPU_INTL_QUOTA_URL;
50
+ return ZHIPU_QUOTA_URL;
51
+ }
52
+ function normalizeUsedPercent(value) {
53
+ const numeric = asNumber(value);
54
+ if (numeric === undefined || !Number.isFinite(numeric))
55
+ return undefined;
56
+ if (numeric < 0)
57
+ return 0;
58
+ if (numeric > 100)
59
+ return 100;
60
+ return numeric;
61
+ }
62
+ function tokenWindowLabel(unit, count) {
63
+ const unitValue = asNumber(unit);
64
+ const countValue = asNumber(count);
65
+ if (unitValue === 3 && countValue && countValue > 0) {
66
+ return `${Math.round(countValue)}h`;
67
+ }
68
+ if (unitValue === 1 && countValue === 7)
69
+ return 'Weekly';
70
+ if (unitValue === 1 && countValue && countValue > 0) {
71
+ return `${Math.round(countValue)}d`;
72
+ }
73
+ if (unitValue === 5 && countValue && countValue > 0) {
74
+ return `${Math.round(countValue)}m`;
75
+ }
76
+ return 'Tokens';
77
+ }
78
+ function formatCountValue(value) {
79
+ if (!Number.isFinite(value))
80
+ return '0';
81
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
82
+ }
83
+ function parseTokenWindow(value) {
84
+ if (value.type !== 'TOKENS_LIMIT')
85
+ return undefined;
86
+ const usedPercent = normalizeUsedPercent(value.percentage);
87
+ if (usedPercent === undefined)
88
+ return undefined;
89
+ return {
90
+ label: tokenWindowLabel(value.unit, value.number),
91
+ remainingPercent: 100 - usedPercent,
92
+ usedPercent,
93
+ resetAt: toIso(value.nextResetTime),
94
+ };
95
+ }
96
+ function parseMcpWindow(value) {
97
+ if (value.type !== 'TIME_LIMIT')
98
+ return undefined;
99
+ const total = asNumber(value.usage);
100
+ const remaining = asNumber(value.remaining);
101
+ if (total === undefined ||
102
+ remaining === undefined ||
103
+ !Number.isFinite(total) ||
104
+ !Number.isFinite(remaining) ||
105
+ total <= 0) {
106
+ return undefined;
107
+ }
108
+ return {
109
+ label: `MCP ${formatCountValue(remaining)}/${formatCountValue(total)}`,
110
+ showPercent: false,
111
+ remainingPercent: Math.max(0, Math.min(100, (remaining / total) * 100)),
112
+ resetAt: toIso(value.nextResetTime),
113
+ };
114
+ }
115
+ async function fetchZhipuCodingPlanQuota({ providerID, providerOptions, auth, config, }) {
116
+ const checkedAt = Date.now();
117
+ const base = {
118
+ providerID,
119
+ adapterID: 'zhipuai-coding-plan',
120
+ label: 'Zhipu Coding Plan',
121
+ shortLabel: 'Zhipu',
122
+ sortOrder: 16,
123
+ };
124
+ const apiKey = resolveApiKey(auth, providerOptions);
125
+ if (!apiKey) {
126
+ return {
127
+ ...base,
128
+ status: 'unavailable',
129
+ checkedAt,
130
+ note: 'missing api key',
131
+ };
132
+ }
133
+ const response = await fetchWithTimeout(quotaUrl(providerOptions?.baseURL), {
134
+ method: 'GET',
135
+ headers: {
136
+ Accept: 'application/json',
137
+ Authorization: apiKey,
138
+ 'Content-Type': 'application/json',
139
+ 'User-Agent': 'opencode-quota-sidebar',
140
+ },
141
+ }, config.quota.requestTimeoutMs).catch(swallow('fetchZhipuCodingPlanQuota:usage'));
142
+ if (!response) {
143
+ return {
144
+ ...base,
145
+ status: 'error',
146
+ checkedAt,
147
+ note: 'network request failed',
148
+ };
149
+ }
150
+ if (!response.ok) {
151
+ return {
152
+ ...base,
153
+ status: 'error',
154
+ checkedAt,
155
+ note: `http ${response.status}`,
156
+ };
157
+ }
158
+ const payload = await response
159
+ .json()
160
+ .catch(swallow('fetchZhipuCodingPlanQuota:json'));
161
+ if (!isRecord(payload)) {
162
+ return {
163
+ ...base,
164
+ status: 'error',
165
+ checkedAt,
166
+ note: 'invalid response',
167
+ };
168
+ }
169
+ if (payload.success !== true || asNumber(payload.code) !== 200) {
170
+ return {
171
+ ...base,
172
+ status: 'error',
173
+ checkedAt,
174
+ note: typeof payload.msg === 'string' && payload.msg
175
+ ? payload.msg
176
+ : 'quota request failed',
177
+ };
178
+ }
179
+ const data = isRecord(payload.data) ? payload.data : undefined;
180
+ const level = typeof data?.level === 'string' && data.level
181
+ ? `${data.level.toUpperCase()} plan`
182
+ : undefined;
183
+ const limits = Array.isArray(data?.limits)
184
+ ? data.limits.filter((item) => isRecord(item))
185
+ : [];
186
+ const token = limits
187
+ .map((item) => parseTokenWindow(item))
188
+ .find((value) => Boolean(value));
189
+ const mcp = limits
190
+ .map((item) => parseMcpWindow(item))
191
+ .find((value) => Boolean(value));
192
+ const windows = [token, mcp].filter((value) => Boolean(value));
193
+ const primary = token || windows[0];
194
+ return {
195
+ ...base,
196
+ status: primary ? 'ok' : 'error',
197
+ checkedAt,
198
+ remainingPercent: primary?.remainingPercent,
199
+ resetAt: primary?.resetAt,
200
+ note: primary ? level : 'missing quota fields',
201
+ windows: windows.length > 0 ? windows : undefined,
202
+ };
203
+ }
204
+ export const zhipuCodingPlanAdapter = {
205
+ id: 'zhipuai-coding-plan',
206
+ label: 'Zhipu Coding Plan',
207
+ shortLabel: 'Zhipu',
208
+ sortOrder: 16,
209
+ normalizeID: (providerID) => providerID === 'zhipuai-coding-plan' ? 'zhipuai-coding-plan' : undefined,
210
+ matchScore: ({ providerID, providerOptions }) => {
211
+ if (providerID === 'zhipuai-coding-plan')
212
+ return 100;
213
+ return isZhipuCodingBaseURL(providerOptions?.baseURL) ? 95 : 0;
214
+ },
215
+ isEnabled: (config) => configuredProviderEnabled(config.quota, 'zhipuai-coding-plan', true),
216
+ fetch: fetchZhipuCodingPlanQuota,
217
+ };
@@ -3,9 +3,10 @@ import { buzzAdapter } from './third_party/buzz.js';
3
3
  import { copilotAdapter } from './core/copilot.js';
4
4
  import { kimiForCodingAdapter } from './core/kimi_for_coding.js';
5
5
  import { openaiAdapter } from './core/openai.js';
6
+ import { zhipuCodingPlanAdapter } from './core/zhipu_coding_plan.js';
6
7
  import { QuotaProviderRegistry } from './registry.js';
7
8
  import { rightCodeAdapter } from './third_party/rightcode.js';
8
9
  import { xyaiVibeAdapter } from './third_party/xyai_vibe.js';
9
10
  export declare function createDefaultProviderRegistry(): QuotaProviderRegistry;
10
- export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, xyaiVibeAdapter, QuotaProviderRegistry, };
11
+ export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, xyaiVibeAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
11
12
  export type { AuthUpdate, AuthValue, ProviderResolveContext, QuotaFetchContext, QuotaProviderAdapter, RefreshedOAuthAuth, } from './types.js';
@@ -3,6 +3,7 @@ import { buzzAdapter } from './third_party/buzz.js';
3
3
  import { copilotAdapter } from './core/copilot.js';
4
4
  import { kimiForCodingAdapter } from './core/kimi_for_coding.js';
5
5
  import { openaiAdapter } from './core/openai.js';
6
+ import { zhipuCodingPlanAdapter } from './core/zhipu_coding_plan.js';
6
7
  import { QuotaProviderRegistry } from './registry.js';
7
8
  import { rightCodeAdapter } from './third_party/rightcode.js';
8
9
  import { xyaiVibeAdapter } from './third_party/xyai_vibe.js';
@@ -12,9 +13,10 @@ export function createDefaultProviderRegistry() {
12
13
  registry.register(buzzAdapter);
13
14
  registry.register(xyaiVibeAdapter);
14
15
  registry.register(kimiForCodingAdapter);
16
+ registry.register(zhipuCodingPlanAdapter);
15
17
  registry.register(openaiAdapter);
16
18
  registry.register(copilotAdapter);
17
19
  registry.register(anthropicAdapter);
18
20
  return registry;
19
21
  }
20
- export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, xyaiVibeAdapter, QuotaProviderRegistry, };
22
+ export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, xyaiVibeAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
package/dist/quota.js CHANGED
@@ -33,7 +33,13 @@ export function quotaSort(left, right) {
33
33
  }
34
34
  export function listDefaultQuotaProviderIDs() {
35
35
  // Keep default report behavior stable for built-in subscription providers.
36
- return ['openai', 'kimi-for-coding', 'github-copilot', 'anthropic'];
36
+ return [
37
+ 'openai',
38
+ 'kimi-for-coding',
39
+ 'zhipuai-coding-plan',
40
+ 'github-copilot',
41
+ 'anthropic',
42
+ ];
37
43
  }
38
44
  export function createQuotaRuntime() {
39
45
  const providerRegistry = createDefaultProviderRegistry();
@@ -1,6 +1,7 @@
1
1
  const PROVIDER_SHORT_LABELS = {
2
2
  openai: 'OpenAI',
3
3
  'kimi-for-coding': 'Kimi',
4
+ 'zhipuai-coding-plan': 'Zhipu',
4
5
  'github-copilot': 'Copilot',
5
6
  anthropic: 'Anthropic',
6
7
  rightcode: 'RC',
@@ -8,6 +9,8 @@ const PROVIDER_SHORT_LABELS = {
8
9
  export function canonicalProviderID(providerID) {
9
10
  if (providerID.startsWith('github-copilot'))
10
11
  return 'github-copilot';
12
+ if (providerID === 'zhipuai-coding-plan')
13
+ return 'zhipuai-coding-plan';
11
14
  return providerID;
12
15
  }
13
16
  export function displayShortLabel(providerID) {
@@ -177,6 +177,7 @@ export function parseQuotaCache(value) {
177
177
  resetLabel: typeof window.resetLabel === 'string'
178
178
  ? window.resetLabel
179
179
  : undefined,
180
+ note: typeof window.note === 'string' ? window.note : undefined,
180
181
  remainingPercent: typeof window.remainingPercent === 'number'
181
182
  ? window.remainingPercent
182
183
  : undefined,
package/dist/types.d.ts CHANGED
@@ -5,6 +5,8 @@ export type QuotaWindow = {
5
5
  showPercent?: boolean;
6
6
  /** Prefix for reset/expiry time text in sidebar (default: Rst). */
7
7
  resetLabel?: string;
8
+ /** Optional detail note rendered inline for the first window in reports. */
9
+ note?: string;
8
10
  remainingPercent?: number;
9
11
  usedPercent?: number;
10
12
  resetAt?: string;
package/dist/usage.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, Cached
6
6
  * fields). This is distinct from the plugin *state* version managed by the
7
7
  * persistence layer; billing version only governs usage-cache staleness.
8
8
  */
9
- export declare const USAGE_BILLING_CACHE_VERSION = 7;
9
+ export declare const USAGE_BILLING_CACHE_VERSION = 9;
10
10
  export type ProviderUsage = {
11
11
  providerID: string;
12
12
  input: number;
package/dist/usage.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * fields). This is distinct from the plugin *state* version managed by the
5
5
  * persistence layer; billing version only governs usage-cache staleness.
6
6
  */
7
- export const USAGE_BILLING_CACHE_VERSION = 7;
7
+ export const USAGE_BILLING_CACHE_VERSION = 9;
8
8
  const MAX_RECENT_PROVIDER_EVENTS = 100;
9
9
  function emptyCacheUsageBucket() {
10
10
  return {
@@ -1,5 +1,5 @@
1
1
  import { TtlValueCache } from './cache.js';
2
- import { API_COST_ENABLED_PROVIDERS, cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostLookupKeys, modelCostKey, parseModelCostRates, } from './cost.js';
2
+ import { API_COST_ENABLED_PROVIDERS, cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, canonicalPricingProviderID, getBundledModelCostMap, modelCostLookupKeys, modelCostKey, parseModelCostRates, } from './cost.js';
3
3
  import { deleteSessionFromDayChunk, dateKeyFromTimestamp, scanAllSessions, updateSessionsInDayChunks, } from './storage.js';
4
4
  import { periodStart } from './period.js';
5
5
  import { debug, debugError, isRecord, mapConcurrent, swallow, } from './helpers.js';
@@ -36,9 +36,10 @@ export function createUsageService(deps) {
36
36
  const cached = modelCostCache.get();
37
37
  if (cached)
38
38
  return cached;
39
+ const fallbackMap = getBundledModelCostMap();
39
40
  const providerClient = deps.client;
40
41
  if (!providerClient.provider?.list) {
41
- return modelCostCache.set({}, 30_000);
42
+ return modelCostCache.set(fallbackMap, 30_000);
42
43
  }
43
44
  const response = await providerClient.provider
44
45
  .list({
@@ -59,7 +60,7 @@ export function createUsageService(deps) {
59
60
  const rawProviderID = typeof provider.id === 'string' ? provider.id : undefined;
60
61
  if (!rawProviderID)
61
62
  return acc;
62
- const canonicalProviderID = canonicalApiCostProviderID(rawProviderID);
63
+ const canonicalProviderID = canonicalPricingProviderID(rawProviderID);
63
64
  const models = provider.models;
64
65
  if (!isRecord(models))
65
66
  return acc;
@@ -83,7 +84,7 @@ export function createUsageService(deps) {
83
84
  }
84
85
  }
85
86
  return acc;
86
- }, {});
87
+ }, fallbackMap);
87
88
  return modelCostCache.set(map, Math.max(30_000, deps.config.quota.refreshMs));
88
89
  };
89
90
  const calcEquivalentApiCost = (message, modelCostMap) => {
@@ -312,6 +313,42 @@ export function createUsageService(deps) {
312
313
  return false;
313
314
  return cached.billingVersion === USAGE_BILLING_CACHE_VERSION;
314
315
  };
316
+ const hasAnySubscriptionProvider = (cached) => {
317
+ const providerIDs = Object.keys(cached.providers);
318
+ if (providerIDs.length === 0)
319
+ return true;
320
+ return providerIDs.some((providerID) => {
321
+ const canonical = canonicalApiCostProviderID(providerID);
322
+ return API_COST_ENABLED_PROVIDERS.has(canonical);
323
+ });
324
+ };
325
+ const shouldRecomputeUsageCache = (cached, hasPricing, hasResolvableApiCostMessage) => {
326
+ if (!isUsageBillingCurrent(cached))
327
+ return true;
328
+ if (!hasPricing)
329
+ return false;
330
+ if (!hasResolvableApiCostMessage)
331
+ return false;
332
+ if (cached.assistantMessages <= 0)
333
+ return false;
334
+ if (cached.apiCost > 0)
335
+ return false;
336
+ if (cached.total <= 0)
337
+ return false;
338
+ if (!hasAnySubscriptionProvider(cached))
339
+ return false;
340
+ return true;
341
+ };
342
+ const hasResolvableApiCostMessages = (entries, modelCostMap) => {
343
+ return entries.some(({ info }) => {
344
+ if (info.role !== 'assistant')
345
+ return false;
346
+ const providerID = canonicalApiCostProviderID(info.providerID);
347
+ if (!API_COST_ENABLED_PROVIDERS.has(providerID))
348
+ return false;
349
+ return modelCostLookupKeys(info.providerID, info.modelID).some((key) => Boolean(modelCostMap[key]));
350
+ });
351
+ };
315
352
  const summarizeSessionUsage = async (sessionID, generationAtStart, options) => {
316
353
  const load = await loadSessionEntries(sessionID);
317
354
  const entries = load.status === 'ok' ? load.entries : undefined;
@@ -333,14 +370,23 @@ export function createUsageService(deps) {
333
370
  return { usage: empty, persist: false };
334
371
  }
335
372
  const modelCostMap = await getModelCostMap();
373
+ const hasPricing = Object.keys(modelCostMap).length > 0;
374
+ const hasResolvablePricing = hasResolvableApiCostMessages(entries, modelCostMap);
336
375
  const staleBillingCache = Boolean(sessionState?.usage) &&
337
376
  !isUsageBillingCurrent(sessionState?.usage);
338
- const forceRescan = forceRescanSessions.has(sessionID) || staleBillingCache;
377
+ const pricingRefreshCache = sessionState?.usage &&
378
+ shouldRecomputeUsageCache(sessionState.usage, hasPricing, hasResolvablePricing);
379
+ const forceRescan = forceRescanSessions.has(sessionID) ||
380
+ staleBillingCache ||
381
+ Boolean(pricingRefreshCache);
339
382
  if (forceRescan)
340
383
  forceRescanSessions.delete(sessionID);
341
384
  if (staleBillingCache) {
342
385
  debug(`usage cache billing refresh for session ${sessionID}`);
343
386
  }
387
+ if (pricingRefreshCache && !staleBillingCache) {
388
+ debug(`usage cache pricing refresh for session ${sessionID}`);
389
+ }
344
390
  const { usage, cursor } = summarizeMessagesIncremental(entries, sessionState?.usage, sessionState?.cursor, forceRescan, {
345
391
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
346
392
  classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
@@ -449,32 +495,6 @@ export function createUsageService(deps) {
449
495
  const usage = emptyUsageSummary();
450
496
  const modelCostMap = await getModelCostMap();
451
497
  const hasPricing = Object.keys(modelCostMap).length > 0;
452
- const hasAnySubscriptionProvider = (cached) => {
453
- const providerIDs = Object.keys(cached.providers);
454
- // Back-compat: older cached chunks may have empty providers.
455
- // In that case, allow recompute so we can persist apiCost.
456
- if (providerIDs.length === 0)
457
- return true;
458
- return providerIDs.some((providerID) => {
459
- const canonical = canonicalApiCostProviderID(providerID);
460
- return API_COST_ENABLED_PROVIDERS.has(canonical);
461
- });
462
- };
463
- const shouldRecomputeUsageCache = (cached) => {
464
- if (!isUsageBillingCurrent(cached))
465
- return true;
466
- if (!hasPricing)
467
- return false;
468
- if (cached.assistantMessages <= 0)
469
- return false;
470
- if (cached.apiCost > 0)
471
- return false;
472
- if (cached.total <= 0)
473
- return false;
474
- if (!hasAnySubscriptionProvider(cached))
475
- return false;
476
- return true;
477
- };
478
498
  if (sessions.length > 0) {
479
499
  const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
480
500
  const load = await loadSessionEntries(session.sessionID);
@@ -499,7 +519,7 @@ export function createUsageService(deps) {
499
519
  classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
500
520
  });
501
521
  const shouldPersistFullUsage = !session.state.usage ||
502
- shouldRecomputeUsageCache(session.state.usage);
522
+ shouldRecomputeUsageCache(session.state.usage, hasPricing, hasResolvableApiCostMessages(entries, modelCostMap));
503
523
  if (!shouldPersistFullUsage) {
504
524
  return {
505
525
  sessionID: session.sessionID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "2.0.14",
3
+ "version": "2.0.19",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",