fix(codex): re-home app-server usage reporting on the harness seam

The deleted text provider's usage hook was the only source for the /status
Codex subscription usage line. Harnesses can now contribute an optional
usage snapshot (provider hooks keep priority; only distinct synthetic hook
owners fall through), and the codex harness reports app-server rate limits
via account/rateLimits/read with the same conversion and account identity
as before. No text provider resurrected; deadcode and SDK surface gates
clean.
This commit is contained in:
Peter Steinberger
2026-07-16 02:20:04 +01:00
parent 9c5eab471c
commit 7eeb1096db
9 changed files with 369 additions and 3 deletions

View File

@@ -90,6 +90,12 @@ export function createCodexAppServerAgentHarness(options: {
return validateCodexAppServerRuntimeArtifact(binding);
},
},
fetchUsageSnapshot: async (ctx) => {
const { fetchCodexAppServerUsageSnapshot } = await import("./src/app-server/usage.js");
return await fetchCodexAppServerUsageSnapshot(ctx, {
pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig,
});
},
supports: (ctx) => {
const provider = ctx.provider.trim().toLowerCase();
if (!providerIds.has(provider)) {

View File

@@ -124,6 +124,7 @@ describe("codex plugin", () => {
sourceVisibleReplies: "message_tool",
});
expect(typeof agentHarnessRegistration.dispose).toBe("function");
expect(typeof agentHarnessRegistration.fetchUsageSnapshot).toBe("function");
expect(mediaProviderRegistration?.id).toBe("codex");
expect(mediaProviderRegistration?.capabilities).toEqual(["image"]);
expect(mediaProviderRegistration?.defaultModels).toEqual({ image: "gpt-5.6-sol" });

View File

@@ -20,7 +20,7 @@ vi.mock("./shared-client.js", () => ({
getLeasedSharedCodexAppServerClient: sharedClientMocks.getSharedCodexAppServerClient,
}));
const { requestCodexAppServerJson } = await import("./request.js");
const { readCodexAppServerUsage, requestCodexAppServerJson } = await import("./request.js");
const expectDeadlineOptions = () =>
expect.objectContaining({ timeoutMs: expect.any(Number), signal: expect.anything() });
@@ -354,4 +354,41 @@ describe("requestCodexAppServerJson sandbox guard", () => {
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
});
it("reads usage and account identity over one isolated client", async () => {
const request = vi.fn(async (method: string) =>
method === "account/rateLimits/read"
? { rateLimitsByLimitId: { codex: { limitId: "codex" } } }
: { account: { email: "codex-account@example.com" } },
);
const closeAndWait = vi.fn(async () => undefined);
sharedClientMocks.createIsolatedCodexAppServerClient.mockResolvedValue({
request,
closeAndWait,
});
await expect(
readCodexAppServerUsage({
timeoutMs: 3_500,
authProfileId: "openai:test",
}),
).resolves.toEqual({
rateLimits: { rateLimitsByLimitId: { codex: { limitId: "codex" } } },
accountEmail: "codex-account@example.com",
});
expect(sharedClientMocks.createIsolatedCodexAppServerClient).toHaveBeenCalledWith(
expect.objectContaining({
authProfileId: "openai:test",
timeoutMs: expect.any(Number),
}),
);
expect(request).toHaveBeenNthCalledWith(
1,
"account/rateLimits/read",
undefined,
expectDeadlineOptions(),
);
expect(request).toHaveBeenNthCalledWith(2, "account/read", {}, expectDeadlineOptions());
expect(closeAndWait).toHaveBeenCalledWith({ exitTimeoutMs: 300, forceKillDelayMs: 200 });
});
});

View File

