fix usage source discovery and session report parity

This commit is contained in:
Mohamed Boudra
2026-09-26 20:52:57 +02:00
parent cd324f1bdc
commit 8757553063
12 changed files with 267 additions and 66 deletions
@@ -1 +1 @@
{ "id": "codex-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "codex-usage-source" }
@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="#1a1a1a"/><path d="M7 12h10M12 7v10" stroke="white" stroke-width="2"/></svg>
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="#211E1E"/>
<path d="M12 18H18V24H6V12H12V18Z" fill="#CFCECD"/>
<path d="M48 12V24H36V12H48Z" fill="#CFCECD"/>
<path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="#211E1E"/>
</svg>

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 333 B

@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { fetchUsage } from "./server/usage.js";
import { discover, fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -8,6 +8,7 @@ export default function contribute(server: PluginServerContext) {
label: "OpenCode Go",
icon: "icon.svg",
input: inputSchema,
discover,
fetch: fetchUsage,
});
return () => {};
@@ -1,33 +1,75 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { fetchUsage } from "./usage.js";
import { discover, fetchUsage } from "./usage.js";
test("reads the OpenCode Go usage windows", async () => {
const requests: Array<{ url: string; authorization: string | null }> = [];
const fetchApi = async (url: string | URL | Request, init?: RequestInit) => {
const headers = new Headers(init?.headers);
requests.push({ url: String(url), authorization: headers.get("Authorization") });
return Response.json({
rollingUsage: { usagePercent: 21, resetInSec: 1800 },
weeklyUsage: { usagePercent: 42 },
monthlyUsage: { usagePercent: 63 },
});
};
const report = await fetchUsage({ apiKey: "fixture-key" }, fetchApi as typeof fetch);
expect(requests).toEqual([
{ url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" },
]);
expect(report.status).toBe("available");
expect(report.windows.map((window) => [window.id, window.usedPct, window.headline])).toEqual([
["rolling", 21, true],
["weekly", 42, undefined],
["monthly", 63, undefined],
]);
const upstreamResponse = {
usage: {
rolling: { status: "ok", percent: 21, resetsAt: "2026-09-26T20:00:00.000Z" },
weekly: { status: "ok", percent: 42, resetsAt: "2026-09-28T00:00:00.000Z" },
monthly: { status: "rate-limited", percent: 100, resetsAt: "2026-10-01T00:00:00.000Z" },
},
};
test("discovers and fetches the default key from read-only auth.json", async () => {
const directory = await mkdtemp(join(tmpdir(), "paseo-go-usage-"));
const path = join(directory, "auth.json");
const content = JSON.stringify({ "opencode-go": { type: "api", key: "fixture-key" } });
try {
await writeFile(path, content);
expect(await discover(path)).toEqual([{}]);
const requests: Array<{ url: string; authorization: string | null }> = [];
const fetchApi = async (url: string | URL | Request, init?: RequestInit) => {
requests.push({
url: String(url),
authorization: new Headers(init?.headers).get("Authorization"),
});
return Response.json(upstreamResponse);
};
const report = await fetchUsage({}, fetchApi as typeof fetch, path);
expect(requests).toEqual([
{ url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" },
]);
expect(report.status).toBe("available");
expect(
report.windows.map((window) => [window.id, window.usedPct, window.resetsAt, window.headline]),
).toEqual([
["rolling", 21, "2026-09-26T20:00:00.000Z", true],
["weekly", 42, "2026-09-28T00:00:00.000Z", undefined],
["monthly", 100, "2026-10-01T00:00:00.000Z", undefined],
]);
// The current upstream endpoint returns windows only; do not invent balances.
expect(report.balances).toBeUndefined();
expect(await readFile(path, "utf8")).toBe(content);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("treats rejected credentials as unavailable", async () => {
test("omits default discovery when auth.json lacks an OpenCode Go API key", async () => {
const directory = await mkdtemp(join(tmpdir(), "paseo-go-usage-"));
const path = join(directory, "auth.json");
try {
await writeFile(path, JSON.stringify({ openai: { type: "oauth", access: "other-token" } }));
expect(await discover(path)).toEqual([]);
const report = await fetchUsage(
{},
async () => {
throw new Error("must not fetch");
},
path,
);
expect(report).toEqual({ account: { key: "default" }, status: "unavailable", windows: [] });
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test.each([401, 403])("maps HTTP %i to unavailable", async (status) => {
const report = await fetchUsage(
{ apiKey: "fixture-key" },
async () => new Response(null, { status: 401 }),
async () => new Response(null, { status }),
);
expect(report.status).toBe("unavailable");
expect(report.windows).toEqual([]);
@@ -1,4 +1,7 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
toneFromUsedPct,
@@ -7,51 +10,75 @@ import {
} from "@getpaseo/plugin/server/usage";
import type { Input } from "../shared/input.js";
const windowSchema = z
.object({ usagePercent: z.number().finite(), resetInSec: z.number().finite().optional() })
const authSchema = z
.object({
"opencode-go": z
.object({ type: z.literal("api"), key: z.string().min(1) })
.passthrough()
.optional(),
})
.passthrough();
const responseSchema = z.object({
rollingUsage: windowSchema.optional(),
weeklyUsage: windowSchema.optional(),
monthlyUsage: windowSchema.optional(),
const windowSchema = z.object({
status: z.enum(["ok", "rate-limited"]),
percent: z.number().finite(),
resetsAt: z.iso.datetime(),
});
const responseSchema = z.object({
usage: z.object({ rolling: windowSchema, weekly: windowSchema, monthly: windowSchema }),
});
function authPath(env: NodeJS.ProcessEnv = process.env): string {
return join(
env.XDG_DATA_HOME || join(env.HOME || homedir(), ".local", "share"),
"opencode",
"auth.json",
);
}
async function readDefaultKey(path = authPath()): Promise<string | null> {
try {
const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
return auth["opencode-go"]?.key ?? null;
} catch {
return null;
}
}
export async function discover(path = authPath()): Promise<Array<{}>> {
return (await readDefaultKey(path)) ? [{}] : [];
}
export async function fetchUsage(
input: Input,
fetchApi: typeof fetch = fetch,
path = authPath(),
): Promise<UsageReport> {
const key = createHash("sha256").update(input.apiKey).digest("hex");
const apiKey = "apiKey" in input ? input.apiKey : await readDefaultKey(path);
if (!apiKey) return { account: { key: "default" }, status: "unavailable", windows: [] };
const key = createHash("sha256").update(apiKey).digest("hex");
const response = await fetchApi("https://opencode.ai/zen/go/v1/usage", {
headers: { Authorization: `Bearer ${input.apiKey}`, Accept: "application/json" },
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403)
return { account: { key }, status: "unavailable", windows: [] };
if (!response.ok) throw new Error(`OpenCode Go usage API returned ${response.status}`);
const data = responseSchema.parse(await response.json());
const now = Date.now();
const windows = (
[
["rolling", "Rolling", data.rollingUsage],
["weekly", "Weekly", data.weeklyUsage],
["monthly", "Monthly", data.monthlyUsage],
["rolling", "Rolling", data.usage.rolling],
["weekly", "Weekly", data.usage.weekly],
["monthly", "Monthly", data.usage.monthly],
] as const
).flatMap(([id, label, value]) =>
value
? [
windowFromUsedPct({
id,
label,
utilizationPct: value.usagePercent,
resetsAt:
value.resetInSec === undefined
? null
: new Date(now + value.resetInSec * 1000).toISOString(),
tone: toneFromUsedPct(value.usagePercent),
headline: id === "rolling",
}),
]
: [],
).map(([id, label, value]) =>
windowFromUsedPct({
id,
label,
utilizationPct: value.percent,
resetsAt: value.resetsAt,
tone: toneFromUsedPct(value.percent),
headline: id === "rolling",
}),
);
return { account: { key }, status: "available", planLabel: "Go", windows };
}
@@ -1,4 +1,7 @@
import { z } from "zod";
export const inputSchema = z.object({ apiKey: z.string().min(1) }).strict();
export const inputSchema = z.union([
z.object({ apiKey: z.string().min(1) }).strict(),
z.object({}).strict(),
]);
export type Input = z.infer<typeof inputSchema>;
@@ -458,6 +458,7 @@ export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession
}
},
getRuntimeInfo: async () => mapRuntimeInfo(provider, await inner.getRuntimeInfo()),
getUsageReference: inner.getUsageReference?.bind(inner),
getAvailableModes: () => inner.getAvailableModes(),
getCurrentMode: () => inner.getCurrentMode(),
setMode: (modeId) => inner.setMode(modeId),
@@ -25,6 +25,7 @@ import type {
} from "../../agent-sdk-types.js";
import type { AgentAttachment } from "@getpaseo/protocol/messages";
import { buildAgentPrompt, renderPromptAttachmentAsText } from "../../prompt-attachments.js";
import { buildProviderRegistry } from "../../provider-registry.js";
interface TestClaudeSession {
translateMessageToEvents(message: SDKMessage): AgentStreamEvent[];
@@ -538,12 +539,43 @@ describe("ClaudeAgentClient binary resolution", () => {
input: { configDir: "/accounts/second" },
});
await session.close();
const custom = await client.createSession(
{ provider: "claude", cwd: process.cwd() },
{ env: { ANTHROPIC_AUTH_TOKEN: "token" } },
);
expect(await custom.getUsageReference?.()).toBeNull();
await custom.close();
});
test.each(["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"])(
"Claude usage reference is null with %s override",
async (name) => {
const client = new ClaudeAgentClient({
logger,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession(
{ provider: "claude", cwd: process.cwd() },
{ env: { [name]: "override" } },
);
expect(await session.getUsageReference?.()).toBeNull();
await session.close();
},
);
test("Claude custom alias keeps its CLAUDE_CONFIG_DIR usage reference", async () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
"work-claude": {
extends: "claude",
label: "Work Claude",
env: { CLAUDE_CONFIG_DIR: "/accounts/work" },
},
},
});
const session = await registry["work-claude"].createClient(logger).createSession({
provider: "work-claude",
cwd: process.cwd(),
});
expect(await session.getUsageReference?.()).toEqual({
source: "claude",
input: { configDir: "/accounts/work" },
});
await session.close();
});
test("resolves the installed Claude Code version", async () => {
@@ -211,6 +211,7 @@ test("Pi usage reference follows the current OAuth model and agent directory", a
"openai-codex": {
type: "oauth",
access: "codex-token",
accountId: "account-1",
refresh: "refresh",
expires: Date.now() + 60_000,
},
@@ -233,7 +234,7 @@ test("Pi usage reference follows the current OAuth model and agent directory", a
});
expect(await session.getUsageReference?.()).toEqual({
source: "codex",
input: { accessToken: "codex-token" },
input: { accessToken: "codex-token", accountId: "account-1" },
});
runtime.state.model = { provider: "anthropic", id: "claude", name: "Claude" };
expect(await session.getUsageReference?.()).toEqual({
@@ -1143,7 +1143,15 @@ export class PiRpcAgentSession implements AgentSession {
!credential.access
)
return null;
return { source, input: { accessToken: credential.access } };
return {
source,
input: {
accessToken: credential.access,
...(source === "codex" && typeof credential.accountId === "string" && credential.accountId
? { accountId: credential.accountId }
: {}),
},
};
} catch {
return null;
}
@@ -1,6 +1,7 @@
import { fileURLToPath } from "node:url";
import { expect, test } from "vitest";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { createTestAgentClient } from "../test-utils/fake-agent-client.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import contribute from "./test-fixtures/session-usage-reference/index.server.js";
@@ -26,6 +27,60 @@ test("resolves a provider plugin session reference through its usage source", as
});
const reports = await client.listUsageReports();
expect(reports.reports.map((entry) => entry.report.account.key)).toContain("from-session");
const missing = await client.createAgent({
provider: "fixture-session-provider",
cwd: directory,
model: "missing",
});
expect((await client.getAgentUsageReport({ agentId: missing.id })).entry).toBeNull();
} finally {
await client.close();
await daemon.close();
}
}, 60_000);
test("agent.get_usage_report resolves source IDs from default built-in and ACP sessions", async () => {
const directory = fileURLToPath(
new URL("./test-fixtures/session-usage-reference/", import.meta.url),
);
const sources = [
["claude", "claude"],
["codex", "codex"],
["copilot", "copilot"],
["cursor", "cursor"],
["kimi", "kimi"],
["generic-acp", "generic-match"],
] as const;
const agentClients = Object.fromEntries(
sources.map(([provider, source]) => {
const client = createTestAgentClient(provider);
const createSession = client.createSession.bind(client);
client.createSession = async (...args) => {
const session = await createSession(...args);
session.getUsageReference = async () => ({ source, input: {} });
return session;
};
return [provider, client];
}),
);
const daemon = await createTestPaseoDaemon({
pluginsEnabled: false,
internalPlugins: [{ id: "fixture-session-usage-reference", directory, contribute }],
agentClients,
providerOverrides: {
cursor: { extends: "acp", label: "Cursor", command: ["cursor-agent", "acp"] },
kimi: { extends: "acp", label: "Kimi", command: ["kimi", "acp"] },
"generic-acp": { extends: "acp", label: "Generic ACP", command: ["generic-acp", "acp"] },
},
});
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
try {
await client.connect();
for (const [provider, source] of sources) {
const agent = await client.createAgent({ provider, cwd: directory });
const result = await client.getAgentUsageReport({ agentId: agent.id });
expect(result.entry?.sourceId, provider).toBe(source);
}
} finally {
await client.close();
await daemon.close();
@@ -6,6 +6,18 @@ import {
import { z } from "zod";
export default function contribute(server: PluginServerContext) {
for (const id of ["claude", "codex", "copilot", "cursor", "kimi", "generic-match"]) {
server.registerUsageSource({
id,
label: id,
input: z.object({}).strict(),
fetch: async () => ({
account: { key: "default" },
status: "available",
windows: [{ id: "hour", label: "Hour", usedPct: 31, headline: true }],
}),
});
}
server.registerUsageSource({
id: "fixture-session-usage",
label: "Fixture session usage",
@@ -25,6 +37,7 @@ export default function contribute(server: PluginServerContext) {
"session.usage_reference",
]);
const listeners = new Set<(event: ProviderEvent) => void>();
const models = new Map<string, string | undefined>();
const emit = (event: ProviderEvent) => {
for (const listener of listeners) listener(event);
};
@@ -36,9 +49,16 @@ export default function contribute(server: PluginServerContext) {
emit({
type: "catalog",
requestId: input.requestId,
catalog: { models: [{ id: "fixture", label: "Fixture" }], modes: [] },
catalog: {
models: [
{ id: "fixture", label: "Fixture" },
{ id: "missing", label: "Missing" },
],
modes: [],
},
});
if (input.type === "session.open") {
models.set(input.sessionId, input.config.model);
emit({
type: "session.opened",
requestId: input.requestId,
@@ -53,7 +73,13 @@ export default function contribute(server: PluginServerContext) {
emit({
type: "usage_reference",
requestId: input.requestId,
reference: { source: "fixture-session-usage", input: { account: "from-session" } },
reference: {
source:
models.get(input.sessionId) === "missing"
? "missing-source"
: "fixture-session-usage",
input: { account: "from-session" },
},
});
if (input.type === "session.close")
emit({ type: "request.completed", requestId: input.requestId });