@howaboua/opencode-usage-plugin 0.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/dist/hooks/command.d.ts +13 -0
  4. package/dist/hooks/command.d.ts.map +1 -0
  5. package/dist/hooks/command.js +53 -0
  6. package/dist/hooks/index.d.ts +4 -0
  7. package/dist/hooks/index.d.ts.map +1 -0
  8. package/dist/hooks/index.js +3 -0
  9. package/dist/hooks/proxy.d.ts +14 -0
  10. package/dist/hooks/proxy.d.ts.map +1 -0
  11. package/dist/hooks/proxy.js +31 -0
  12. package/dist/hooks/session.d.ts +8 -0
  13. package/dist/hooks/session.d.ts.map +1 -0
  14. package/dist/hooks/session.js +16 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +53 -0
  18. package/dist/providers/base.d.ts +13 -0
  19. package/dist/providers/base.d.ts.map +1 -0
  20. package/dist/providers/base.js +5 -0
  21. package/dist/providers/codex/headers.d.ts +15 -0
  22. package/dist/providers/codex/headers.d.ts.map +1 -0
  23. package/dist/providers/codex/headers.js +25 -0
  24. package/dist/providers/codex/index.d.ts +12 -0
  25. package/dist/providers/codex/index.d.ts.map +1 -0
  26. package/dist/providers/codex/index.js +66 -0
  27. package/dist/providers/codex/response.d.ts +75 -0
  28. package/dist/providers/codex/response.d.ts.map +1 -0
  29. package/dist/providers/codex/response.js +59 -0
  30. package/dist/providers/index.d.ts +6 -0
  31. package/dist/providers/index.d.ts.map +1 -0
  32. package/dist/providers/index.js +8 -0
  33. package/dist/providers/proxy/config.d.ts +6 -0
  34. package/dist/providers/proxy/config.d.ts.map +1 -0
  35. package/dist/providers/proxy/config.js +52 -0
  36. package/dist/providers/proxy/fetch.d.ts +6 -0
  37. package/dist/providers/proxy/fetch.d.ts.map +1 -0
  38. package/dist/providers/proxy/fetch.js +30 -0
  39. package/dist/providers/proxy/format.d.ts +6 -0
  40. package/dist/providers/proxy/format.d.ts.map +1 -0
  41. package/dist/providers/proxy/format.js +102 -0
  42. package/dist/providers/proxy/index.d.ts +11 -0
  43. package/dist/providers/proxy/index.d.ts.map +1 -0
  44. package/dist/providers/proxy/index.js +116 -0
  45. package/dist/providers/proxy/types.d.ts +119 -0
  46. package/dist/providers/proxy/types.d.ts.map +1 -0
  47. package/dist/providers/proxy/types.js +4 -0
  48. package/dist/state.d.ts +18 -0
  49. package/dist/state.d.ts.map +1 -0
  50. package/dist/state.js +13 -0
  51. package/dist/tools/index.d.ts +3 -0
  52. package/dist/tools/index.d.ts.map +1 -0
  53. package/dist/tools/index.js +2 -0
  54. package/dist/tools/proxy-limits.d.ts +16 -0
  55. package/dist/tools/proxy-limits.d.ts.map +1 -0
  56. package/dist/tools/proxy-limits.js +33 -0
  57. package/dist/tools/usage.d.ts +10 -0
  58. package/dist/tools/usage.d.ts.map +1 -0
  59. package/dist/tools/usage.js +12 -0
  60. package/dist/types.d.ts +65 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +18 -0
  63. package/dist/ui/index.d.ts +2 -0
  64. package/dist/ui/index.d.ts.map +1 -0
  65. package/dist/ui/index.js +1 -0
  66. package/dist/ui/status.d.ts +22 -0
  67. package/dist/ui/status.d.ts.map +1 -0
  68. package/dist/ui/status.js +142 -0
  69. package/dist/usage/fetch.d.ts +11 -0
  70. package/dist/usage/fetch.d.ts.map +1 -0
  71. package/dist/usage/fetch.js +80 -0
  72. package/dist/usage/index.d.ts +2 -0
  73. package/dist/usage/index.d.ts.map +1 -0
  74. package/dist/usage/index.js +1 -0
  75. package/dist/usage/registry.d.ts +19 -0
  76. package/dist/usage/registry.d.ts.map +1 -0
  77. package/dist/usage/registry.js +30 -0
  78. package/dist/utils/headers.d.ts +8 -0
  79. package/dist/utils/headers.d.ts.map +1 -0
  80. package/dist/utils/headers.js +33 -0
  81. package/dist/utils/index.d.ts +3 -0
  82. package/dist/utils/index.d.ts.map +1 -0
  83. package/dist/utils/index.js +2 -0
  84. package/dist/utils/paths.d.ts +7 -0
  85. package/dist/utils/paths.d.ts.map +1 -0
  86. package/dist/utils/paths.js +24 -0
  87. package/package.json +28 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 @howaboua
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,88 @@
1
+ # OpenCode Usage Plugin
2
+
3
+ Track AI provider rate limits and quotas in real-time.
4
+
5
+ ## Features
6
+
7
+ - **Live rate limits** – See Codex/OpenAI hourly/weekly limits at a glance
8
+ - **Proxy quota stats** – Monitor Mirrowel Proxy credentials and tier usage
9
+ - **Inline status** – Results appear directly in your chat, no context switching
10
+ - **Zero setup** – Auto-detects providers from your existing config
11
+
12
+ ## Installation
13
+
14
+ Add to your `opencode.json`:
15
+
16
+ ```json
17
+ {
18
+ "$schema": "https://opencode.ai/config.json",
19
+ "plugins": ["@howaboua/opencode-usage-plugin"]
20
+ }
21
+ ```
22
+
23
+ OpenCode installs dependencies automatically on next launch.
24
+
25
+ ## Usage
26
+
27
+ ### Check all providers
28
+
29
+ ```
30
+ /usage
31
+ ```
32
+
33
+ ### Check specific provider
34
+
35
+ ```
36
+ /usage codex
37
+ /usage proxy
38
+ ```
39
+
40
+ ### Support the proxy
41
+
42
+ ```
43
+ /usage support
44
+ ```
45
+
46
+ ## Supported Providers
47
+
48
+ | Provider | Source |
49
+ |----------|--------|
50
+ | **Codex / OpenAI** | Auth tokens + `/wham/usage` endpoint |
51
+ | **Mirrowel Proxy** | Local `/v1/quota-stats` endpoint |
52
+
53
+ ## Configuration
54
+
55
+ Optional config at `~/.config/opencode/usage-config.jsonc`:
56
+
57
+ ```jsonc
58
+ {
59
+ // Proxy server endpoint
60
+ "endpoint": "http://localhost:8000",
61
+
62
+ // API key for proxy auth
63
+ "apiKey": "your-key",
64
+
65
+ // Request timeout (ms)
66
+ "timeout": 10000,
67
+
68
+ // Show/hide providers in /usage output
69
+ "providers": {
70
+ "openai": true,
71
+ "proxy": true
72
+ }
73
+ }
74
+ ```
75
+
76
+ If missing, the plugin creates a default template on first run.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ # Check DB contents
82
+ bun run debug-db.ts
83
+
84
+ # Verify path resolution
85
+ bun run debug-path.ts
86
+ ```
87
+
88
+ See `AGENTS.md` for internal architecture.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Implements /usage command handling and config registration.
3
+ * Fetches live usage snapshots and renders a status message.
4
+ */
5
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
6
+ import type { UsageState } from "../state";
7
+ type UsageClient = PluginInput["client"];
8
+ export declare function commandHooks(options: {
9
+ client: UsageClient;
10
+ state: UsageState;
11
+ }): Pick<Hooks, "command.execute.before" | "config">;
12
+ export {};
13
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/hooks/command.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAI1C,KAAK,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAWxC,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,UAAU,CAAA;CAClB,GAAG,IAAI,CAAC,KAAK,EAAE,wBAAwB,GAAG,QAAQ,CAAC,CAmDnD"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Implements /usage command handling and config registration.
3
+ * Fetches live usage snapshots and renders a status message.
4
+ */
5
+ import { fetchUsageSnapshots, resolveProviderFilter } from "../usage";
6
+ import { renderUsageStatus, sendStatusMessage } from "../ui";
7
+ export function commandHooks(options) {
8
+ return {
9
+ config: async (input) => {
10
+ const config = input;
11
+ config.command ??= {};
12
+ config.command["usage"] = {
13
+ template: "/usage",
14
+ description: "Show API usage and rate limits (codex/proxy or all)",
15
+ };
16
+ },
17
+ "command.execute.before": async (input) => {
18
+ if (input.command !== "usage")
19
+ return;
20
+ const args = input.arguments?.trim() || "";
21
+ if (args === "support") {
22
+ await sendStatusMessage({
23
+ client: options.client,
24
+ state: options.state,
25
+ sessionID: input.sessionID,
26
+ text: "▣ Support Mirrowel Proxy\n\nSupport our lord and savior: https://ko-fi.com/mirrowel",
27
+ });
28
+ throw new Error("__USAGE_SUPPORT_HANDLED__");
29
+ }
30
+ const filter = args || undefined;
31
+ const targetProvider = resolveProviderFilter(filter);
32
+ let effectiveFilter = targetProvider ? filter : undefined;
33
+ const snapshots = await fetchUsageSnapshots(effectiveFilter);
34
+ const filteredSnapshots = snapshots.filter(s => {
35
+ if (targetProvider)
36
+ return true; // User explicitly asked for it
37
+ if (s.provider === "codex")
38
+ return options.state.availableProviders.codex;
39
+ if (s.provider === "proxy")
40
+ return options.state.availableProviders.proxy;
41
+ return true;
42
+ });
43
+ await renderUsageStatus({
44
+ client: options.client,
45
+ state: options.state,
46
+ sessionID: input.sessionID,
47
+ snapshots: filteredSnapshots,
48
+ filter: effectiveFilter,
49
+ });
50
+ throw new Error("__USAGE_COMMAND_HANDLED__");
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,4 @@
1
+ export { commandHooks } from "./command";
2
+ export { sessionHooks } from "./session";
3
+ export { proxyHooks, markSilent } from "./proxy";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { commandHooks } from "./command";
2
+ export { sessionHooks } from "./session";
3
+ export { proxyHooks, markSilent } from "./proxy";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Hooks for proxy-limits functionality.
3
+ * Manages silent responses after tool execution.
4
+ */
5
+ export declare function markSilent(sessionID: string, messageID: string): void;
6
+ export declare function proxyHooks(): {
7
+ "experimental.text.complete": (input: {
8
+ sessionID: string;
9
+ messageID: string;
10
+ }, output: {
11
+ text: string;
12
+ }) => Promise<void>;
13
+ };
14
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/hooks/proxy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAI9D;AAWD,wBAAgB,UAAU;0CAGb;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,UACvC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;EAM7B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hooks for proxy-limits functionality.
3
+ * Manages silent responses after tool execution.
4
+ */
5
+ const silence = new Map();
6
+ export function markSilent(sessionID, messageID) {
7
+ const current = silence.get(sessionID) ?? new Set();
8
+ current.add(messageID);
9
+ if (!silence.has(sessionID))
10
+ silence.set(sessionID, current);
11
+ }
12
+ function shouldSilence(sessionID, messageID) {
13
+ const current = silence.get(sessionID);
14
+ if (!current)
15
+ return false;
16
+ if (!current.has(messageID))
17
+ return false;
18
+ current.delete(messageID);
19
+ if (current.size === 0)
20
+ silence.delete(sessionID);
21
+ return true;
22
+ }
23
+ export function proxyHooks() {
24
+ return {
25
+ "experimental.text.complete": async (input, output) => {
26
+ if (!shouldSilence(input.sessionID, input.messageID))
27
+ return;
28
+ output.text = "";
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Captures active session context for usage notifications.
3
+ * Updates the shared state on every chat parameter update.
4
+ */
5
+ import type { Hooks } from "@opencode-ai/plugin";
6
+ import type { UsageState } from "../state";
7
+ export declare function sessionHooks(state: UsageState): Pick<Hooks, "chat.params">;
8
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/hooks/session.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAW1E"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Captures active session context for usage notifications.
3
+ * Updates the shared state on every chat parameter update.
4
+ */
5
+ export function sessionHooks(state) {
6
+ return {
7
+ "chat.params": async (input) => {
8
+ state.sessionID = input.sessionID;
9
+ state.agent = input.agent;
10
+ state.model = {
11
+ providerID: input.model.providerID,
12
+ modelID: input.model.id,
13
+ };
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Plugin entry point for Usage Tracking.
3
+ * Wires hooks and tools for live usage snapshots.
4
+ */
5
+ import type { Plugin } from "@opencode-ai/plugin";
6
+ export declare const UsagePlugin: Plugin;
7
+ export default UsagePlugin;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAOjD,eAAO,MAAM,WAAW,EAAE,MA+CzB,CAAA;AAED,eAAe,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Plugin entry point for Usage Tracking.
3
+ * Wires hooks and tools for live usage snapshots.
4
+ */
5
+ import { commandHooks, sessionHooks, proxyHooks, markSilent } from "./hooks";
6
+ import { createUsageState } from "./state";
7
+ import { usageTool, createProxyLimitsTool } from "./tools";
8
+ import { loadAuths } from "./usage/fetch";
9
+ import { loadProxyConfig } from "./providers/proxy/config";
10
+ export const UsagePlugin = async ({ client }) => {
11
+ const state = createUsageState();
12
+ try {
13
+ const [auths, proxyConfig] = await Promise.all([
14
+ loadAuths().catch(() => ({})),
15
+ loadProxyConfig().catch(() => null),
16
+ ]);
17
+ state.availableProviders.codex =
18
+ proxyConfig?.providers?.openai !== undefined
19
+ ? proxyConfig.providers.openai
20
+ : Boolean("codex" in auths && auths["codex"] || "openai" in auths && auths["openai"]);
21
+ state.availableProviders.proxy =
22
+ proxyConfig?.providers?.proxy !== undefined ? proxyConfig.providers.proxy : Boolean(proxyConfig?.endpoint);
23
+ }
24
+ catch { }
25
+ async function sendStatusMessage(sessionID, text) {
26
+ await client.session.prompt({
27
+ path: { id: sessionID },
28
+ body: {
29
+ noReply: true,
30
+ parts: [
31
+ {
32
+ type: "text",
33
+ text,
34
+ ignored: true,
35
+ },
36
+ ],
37
+ },
38
+ });
39
+ }
40
+ const proxyHookHandlers = proxyHooks();
41
+ const commandHookHandlers = commandHooks({ client, state });
42
+ return {
43
+ config: commandHookHandlers.config,
44
+ "command.execute.before": commandHookHandlers["command.execute.before"],
45
+ ...sessionHooks(state),
46
+ ...proxyHookHandlers,
47
+ tool: {
48
+ "usage.get": usageTool(),
49
+ "proxy-limits": createProxyLimitsTool(sendStatusMessage, markSilent),
50
+ },
51
+ };
52
+ };
53
+ export default UsagePlugin;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Provider interface for fetching and parsing usage snapshots.
3
+ * Each provider encapsulates its own auth and response logic.
4
+ */
5
+ import type { UsageSnapshot } from "../types";
6
+ export interface UsageProvider<TAuth = unknown> {
7
+ id: string;
8
+ displayName: string;
9
+ usageEndpoint?: string;
10
+ parseRateLimitHeaders?: (headers: Record<string, string>) => UsageSnapshot | null;
11
+ fetchUsage?: (auth: TAuth) => Promise<UsageSnapshot | null>;
12
+ }
13
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/providers/base.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,MAAM,WAAW,aAAa,CAAC,KAAK,GAAG,OAAO;IAC5C,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,aAAa,GAAG,IAAI,CAAA;IACjF,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;CAC5D"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Provider interface for fetching and parsing usage snapshots.
3
+ * Each provider encapsulates its own auth and response logic.
4
+ */
5
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Codex header parsing helpers for rate limits and credits.
3
+ * Uses shared header utilities for safe parsing.
4
+ */
5
+ export declare function parseWindow(headers: Headers, prefix: "primary" | "secondary"): {
6
+ usedPercent: number;
7
+ windowMinutes: number | null;
8
+ resetsAt: number | null;
9
+ } | null;
10
+ export declare function parseCredits(headers: Headers): {
11
+ hasCredits: boolean;
12
+ unlimited: boolean;
13
+ balance: string | null;
14
+ } | null;
15
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../../src/providers/codex/headers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,WAAW;;;;SAQ5E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO;;;;SAQ5C"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Codex header parsing helpers for rate limits and credits.
3
+ * Uses shared header utilities for safe parsing.
4
+ */
5
+ import { parseBooleanHeader, parseIntegerHeader, parseNumberHeader } from "../../utils";
6
+ export function parseWindow(headers, prefix) {
7
+ const usedPercent = parseNumberHeader(headers, `x-codex-${prefix}-used-percent`);
8
+ if (usedPercent === null)
9
+ return null;
10
+ return {
11
+ usedPercent,
12
+ windowMinutes: parseIntegerHeader(headers, `x-codex-${prefix}-window-minutes`),
13
+ resetsAt: parseIntegerHeader(headers, `x-codex-${prefix}-reset-at`),
14
+ };
15
+ }
16
+ export function parseCredits(headers) {
17
+ const hasCredits = parseBooleanHeader(headers, "x-codex-credits-has-credits");
18
+ if (hasCredits === null)
19
+ return null;
20
+ return {
21
+ hasCredits,
22
+ unlimited: parseBooleanHeader(headers, "x-codex-credits-unlimited") ?? false,
23
+ balance: headers.get("x-codex-credits-balance"),
24
+ };
25
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * OpenAI/Codex usage provider for plan and rate-limit snapshots.
3
+ * Uses the /wham/usage endpoint plus rate-limit headers for fallback.
4
+ */
5
+ import type { UsageProvider } from "../base";
6
+ type CodexAuth = {
7
+ access?: string;
8
+ accountId?: string;
9
+ };
10
+ export declare const CodexProvider: UsageProvider<CodexAuth>;
11
+ export type { CodexAuth };
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/codex/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAI5C,KAAK,SAAS,GAAG;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,SAAS,CA0DlD,CAAA;AAED,YAAY,EAAE,SAAS,EAAE,CAAA"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * OpenAI/Codex usage provider for plan and rate-limit snapshots.
3
+ * Uses the /wham/usage endpoint plus rate-limit headers for fallback.
4
+ */
5
+ import { parseCredits, parseWindow } from "./headers";
6
+ import { toCreditsSnapshot, toPlanType, toRateLimitWindow, usageResponseSchema } from "./response";
7
+ export const CodexProvider = {
8
+ id: "codex",
9
+ displayName: "OpenAI",
10
+ usageEndpoint: "https://chatgpt.com/backend-api/wham/usage",
11
+ parseRateLimitHeaders: (headers) => {
12
+ const parsedHeaders = new Headers(headers);
13
+ const primary = parseWindow(parsedHeaders, "primary");
14
+ const secondary = parseWindow(parsedHeaders, "secondary");
15
+ const credits = parseCredits(parsedHeaders);
16
+ if (!primary && !secondary && !credits)
17
+ return null;
18
+ return {
19
+ timestamp: Date.now(),
20
+ updatedAt: Date.now(),
21
+ provider: "codex",
22
+ planType: null,
23
+ primary,
24
+ secondary,
25
+ codeReview: null,
26
+ credits,
27
+ };
28
+ },
29
+ fetchUsage: async (auth) => {
30
+ const accessToken = auth.access;
31
+ if (!accessToken)
32
+ return null;
33
+ const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
34
+ headers: {
35
+ Authorization: `Bearer ${accessToken}`,
36
+ ...(auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {}),
37
+ },
38
+ }).catch(() => null);
39
+ if (!response?.ok)
40
+ return null;
41
+ const data = await response.json().catch(() => null);
42
+ if (!data)
43
+ return null;
44
+ const parsed = usageResponseSchema.safeParse(data);
45
+ if (!parsed.success)
46
+ return null;
47
+ const rateLimit = parsed.data.rate_limit;
48
+ const primary = toRateLimitWindow(rateLimit.primary_window);
49
+ const secondary = toRateLimitWindow(rateLimit.secondary_window);
50
+ const credits = toCreditsSnapshot(parsed.data.credits);
51
+ const codeReview = parsed.data.code_review_rate_limit?.primary_window
52
+ ? toRateLimitWindow(parsed.data.code_review_rate_limit.primary_window)
53
+ : null;
54
+ const planType = toPlanType(parsed.data.plan_type);
55
+ return {
56
+ timestamp: Date.now(),
57
+ updatedAt: Date.now(),
58
+ provider: "codex",
59
+ planType,
60
+ primary,
61
+ secondary,
62
+ codeReview,
63
+ credits,
64
+ };
65
+ },
66
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Codex usage response schemas and transformations.
3
+ * Keeps API-specific shapes out of provider logic.
4
+ */
5
+ import z from "zod";
6
+ import { type PlanType } from "../../types";
7
+ type UsageResponseWindow = {
8
+ used_percent: number;
9
+ limit_window_seconds: number;
10
+ reset_after_seconds: number;
11
+ reset_at: number;
12
+ };
13
+ type UsageResponse = {
14
+ plan_type: string | null;
15
+ rate_limit: {
16
+ allowed: boolean;
17
+ limit_reached: boolean;
18
+ primary_window: UsageResponseWindow | null;
19
+ secondary_window: UsageResponseWindow | null;
20
+ };
21
+ code_review_rate_limit?: {
22
+ primary_window: UsageResponseWindow | null;
23
+ } | null;
24
+ credits: {
25
+ has_credits: boolean;
26
+ unlimited: boolean;
27
+ balance: string | null;
28
+ } | null;
29
+ };
30
+ export declare const usageResponseSchema: z.ZodObject<{
31
+ plan_type: z.ZodNullable<z.ZodString>;
32
+ rate_limit: z.ZodObject<{
33
+ allowed: z.ZodBoolean;
34
+ limit_reached: z.ZodBoolean;
35
+ primary_window: z.ZodNullable<z.ZodObject<{
36
+ used_percent: z.ZodNumber;
37
+ limit_window_seconds: z.ZodNumber;
38
+ reset_after_seconds: z.ZodNumber;
39
+ reset_at: z.ZodNumber;
40
+ }, z.core.$strip>>;
41
+ secondary_window: z.ZodNullable<z.ZodObject<{
42
+ used_percent: z.ZodNumber;
43
+ limit_window_seconds: z.ZodNumber;
44
+ reset_after_seconds: z.ZodNumber;
45
+ reset_at: z.ZodNumber;
46
+ }, z.core.$strip>>;
47
+ }, z.core.$strip>;
48
+ code_review_rate_limit: z.ZodOptional<z.ZodNullable<z.ZodObject<{
49
+ primary_window: z.ZodNullable<z.ZodObject<{
50
+ used_percent: z.ZodNumber;
51
+ limit_window_seconds: z.ZodNumber;
52
+ reset_after_seconds: z.ZodNumber;
53
+ reset_at: z.ZodNumber;
54
+ }, z.core.$strip>>;
55
+ }, z.core.$strip>>>;
56
+ credits: z.ZodNullable<z.ZodObject<{
57
+ has_credits: z.ZodBoolean;
58
+ unlimited: z.ZodBoolean;
59
+ balance: z.ZodNullable<z.ZodString>;
60
+ }, z.core.$strip>>;
61
+ }, z.core.$strip>;
62
+ export type ParsedUsageResponse = z.infer<typeof usageResponseSchema>;
63
+ export declare function toRateLimitWindow(window: UsageResponseWindow | null): {
64
+ usedPercent: number;
65
+ windowMinutes: number;
66
+ resetsAt: number;
67
+ } | null;
68
+ export declare function toCreditsSnapshot(credits: UsageResponse["credits"]): {
69
+ hasCredits: boolean;
70
+ unlimited: boolean;
71
+ balance: string | null;
72
+ } | null;
73
+ export declare function toPlanType(value: UsageResponse["plan_type"]): PlanType | null;
74
+ export {};
75
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../../src/providers/codex/response.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,CAAC,MAAM,KAAK,CAAA;AACnB,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtD,KAAK,mBAAmB,GAAG;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE;QACV,OAAO,EAAE,OAAO,CAAA;QAChB,aAAa,EAAE,OAAO,CAAA;QACtB,cAAc,EAAE,mBAAmB,GAAG,IAAI,CAAA;QAC1C,gBAAgB,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAC7C,CAAA;IACD,sBAAsB,CAAC,EAAE;QACvB,cAAc,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAC3C,GAAG,IAAI,CAAA;IACR,OAAO,EAAE;QACP,WAAW,EAAE,OAAO,CAAA;QACpB,SAAS,EAAE,OAAO,CAAA;QAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KACvB,GAAG,IAAI,CAAA;CACT,CAAA;AASD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqBK,CAAA;AAErC,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAErE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;;;;SAOnE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC;;;;SAOlE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,QAAQ,GAAG,IAAI,CAI7E"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Codex usage response schemas and transformations.
3
+ * Keeps API-specific shapes out of provider logic.
4
+ */
5
+ import z from "zod";
6
+ import { PlanTypes } from "../../types";
7
+ const usageResponseWindowSchema = z.object({
8
+ used_percent: z.number(),
9
+ limit_window_seconds: z.number(),
10
+ reset_after_seconds: z.number(),
11
+ reset_at: z.number(),
12
+ });
13
+ export const usageResponseSchema = z.object({
14
+ plan_type: z.string().nullable(),
15
+ rate_limit: z.object({
16
+ allowed: z.boolean(),
17
+ limit_reached: z.boolean(),
18
+ primary_window: usageResponseWindowSchema.nullable(),
19
+ secondary_window: usageResponseWindowSchema.nullable(),
20
+ }),
21
+ code_review_rate_limit: z
22
+ .object({
23
+ primary_window: usageResponseWindowSchema.nullable(),
24
+ })
25
+ .nullable()
26
+ .optional(),
27
+ credits: z
28
+ .object({
29
+ has_credits: z.boolean(),
30
+ unlimited: z.boolean(),
31
+ balance: z.string().nullable(),
32
+ })
33
+ .nullable(),
34
+ });
35
+ export function toRateLimitWindow(window) {
36
+ if (!window)
37
+ return null;
38
+ return {
39
+ usedPercent: window.used_percent,
40
+ windowMinutes: Math.round(window.limit_window_seconds / 60),
41
+ resetsAt: window.reset_at,
42
+ };
43
+ }
44
+ export function toCreditsSnapshot(credits) {
45
+ if (!credits)
46
+ return null;
47
+ return {
48
+ hasCredits: credits.has_credits,
49
+ unlimited: credits.unlimited,
50
+ balance: credits.balance,
51
+ };
52
+ }
53
+ export function toPlanType(value) {
54
+ if (!value)
55
+ return null;
56
+ if (!PlanTypes.includes(value))
57
+ return null;
58
+ return value;
59
+ }
@@ -0,0 +1,6 @@
1
+ import type { UsageProvider } from "./base";
2
+ export declare const providers: Record<string, UsageProvider<unknown>>;
3
+ export { CodexProvider } from "./codex";
4
+ export { ProxyProvider } from "./proxy";
5
+ export type { UsageProvider } from "./base";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAA;AAI3C,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAG5D,CAAA;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { CodexProvider } from "./codex";
2
+ import { ProxyProvider } from "./proxy";
3
+ export const providers = {
4
+ [CodexProvider.id]: CodexProvider,
5
+ [ProxyProvider.id]: ProxyProvider,
6
+ };
7
+ export { CodexProvider } from "./codex";
8
+ export { ProxyProvider } from "./proxy";