@leo000001/opencode-quota-sidebar 2.0.15 → 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,11 +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[];
19
29
  export declare function getBundledModelCostMap(): {
20
30
  [x: string]: ModelCostRates;
21
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
+ }[];
22
51
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
23
52
  export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
24
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,6 +158,9 @@ 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
  }
78
166
  function anthropicPricing(input, output, options) {
@@ -96,77 +184,210 @@ function anthropicPricing(input, output, options) {
96
184
  : undefined,
97
185
  };
98
186
  }
99
- const BUNDLED_MODEL_COST_RATES = [
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 = [
100
204
  {
101
- providerID: 'anthropic',
102
- modelID: 'claude-opus-4-6',
205
+ provider: 'anthropic',
206
+ model: 'claude-opus-4-6',
103
207
  rates: anthropicPricing(5, 25),
208
+ source: 'official-doc',
209
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
104
210
  },
105
211
  {
106
- providerID: 'anthropic',
107
- modelID: 'claude-opus-4-5',
212
+ provider: 'anthropic',
213
+ model: 'claude-opus-4-5',
108
214
  rates: anthropicPricing(5, 25),
215
+ source: 'official-doc',
216
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
109
217
  },
110
218
  {
111
- providerID: 'anthropic',
112
- modelID: 'claude-opus-4-1',
219
+ provider: 'anthropic',
220
+ model: 'claude-opus-4-1',
113
221
  rates: anthropicPricing(15, 75),
222
+ source: 'official-doc',
223
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
114
224
  },
115
225
  {
116
- providerID: 'anthropic',
117
- modelID: 'claude-opus-4',
226
+ provider: 'anthropic',
227
+ model: 'claude-opus-4',
118
228
  rates: anthropicPricing(15, 75),
229
+ source: 'official-doc',
230
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
119
231
  },
120
232
  {
121
- providerID: 'anthropic',
122
- modelID: 'claude-sonnet-4-6',
233
+ provider: 'anthropic',
234
+ model: 'claude-sonnet-4-6',
123
235
  rates: anthropicPricing(3, 15),
236
+ source: 'official-doc',
237
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
124
238
  },
125
239
  {
126
- providerID: 'anthropic',
127
- modelID: 'claude-sonnet-4-5',
240
+ provider: 'anthropic',
241
+ model: 'claude-sonnet-4-5',
128
242
  rates: anthropicPricing(3, 15, {
129
243
  longContextInput: 6,
130
244
  longContextOutput: 22.5,
131
245
  }),
246
+ source: 'official-doc',
247
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
132
248
  },
133
249
  {
134
- providerID: 'anthropic',
135
- modelID: 'claude-sonnet-4',
250
+ provider: 'anthropic',
251
+ model: 'claude-sonnet-4',
136
252
  rates: anthropicPricing(3, 15, {
137
253
  longContextInput: 6,
138
254
  longContextOutput: 22.5,
139
255
  }),
256
+ source: 'official-doc',
257
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
140
258
  },
141
259
  {
142
- providerID: 'anthropic',
143
- modelID: 'claude-3-7-sonnet',
260
+ provider: 'anthropic',
261
+ model: 'claude-3-7-sonnet',
144
262
  rates: anthropicPricing(3, 15),
263
+ source: 'official-doc',
264
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
145
265
  },
146
266
  {
147
- providerID: 'anthropic',
148
- modelID: 'claude-3-5-sonnet',
267
+ provider: 'anthropic',
268
+ model: 'claude-3-5-sonnet',
149
269
  rates: anthropicPricing(3, 15),
270
+ source: 'official-doc',
271
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
150
272
  },
151
273
  {
152
- providerID: 'anthropic',
153
- modelID: 'claude-haiku-4-5',
274
+ provider: 'anthropic',
275
+ model: 'claude-haiku-4-5',
154
276
  rates: anthropicPricing(1, 5),
277
+ source: 'official-doc',
278
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
155
279
  },
156
280
  {
157
- providerID: 'anthropic',
158
- modelID: 'claude-3-5-haiku',
281
+ provider: 'anthropic',
282
+ model: 'claude-3-5-haiku',
159
283
  rates: anthropicPricing(0.8, 4),
284
+ source: 'official-doc',
285
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
160
286
  },
161
287
  {
162
- providerID: 'anthropic',
163
- modelID: 'claude-3-opus',
288
+ provider: 'anthropic',
289
+ model: 'claude-3-opus',
164
290
  rates: anthropicPricing(15, 75),
291
+ source: 'official-doc',
292
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
165
293
  },
166
294
  {
167
- providerID: 'anthropic',
168
- modelID: 'claude-3-haiku',
295
+ provider: 'anthropic',
296
+ model: 'claude-3-haiku',
169
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',
170
391
  },
171
392
  ];
172
393
  export function modelCostKey(providerID, modelID) {
@@ -174,17 +395,25 @@ export function modelCostKey(providerID, modelID) {
174
395
  }
175
396
  export function modelCostLookupKeys(providerID, modelID) {
176
397
  const keys = [];
177
- const canonicalProviderID = canonicalApiCostProviderID(providerID);
398
+ const canonicalProviderID = canonicalPricingProviderID(providerID);
178
399
  const push = (key) => {
179
400
  if (!keys.includes(key))
180
401
  keys.push(key);
181
402
  };
182
- const modelIDs = canonicalProviderID === 'anthropic'
403
+ const modelIDsFor = (options) => canonicalProviderID === 'anthropic'
183
404
  ? anthropicModelAliases(modelID)
184
- : [modelID];
185
- 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()) {
186
411
  push(modelCostKey(providerID, candidateModelID));
187
- if (canonicalProviderID !== providerID) {
412
+ }
413
+ if (canonicalProviderID !== providerID) {
414
+ for (const candidateModelID of modelIDsFor({
415
+ canonicalProviderKeys: true,
416
+ })) {
188
417
  push(modelCostKey(canonicalProviderID, candidateModelID));
189
418
  }
190
419
  }
@@ -197,8 +426,8 @@ export function modelCostLookupKeys(providerID, modelID) {
197
426
  }
198
427
  function createBundledModelCostMap() {
199
428
  const map = {};
200
- for (const entry of BUNDLED_MODEL_COST_RATES) {
201
- for (const key of modelCostLookupKeys(entry.providerID, entry.modelID)) {
429
+ for (const entry of BUNDLED_CANONICAL_PRICE_ENTRIES) {
430
+ for (const key of modelCostLookupKeys(entry.provider, entry.model)) {
202
431
  map[key] = entry.rates;
203
432
  }
204
433
  }
@@ -208,6 +437,17 @@ const BUNDLED_MODEL_COST_MAP = createBundledModelCostMap();
208
437
  export function getBundledModelCostMap() {
209
438
  return { ...BUNDLED_MODEL_COST_MAP };
210
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
+ }
211
451
  export function parseModelCostRates(value) {
212
452
  if (!isRecord(value))
213
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 = 8;
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 = 8;
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, getBundledModelCostMap, 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';
@@ -60,7 +60,7 @@ export function createUsageService(deps) {
60
60
  const rawProviderID = typeof provider.id === 'string' ? provider.id : undefined;
61
61
  if (!rawProviderID)
62
62
  return acc;
63
- const canonicalProviderID = canonicalApiCostProviderID(rawProviderID);
63
+ const canonicalProviderID = canonicalPricingProviderID(rawProviderID);
64
64
  const models = provider.models;
65
65
  if (!isRecord(models))
66
66
  return acc;
@@ -313,6 +313,42 @@ export function createUsageService(deps) {
313
313
  return false;
314
314
  return cached.billingVersion === USAGE_BILLING_CACHE_VERSION;
315
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
+ };
316
352
  const summarizeSessionUsage = async (sessionID, generationAtStart, options) => {
317
353
  const load = await loadSessionEntries(sessionID);
318
354
  const entries = load.status === 'ok' ? load.entries : undefined;
@@ -334,14 +370,23 @@ export function createUsageService(deps) {
334
370
  return { usage: empty, persist: false };
335
371
  }
336
372
  const modelCostMap = await getModelCostMap();
373
+ const hasPricing = Object.keys(modelCostMap).length > 0;
374
+ const hasResolvablePricing = hasResolvableApiCostMessages(entries, modelCostMap);
337
375
  const staleBillingCache = Boolean(sessionState?.usage) &&
338
376
  !isUsageBillingCurrent(sessionState?.usage);
339
- 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);
340
382
  if (forceRescan)
341
383
  forceRescanSessions.delete(sessionID);
342
384
  if (staleBillingCache) {
343
385
  debug(`usage cache billing refresh for session ${sessionID}`);
344
386
  }
387
+ if (pricingRefreshCache && !staleBillingCache) {
388
+ debug(`usage cache pricing refresh for session ${sessionID}`);
389
+ }
345
390
  const { usage, cursor } = summarizeMessagesIncremental(entries, sessionState?.usage, sessionState?.cursor, forceRescan, {
346
391
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
347
392
  classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
@@ -450,32 +495,6 @@ export function createUsageService(deps) {
450
495
  const usage = emptyUsageSummary();
451
496
  const modelCostMap = await getModelCostMap();
452
497
  const hasPricing = Object.keys(modelCostMap).length > 0;
453
- const hasAnySubscriptionProvider = (cached) => {
454
- const providerIDs = Object.keys(cached.providers);
455
- // Back-compat: older cached chunks may have empty providers.
456
- // In that case, allow recompute so we can persist apiCost.
457
- if (providerIDs.length === 0)
458
- return true;
459
- return providerIDs.some((providerID) => {
460
- const canonical = canonicalApiCostProviderID(providerID);
461
- return API_COST_ENABLED_PROVIDERS.has(canonical);
462
- });
463
- };
464
- const shouldRecomputeUsageCache = (cached) => {
465
- if (!isUsageBillingCurrent(cached))
466
- return true;
467
- if (!hasPricing)
468
- return false;
469
- if (cached.assistantMessages <= 0)
470
- return false;
471
- if (cached.apiCost > 0)
472
- return false;
473
- if (cached.total <= 0)
474
- return false;
475
- if (!hasAnySubscriptionProvider(cached))
476
- return false;
477
- return true;
478
- };
479
498
  if (sessions.length > 0) {
480
499
  const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
481
500
  const load = await loadSessionEntries(session.sessionID);
@@ -500,7 +519,7 @@ export function createUsageService(deps) {
500
519
  classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
501
520
  });
502
521
  const shouldPersistFullUsage = !session.state.usage ||
503
- shouldRecomputeUsageCache(session.state.usage);
522
+ shouldRecomputeUsageCache(session.state.usage, hasPricing, hasResolvableApiCostMessages(entries, modelCostMap));
504
523
  if (!shouldPersistFullUsage) {
505
524
  return {
506
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.15",
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",