@@ -116,12 +116,91 @@ type CodexAppServerScopedRequest = <T = JsonValue | undefined>(request: {
requestParams?: unknown;
}) => Promise<T>;
const CODEX_USAGE_ISOLATED_SHUTDOWN = { forceKillDelayMs: 200, exitTimeoutMs: 300 } as const;
const CODEX_ACCOUNT_READ_MAX_TIMEOUT_MS = 4_000;
const CODEX_ACCOUNT_READ_DEADLINE_MARGIN_MS = 250;
const CODEX_USAGE_DEADLINE_RESERVE_MS =
CODEX_USAGE_ISOLATED_SHUTDOWN.forceKillDelayMs +
CODEX_USAGE_ISOLATED_SHUTDOWN.exitTimeoutMs +
CODEX_ACCOUNT_READ_DEADLINE_MARGIN_MS;
/** Reads rate limits and best-effort account identity from one isolated app-server session. */
export async function readCodexAppServerUsage(options: {
timeoutMs: number;
agentDir?: string;
authProfileId?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
startOptions?: CodexAppServerStartOptions;
}): Promise<{ rateLimits: JsonValue; accountEmail?: string }> {
const deadline = Date.now() + options.timeoutMs;
return await withCodexAppServerJsonClient(
{
timeoutMs: options.timeoutMs,
timeoutMessage: "codex app-server usage read timed out",
agentDir: options.agentDir,
...(options.authProfileId ? { authProfileId: options.authProfileId } : {}),
config: options.config,
startOptions: options.startOptions,
isolated: true,
// A throwaway read-only child: bound shutdown inside the outer usage deadline.
isolatedShutdown: CODEX_USAGE_ISOLATED_SHUTDOWN,
},
async (request) => {
const rateLimits = await request<JsonValue>({ method: "account/rateLimits/read" });
const accountEmail = await readCodexAccountEmailBestEffort(request, deadline);
return { rateLimits, ...(accountEmail ? { accountEmail } : {}) };
},
);
}
function extractCodexAccountEmail(value: unknown): string | undefined {
if (!value || typeof value !== "object") {
return undefined;
}
const record = value as { account?: unknown; email?: unknown; accountEmail?: unknown };
const account =
record.account && typeof record.account === "object"
? (record.account as { email?: unknown; accountEmail?: unknown })
: record;
const email = account.email ?? account.accountEmail;
return typeof email === "string" && email.trim() ? email.trim() : undefined;
}
async function readCodexAccountEmailBestEffort(
request: CodexAppServerScopedRequest,
deadline: number,
): Promise<string | undefined> {
const boundMs = Math.min(
CODEX_ACCOUNT_READ_MAX_TIMEOUT_MS,
deadline - Date.now() - CODEX_USAGE_DEADLINE_RESERVE_MS,
);
if (boundMs <= 0) {
return undefined;
}
const read = request<unknown>({ method: "account/read", requestParams: {} }).then(
(account) => extractCodexAccountEmail(account),
() => undefined,
);
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<undefined>((resolve) => {
timer = setTimeout(() => resolve(undefined), boundMs);
timer.unref?.();
});
try {
return await Promise.race([read, timeout]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
/**
* Runs several guarded requests over one acquired client (shared lease or
* isolated child) so related reads see the same app-server session. The whole
* callback re-runs once when the client's start selection changed underneath it.
*/
export async function withCodexAppServerJsonClient<T>(
async function withCodexAppServerJsonClient<T>(
params: {
timeoutMs?: number;
timeoutMessage?: string;

View File

@@ -0,0 +1,67 @@
// Codex usage tests cover the harness-owned provider-usage contribution.
import type { ProviderFetchUsageSnapshotContext } from "openclaw/plugin-sdk/plugin-entry";
import { CODEX_APP_SERVER_AUTH_MARKER } from "openclaw/plugin-sdk/provider-usage";
import { describe, expect, it, vi } from "vitest";
import { fetchCodexAppServerUsageSnapshot } from "./usage.js";
function usageContext(
overrides: Partial<ProviderFetchUsageSnapshotContext> = {},
): ProviderFetchUsageSnapshotContext {
return {
config: {},
env: {},
provider: "openai",
token: CODEX_APP_SERVER_AUTH_MARKER,
timeoutMs: 3_500,
fetchFn: fetch,
...overrides,
};
}
describe("Codex app-server provider usage", () => {
it("contributes OpenAI usage windows for the synthetic app-server credential", async () => {
const readUsage = vi.fn(async () => ({
rateLimits: {
rateLimitsByLimitId: {
codex: {
limitId: "codex",
primary: {
usedPercent: 9,
windowDurationMins: 300,
resetsAt: 1_700_003_600,
},
},
},
},
accountEmail: "codex-account@example.com",
}));
await expect(fetchCodexAppServerUsageSnapshot(usageContext(), { readUsage })).resolves.toEqual({
provider: "openai",
displayName: "OpenAI",
windows: [{ label: "5h", usedPercent: 9, resetAt: 1_700_003_600_000 }],
plan: undefined,
accountEmail: "codex-account@example.com",
});
expect(readUsage).toHaveBeenCalledWith({
timeoutMs: 3_500,
agentDir: undefined,
config: {},
startOptions: expect.objectContaining({
command: "codex",
commandSource: "managed",
}),
});
});
it("ignores ordinary OpenAI credentials", async () => {
const readUsage = vi.fn();
await expect(
fetchCodexAppServerUsageSnapshot(usageContext({ token: "test-token-placeholder" }), {
readUsage,
}),
).resolves.toBeNull();
expect(readUsage).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,42 @@
/** Builds provider-usage snapshots from the Codex app-server account surface. */
import type { ProviderFetchUsageSnapshotContext } from "openclaw/plugin-sdk/plugin-entry";
import {
CODEX_APP_SERVER_AUTH_MARKER,
type ProviderUsageSnapshot,
} from "openclaw/plugin-sdk/agent-runtime";
import { resolveCodexAppServerRuntimeOptions } from "./config.js";
import { buildCodexAppServerUsageSnapshot } from "./rate-limits.js";
import { readCodexAppServerUsage } from "./request.js";
type CodexAppServerUsageRead = {
rateLimits: unknown;
accountEmail?: string;
};
type CodexAppServerUsageReader = (
options: Parameters<typeof readCodexAppServerUsage>[0],
) => Promise<CodexAppServerUsageRead>;
/** Handles the synthetic usage credential for a Codex-backed OpenAI route. */
export async function fetchCodexAppServerUsageSnapshot(
ctx: ProviderFetchUsageSnapshotContext,
options: {
pluginConfig?: unknown;
readUsage?: CodexAppServerUsageReader;
} = {},
): Promise<ProviderUsageSnapshot | null> {
if (ctx.token !== CODEX_APP_SERVER_AUTH_MARKER) {
return null;
}
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
const usage = await (options.readUsage ?? readCodexAppServerUsage)({
timeoutMs: ctx.timeoutMs,
agentDir: ctx.agentDir,
...(ctx.authProfileId ? { authProfileId: ctx.authProfileId } : {}),
config: ctx.config,
startOptions: appServer.start,
});
const snapshot = buildCodexAppServerUsageSnapshot(usage.rateLimits);
const accountEmail = ctx.email ?? usage.accountEmail;
return accountEmail && !snapshot.error ? { ...snapshot, accountEmail } : snapshot;
}

View File

@@ -196,12 +196,29 @@ type AgentHarnessAuthBindingCapability = {
};
};
type AgentHarnessProviderUsageCapability = {
/**
* Contributes runtime-owned quota data without registering a text provider.
* Provider usage hooks remain authoritative when both surfaces exist.
*/
fetchUsageSnapshot?: (
ctx: import("../../plugins/provider-runtime.types.js").ProviderFetchUsageSnapshotContext,
) =>
| Promise<
import("../../infra/provider-usage.types.js").ProviderUsageSnapshot | null | undefined
>
| import("../../infra/provider-usage.types.js").ProviderUsageSnapshot
| null
| undefined;
};
export type AgentHarness = AgentHarnessRunCapability &
AgentHarnessSideQuestionCapability &
AgentHarnessClassificationCapability &
AgentHarnessCompactionCapability &
AgentHarnessRuntimeArtifactCapability &
AgentHarnessAuthBindingCapability &
AgentHarnessProviderUsageCapability &
AgentHarnessSessionLifecycleCapability;
export type RegisteredAgentHarness = {

View File

@@ -11,6 +11,7 @@ import {
import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js";
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
import { resolveGpt5SystemPromptContribution } from "../agents/gpt5-prompt-overlay.js";
import { getRegisteredAgentHarness } from "../agents/harness/registry.js";
import {
applyPluginTextReplacements,
mergePluginTextTransforms,
@@ -688,7 +689,36 @@ export async function resolveProviderUsageSnapshotWithPlugin(params: {
env?: NodeJS.ProcessEnv;
context: ProviderFetchUsageSnapshotContext;
}) {
return await resolveProviderRuntimePlugin(params)?.fetchUsageSnapshot?.(params.context);
const providerHook = resolveProviderRuntimePlugin(params)?.fetchUsageSnapshot;
if (providerHook) {
const snapshot = await providerHook(params.context);
if (snapshot != null) {
return snapshot;
}
}
// A distinct hook owner is an explicit synthetic contribution route. Avoid
// probing harness manifests for ordinary provider usage misses.
if (params.provider === params.context.provider) {
return undefined;
}
let harness = getRegisteredAgentHarness(params.provider)?.harness;
if (!harness) {
const workspaceDir =
params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState() ?? process.cwd();
const { ensureSelectedAgentHarnessPlugin } =
await import("../agents/harness/runtime-plugin.js");
await ensureSelectedAgentHarnessPlugin({
provider: params.context.provider,
modelId: "",
config: params.config,
agentHarnessId: params.provider,
workspaceDir,
});
harness = getRegisteredAgentHarness(params.provider)?.harness;
}
return await harness?.fetchUsageSnapshot?.(params.context);
}
export type ProviderUsagePluginDescriptor = {

View File

@@ -0,0 +1,87 @@
// Verifies provider usage can be contributed by a runtime harness without a text provider.
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearAgentHarnesses, registerAgentHarness } from "../agents/harness/registry.js";
import { resolveProviderUsageSnapshotWithPlugin } from "./provider-runtime.js";
vi.mock("./provider-hook-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./provider-hook-runtime.js")>();
return { ...actual, resolveProviderRuntimePlugin: () => undefined };
});
describe("provider runtime harness usage", () => {
afterEach(() => {
clearAgentHarnesses();
});
it("routes a synthetic hook id to the matching harness", async () => {
const fetchUsageSnapshot = vi.fn(async () => ({
provider: "openai" as const,
displayName: "OpenAI",
windows: [{ label: "5h", usedPercent: 9 }],
}));
registerAgentHarness({
id: "codex",
label: "Codex",
supports: () => ({ supported: true }),
runAttempt: async () => {
throw new Error("not used");
},
fetchUsageSnapshot,
});
await expect(
resolveProviderUsageSnapshotWithPlugin({
provider: "codex",
config: {},
env: {},
workspaceDir: process.cwd(),
context: {
config: {},
env: {},
provider: "openai",
token: "test-token-placeholder",
timeoutMs: 5_000,
fetchFn: fetch,
},
}),
).resolves.toEqual({
provider: "openai",
displayName: "OpenAI",
windows: [{ label: "5h", usedPercent: 9 }],
});
expect(fetchUsageSnapshot).toHaveBeenCalledWith(
expect.objectContaining({ provider: "openai", token: "test-token-placeholder" }),
);
});
it("does not probe a harness for an ordinary provider usage miss", async () => {
const fetchUsageSnapshot = vi.fn();
registerAgentHarness({
id: "openai",
label: "OpenAI harness",
supports: () => ({ supported: true }),
runAttempt: async () => {
throw new Error("not used");
},
fetchUsageSnapshot,
});
await expect(
resolveProviderUsageSnapshotWithPlugin({
provider: "openai",
config: {},
env: {},
workspaceDir: process.cwd(),
context: {
config: {},
env: {},
provider: "openai",
token: "test-token-placeholder",
timeoutMs: 5_000,
fetchFn: fetch,
},
}),
).resolves.toBeUndefined();
expect(fetchUsageSnapshot).not.toHaveBeenCalled();
});
});