@leo000001/opencode-quota-sidebar 2.0.2 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Add built-in `kimi-for-coding` subscription quota support via `GET https://api.kimi.com/coding/v1/usages`.
6
+ - Parse Kimi's `5h` and `Weekly` windows, including reset timestamps, and render them like other subscription providers.
7
+ - Accept OpenCode provider discovery responses that expose Kimi API keys through provider `key` fields.
5
8
  - Add Buzz API balance support for OpenAI-compatible providers that use a Buzz `baseURL`.
6
9
  - Document Buzz configuration, rendering, and outbound billing endpoints.
7
10
  - Keep session measured cost aligned with OpenCode root-session `message.cost` while still including descendant subagent usage in API-equivalent cost.
package/CONTRIBUTING.md CHANGED
@@ -19,6 +19,7 @@ The plugin now uses a provider adapter registry, so adding a new provider does n
19
19
  - `baseURL` match: best for OpenAI-compatible relays such as RightCode or Buzz
20
20
  - Prefix/variant normalization: best when one provider has multiple runtime IDs
21
21
  - Balance-only providers should prefer `balance` over inventing fake percent windows
22
+ - Built-in API-key providers such as `kimi-for-coding` may need both: direct ID matching for the canonical provider and support for OpenCode's discovered `key -> options.apiKey` bridge
22
23
 
23
24
  ## Add a new provider
24
25
 
@@ -70,6 +71,10 @@ If your provider is an OpenAI-compatible relay, prefer matching on
70
71
  `providerOptions.baseURL` instead of the runtime `providerID`; that keeps custom
71
72
  aliases working without extra user config.
72
73
 
74
+ If your provider is built into OpenCode and already has a stable runtime ID
75
+ (for example `kimi-for-coding`), prefer a direct provider-ID match first, then
76
+ add a `baseURL` fallback only when it helps older/custom runtime shapes.
77
+
73
78
  If the new provider should appear in default `quota_summary` reports even when
74
79
  it has not yet been used in the current session, also update
75
80
  `listDefaultQuotaProviderIDs()` in `src/quota.ts`.
@@ -98,7 +103,7 @@ At minimum:
98
103
  - format output if using special fields (e.g. `balance`)
99
104
  - cache compatibility if the change replaces an older snapshot shape
100
105
  - mixed-provider rendering if the new provider will commonly appear next to
101
- OpenAI/Copilot/RightCode in sidebar or toast output
106
+ OpenAI/Copilot/Kimi/RightCode in sidebar or toast output
102
107
 
103
108
  If the provider introduces new rendering rules or multi-window behavior, add
104
109
  coverage in both `src/__tests__/quota.test.ts` and `src/__tests__/format.test.ts`.
package/README.md CHANGED
@@ -41,13 +41,14 @@ On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/in
41
41
 
42
42
  ## Supported quota providers
43
43
 
44
- | Provider | Endpoint | Auth | Status |
45
- | -------------- | -------------------------------------- | --------------- | --------------------------------------- |
46
- | OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
47
- | GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
48
- | RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
49
- | Buzz | `buzzai.cc/v1/dashboard/billing/*` | API key | Balance only (computed from total-used) |
50
- | Anthropic | `api.anthropic.com/api/oauth/usage` | OAuth | Multi-window (5h + weekly / plan-based) |
44
+ | Provider | Endpoint | Auth | Status |
45
+ | -------------- | -------------------------------------- | --------------- | --------------------------------------- |
46
+ | OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
47
+ | GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
48
+ | Kimi For Coding | `api.kimi.com/coding/v1/usages` | API key | Multi-window subscription (5h + weekly) |
49
+ | RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
50
+ | Buzz | `buzzai.cc/v1/dashboard/billing/*` | API key | Balance only (computed from total-used) |
51
+ | Anthropic | `api.anthropic.com/api/oauth/usage` | OAuth | Multi-window (5h + weekly / plan-based) |
51
52
 
52
53
  Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware AI, etc.)? See [CONTRIBUTING.md](CONTRIBUTING.md).
