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
11 changes: 9 additions & 2 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { getAccountCredential, getAccountSet, getCredential } from "../oauth/sto
import { antigravityUserAgent } from "../adapters/client-fingerprint";
import { apiKeyPoolEntryId } from "./api-keys";
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
import type { OcxConfig, OcxProviderConfig } from "../types";
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
import {
Expand Down Expand Up @@ -2084,7 +2084,14 @@ async function maybeFetchProviderQuota(
&& isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
return fetchCommandCodeQuota(name, provider);
}
if ((provider.authMode ?? "key") === "key" && name === "opencode-go") {
// Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
// setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
// silently had no quota panel and no `ocx provider quota --json` report while the literal
// name was the gate. `registryEntryForProviderDestination` is the existing predicate for
// exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
// a different adapter is still not OpenCode Go. The defensive URL check inside
// `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
return fetchOpenCodeGoQuota(name, provider);
}
if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
Expand Down
57 changes: 57 additions & 0 deletions tests/opencode-go-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,61 @@ describe("OpenCode Go provider quota", () => {
expect(fetchCalls).toBe(0);
expect(result.reports).toEqual([]);
});

// #1924: a multi-account setup points several rows at the same OpenCode Go endpoint under
// names the registry has never heard of. Gating dispatch on the literal name `opencode-go`
// meant those rows had no dashboard quota panel and no `ocx provider quota --json` report,
// even though each one holds a working key for the same upstream.
test("a canonical sibling row under any name is probed and reported", async () => {
const bearers: string[] = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
const headers = init?.headers as Record<string, string> | undefined;
if (headers?.Authorization) bearers.push(headers.Authorization);
return new Response(JSON.stringify({
usage: { rolling: { status: "ok", percent: 12, resetsAt: "2026-08-12T20:00:00.000Z" } },
}), { status: 200, headers: { "content-type": "application/json" } });
}) as typeof fetch;

const config = openCodeGoConfig();
config.providers["opencode-go-2"] = {
adapter: "openai-chat",
authMode: "key",
baseUrl: "https://opencode.ai/zen/go/v1",
// Kept under the privacy scanner's bearer-token length floor: a longer fixture reads as
// a real credential to `privacy:scan` even inside a test.
apiKey: "sibling-secret",
};

const result = await fetchProviderQuotaReports(config, true);

expect(result.reports.map(report => report.provider).sort()).toEqual(["opencode-go", "opencode-go-2"]);
expect(bearers.sort()).toEqual(["Bearer opencode-go-secret", "Bearer sibling-secret"]);
expect(JSON.stringify(result)).not.toContain("sibling-secret");
});

// The distinction between "routes to the OpenCode Go endpoint" and "is the OpenCode Go
// provider": a bare URL match would probe this row, but a different adapter speaks a
// different protocol to that host and is not the provider whose quota shape we parse.
test("a canonical URL behind a different adapter is not OpenCode Go", async () => {
let fetchCalls = 0;
globalThis.fetch = (async () => {
fetchCalls += 1;
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
}) as typeof fetch;

const result = await fetchProviderQuotaReports({
defaultProvider: "not-really-go",
providers: {
"not-really-go": {
adapter: "anthropic",
authMode: "key",
baseUrl: "https://opencode.ai/zen/go/v1",
apiKey: "unrelated-secret",
},
},
} as OcxConfig, true);

expect(fetchCalls).toBe(0);
expect(result.reports).toEqual([]);
});
});
Loading