From 22de3b3aa0ec9bcbcdd8c0f9204f7ca0bd25a0a3 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:03:31 +0200 Subject: [PATCH] feat: add OpenCode Go quota usage --- src/providers/quota.ts | 57 +++++++++++++++++++++ tests/opencode-go-quota.test.ts | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 tests/opencode-go-quota.test.ts diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 8278df273..9891a2896 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -39,6 +39,8 @@ export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024; const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"; const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`; const A6API_BASE_URL = "https://api.a6api.com"; +const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1"; +const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`; const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; const CLINE_BASE_URL = "https://api.cline.bot"; @@ -309,6 +311,10 @@ function isCanonicalA6apiBaseUrl(baseUrl: string): boolean { return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`; } +function isCanonicalOpenCodeGoBaseUrl(baseUrl: string): boolean { + return normalizedBaseUrl(baseUrl) === OPENCODE_GO_BASE_URL; +} + function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean { const normalized = normalizedBaseUrl(baseUrl); return normalized === OPENROUTER_BASE_URL; @@ -456,6 +462,54 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro }); } +function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?: number } | null { + const row = asRecord(value); + if (!row) return null; + const percent = normalizePercent(row.percent); + if (percent === undefined) return null; + const resetAt = normalizeResetAt(row.resetsAt); + return { percent, ...(resetAt !== undefined ? { resetAt } : {}) }; +} + +async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise { + // Never send a configured API key when the provider destination is not the built-in Go endpoint. + if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(OPENCODE_GO_USAGE_URL, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await readQuotaJson(response)); + const usage = asRecord(body?.usage); + if (!usage) return null; + const rolling = parseOpenCodeGoUsageWindow(usage.rolling); + const weekly = parseOpenCodeGoUsageWindow(usage.weekly); + const monthly = parseOpenCodeGoUsageWindow(usage.monthly); + const quota: ProviderQuota = { + ...(rolling ? { + fiveHourPercent: rolling.percent, + ...(rolling.resetAt !== undefined ? { fiveHourResetAt: rolling.resetAt } : {}), + } : {}), + ...(weekly ? { + weeklyPercent: weekly.percent, + ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}), + } : {}), + ...(monthly ? { + monthlyPercent: monthly.percent, + ...(monthly.resetAt !== undefined ? { monthlyResetAt: monthly.resetAt } : {}), + } : {}), + updatedAt: Date.now(), + }; + return report(provider, "opencode-go:usage", quota); +} + /** * OpenRouter `GET /api/v1/key` — the key's own credit balance and optional * per-key spending cap. `limit` is the configured cap (absent = uncapped); @@ -1858,6 +1912,9 @@ async function maybeFetchProviderQuota( if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) { return fetchKimiQuota(name, provider); } + if ((provider.authMode ?? "key") === "key" && name === "opencode-go") { + return fetchOpenCodeGoQuota(name, provider); + } if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) { return fetchA6apiQuota(name, provider); } diff --git a/tests/opencode-go-quota.test.ts b/tests/opencode-go-quota.test.ts new file mode 100644 index 000000000..ece8d22b5 --- /dev/null +++ b/tests/opencode-go-quota.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../src/providers/quota"; +import type { OcxConfig } from "../src/types"; + +const originalFetch = globalThis.fetch; + +function openCodeGoConfig(baseUrl = "https://opencode.ai/zen/go/v1"): OcxConfig { + return { + defaultProvider: "opencode-go", + providers: { + "opencode-go": { + adapter: "openai-chat", + authMode: "key", + baseUrl, + apiKey: "opencode-go-secret", + }, + }, + } as OcxConfig; +} + +beforeEach(() => { + clearProviderQuotaCache(); +}); + +afterEach(() => { + globalThis.fetch = originalFetch; + clearProviderQuotaCache(); +}); + +describe("OpenCode Go provider quota", () => { + test("maps the official usage endpoint into canonical quota windows", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const headers = init?.headers as Record | undefined; + seen.push({ + url: String(input), + authorization: headers?.Authorization, + redirect: init?.redirect, + }); + return new Response(JSON.stringify({ + usage: { + rolling: { status: "ok", percent: 12, resetsAt: "2026-08-12T20:00:00.000Z" }, + weekly: { status: "ok", percent: 8, resetsAt: "2026-08-17T00:00:00.000Z" }, + monthly: { status: "ok", percent: 35, resetsAt: "2026-09-01T00:00:00.000Z" }, + }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(openCodeGoConfig(), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.provider).toBe("opencode-go"); + expect(result.reports[0]?.source).toBe("opencode-go:usage"); + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 12, + fiveHourResetAt: Date.parse("2026-08-12T20:00:00.000Z"), + weeklyPercent: 8, + weeklyResetAt: Date.parse("2026-08-17T00:00:00.000Z"), + monthlyPercent: 35, + monthlyResetAt: Date.parse("2026-09-01T00:00:00.000Z"), + updatedAt: expect.any(Number), + }); + expect(seen).toEqual([{ + url: "https://opencode.ai/zen/go/v1/usage", + authorization: "Bearer opencode-go-secret", + redirect: "error", + }]); + expect(JSON.stringify(result)).not.toContain("opencode-go-secret"); + }); + + test("does not probe quota for a noncanonical OpenCode Go destination", async () => { + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + return new Response(JSON.stringify({ + usage: { + rolling: { percent: 1, resetsAt: "2026-08-12T20:00:00.000Z" }, + }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + openCodeGoConfig("https://example.invalid/zen/go/v1"), + true, + ); + + expect(fetchCalls).toBe(0); + expect(result.reports).toEqual([]); + }); +});