53
54
 
@@ -71,16 +72,25 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
71
72
  - `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
72
73
  - `quota_show` — toggle sidebar title display on/off (state persists across sessions)
73
74
  - After startup, titles are restored immediately when persisted display mode is OFF; when persisted display mode is ON, touched titles refresh on startup and the rest update on the next relevant session/message event or when `quota_show` is toggled
74
- - Quota connectors:
75
- - OpenAI Codex OAuth (`/backend-api/wham/usage`)
76
- - GitHub Copilot OAuth (`/copilot_internal/user`)
77
- - RightCode API key (`/account/summary`)
78
- - Buzz API key (`/v1/dashboard/billing/subscription` + `/v1/dashboard/billing/usage`)
79
- - Anthropic Claude OAuth (`/api/oauth/usage`, with beta header)
75
+ - Quota connectors:
76
+ - OpenAI Codex OAuth (`/backend-api/wham/usage`)
77
+ - GitHub Copilot OAuth (`/copilot_internal/user`)
78
+ - Kimi For Coding API key (`/usages`, built-in `kimi-for-coding` provider)
79
+ - RightCode API key (`/account/summary`)
80
+ - Buzz API key (`/v1/dashboard/billing/subscription` + `/v1/dashboard/billing/usage`)
81
+ - Anthropic Claude OAuth (`/api/oauth/usage`, with beta header)
80
82
  - OpenAI OAuth quota checks auto-refresh expired access token (using refresh token)
81
- - API key providers still show usage aggregation (quota only applies to subscription providers)
82
- - Incremental usage aggregation — only processes new messages since last cursor
83
- - Sidebar token units are adaptive (`k`/`m` with one decimal where applicable)
83
+ - API key providers still show usage aggregation (quota only applies to subscription providers)
84
+ - Incremental usage aggregation — only processes new messages since last cursor
85
+ - Sidebar token units are adaptive (`k`/`m` with one decimal where applicable)
86
+
87
+ ### Kimi For Coding notes
88
+
89
+ - OpenCode's built-in provider ID is `kimi-for-coding` and its runtime base URL is `https://api.kimi.com/coding/v1`.
90
+ - The plugin treats Kimi as a subscription quota source, not a balance source.
91
+ - Quota data is read from `GET https://api.kimi.com/coding/v1/usages`.
92
+ - The current implementation maps the short rolling window in `limits[]` to `5h` and the top-level `usage` block to `Weekly`.
93
+ - Rendering follows the same compact reset formatting as OpenAI: short windows show `Rst MM-DD HH:MM` when they cross days, and longer windows show `Rst MM-DD`.
84
94
 
85
95
  ## Storage layout
86
96
 
@@ -455,12 +465,13 @@ Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
455
465
  ## Security & privacy notes
456
466
 
457
467
  - The plugin reads OpenCode credentials from `<opencode-data>/auth.json`.
458
- - If enabled, quota checks call external endpoints:
459
- - OpenAI Codex: `https://chatgpt.com/backend-api/wham/usage`
460
- - GitHub Copilot: `https://api.github.com/copilot_internal/user`
461
- - RightCode: `https://www.right.codes/account/summary`
462
- - Buzz: `https://buzzai.cc/v1/dashboard/billing/subscription` and `https://buzzai.cc/v1/dashboard/billing/usage`
463
- - Anthropic: `https://api.anthropic.com/api/oauth/usage`
468
+ - If enabled, quota checks call external endpoints:
469
+ - OpenAI Codex: `https://chatgpt.com/backend-api/wham/usage`
470
+ - GitHub Copilot: `https://api.github.com/copilot_internal/user`
471
+ - Kimi For Coding: `https://api.kimi.com/coding/v1/usages`
472
+ - RightCode: `https://www.right.codes/account/summary`
473
+ - Buzz: `https://buzzai.cc/v1/dashboard/billing/subscription` and `https://buzzai.cc/v1/dashboard/billing/usage`
474
+ - Anthropic: `https://api.anthropic.com/api/oauth/usage`
464
475
  - **Screen-sharing warning**: Session titles and toasts surface usage/quota
