Keep usage logins visible and explain unavailable quota (#5876)

* fix(usage): keep existing logins visible and explain unavailable quota

Credential discovery must preserve accounts whose login exists even when it has expired. Keep vendor failures on the discovered account and isolate discovery failures without inventing provider cards.

* test(usage): align credential route tests with account discovery
This commit is contained in:
Mohamed Boudra
2026-10-02 00:30:11 +01:00
committed by GitHub
parent b5b43edd65
commit 05e94ab52e
70 changed files with 1919 additions and 1110 deletions
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import { discover, fetchUsage, identify } from "./usage.js";
import { discover, fetchUsage } from "./usage.js";
import { inputSchema } from "../shared/input.js";
@@ -22,7 +22,7 @@ afterEach(async () => {
test("discovery input rejects an explicit API key", () => {
expect(inputSchema.safeParse({ apiKey: "unused" }).success).toBe(false);
expect(inputSchema.parse({})).toEqual({});
expect(() => inputSchema.parse({})).toThrow();
});
const upstreamResponse = {
@@ -39,7 +39,9 @@ test("discovers and fetches the default key from read-only auth.json", async ()
const content = JSON.stringify({ "opencode-go": { type: "api", key: "fixture-key" } });
try {
await writeFile(path, content);
expect(await discover(path)).toEqual([{}]);
expect(await discover(path)).toEqual([
{ key: expect.stringMatching(/^[a-f0-9]{64}$/), input: { path } },
]);
const requests: Array<{ url: string; authorization: string | null }> = [];
const fetchApi = async (url: string | URL | Request, init?: RequestInit) => {
requests.push({
@@ -48,8 +50,7 @@ test("discovers and fetches the default key from read-only auth.json", async ()
});
return Response.json(upstreamResponse);
};
const report = await fetchUsage({}, fetchApi as typeof fetch, path);
expect((await identify({}, path))?.key).toMatch(/^[a-f0-9]{64}$/);
const report = await fetchUsage({ path }, fetchApi as typeof fetch);
expect(requests).toEqual([
{ url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" },
]);
@@ -75,38 +76,29 @@ test("omits default discovery when auth.json lacks an OpenCode Go API key", asyn
try {
await writeFile(path, JSON.stringify({ openai: { type: "oauth", access: "other-token" } }));
expect(await discover(path)).toEqual([]);
const report = await fetchUsage(
{},
async () => {
await expect(
fetchUsage({ path }, async () => {
throw new Error("must not fetch");
},
path,
);
expect(report).toEqual({ status: "unavailable", windows: [] });
}),
).rejects.toThrow("login store no longer exists");
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test.each([401, 403])("maps HTTP %i to unavailable", async (status) => {
const report = await fetchUsage({}, async () => new Response(null, { status }), fixturePath);
expect(report.status).toBe("unavailable");
expect(report.windows).toEqual([]);
const report = await fetchUsage(
{ path: fixturePath },
async () => new Response(null, { status }),
);
expect(report).toEqual({
status: "unavailable",
problem: { kind: "rejected", status, refreshedBy: "opencode" },
});
});
test("identify returns a key when fetch finds OpenCode Go credentials", async () => {
let requested = false;
await fetchUsage(
{},
async () => {
requested = true;
return new Response(null, { status: 401 });
},
fixturePath,
);
expect(requested).toBe(true);
const identity = await identify({}, fixturePath);
expect(identity?.key).toMatch(/^[a-f0-9]{64}$/);
expect(identity?.key).not.toBe("fixture-secret");
expect(await identify({}, fixturePath)).toEqual(identity);
test.each(["missing file", "unrelated file"])("discovers no accounts for %s", async (scenario) => {
const path = join(fixtureDirectory, "missing.json");
if (scenario === "unrelated file") await writeFile(path, "{}");
expect(await discover(path)).toEqual([]);
});
@@ -4,6 +4,8 @@ import { join } from "node:path";
import { z } from "zod";
import {
hashAccountKey,
unavailable,
type UsageAccount,
toneFromUsedPct,
windowFromUsedPct,
type UsageReport,
@@ -44,23 +46,22 @@ export async function readDefaultKey(path = authPath()): Promise<string | null>
}
}
export async function discover(path = authPath()): Promise<Array<{}>> {
return (await readDefaultKey(path)) ? [{}] : [];
export async function discover(path = authPath()): Promise<UsageAccount[]> {
return (await readDefaultKey(path)) ? [{ key: hashAccountKey(path), input: { path } }] : [];
}
export async function fetchUsage(
_input: Input,
input: Input,
fetchApi: typeof fetch = fetch,
path = authPath(),
): Promise<UsageReport> {
const apiKey = await readDefaultKey(path);
if (!apiKey) return { status: "unavailable", windows: [] };
const apiKey = await readDefaultKey(input.path);
if (!apiKey) throw new Error("OpenCode Go login store no longer exists");
const response = await fetchApi("https://opencode.ai/zen/go/v1/usage", {
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403)
return { status: "unavailable", windows: [] };
return unavailable({ kind: "rejected", status: response.status, refreshedBy: "opencode" });
if (!response.ok) throw new Error(`OpenCode Go usage API returned ${response.status}`);
const data = responseSchema.parse(await response.json());
const windows = (
@@ -82,8 +83,3 @@ export async function fetchUsage(
);
return { status: "available", planLabel: "Go", windows };
}
export async function identify(_input: Input, path = authPath()) {
const key = await readDefaultKey(path);
return key ? { key: hashAccountKey(key) } : null;
}