Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<ProviderQuotaProbeResult> {
// 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);
Expand Down Expand Up @@ -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);
}
Expand Down
90 changes: 90 additions & 0 deletions tests/opencode-go-quota.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> | 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([]);
});
});
Loading