465
476
  information. If you are screen-sharing or recording, consider toggling the
466
477
  sidebar display off (`/qtoggle` or `quota_show` tool) to avoid leaking
@@ -470,8 +481,9 @@ Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
470
481
  - OpenAI OAuth token refresh is disabled by default; set
471
482
  `quota.refreshAccessToken=true` if you want the plugin to refresh access
472
483
  tokens when expired.
473
- - Anthropic quota currently uses a beta/internal-style OAuth usage endpoint and
474
- request header; response fields may change without notice.
484
+ - Anthropic quota currently uses a beta/internal-style OAuth usage endpoint and
485
+ request header; response fields may change without notice.
486
+ - Kimi For Coding quota uses the current `/usages` response shape exposed by the Kimi coding service; if Kimi changes that payload, window parsing may need to be updated.
475
487
  - State/chunk file writes refuse to write through symlinked targets (best-effort defense-in-depth).
476
488
  - The `OPENCODE_QUOTA_DATA_HOME` env var overrides the OpenCode data directory
477
489
  path (for testing); do not set this in production.
package/SECURITY.md CHANGED
@@ -25,7 +25,7 @@ We will acknowledge reports as quickly as possible and provide a remediation tim
25
25
  - Keep debug logs free of secrets.
26
26
  - Prefer fail-closed behavior for writes (already enforced via symlink checks and atomic writes).
27
27
  - Quota fetching may contact provider-operated endpoints such as OpenAI, GitHub,
28
- RightCode, Buzz, and Anthropic; review any new provider integration for
28
+ Kimi, RightCode, Buzz, and Anthropic; review any new provider integration for
29
29
  outbound data exposure and header/token handling.
30
30
  - Some quota integrations rely on beta or internal-style endpoints; document
31
31
  instability risks clearly and avoid assuming long-term API compatibility.
