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
12 changes: 11 additions & 1 deletion open-sse/providers/registry/opencode-go.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ export default {
textIcon: "OC",
website: "https://opencode.ai/auth",
notice: {
text: "OpenCode Go subscription: $5/mo (then 0/mo). Access to Kimi, GLM, Qwen, MiMo, MiniMax models.",
text: "OpenCode Go subscription: $5 first month, then $10/mo. Access to Kimi, GLM, Qwen, MiMo, MiniMax models.",
apiKeyUrl: "https://opencode.ai/auth",
},
},
category: "apikey",
transport: {
baseUrl: "https://opencode.ai/zen/go/v1/chat/completions",
headers: {},
usage: {
url: "https://opencode.ai/zen/go/v1/usage",
},
},
models: [
{ id: "glm-5.2", name: "GLM 5.2" },
Expand All @@ -38,4 +41,11 @@ export default {
{ id: "qwen3.7-plus", name: "Qwen 3.7 Plus", targetFormat: "claude" },
{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus", targetFormat: "claude" },
],
features: {
// Go is a subscription with rolling/weekly/monthly windows, but it authenticates
// with a plain API key (category "apikey"), so usageApikey is required for the
// /api/usage route to accept the connection.
usage: true,
usageApikey: true,
},
};
2 changes: 2 additions & 0 deletions open-sse/services/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getCodeBuddyCnUsage, getCodeBuddyIntlUsage } from "./usage/codebuddy-cn
import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getKimiUsage } from "./usage/kimi.js";
import { getDeepseekUsage } from "./usage/deepseek.js";
import { getOpenCodeGoUsage } from "./usage/opencode-go.js";
import { resolveQoderCredentials } from "./qoderModels.js";
import {
getIflowUsage,
Expand Down Expand Up @@ -54,6 +55,7 @@ const USAGE_HANDLERS = {
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
"opencode-go": (c) => getOpenCodeGoUsage(c.apiKey, c.proxyOptions),
};

export async function getUsageForProvider(connection, proxyOptions = null) {
Expand Down
125 changes: 125 additions & 0 deletions open-sse/services/usage/opencode-go.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* OpenCode Go usage — GET https://opencode.ai/zen/go/v1/usage
* Auth: Bearer <apiKey> (same Go key used for chat/completions — no session cookie)
*
* Shipped 2026-08-11 (anomalyco/opencode#16513, reshaped by d4704347). Not in the
* public docs yet, so the response shape below is taken from the deployed endpoint:
*
* { "usage": {
* "rolling": { "status": "ok", "percent": 0, "resetsAt": "2026-08-12T20:23:13.083Z" },
* "weekly": { "status": "ok", "percent": 0, "resetsAt": "2026-08-17T00:00:00.083Z" },
* "monthly": { "status": "ok", "percent": 91, "resetsAt": "2026-08-25T01:33:44.083Z" } } }
*
* `status` is "ok" | "rate-limited"; `percent` is a floored integer 0-100. The server
* exposes no dollar amounts — it holds limits in micro-cents internally and only ever
* returns the percentage, so these quotas are percent-only.
*/

import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U, parseResetTime, toFiniteNumber } from "./shared.js";

const USAGE_URL = U("opencode-go").url;

// Provider's own vocabulary from the workspace console. The rolling window length
// is server-configured (limits.rollingWindow) and is NOT echoed in the response,
// so don't label it "(5h)" — only the absolute resetsAt is trustworthy.
const WINDOW_LABELS = {
rolling: "Rolling",
weekly: "Weekly",
monthly: "Monthly",
};

// Returns null when the window carries no usable percent. Defaulting a missing
// percent to 0 would publish the window as 100% remaining, i.e. malformed data
// would read as full headroom — the most dangerous direction to be wrong in for
// a quota display. Parsed via toFiniteNumber so a numeric string still counts.
function formatWindow(window) {
const percent = toFiniteNumber(window?.percent, NaN);
if (!Number.isFinite(percent)) return null;

const used = Math.max(0, Math.min(100, percent));
return {
used,
total: 100,
// Percent-only: never set absolute `remaining` — QuotaTable reads it as a
// 0-100 percentage (same trap as Qoder/grok-cli).
remainingPercentage: 100 - used,
resetAt: parseResetTime(window?.resetsAt ?? null),
unlimited: false,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* @param {string|null|undefined} apiKey
* @param {object|null} proxyOptions
*/
export async function getOpenCodeGoUsage(apiKey = null, proxyOptions = null) {
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
return { message: "OpenCode Go API key not available. Add a key to view usage." };
}

try {
const response = await proxyAwareFetch(
USAGE_URL,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey.trim()}`,
Accept: "application/json",
},
},
proxyOptions,
);

const data = await response.json().catch(() => null);
// Errors come back as { type: "error", error: { type, message } }
const errorType = data?.error?.type || null;
const errorMessage = data?.error?.message || null;

if (response.status === 401) {
return { message: errorMessage || "OpenCode Go authentication failed. Check the API key." };
}

// A valid key with no Go subscription 403s — distinct from an auth failure,
// and not something re-authorizing will fix.
if (response.status === 403 || errorType === "EntitlementError") {
return {
plan: "OpenCode Go",
message: errorMessage || "OpenCode Go subscription required.",
};
}

if (!response.ok) {
return {
plan: "OpenCode Go",
message: `OpenCode Go usage API error (${response.status})${errorMessage ? `: ${errorMessage}` : ""}`,
};
}

const usage = data?.usage;
if (!usage || typeof usage !== "object") {
return { message: "OpenCode Go usage response was not in the expected shape." };
}

const quotas = {};
let limitReached = false;
for (const [key, label] of Object.entries(WINDOW_LABELS)) {
const window = usage[key];
if (!window || typeof window !== "object") continue;
// Check status before the percent gate: "rate-limited" is authoritative on
// its own, so a window with a missing percent must still raise the flag.
if (window.status === "rate-limited") limitReached = true;
const quota = formatWindow(window);
if (!quota) continue;
quotas[label] = quota;
}

if (Object.keys(quotas).length === 0) {
return { plan: "OpenCode Go", message: "OpenCode Go connected. No usage windows reported.", quotas: {} };
}

return { plan: "OpenCode Go", limitReached, quotas };
} catch (error) {
return { message: `OpenCode Go error: ${error.message}` };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,22 @@ export function parseQuotaData(provider, data) {
}
break;

case "opencode-go":
// Go subscription windows (Rolling / Weekly / Monthly) as usage %.
// remainingPercentage only — no absolute remaining (UI treats remaining as %).
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]) => {
normalizedQuotas.push({
name,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
}
break;

default:
// Generic fallback for unknown providers
if (data.quotas) {
Expand Down
191 changes: 191 additions & 0 deletions tests/unit/opencode-go-usage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));

import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
import {
USAGE_SUPPORTED_PROVIDERS,
USAGE_APIKEY_PROVIDERS,
} from "../../src/shared/constants/providers.js";
import {
parseQuotaData,
getRemainingPercentage,
} from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";

const USAGE_URL = "https://opencode.ai/zen/go/v1/usage";

function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}

// Captured verbatim from the live endpoint on 2026-08-12, the day after
// anomalyco/opencode#16513 shipped. The endpoint is still undocumented, so this
// payload — not the PR description or the issue proposals — is the contract.
const SAMPLE_USAGE = {
usage: {
rolling: { status: "ok", percent: 0, resetsAt: "2026-08-12T20:23:13.083Z" },
weekly: { status: "ok", percent: 0, resetsAt: "2026-08-17T00:00:00.083Z" },
monthly: { status: "ok", percent: 91, resetsAt: "2026-08-25T01:33:44.083Z" },
},
};

const conn = { provider: "opencode-go", apiKey: "sk-test" };

describe("opencode-go usage", () => {
beforeEach(() => {
proxyAwareFetch.mockReset();
});

it("is registered as an apikey-eligible usage provider", () => {
expect(USAGE_SUPPORTED_PROVIDERS).toContain("opencode-go");
// Go authenticates with a plain API key, so the /api/usage route only
// accepts the connection when usageApikey is also set.
expect(USAGE_APIKEY_PROVIDERS).toContain("opencode-go");
});

it("calls the usage endpoint with bearer auth", async () => {
proxyAwareFetch.mockResolvedValue(jsonResponse(SAMPLE_USAGE));
await getUsageForProvider(conn);

const [url, options] = proxyAwareFetch.mock.calls[0];
expect(url).toBe(USAGE_URL);
expect(options.headers.Authorization).toBe("Bearer sk-test");
});

it("maps the three windows to percent-based quotas", async () => {
proxyAwareFetch.mockResolvedValue(jsonResponse(SAMPLE_USAGE));
const usage = await getUsageForProvider(conn);

expect(usage.plan).toBe("OpenCode Go");
expect(usage.limitReached).toBe(false);
expect(Object.keys(usage.quotas)).toEqual(["Rolling", "Weekly", "Monthly"]);
expect(usage.quotas.Monthly).toEqual({
used: 91,
total: 100,
remainingPercentage: 9,
resetAt: "2026-08-25T01:33:44.083Z",
unlimited: false,
});
});

it("renders remaining percentage the right way round in the dashboard", async () => {
proxyAwareFetch.mockResolvedValue(jsonResponse(SAMPLE_USAGE));
const usage = await getUsageForProvider(conn);
const rows = parseQuotaData("opencode-go", usage);

const monthly = rows.find((r) => r.name === "Monthly");
// 91% used must read as 9% remaining, not 91%.
expect(getRemainingPercentage(monthly)).toBe(9);
// Absolute `remaining` must stay unset — the UI treats it as a 0-100 percent.
expect(monthly.remaining).toBeUndefined();
expect(getRemainingPercentage(rows.find((r) => r.name === "Rolling"))).toBe(100);
});

it("flags limitReached when any window is rate-limited", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse({
usage: {
...SAMPLE_USAGE.usage,
monthly: { status: "rate-limited", percent: 100, resetsAt: "2026-08-25T01:33:44.083Z" },
},
}),
);
const usage = await getUsageForProvider(conn);

expect(usage.limitReached).toBe(true);
expect(usage.quotas.Monthly.remainingPercentage).toBe(0);
});

it("drops a window with no usable percent rather than showing it as full", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse({
usage: {
...SAMPLE_USAGE.usage,
weekly: { status: "ok", resetsAt: "2026-08-17T00:00:00.083Z" }, // percent missing
},
}),
);
const usage = await getUsageForProvider(conn);

// Defaulting the missing percent to 0 would publish "100% remaining" and
// tell the user they have headroom they may not have.
expect(usage.quotas).not.toHaveProperty("Weekly");
expect(Object.keys(usage.quotas)).toEqual(["Rolling", "Monthly"]);
expect(parseQuotaData("opencode-go", usage).find((r) => r.name === "Weekly")).toBeUndefined();
});

it("still flags limitReached when a rate-limited window omits percent", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse({
usage: {
...SAMPLE_USAGE.usage,
monthly: { status: "rate-limited", resetsAt: "2026-08-25T01:33:44.083Z" },
},
}),
);
const usage = await getUsageForProvider(conn);

// status is authoritative on its own — dropping the unusable percent must
// not also discard the "you are throttled" signal.
expect(usage.quotas).not.toHaveProperty("Monthly");
expect(usage.limitReached).toBe(true);
});

it("accepts a numeric string percent", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse({ usage: { monthly: { status: "ok", percent: "91", resetsAt: null } } }),
);
const usage = await getUsageForProvider(conn);
expect(usage.quotas.Monthly.used).toBe(91);
});

it("reports a missing subscription (403) distinctly from an auth failure", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse(
{ type: "error", error: { type: "EntitlementError", message: "OpenCode Go subscription required." } },
403,
),
);
const usage = await getUsageForProvider(conn);

expect(usage.message).toBe("OpenCode Go subscription required.");
// Must NOT read as an auth-expired message — re-authorizing cannot fix it.
expect(usage.message.toLowerCase()).not.toContain("unauthorized");
expect(usage.quotas).toBeUndefined();
});

it("surfaces a 401 as an auth failure", async () => {
proxyAwareFetch.mockResolvedValue(
jsonResponse({ type: "error", error: { type: "AuthError", message: "Unauthorized" } }, 401),
);
expect((await getUsageForProvider(conn)).message).toBe("Unauthorized");
});

it("degrades to a message when the endpoint disappears or changes shape", async () => {
// The endpoint is a day old and undocumented: a rollback serves the SPA 404 HTML.
proxyAwareFetch.mockResolvedValue(
new Response("<html>404</html>", { status: 404, headers: { "Content-Type": "text/html" } }),
);
expect(await getUsageForProvider(conn)).toMatchObject({ plan: "OpenCode Go" });
expect((await getUsageForProvider(conn)).quotas).toBeUndefined();

proxyAwareFetch.mockResolvedValue(jsonResponse({ unexpected: true }));
expect((await getUsageForProvider(conn)).message).toMatch(/expected shape/);
});

it("never throws on a missing key or a network error", async () => {
expect((await getUsageForProvider({ provider: "opencode-go", apiKey: null })).message).toMatch(
/API key not available/,
);
expect(proxyAwareFetch).not.toHaveBeenCalled();

proxyAwareFetch.mockRejectedValue(new Error("boom"));
expect((await getUsageForProvider(conn)).message).toBe("OpenCode Go error: boom");
});
});