oc-usage-limits-plugin 0.0.1 → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mynameistito
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # oc-usage-limits-plugin
2
+
3
+ OpenCode TUI plugin that shows Codex and ZAI usage limits in the sidebar and prompt footer.
4
+
5
+ ## Features
6
+
7
+ - Adds a `Usage Limits` block under the sidebar `Context` section.
8
+ - Shows current Codex usage windows from OpenAI/Codex auth.
9
+ - Shows current ZAI quota windows from ZAI Coding Plan auth.
10
+ - Adds compact prompt-footer usage when the current session uses an OpenAI or ZAI Coding Plan model.
11
+ - Providers are toggled from `~/.config/opencode/usage-limits.jsonc`.
12
+ - Reads OpenCode-connected credentials first, then falls back to explicit config/env credentials.
13
+
14
+ ## Install
15
+
16
+ Add the TUI plugin to `~/.config/opencode/tui.json`:
17
+
18
+ ```jsonc
19
+ {
20
+ "$schema": "https://opencode.ai/tui.json",
21
+ "plugin": ["oc-usage-limits-plugin"],
22
+ }
23
+ ```
24
+
25
+ OpenCode TUI plugins are configured in `tui.json`, not `opencode.jsonc`.
26
+
27
+ Restart OpenCode after changing TUI plugin config.
28
+
29
+ ## Usage Config
30
+
31
+ Create `~/.config/opencode/usage-limits.jsonc`:
32
+
33
+ ```jsonc
34
+ {
35
+ "$schema": "https://raw.githubusercontent.com/mynameistito/oc-usage-limits-plugin/main/usage-limits.schema.json",
36
+ "enabled": true,
37
+ "refreshIntervalSeconds": 60,
38
+ "requestTimeoutMs": 10000,
39
+ "showErrors": true,
40
+ "providers": {
41
+ "codex": {
42
+ "enabled": true,
43
+ "label": "Codex",
44
+ "authPath": "~/.codex/auth.json",
45
+ },
46
+ "zai": {
47
+ "enabled": true,
48
+ "label": "ZAI",
49
+ "authPath": "~/.local/share/opencode/auth.json",
50
+ "apiKey": "{env:OC_ZAI_API_KEY}",
51
+ "authorizationScheme": "raw",
52
+ },
53
+ },
54
+ }
55
+ ```
56
+
57
+ Disabled providers are hidden:
58
+
59
+ ```jsonc
60
+ "providers": {
61
+ "codex": { "enabled": true },
62
+ "zai": { "enabled": false }
63
+ }
64
+ ```
65
+
66
+ ## Credential Lookup
67
+
68
+ Codex lookup order:
69
+
70
+ 1. OpenCode auth at `~/.local/share/opencode/auth.json`, provider `openai`.
71
+ 2. Codex auth file from `authPath`, default `~/.codex/auth.json`.
72
+
73
+ ZAI lookup order:
74
+
75
+ 1. Config `authPath`, which can point at OpenCode auth JSON or a simple `{ "key": "..." }` / `{ "apiKey": "..." }` JSON file.
76
+ 2. OpenCode auth at `~/.local/share/opencode/auth.json`, provider `zai-coding-plan`.
77
+ 3. OpenCode auth provider `zai`.
78
+ 4. Config `apiKey`, including `{env:OC_ZAI_API_KEY}` references.
79
+
80
+ ## Display
81
+
82
+ Sidebar rows look like:
83
+
84
+ ```txt
85
+ Usage Limits
86
+ codex
87
+ 5h: 42% used resets 1h 2m
88
+ weekly: 12% used resets 3d 4h
89
+ ZAI
90
+ tokens: 18% used resets 2h
91
+ MCP: 6% used
92
+ ```
93
+
94
+ Prompt footer shows compact usage when the current session model belongs to a supported provider:
95
+
96
+ ```txt
97
+ 5h: 42% used resets 1h 2m
98
+ ```
99
+
100
+ Provider mapping:
101
+
102
+ - OpenCode provider `openai` -> Codex usage.
103
+ - OpenCode provider `zai-coding-plan` -> ZAI token usage.
104
+
105
+ ## Development
106
+
107
+ ```powershell
108
+ bun install
109
+ bun run typecheck
110
+ ```
111
+
112
+ The package exposes a TUI entrypoint at `oc-usage-limits-plugin/tui` for OpenCode's package plugin loader.
113
+
114
+ ## Notes
115
+
116
+ - The refresh interval defaults to 60 seconds.
117
+ - The effective minimum refresh interval is 15 seconds.
118
+ - Errors are intentionally short and do not include auth tokens or response bodies.
@@ -0,0 +1,8 @@
1
+ //#region src/index.d.ts
2
+ /** OpenCode plugin module exported for the `oc-usage-limits-plugin/tui` entry. */
3
+ declare const _default: {
4
+ id: string;
5
+ tui: import("@opencode-ai/plugin/tui").TuiPlugin;
6
+ };
7
+ //#endregion
8
+ export { _default as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,848 @@
1
+ import { For, Show, createSignal } from "solid-js";
2
+ import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { readFile } from "node:fs/promises";
6
+ //#region src/format.ts
7
+ /**
8
+ * Formats a positive duration in seconds into the compact label used by the TUI.
9
+ *
10
+ * Values that are missing, invalid, or already elapsed are displayed as `now`.
11
+ * Positive values are rounded up to the next minute so near-future resets never
12
+ * appear as zero minutes remaining.
13
+ *
14
+ * @param seconds - Duration, in seconds, until the event occurs.
15
+ * @returns A short human-readable duration such as `3m`, `2h 15m`, or `1d 4h`.
16
+ */
17
+ const duration = (seconds) => {
18
+ if (seconds === null || !Number.isFinite(seconds) || seconds <= 0) return "now";
19
+ const minutes = Math.ceil(seconds / 60);
20
+ if (minutes < 60) return `${minutes}m`;
21
+ const hours = Math.floor(minutes / 60);
22
+ const remainder = minutes % 60;
23
+ if (hours < 24) return remainder === 0 ? `${hours}h` : `${hours}h ${remainder}m`;
24
+ const days = Math.floor(hours / 24);
25
+ const hourRemainder = hours % 24;
26
+ return hourRemainder === 0 ? `${days}d` : `${days}d ${hourRemainder}h`;
27
+ };
28
+ /**
29
+ * Formats a nullable usage percentage for display.
30
+ *
31
+ * @param value - The percentage to render, or `null` when the provider did not
32
+ * report a percentage.
33
+ * @returns A rounded usage string, or `? used` when usage is unknown.
34
+ */
35
+ const formatPercent = (value) => value === null ? "? used" : `${Math.round(value)}% used`;
36
+ /**
37
+ * Builds the primary line of text for a usage window in the sidebar panel.
38
+ *
39
+ * @param window - The provider usage window to summarize.
40
+ * @returns A label and percentage pair such as `daily: 42% used`.
41
+ */
42
+ const windowMainText = (window) => `${window.label}: ${formatPercent(window.usedPercent)}`;
43
+ /**
44
+ * Builds the compact prompt-footer text for the active provider's primary window.
45
+ *
46
+ * @param window - The active provider usage window to summarize.
47
+ * @returns A compact percentage label such as `daily: 42% used`.
48
+ */
49
+ const bottomWindowMainText = (window) => `${window.label}: ${formatPercent(window.usedPercent)}`;
50
+ /**
51
+ * Formats the reset suffix for a usage window.
52
+ *
53
+ * @param window - The usage window whose reset time should be rendered.
54
+ * @returns A leading-space suffix such as ` resets 12m`, or an empty string when
55
+ * the provider did not report a reset countdown.
56
+ */
57
+ const windowResetText = (window) => window.resetAfterSeconds === null ? "" : ` resets ${duration(window.resetAfterSeconds)}`;
58
+ /**
59
+ * Converts a provider-reported rolling-window size into a stable display label.
60
+ *
61
+ * Provider APIs can return slightly imprecise window lengths, so expected
62
+ * windows are matched within a 5% tolerance before falling back to the caller's
63
+ * label.
64
+ *
65
+ * @param seconds - The provider-reported limit window length in seconds.
66
+ * @param fallback - Label to use when the window does not match a known size.
67
+ * @returns A normalized label such as `5h`, `daily`, `weekly`, or `monthly`.
68
+ */
69
+ const limitLabelForWindow = (seconds, fallback) => {
70
+ const minutes = Math.ceil(seconds / 60);
71
+ const roughly = (expected) => minutes >= expected * .95 && minutes <= expected * 1.05;
72
+ const hour = 60;
73
+ const day = 24 * hour;
74
+ if (roughly(5 * hour)) return "5h";
75
+ if (roughly(day)) return "daily";
76
+ if (roughly(7 * day)) return "weekly";
77
+ if (roughly(30 * day)) return "monthly";
78
+ return fallback;
79
+ };
80
+ //#endregion
81
+ //#region src/components.tsx
82
+ /**
83
+ * Chooses the status-dot color for a usage percentage.
84
+ *
85
+ * @param usedPercent - Percentage consumed, or `null` when unknown.
86
+ * @param theme - Active OpenCode TUI theme.
87
+ * @returns A theme color indicating healthy, warning, error, or unknown usage.
88
+ */
89
+ const dotColor = (usedPercent, theme) => {
90
+ if (usedPercent === null) return theme.textMuted;
91
+ if (usedPercent >= 90) return theme.error;
92
+ if (usedPercent >= 70) return theme.warning;
93
+ return theme.success;
94
+ };
95
+ /**
96
+ * Renders the sidebar usage-limits panel.
97
+ *
98
+ * The panel lists every enabled provider, shows loading and stale states, and can
99
+ * optionally display provider fetch errors.
100
+ *
101
+ * @param props - Provider states, error visibility, and active TUI theme.
102
+ * @returns Solid/OpenTUI JSX for the sidebar content slot.
103
+ */
104
+ const UsageLimitsPanel = (props) => /* @__PURE__ */ jsx(Show, {
105
+ when: props.states.some((state) => state.status !== "disabled"),
106
+ children: /* @__PURE__ */ jsxs("box", { children: [/* @__PURE__ */ jsx("text", {
107
+ fg: props.theme.text,
108
+ children: /* @__PURE__ */ jsx("b", { children: "Usage Limits" })
109
+ }), /* @__PURE__ */ jsx(For, {
110
+ each: props.states,
111
+ children: (state) => /* @__PURE__ */ jsx(Show, {
112
+ when: state.status !== "disabled",
113
+ children: /* @__PURE__ */ jsxs("box", { children: [
114
+ /* @__PURE__ */ jsxs("text", {
115
+ fg: props.theme.text,
116
+ children: [state.label, /* @__PURE__ */ jsxs(Show, {
117
+ when: state.status === "ready" && state.stale,
118
+ children: [" ", "stale"]
119
+ })]
120
+ }),
121
+ /* @__PURE__ */ jsx(Show, {
122
+ when: state.status === "loading",
123
+ children: /* @__PURE__ */ jsx("text", {
124
+ fg: props.theme.textMuted,
125
+ children: " loading..."
126
+ })
127
+ }),
128
+ /* @__PURE__ */ jsx(Show, {
129
+ when: state.status === "ready",
130
+ children: /* @__PURE__ */ jsx(For, {
131
+ each: state.status === "ready" ? state.data.windows : [],
132
+ children: (window) => /* @__PURE__ */ jsxs("box", {
133
+ flexDirection: "row",
134
+ gap: 1,
135
+ children: [/* @__PURE__ */ jsx("text", {
136
+ flexShrink: 0,
137
+ fg: dotColor(window.usedPercent, props.theme),
138
+ children: "•"
139
+ }), /* @__PURE__ */ jsxs("text", {
140
+ fg: props.theme.text,
141
+ children: [windowMainText(window), /* @__PURE__ */ jsx("span", {
142
+ style: { fg: props.theme.textMuted },
143
+ children: windowResetText(window)
144
+ })]
145
+ })]
146
+ })
147
+ })
148
+ }),
149
+ /* @__PURE__ */ jsx(Show, {
150
+ when: state.status === "error" && props.showErrors,
151
+ children: /* @__PURE__ */ jsx(Show, {
152
+ fallback: /* @__PURE__ */ jsxs("text", {
153
+ fg: props.theme.error,
154
+ children: [" ", state.status === "error" ? state.message : "error"]
155
+ }),
156
+ when: state.status === "error" ? state.previous : void 0,
157
+ children: (previous) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(For, {
158
+ each: previous().windows,
159
+ children: (window) => /* @__PURE__ */ jsxs("box", {
160
+ flexDirection: "row",
161
+ gap: 1,
162
+ children: [/* @__PURE__ */ jsx("text", {
163
+ flexShrink: 0,
164
+ fg: dotColor(window.usedPercent, props.theme),
165
+ children: "•"
166
+ }), /* @__PURE__ */ jsxs("text", {
167
+ fg: props.theme.text,
168
+ children: [windowMainText(window), /* @__PURE__ */ jsx("span", {
169
+ style: { fg: props.theme.textMuted },
170
+ children: windowResetText(window)
171
+ })]
172
+ })]
173
+ })
174
+ }), /* @__PURE__ */ jsxs("text", {
175
+ fg: props.theme.error,
176
+ children: [" ", state.status === "error" ? state.message : "error"]
177
+ })] })
178
+ })
179
+ })
180
+ ] })
181
+ })
182
+ })] })
183
+ });
184
+ /**
185
+ * Renders the compact active-provider usage indicator in the prompt footer.
186
+ *
187
+ * @param props - Active usage window and active TUI theme.
188
+ * @returns Solid/OpenTUI JSX for the prompt footer slot.
189
+ */
190
+ const BottomUsage = (props) => /* @__PURE__ */ jsx(Show, {
191
+ when: props.window,
192
+ children: (window) => /* @__PURE__ */ jsxs("text", {
193
+ fg: props.theme.text,
194
+ children: [bottomWindowMainText(window()), /* @__PURE__ */ jsx("span", {
195
+ style: { fg: props.theme.textMuted },
196
+ children: windowResetText(window())
197
+ })]
198
+ })
199
+ });
200
+ //#endregion
201
+ //#region src/utils.ts
202
+ /**
203
+ * Normalizes an arbitrary number into the inclusive percentage range used by UI.
204
+ *
205
+ * @param value - Provider-reported percentage value.
206
+ * @returns A finite number clamped between `0` and `100`.
207
+ */
208
+ const clampPercent = (value) => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
209
+ /**
210
+ * Checks whether a value is a plain object-like record.
211
+ *
212
+ * This intentionally excludes arrays because provider API payloads are parsed as
213
+ * `unknown` and object fields are accessed only after this guard succeeds.
214
+ *
215
+ * @param value - Value to narrow.
216
+ * @returns `true` when the value can be safely indexed as a record.
217
+ */
218
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
219
+ const stripTrailingCommas = (input) => {
220
+ let output = "";
221
+ let inString = false;
222
+ let quote = "";
223
+ let escaped = false;
224
+ for (let index = 0; index < input.length; index += 1) {
225
+ const char = input[index];
226
+ if (inString) {
227
+ output += char;
228
+ if (escaped) escaped = false;
229
+ else if (char === "\\") escaped = true;
230
+ else if (char === quote) inString = false;
231
+ continue;
232
+ }
233
+ if (char === "\"" || char === "'") {
234
+ inString = true;
235
+ quote = char;
236
+ output += char;
237
+ continue;
238
+ }
239
+ if (char === ",") {
240
+ let lookahead = index + 1;
241
+ while (/\s/u.test(input[lookahead] ?? "")) lookahead += 1;
242
+ if (input[lookahead] === "}" || input[lookahead] === "]") continue;
243
+ }
244
+ output += char;
245
+ }
246
+ return output;
247
+ };
248
+ /**
249
+ * Removes JSONC comments and trailing commas while preserving string contents.
250
+ *
251
+ * The plugin accepts small user-authored config files without adding a JSONC
252
+ * dependency. Both line comments and block comments are stripped, but comment
253
+ * markers inside quoted strings are left untouched.
254
+ *
255
+ * @param input - Raw JSONC text.
256
+ * @returns JSON-compatible text suitable for `JSON.parse`.
257
+ */
258
+ const stripJsonComments = (input) => {
259
+ let output = "";
260
+ let inString = false;
261
+ let quote = "";
262
+ let escaped = false;
263
+ for (let index = 0; index < input.length; index += 1) {
264
+ const char = input[index];
265
+ const next = input[index + 1];
266
+ if (inString) {
267
+ output += char;
268
+ if (escaped) escaped = false;
269
+ else if (char === "\\") escaped = true;
270
+ else if (char === quote) inString = false;
271
+ continue;
272
+ }
273
+ if (char === "\"" || char === "'") {
274
+ inString = true;
275
+ quote = char;
276
+ output += char;
277
+ continue;
278
+ }
279
+ if (char === "/" && next === "/") {
280
+ while (index < input.length && input[index] !== "\n") index += 1;
281
+ output += "\n";
282
+ continue;
283
+ }
284
+ if (char === "/" && next === "*") {
285
+ index += 2;
286
+ while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) index += 1;
287
+ index += 1;
288
+ continue;
289
+ }
290
+ output += char;
291
+ }
292
+ return stripTrailingCommas(output);
293
+ };
294
+ /**
295
+ * Expands a leading home-directory marker in a filesystem path.
296
+ *
297
+ * @param value - Path that may start with `~`, `~/`, or `~\`.
298
+ * @returns The path with a leading home marker replaced by the user's home path.
299
+ */
300
+ const expandHome = (value) => value === "~" || value.startsWith("~/") || value.startsWith("~\\") ? path.join(homedir(), value.slice(2)) : value;
301
+ /**
302
+ * Reads and parses a JSON or JSONC file.
303
+ *
304
+ * A leading `~` in the path is expanded before reading. The generic type is a
305
+ * caller-provided assertion; callers should still validate external data before
306
+ * relying on specific fields.
307
+ *
308
+ * @param filePath - Absolute path, relative path, or home-relative path to read.
309
+ * @returns The parsed JSON value typed as `T`.
310
+ */
311
+ const readJsonFile = async (filePath) => {
312
+ const raw = await readFile(expandHome(filePath), "utf-8");
313
+ return JSON.parse(stripJsonComments(raw));
314
+ };
315
+ /**
316
+ * Resolves a config value that may reference an environment variable.
317
+ *
318
+ * Values in the form `{env:NAME}` are replaced with `process.env.NAME`. Any
319
+ * other non-empty string is returned unchanged.
320
+ *
321
+ * @param value - Raw config value or environment reference.
322
+ * @returns The resolved value, unchanged literal, or `undefined` when absent.
323
+ */
324
+ const resolveEnvReference = (value) => {
325
+ if (!value) return;
326
+ const envMatch = /^\{env:(?<name>[^}]+)\}$/iu.exec(value.trim());
327
+ if (envMatch?.groups?.name) return process.env[envMatch.groups.name];
328
+ return value;
329
+ };
330
+ /**
331
+ * Fetches a JSON endpoint with a timeout and normalized provider-facing errors.
332
+ *
333
+ * HTTP status codes that commonly matter for auth and quota diagnostics are
334
+ * mapped to stable messages, while successful non-JSON responses are rejected as
335
+ * invalid provider payloads.
336
+ *
337
+ * @param url - Endpoint URL to request.
338
+ * @param init - Fetch options such as method and headers.
339
+ * @param timeoutMs - Timeout in milliseconds when `init.signal` is not supplied.
340
+ * @returns The parsed JSON payload as `unknown` for caller-side validation.
341
+ * @throws {Error} When the response is unsuccessful or cannot be parsed as JSON.
342
+ */
343
+ const fetchJson = async (url, init, timeoutMs) => {
344
+ const response = await fetch(url, {
345
+ ...init,
346
+ signal: init.signal ?? AbortSignal.timeout(timeoutMs)
347
+ });
348
+ const body = await response.text();
349
+ if (!response.ok) {
350
+ if (response.status === 401) throw new Error("unauthorized");
351
+ if (response.status === 403) throw new Error("forbidden");
352
+ if (response.status === 429) throw new Error("rate limited");
353
+ throw new Error(`HTTP ${response.status}`);
354
+ }
355
+ try {
356
+ return JSON.parse(body);
357
+ } catch {
358
+ throw new Error("invalid JSON");
359
+ }
360
+ };
361
+ //#endregion
362
+ //#region src/config.ts
363
+ /** Default user configuration path for this plugin. */
364
+ const CONFIG_PATH = path.join(homedir(), ".config", "opencode", "usage-limits.jsonc");
365
+ /** Default OpenCode auth path shared by installed providers. */
366
+ const OPENCODE_AUTH_PATH = path.join(homedir(), ".local", "share", "opencode", "auth.json");
367
+ const numericConfigValue = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
368
+ /**
369
+ * Loads the usage-limits plugin configuration from OpenCode's config directory.
370
+ *
371
+ * Missing files, unreadable files, and invalid JSONC all resolve to conservative
372
+ * defaults so the plugin can start without interrupting the TUI. Partial config
373
+ * files are merged with the same defaults.
374
+ *
375
+ * @returns The fully-populated plugin configuration.
376
+ */
377
+ const loadConfig = async () => {
378
+ const fallback = {
379
+ enabled: true,
380
+ providers: {},
381
+ refreshIntervalSeconds: 60,
382
+ requestTimeoutMs: 1e4,
383
+ showErrors: true
384
+ };
385
+ try {
386
+ const config = await readJsonFile(CONFIG_PATH);
387
+ return {
388
+ enabled: config.enabled ?? fallback.enabled,
389
+ providers: config.providers ?? fallback.providers,
390
+ refreshIntervalSeconds: numericConfigValue(config.refreshIntervalSeconds, fallback.refreshIntervalSeconds),
391
+ requestTimeoutMs: numericConfigValue(config.requestTimeoutMs, fallback.requestTimeoutMs),
392
+ showErrors: config.showErrors ?? fallback.showErrors
393
+ };
394
+ } catch {
395
+ return fallback;
396
+ }
397
+ };
398
+ /**
399
+ * Loads OpenCode's shared auth file for provider credentials.
400
+ *
401
+ * This file may not exist for every installation or provider. Auth loading is
402
+ * therefore best-effort and returns an empty object when credentials are absent
403
+ * or unreadable.
404
+ *
405
+ * @returns The parsed OpenCode auth payload, or an empty auth object.
406
+ */
407
+ const loadOpenCodeAuth = async () => {
408
+ try {
409
+ return await readJsonFile(OPENCODE_AUTH_PATH);
410
+ } catch {
411
+ return {};
412
+ }
413
+ };
414
+ //#endregion
415
+ //#region src/providers/codex.ts
416
+ /** Default ChatGPT backend base URL used for Codex usage requests. */
417
+ const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
418
+ /**
419
+ * Reads Codex credentials from the Codex CLI auth file.
420
+ *
421
+ * @param authPath - Optional path override. Defaults to `~/.codex/auth.json`.
422
+ * @returns Access token and ChatGPT account ID required by the Codex usage API.
423
+ * @throws {Error} When the auth file is missing or does not contain credentials.
424
+ * @throws {TypeError} When the auth file contains credentials with invalid types.
425
+ */
426
+ const readCodexAuthFile = async (authPath) => {
427
+ const auth = await readJsonFile(authPath ?? "~/.codex/auth.json");
428
+ if (!isRecord(auth) || !isRecord(auth.tokens)) throw new Error("missing Codex auth");
429
+ const access = auth.tokens.access_token;
430
+ const accountId = auth.tokens.account_id;
431
+ if (typeof access !== "string" || typeof accountId !== "string") throw new TypeError("invalid Codex credential types");
432
+ return {
433
+ access,
434
+ accountId
435
+ };
436
+ };
437
+ /**
438
+ * Converts a raw Codex rate-limit window into the plugin's normalized shape.
439
+ *
440
+ * @param value - Unknown `primary_window` or `secondary_window` payload.
441
+ * @param fallback - Label used when the provider does not report a known window
442
+ * length.
443
+ * @returns A normalized usage window, or `null` for invalid payloads.
444
+ */
445
+ const codexWindow = (value, fallback) => {
446
+ if (!isRecord(value)) return null;
447
+ const used = typeof value.used_percent === "number" ? clampPercent(value.used_percent) : null;
448
+ const resetAfter = typeof value.reset_after_seconds === "number" ? value.reset_after_seconds : null;
449
+ const windowSeconds = typeof value.limit_window_seconds === "number" ? value.limit_window_seconds : 0;
450
+ const resetAt = typeof value.reset_at === "number" && value.reset_at > 0 ? /* @__PURE__ */ new Date(value.reset_at * 1e3) : null;
451
+ return {
452
+ label: windowSeconds > 0 ? limitLabelForWindow(windowSeconds, fallback) : fallback,
453
+ remainingPercent: used === null ? null : 100 - used,
454
+ resetAfterSeconds: resetAfter,
455
+ resetsAt: resetAt,
456
+ usedPercent: used
457
+ };
458
+ };
459
+ /**
460
+ * Fetches and normalizes Codex usage limits.
461
+ *
462
+ * Credentials are read from OpenCode auth when available, otherwise from the
463
+ * Codex CLI auth file. The returned windows represent the primary and secondary
464
+ * Codex rate-limit windows reported by ChatGPT's backend API.
465
+ *
466
+ * @param config - Optional Codex provider configuration.
467
+ * @param openCodeAuth - Shared OpenCode auth payload.
468
+ * @param timeoutMs - Request timeout in milliseconds.
469
+ * @returns Normalized Codex usage data.
470
+ * @throws {Error} When credentials are missing or the provider response is invalid.
471
+ */
472
+ const fetchCodexUsage = async (config, openCodeAuth, timeoutMs) => {
473
+ const { openai } = openCodeAuth;
474
+ const credentials = typeof openai?.access === "string" && openai.access.trim() !== "" && typeof openai.accountId === "string" && openai.accountId.trim() !== "" ? {
475
+ access: openai.access,
476
+ accountId: openai.accountId
477
+ } : await readCodexAuthFile(config?.authPath);
478
+ const payload = await fetchJson(`${(config?.baseUrl ?? DEFAULT_CODEX_BASE_URL).replace(/\/$/u, "")}/wham/usage`, {
479
+ headers: {
480
+ Authorization: `Bearer ${credentials.access}`,
481
+ "ChatGPT-Account-Id": credentials.accountId,
482
+ "User-Agent": "opencode-usage-limits"
483
+ },
484
+ method: "GET"
485
+ }, timeoutMs);
486
+ if (!isRecord(payload)) throw new Error("invalid Codex usage");
487
+ const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : void 0;
488
+ const windows = [codexWindow(rateLimit?.primary_window, "usage"), codexWindow(rateLimit?.secondary_window, "secondary")].filter((item) => item !== null);
489
+ const resetCredits = isRecord(payload.rate_limit_reset_credits) && typeof payload.rate_limit_reset_credits.available_count === "number" ? payload.rate_limit_reset_credits.available_count : null;
490
+ return {
491
+ capturedAt: /* @__PURE__ */ new Date(),
492
+ id: "codex",
493
+ label: config?.label ?? "Codex",
494
+ metadata: { resetCredits },
495
+ tierName: typeof payload.plan_type === "string" ? payload.plan_type : void 0,
496
+ windows
497
+ };
498
+ };
499
+ //#endregion
500
+ //#region src/providers/zai-coding-plan.ts
501
+ /** ZAI Coding Plan quota endpoint used to fetch usage limits. */
502
+ const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
503
+ /**
504
+ * Infers the ZAI plan tier from the provider's prompt/time quota total.
505
+ *
506
+ * @param total - Total quota reported by the ZAI time-limit payload.
507
+ * @returns The inferred tier name, or `undefined` when it cannot be inferred.
508
+ */
509
+ const inferZaiTier = (total) => {
510
+ if (total === null) return;
511
+ if (total >= 1400) return "Max";
512
+ if (total >= 300) return "Pro";
513
+ if (total > 0) return "Lite";
514
+ };
515
+ /**
516
+ * Extracts a ZAI API key from any supported auth object shape.
517
+ *
518
+ * The plugin accepts both direct `{ key }`/`{ apiKey }` objects and the nested
519
+ * shapes used by OpenCode auth.
520
+ *
521
+ * @param value - Unknown auth payload to inspect.
522
+ * @returns The first recognized API key.
523
+ */
524
+ const keyFromZaiAuth = (value) => {
525
+ if (!isRecord(value)) return;
526
+ if (typeof value.key === "string") return value.key;
527
+ if (typeof value.apiKey === "string") return value.apiKey;
528
+ const zaiCodingPlan = value["zai-coding-plan"];
529
+ if (isRecord(zaiCodingPlan) && typeof zaiCodingPlan.key === "string") return zaiCodingPlan.key;
530
+ if (isRecord(value.zai) && typeof value.zai.key === "string") return value.zai.key;
531
+ };
532
+ /**
533
+ * Attempts to load a ZAI API key from a configured auth path.
534
+ *
535
+ * Missing or invalid files are ignored so other credential sources can still be
536
+ * tried by the provider adapter.
537
+ *
538
+ * @param authPath - Optional auth file path.
539
+ * @returns A ZAI API key when the file exists and contains one.
540
+ */
541
+ const readZaiAuthPathKey = async (authPath) => {
542
+ if (!authPath) return;
543
+ try {
544
+ return keyFromZaiAuth(await readJsonFile(authPath));
545
+ } catch {
546
+ return;
547
+ }
548
+ };
549
+ /**
550
+ * Converts one raw ZAI limit entry into a normalized usage window.
551
+ *
552
+ * Token limits become the primary `tokens` quota window. Time limits become an
553
+ * `MCP` count-based window and also expose the total prompt quota used to infer
554
+ * the user's ZAI tier.
555
+ *
556
+ * @param limit - Raw limit object from the ZAI quota API.
557
+ * @returns The normalized window plus any prompt total discovered on the entry.
558
+ */
559
+ const zaiWindowFromLimit = (limit) => {
560
+ const usedPercent = typeof limit.percentage === "number" ? clampPercent(limit.percentage) : null;
561
+ const resetsAt = typeof limit.nextResetTime === "number" ? new Date(limit.nextResetTime) : null;
562
+ const current = typeof limit.currentValue === "number" ? limit.currentValue : void 0;
563
+ const total = typeof limit.usage === "number" ? limit.usage : void 0;
564
+ if (limit.type === "TOKENS_LIMIT") return {
565
+ promptTotal: null,
566
+ window: {
567
+ label: "tokens",
568
+ remainingPercent: usedPercent === null ? null : 100 - usedPercent,
569
+ resetAfterSeconds: resetsAt ? Math.max(0, Math.ceil((resetsAt.getTime() - Date.now()) / 1e3)) : null,
570
+ resetsAt,
571
+ usedPercent
572
+ }
573
+ };
574
+ if (limit.type === "TIME_LIMIT") return {
575
+ promptTotal: total ?? null,
576
+ window: {
577
+ current,
578
+ label: "MCP",
579
+ remainingPercent: usedPercent === null ? null : 100 - usedPercent,
580
+ resetAfterSeconds: null,
581
+ resetsAt: null,
582
+ total,
583
+ usedPercent
584
+ }
585
+ };
586
+ return {
587
+ promptTotal: null,
588
+ window: null
589
+ };
590
+ };
591
+ /**
592
+ * Fetches and normalizes ZAI Coding Plan usage limits.
593
+ *
594
+ * Credential lookup checks, in order, the configured auth path, OpenCode auth,
595
+ * and a configured literal or environment-backed API key.
596
+ *
597
+ * @param config - Optional ZAI provider configuration.
598
+ * @param openCodeAuth - Shared OpenCode auth payload.
599
+ * @param timeoutMs - Request timeout in milliseconds.
600
+ * @returns Normalized ZAI usage data.
601
+ * @throws {Error} When no API key is available or the provider response is invalid.
602
+ */
603
+ const fetchZaiCodingPlanUsage = async (config, openCodeAuth, timeoutMs) => {
604
+ const configuredKey = resolveEnvReference(config?.apiKey);
605
+ const apiKey = await readZaiAuthPathKey(config?.authPath) ?? keyFromZaiAuth(openCodeAuth) ?? configuredKey;
606
+ if (!apiKey) throw new Error("missing ZAI key");
607
+ const payload = await fetchJson(ZAI_QUOTA_URL, {
608
+ headers: {
609
+ "Accept-Language": "en-US,en",
610
+ Authorization: (config?.authorizationScheme ?? "raw") === "bearer" ? `Bearer ${apiKey}` : apiKey,
611
+ "Content-Type": "application/json"
612
+ },
613
+ method: "GET"
614
+ }, timeoutMs);
615
+ if (!isRecord(payload) || !isRecord(payload.data) || !Array.isArray(payload.data.limits)) throw new Error("invalid ZAI usage");
616
+ const windows = [];
617
+ let promptTotal = null;
618
+ for (const limit of payload.data.limits) {
619
+ if (!isRecord(limit) || typeof limit.type !== "string") continue;
620
+ const usage = zaiWindowFromLimit(limit);
621
+ if (usage.window) windows.push(usage.window);
622
+ if (usage.promptTotal !== null) ({promptTotal} = usage);
623
+ }
624
+ return {
625
+ capturedAt: /* @__PURE__ */ new Date(),
626
+ id: "zai",
627
+ label: config?.label ?? "ZAI",
628
+ tierName: inferZaiTier(promptTotal),
629
+ windows
630
+ };
631
+ };
632
+ //#endregion
633
+ //#region src/providers.ts
634
+ /**
635
+ * Fetches usage data for a configured provider.
636
+ *
637
+ * This dispatches to the provider-specific adapter while keeping plugin refresh
638
+ * code independent of each provider's authentication and response format.
639
+ *
640
+ * @param id - Provider adapter to fetch.
641
+ * @param config - Optional provider-specific configuration.
642
+ * @param openCodeAuth - Shared OpenCode auth payload.
643
+ * @param timeoutMs - Request timeout in milliseconds.
644
+ * @returns Normalized provider usage data.
645
+ */
646
+ const fetchProvider = (id, config, openCodeAuth, timeoutMs) => {
647
+ if (id === "codex") return fetchCodexUsage(config, openCodeAuth, timeoutMs);
648
+ if (id === "zai") return fetchZaiCodingPlanUsage(config, openCodeAuth, timeoutMs);
649
+ throw new Error(`unknown provider: ${id}`);
650
+ };
651
+ /**
652
+ * Returns enabled provider configurations in the order they should appear in UI.
653
+ *
654
+ * Providers are opt-in: a provider is included only when its config sets
655
+ * `enabled: true`.
656
+ *
657
+ * @param config - Fully resolved plugin configuration.
658
+ * @returns Tuples of provider IDs and their config objects.
659
+ */
660
+ const getProviderConfigs = (config) => ["codex", "zai"].flatMap((id) => {
661
+ const provider = config.providers[id];
662
+ if (provider?.enabled !== true) return [];
663
+ return [[id, provider]];
664
+ });
665
+ //#endregion
666
+ //#region src/session.ts
667
+ /**
668
+ * Extracts an OpenCode provider identifier from a session message-like value.
669
+ *
670
+ * OpenCode message shapes have changed over time, so the provider may be present
671
+ * either directly on the message or nested under `message.model`.
672
+ *
673
+ * @param message - Unknown message payload from OpenCode session state.
674
+ * @returns The provider identifier when present.
675
+ */
676
+ const getProviderFromMessage = (message) => {
677
+ if (!isRecord(message)) return;
678
+ if (typeof message.providerID === "string") return message.providerID;
679
+ if (isRecord(message.model) && typeof message.model.providerID === "string") return message.model.providerID;
680
+ };
681
+ /**
682
+ * Finds the provider currently represented by a session's latest messages.
683
+ *
684
+ * Messages are scanned from newest to oldest so the returned provider reflects
685
+ * the most recent model/provider selection in the active conversation.
686
+ *
687
+ * @param messages - OpenCode session messages.
688
+ * @returns The latest provider identifier, or `undefined` when unavailable.
689
+ */
690
+ const currentProviderID = (messages) => {
691
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
692
+ const providerID = getProviderFromMessage(messages[index]);
693
+ if (providerID) return providerID;
694
+ }
695
+ };
696
+ /**
697
+ * Selects the usage window that should be shown in the prompt footer.
698
+ *
699
+ * OpenCode provider IDs are mapped to this plugin's provider IDs, then the most
700
+ * useful window is selected from the current provider state. If the latest fetch
701
+ * failed, the last successful data attached to the error state is used.
702
+ *
703
+ * @param states - Current provider states maintained by the plugin.
704
+ * @param providerID - OpenCode provider identifier for the active session.
705
+ * @returns The best usage window for the active provider, or `null` if none can
706
+ * be shown.
707
+ */
708
+ const usageForProvider = (states, providerID) => {
709
+ let usageID = null;
710
+ if (providerID === "openai") usageID = "codex";
711
+ if (providerID === "zai-coding-plan") usageID = "zai";
712
+ if (!usageID) return null;
713
+ const state = states.find((item) => item.id === usageID);
714
+ let data;
715
+ if (state?.status === "ready") ({data} = state);
716
+ if (state?.status === "error") data = state.previous;
717
+ if (!data) return null;
718
+ if (usageID === "zai") return data.windows.find((window) => window.label === "tokens") ?? data.windows[0] ?? null;
719
+ return data.windows.find((window) => window.label === "5h") ?? data.windows[0] ?? null;
720
+ };
721
+ //#endregion
722
+ //#region src/plugin.tsx
723
+ /**
724
+ * OpenCode TUI plugin entry point.
725
+ *
726
+ * The plugin periodically loads configuration, fetches enabled provider usage,
727
+ * stores the latest successful result for stale/error fallback, and registers UI
728
+ * slots for both the sidebar panel and prompt-footer indicator.
729
+ *
730
+ * @param api - OpenCode TUI plugin API supplied at plugin initialization.
731
+ */
732
+ const tui = async (api) => {
733
+ const [states, setStates] = createSignal([]);
734
+ const [showErrors, setShowErrors] = createSignal(true);
735
+ let lastSuccess = /* @__PURE__ */ new Map();
736
+ let refreshIntervalSeconds = 60;
737
+ /**
738
+ * Refreshes configuration and usage data for every enabled provider.
739
+ *
740
+ * Existing ready or error states are kept visible while new requests are in
741
+ * flight. Failed refreshes retain the last successful usage payload so the UI
742
+ * can still show stale usage alongside the error message.
743
+ */
744
+ const refresh = async () => {
745
+ const config = await loadConfig();
746
+ setShowErrors(config.showErrors);
747
+ ({refreshIntervalSeconds} = config);
748
+ if (!config.enabled) {
749
+ setStates([]);
750
+ return;
751
+ }
752
+ const effectiveRefreshIntervalSeconds = Math.max(15, refreshIntervalSeconds);
753
+ const providers = getProviderConfigs(config);
754
+ const previous = new Map(states().map((state) => [state.id, state]));
755
+ setStates(providers.map(([id, provider]) => {
756
+ const label = provider.label ?? (id === "codex" ? "Codex" : "ZAI");
757
+ const current = previous.get(id);
758
+ if (current?.status === "ready" || current?.status === "error") return current;
759
+ return {
760
+ id,
761
+ label,
762
+ status: "loading"
763
+ };
764
+ }));
765
+ const openCodeAuth = await loadOpenCodeAuth();
766
+ const nextStates = await Promise.all(providers.map(async ([id, provider]) => {
767
+ const label = provider.label ?? (id === "codex" ? "Codex" : "ZAI");
768
+ try {
769
+ const data = await fetchProvider(id, provider, openCodeAuth, config.requestTimeoutMs);
770
+ lastSuccess.set(id, data);
771
+ return {
772
+ data,
773
+ id,
774
+ label,
775
+ stale: false,
776
+ status: "ready"
777
+ };
778
+ } catch (error) {
779
+ const message = error instanceof Error ? error.message : "usage unavailable";
780
+ const previousData = lastSuccess.get(id);
781
+ if (previousData) return {
782
+ id,
783
+ label,
784
+ message,
785
+ previous: previousData,
786
+ status: "error"
787
+ };
788
+ return {
789
+ id,
790
+ label,
791
+ message,
792
+ status: "error"
793
+ };
794
+ }
795
+ }));
796
+ const staleAfterMs = effectiveRefreshIntervalSeconds * 2 * 1e3;
797
+ setStates(nextStates.map((state) => {
798
+ if (state.status !== "ready") return state;
799
+ return {
800
+ ...state,
801
+ stale: Date.now() - state.data.capturedAt.getTime() > staleAfterMs
802
+ };
803
+ }));
804
+ };
805
+ await refresh();
806
+ let disposed = false;
807
+ let timer;
808
+ const scheduleRefresh = () => {
809
+ timer = setTimeout(async () => {
810
+ await refresh();
811
+ if (!disposed) scheduleRefresh();
812
+ }, Math.max(15, refreshIntervalSeconds) * 1e3);
813
+ };
814
+ scheduleRefresh();
815
+ api.lifecycle.onDispose(() => {
816
+ disposed = true;
817
+ if (timer) clearTimeout(timer);
818
+ lastSuccess = /* @__PURE__ */ new Map();
819
+ });
820
+ api.slots.register({
821
+ order: 101,
822
+ slots: {
823
+ session_prompt_right(ctx, props) {
824
+ const providerID = currentProviderID(api.state.session.messages(props.session_id));
825
+ return /* @__PURE__ */ jsx(BottomUsage, {
826
+ theme: ctx.theme.current,
827
+ window: usageForProvider(states(), providerID)
828
+ });
829
+ },
830
+ sidebar_content(ctx) {
831
+ return /* @__PURE__ */ jsx(UsageLimitsPanel, {
832
+ showErrors: showErrors(),
833
+ states: states(),
834
+ theme: ctx.theme.current
835
+ });
836
+ }
837
+ }
838
+ });
839
+ };
840
+ //#endregion
841
+ //#region src/index.ts
842
+ /** OpenCode plugin module exported for the `oc-usage-limits-plugin/tui` entry. */
843
+ var src_default = {
844
+ id: "mynameistito.usage-limits",
845
+ tui
846
+ };
847
+ //#endregion
848
+ export { src_default as default };
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/mynameistito/oc-usage-limits-plugin/main/usage-limits.schema.json",
3
+ "enabled": true,
4
+ "refreshIntervalSeconds": 60,
5
+ "requestTimeoutMs": 10000,
6
+ "showErrors": true,
7
+ "providers": {
8
+ "codex": {
9
+ "enabled": true,
10
+ "label": "Codex",
11
+ "authPath": "~/.codex/auth.json",
12
+ },
13
+ "zai": {
14
+ "enabled": true,
15
+ "label": "ZAI",
16
+ "authPath": "~/.local/share/opencode/auth.json",
17
+ "apiKey": "{env:OC_ZAI_API_KEY}",
18
+ "authorizationScheme": "raw",
19
+ },
20
+ },
21
+ }
package/package.json CHANGED
@@ -1,24 +1,74 @@
1
1
  {
2
2
  "name": "oc-usage-limits-plugin",
3
- "version": "0.0.1",
4
- "description": "OpenCode TUI plugin that shows usage limits in the sidebar and prompt footer.",
5
- "keywords": ["opencode", "opencode-plugin", "tui", "usage-limits"],
6
- "homepage": "https://github.com/mynameistito/oc-usage-limits#readme",
3
+ "version": "1.0.1",
4
+ "description": "OpenCode TUI plugin that shows Codex and ZAI usage limits in the sidebar and prompt footer.",
5
+ "keywords": [
6
+ "codex",
7
+ "opencode",
8
+ "opencode-plugin",
9
+ "tui",
10
+ "usage-limits",
11
+ "zai"
12
+ ],
13
+ "homepage": "https://github.com/mynameistito/oc-usage-limits-plugin#readme",
7
14
  "bugs": {
8
- "url": "https://github.com/mynameistito/oc-usage-limits/issues"
15
+ "url": "https://github.com/mynameistito/oc-usage-limits-plugin/issues"
9
16
  },
10
17
  "license": "MIT",
11
18
  "repository": {
12
19
  "type": "git",
13
- "url": "git+https://github.com/mynameistito/oc-usage-limits.git"
20
+ "url": "git+https://github.com/mynameistito/oc-usage-limits-plugin.git"
14
21
  },
22
+ "files": [
23
+ "dist",
24
+ "examples",
25
+ "usage-limits.schema.json",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
15
29
  "type": "module",
16
30
  "exports": {
17
- ".": "./index.ts",
18
- "./tui": "./index.ts"
31
+ "./tui": {
32
+ "types": "./dist/index.d.mts",
33
+ "import": "./dist/index.mjs"
34
+ },
35
+ "./schema": "./usage-limits.schema.json"
19
36
  },
20
- "files": ["index.ts"],
21
37
  "publishConfig": {
22
38
  "access": "public"
39
+ },
40
+ "scripts": {
41
+ "changeset": "changeset",
42
+ "changeset-add": "bun ./scripts/changeset-add.ts",
43
+ "build": "tsdown",
44
+ "check": "ultracite check",
45
+ "knip": "bunx knip@latest",
46
+ "prepack": "bun run build",
47
+ "release": "changeset publish",
48
+ "test": "bun test",
49
+ "typecheck": "tsgo --noEmit",
50
+ "version": "changeset version",
51
+ "fix": "ultracite fix",
52
+ "prepare": "lefthook install"
53
+ },
54
+ "devDependencies": {
55
+ "@changesets/cli": "^2.31.0",
56
+ "@opencode-ai/plugin": "1.17.9",
57
+ "@opentui/core": "0.4.1",
58
+ "@opentui/solid": "0.4.1",
59
+ "@types/bun": "^1.3.14",
60
+ "@types/node": "^26.0.0",
61
+ "@typescript/native-preview": "^7.0.0-dev.20260622.1",
62
+ "lefthook": "^2.1.9",
63
+ "oxfmt": "^0.56.0",
64
+ "oxlint": "^1.71.0",
65
+ "solid-js": "1.9.12",
66
+ "tsdown": "^0.22.3",
67
+ "ultracite": "7.8.3"
68
+ },
69
+ "peerDependencies": {
70
+ "@opencode-ai/plugin": ">=1.17.9",
71
+ "@opentui/solid": ">=0.4.1",
72
+ "solid-js": ">=1.9.12"
23
73
  }
24
74
  }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/mynameistito/oc-usage-limits-plugin/main/usage-limits.schema.json",
4
+ "title": "OpenCode Usage Limits Plugin Config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "enabled": { "type": "boolean" },
10
+ "refreshIntervalSeconds": { "type": "number", "minimum": 15 },
11
+ "requestTimeoutMs": { "type": "number", "minimum": 1000 },
12
+ "showErrors": { "type": "boolean" },
13
+ "providers": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "codex": { "$ref": "#/$defs/provider" },
18
+ "zai": { "$ref": "#/$defs/provider" }
19
+ }
20
+ }
21
+ },
22
+ "$defs": {
23
+ "provider": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "enabled": { "type": "boolean" },
28
+ "label": { "type": "string" },
29
+ "authPath": { "type": "string" },
30
+ "apiKey": { "type": "string" },
31
+ "authorizationScheme": { "enum": ["raw", "bearer"] },
32
+ "baseUrl": { "type": "string" }
33
+ }
34
+ }
35
+ }
36
+ }
package/index.ts DELETED
@@ -1,5 +0,0 @@
1
- const plugin = {
2
- id: "mynameistito.usage-limits",
3
- };
4
-
5
- export default plugin;