@@ -0,0 +1,2 @@
1
+ import type { QuotaProviderAdapter } from '../types.js';
2
+ export declare const kimiForCodingAdapter: QuotaProviderAdapter;
@@ -0,0 +1,219 @@
1
+ import { isRecord, swallow } from '../../helpers.js';
2
+ import { asNumber, configuredProviderEnabled, fetchWithTimeout, sanitizeBaseURL, toIso, } from '../common.js';
3
+ const KIMI_FOR_CODING_BASE_URL = 'https://api.kimi.com/coding/v1';
4
+ function resolveApiKey(auth, providerOptions) {
5
+ const optionKey = providerOptions?.apiKey;
6
+ if (typeof optionKey === 'string' && optionKey)
7
+ return optionKey;
8
+ if (!auth)
9
+ return undefined;
10
+ if (auth.type === 'api' && typeof auth.key === 'string' && auth.key) {
11
+ return auth.key;
12
+ }
13
+ if (auth.type === 'wellknown') {
14
+ if (typeof auth.key === 'string' && auth.key)
15
+ return auth.key;
16
+ if (typeof auth.token === 'string' && auth.token)
17
+ return auth.token;
18
+ }
19
+ if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
20
+ return auth.access;
21
+ }
22
+ return undefined;
23
+ }
24
+ function isKimiCodingBaseURL(value) {
25
+ const normalized = sanitizeBaseURL(value);
26
+ if (!normalized)
27
+ return false;
28
+ try {
29
+ const parsed = new URL(normalized);
30
+ if (parsed.protocol !== 'https:')
31
+ return false;
32
+ const pathname = parsed.pathname.replace(/\/+$/, '');
33
+ return parsed.host === 'api.kimi.com' && pathname === '/coding/v1';
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ function usagesUrl(baseURL) {
40
+ const normalized = sanitizeBaseURL(baseURL);
41
+ if (isKimiCodingBaseURL(normalized)) {
42
+ return `${normalized}/usages`;
43
+ }
44
+ return `${KIMI_FOR_CODING_BASE_URL}/usages`;
45
+ }
46
+ function percentFromQuota(limit, remaining) {
47
+ const total = asNumber(limit) ??
48
+ (typeof limit === 'string' && limit.trim() ? Number(limit) : undefined);
49
+ const left = asNumber(remaining) ??
50
+ (typeof remaining === 'string' && remaining.trim()
51
+ ? Number(remaining)
52
+ : undefined);
53
+ if (total === undefined || left === undefined || total <= 0)
54
+ return undefined;
55
+ if (!Number.isFinite(total) || !Number.isFinite(left))
56
+ return undefined;
57
+ return Math.max(0, Math.min(100, (left / total) * 100));
58
+ }
59
+ function windowLabel(duration, timeUnit) {
60
+ if (timeUnit === 'TIME_UNIT_MINUTE' && duration === 300)
61
+ return '5h';
62
+ if (timeUnit === 'TIME_UNIT_DAY' && duration === 7)
63
+ return 'Weekly';
64
+ if (timeUnit === 'TIME_UNIT_MINUTE' && duration && duration > 0) {
65
+ const hours = duration / 60;
66
+ if (hours <= 24)
67
+ return `${Math.round(hours)}h`;
68
+ }
69
+ if (timeUnit === 'TIME_UNIT_HOUR' && duration && duration > 0) {
70
+ if (duration <= 24)
71
+ return `${Math.round(duration)}h`;
72
+ const days = duration / 24;
73
+ if (days <= 6)
74
+ return `${Math.round(days)}d`;
75
+ }
76
+ if (timeUnit === 'TIME_UNIT_DAY' && duration && duration > 0) {
77
+ if (duration <= 6)
78
+ return `${Math.round(duration)}d`;
79
+ if (duration === 7)
80
+ return 'Weekly';
81
+ }
82
+ return undefined;
83
+ }
84
+ function parseWindow(value) {
85
+ if (!isRecord(value))
86
+ return undefined;
87
+ const window = isRecord(value.window) ? value.window : undefined;
88
+ const detail = isRecord(value.detail) ? value.detail : undefined;
89
+ if (!window || !detail)
90
+ return undefined;
91
+ const duration = asNumber(window.duration);
92
+ const timeUnit = typeof window.timeUnit === 'string' ? window.timeUnit : undefined;
93
+ const label = windowLabel(duration, timeUnit);
94
+ const remainingPercent = percentFromQuota(detail.limit, detail.remaining);
95
+ if (!label || remainingPercent === undefined)
96
+ return undefined;
97
+ return {
98
+ label,
99
+ remainingPercent,
100
+ resetAt: toIso(detail.resetTime),
101
+ };
102
+ }
103
+ function dedupeWindows(windows) {
104
+ const seen = new Set();
105
+ const deduped = [];
106
+ for (const window of windows) {
107
+ const key = `${window.label}|${window.resetAt || ''}|${window.remainingPercent ?? ''}`;
108
+ if (seen.has(key))
109
+ continue;
110
+ seen.add(key);
111
+ deduped.push(window);
112
+ }
113
+ return deduped;
114
+ }
115
+ async function fetchKimiForCodingQuota({ providerID, providerOptions, auth, config, }) {
116
+ const checkedAt = Date.now();
117
+ const base = {
118
+ providerID,
119
+ adapterID: 'kimi-for-coding',
120
+ label: 'Kimi For Coding',
121
+ shortLabel: 'Kimi',
122
+ sortOrder: 15,
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(usagesUrl(providerOptions?.baseURL), {
134
+ method: 'GET',
135
+ headers: {
136
+ Accept: 'application/json',
137
+ Authorization: `Bearer ${apiKey}`,
138
+ 'User-Agent': 'opencode-quota-sidebar',
139
+ },
140
+ }, config.quota.requestTimeoutMs).catch(swallow('fetchKimiForCodingQuota:usage'));
141
+ if (!response) {
142
+ return {
143
+ ...base,
144
+ status: 'error',
145
+ checkedAt,
146
+ note: 'network request failed',
147
+ };
148
+ }
149
+ if (!response.ok) {
150
+ return {
151
+ ...base,
152
+ status: 'error',
153
+ checkedAt,
154
+ note: `http ${response.status}`,
155
+ };
156
+ }
157
+ const payload = await response
158
+ .json()
159
+ .catch(swallow('fetchKimiForCodingQuota:json'));
160
+ if (!isRecord(payload)) {
161
+ return {
162
+ ...base,
163
+ status: 'error',
164
+ checkedAt,
165
+ note: 'invalid response',
166
+ };
167
+ }
168
+ const windows = Array.isArray(payload.limits)
169
+ ? payload.limits.map((item) => parseWindow(item)).filter(Boolean)
170
+ : [];
171
+ const usage = isRecord(payload.usage) ? payload.usage : undefined;
172
+ const topLevelRemainingPercent = usage
173
+ ? percentFromQuota(usage.limit, usage.remaining)
174
+ : undefined;
175
+ const topLevelResetAt = usage ? toIso(usage.resetTime) : undefined;
176
+ const allWindows = dedupeWindows([
177
+ ...windows,
178
+ topLevelRemainingPercent !== undefined
179
+ ? {
180
+ label: 'Weekly',
181
+ remainingPercent: topLevelRemainingPercent,
182
+ resetAt: topLevelResetAt,
183
+ }
184
+ : undefined,
185
+ ].filter((value) => Boolean(value))).sort((left, right) => {
186
+ const order = (label) => {
187
+ if (label === '5h')
188
+ return 0;
189
+ if (label === 'Weekly')
190
+ return 1;
191
+ return 2;
192
+ };
193
+ return order(left.label) - order(right.label);
194
+ });
195
+ const primary = allWindows[0];
196
+ return {
197
+ ...base,
198
+ status: primary ? 'ok' : 'error',
199
+ checkedAt,
200
+ remainingPercent: primary?.remainingPercent,
201
+ resetAt: primary?.resetAt,
202
+ note: primary ? undefined : 'missing quota fields',
203
+ windows: allWindows.length > 0 ? allWindows : undefined,
204
+ };
205
+ }
206
+ export const kimiForCodingAdapter = {
207
+ id: 'kimi-for-coding',
208
+ label: 'Kimi For Coding',
209
+ shortLabel: 'Kimi',
210
+ sortOrder: 15,
211
+ normalizeID: (providerID) => providerID === 'kimi-for-coding' ? 'kimi-for-coding' : undefined,
212
+ matchScore: ({ providerID, providerOptions }) => {
213
+ if (providerID === 'kimi-for-coding')
214
+ return 100;
215
+ return isKimiCodingBaseURL(providerOptions?.baseURL) ? 95 : 0;
216
+ },
217
+ isEnabled: (config) => configuredProviderEnabled(config.quota, 'kimi-for-coding', true),
218
+ fetch: fetchKimiForCodingQuota,
219
+ };
@@ -1,9 +1,10 @@
1
1
  import { anthropicAdapter } from './core/anthropic.js';
2
2
  import { buzzAdapter } from './third_party/buzz.js';
3
3
  import { copilotAdapter } from './core/copilot.js';
4
+ import { kimiForCodingAdapter } from './core/kimi_for_coding.js';
4
5
  import { openaiAdapter } from './core/openai.js';
5
6
  import { QuotaProviderRegistry } from './registry.js';
6
7
  import { rightCodeAdapter } from './third_party/rightcode.js';
7
8
  export declare function createDefaultProviderRegistry(): QuotaProviderRegistry;
8
- export { anthropicAdapter, buzzAdapter, copilotAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
9
+ export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
9
10
  export type { AuthUpdate, AuthValue, ProviderResolveContext, QuotaFetchContext, QuotaProviderAdapter, RefreshedOAuthAuth, } from './types.js';
@@ -1,6 +1,7 @@
1
1
  import { anthropicAdapter } from './core/anthropic.js';
2
2
  import { buzzAdapter } from './third_party/buzz.js';
3
3
  import { copilotAdapter } from './core/copilot.js';
4
+ import { kimiForCodingAdapter } from './core/kimi_for_coding.js';
4
5
  import { openaiAdapter } from './core/openai.js';
5
6
  import { QuotaProviderRegistry } from './registry.js';
6
7
  import { rightCodeAdapter } from './third_party/rightcode.js';
@@ -8,9 +9,10 @@ export function createDefaultProviderRegistry() {
8
9
  const registry = new QuotaProviderRegistry();
9
10
  registry.register(rightCodeAdapter);
10
11
  registry.register(buzzAdapter);
12
+ registry.register(kimiForCodingAdapter);
11
13
  registry.register(openaiAdapter);
12
14
  registry.register(copilotAdapter);
13
15
  registry.register(anthropicAdapter);
14
16
  return registry;
15
17
  }
16
- export { anthropicAdapter, buzzAdapter, copilotAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
18
+ export { anthropicAdapter, buzzAdapter, copilotAdapter, kimiForCodingAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
package/dist/quota.js CHANGED
@@ -33,7 +33,7 @@ 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', 'github-copilot', 'anthropic'];
36
+ return ['openai', 'kimi-for-coding', 'github-copilot', 'anthropic'];
37
37
  }
38
38
  export function createQuotaRuntime() {
39
39
  const providerRegistry = createDefaultProviderRegistry();
@@ -1,5 +1,6 @@
1
1
  const PROVIDER_SHORT_LABELS = {
2
2
  openai: 'OpenAI',
3
+ 'kimi-for-coding': 'Kimi',
3
4
  'github-copilot': 'Copilot',
4
5
  anthropic: 'Anthropic',
5
6
  rightcode: 'RC',
@@ -107,13 +107,21 @@ export function createQuotaService(deps) {
107
107
  const record = item;
108
108
  const id = record.id;
109
109
  const options = record.options;
110
+ const key = record.key;
110
111
  if (typeof id !== 'string')
111
112
  return acc;
112
113
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
113
- acc[id] = {};
114
+ acc[id] =
115
+ typeof key === 'string' && key ? { apiKey: key } : {};
114
116
  return acc;
115
117
  }
116
- acc[id] = options;
118
+ const optionsRecord = options;
119
+ acc[id] = {
120
+ ...optionsRecord,
121
+ ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
122
+ ? { apiKey: key }
123
+ : {}),
124
+ };
117
125
  return acc;
118
126
  }, {})
119
127
  : {};
@@ -159,13 +167,20 @@ export function createQuotaService(deps) {
159
167
  const record = item;
160
168
  const id = record.id;
161
169
  const options = record.options;
170
+ const key = record.key;
162
171
  if (typeof id !== 'string')
163
172
  return acc;
164
173
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
165
- acc[id] = {};
174
+ acc[id] = typeof key === 'string' && key ? { apiKey: key } : {};
166
175
  return acc;
167
176
  }
168
- acc[id] = options;
177
+ const optionsRecord = options;
178
+ acc[id] = {
179
+ ...optionsRecord,
180
+ ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
181
+ ? { apiKey: key }
182
+ : {}),
183
+ };
169
184
  return acc;
170
185
  }, {})
171
186
  : {};
@@ -187,15 +202,22 @@ export function createQuotaService(deps) {
187
202
  const record = item;
188
203
  const id = record.id;
189
204
  const options = record.options;
205
+ const key = record.key;
190
206
  if (typeof id !== 'string')
191
207
  return acc;
192
208
  if (!options ||
193
209
  typeof options !== 'object' ||
194
210
  Array.isArray(options)) {
195
- acc[id] = {};
211
+ acc[id] = typeof key === 'string' && key ? { apiKey: key } : {};
196
212
  return acc;
197
213
  }
198
- acc[id] = options;
214
+ const optionsRecord = options;
215
+ acc[id] = {
216
+ ...optionsRecord,
217
+ ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
218
+ ? { apiKey: key }
219
+ : {}),
220
+ };
199
221
  return acc;
200
222
  }, {})
201
223
  : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",