fix: report unavailable Codex runtime in model status

This commit is contained in:
Shakker
2026-07-18 07:13:45 +01:00
committed by Shakker
parent 564a384f25
commit cc62bc7e0b
2 changed files with 341 additions and 131 deletions
+242 -131
View File
@@ -23,6 +23,11 @@ import {
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import {
resolveAgentHarnessOwnerPluginIds,
resolveAgentHarnessRuntimeAvailability,
type AgentHarnessRuntimeAvailability,
} from "../../agents/harness/runtime-plugin.js";
import {
createModelAuthAvailabilityResolver,
type ModelAuthAvailabilityEvaluation,
@@ -129,17 +134,20 @@ type StatusProviderRouteAuth =
kind: "legacy";
evaluation: ModelAuthAvailabilityEvaluation;
usesCodexRuntimeAuth: boolean;
runtimeAvailability?: AgentHarnessRuntimeAvailability;
}
| {
kind: "route";
route: ProviderModelRouteCandidate;
evaluation: ModelAuthAvailabilityEvaluation;
usesCodexRuntimeAuth: boolean;
runtimeAvailability?: AgentHarnessRuntimeAvailability;
}
| {
kind: "indeterminate";
evaluation: ModelAuthAvailabilityEvaluation;
usesCodexRuntimeAuth: boolean;
runtimeAvailability?: AgentHarnessRuntimeAvailability;
}
| {
kind: "incompatible";
@@ -147,6 +155,7 @@ type StatusProviderRouteAuth =
message: string;
evaluation: ModelAuthAvailabilityEvaluation;
usesCodexRuntimeAuth: false;
runtimeAvailability?: undefined;
};
type StatusProviderUseRef = {
@@ -164,6 +173,28 @@ type StatusProviderUse = {
routeAuth: StatusProviderRouteAuth;
};
type StatusRuntimeAuthStatus = "usable" | "missing" | "indeterminate";
type StatusRuntimeAuthRouteBase = {
provider: string;
runtime: string;
authProvider: string;
effective: ProviderAuthOverview["effective"];
};
type StatusRuntimeAuthRoute =
| (StatusRuntimeAuthRouteBase & {
status: StatusRuntimeAuthStatus;
})
| (StatusRuntimeAuthRouteBase & {
status: "unavailable";
authStatus: StatusRuntimeAuthStatus;
runtimeStatus: "unavailable";
runtimeReason: Extract<AgentHarnessRuntimeAvailability, { status: "unavailable" }>["reason"];
runtimeDetail: string;
runtimePluginIds: string[];
});
type StatusModelRouteIssue =
| {
kind: "incompatible";
@@ -362,6 +393,49 @@ export async function modelsStatusCommand(
workspaceDir,
env: process.env,
});
const selectedPluginRootDirs = new Map(
[...metadataSnapshot.byPluginId].map(([pluginId, plugin]) => [pluginId, plugin.rootDir]),
);
const { runPluginPayloadSmokeCheckForManifestRecords } =
await import("../../cli/update-cli/plugin-payload-validation.js");
const codexRuntimeAvailabilityByProvider = new Map<
string,
Promise<AgentHarnessRuntimeAvailability>
>();
const resolveCodexRuntimeAvailability = (
provider: string,
): Promise<AgentHarnessRuntimeAvailability> => {
const cached = codexRuntimeAvailabilityByProvider.get(provider);
if (cached) {
return cached;
}
const pending = (async () => {
const ownerPluginIds = resolveAgentHarnessOwnerPluginIds({
runtime: "codex",
provider,
config: cfg,
workspaceDir,
});
const pluginPayloadSmoke = await runPluginPayloadSmokeCheckForManifestRecords({
plugins: ownerPluginIds.flatMap((pluginId) => {
const plugin = metadataSnapshot.byPluginId.get(pluginId);
return plugin ? [plugin] : [];
}),
env: process.env,
});
return resolveAgentHarnessRuntimeAvailability({
runtime: "codex",
provider,
config: cfg,
workspaceDir,
payloadFailures: pluginPayloadSmoke.failures,
payloadCheckedPluginIds: pluginPayloadSmoke.checked,
selectedPluginRootDirs,
});
})();
codexRuntimeAvailabilityByProvider.set(provider, pending);
return pending;
};
const cleanupPluginMetadataSnapshot = installCommandPluginMetadataSnapshot({
snapshot: metadataSnapshot,
config: cfg,
@@ -571,94 +645,104 @@ export async function modelsStatusCommand(
metadataSnapshot,
});
let authResolver = createStatusAuthResolver(store);
const resolveProviderUses = (resolver: ModelAuthAvailabilityResolver): StatusProviderUse[] =>
providerUseRefs.map((usage) => {
const observedRoutes = routeSourcesByModel.get(
modelCatalogLogicalKey({ provider: usage.provider, id: usage.model }),
);
const ref = {
modelId: usage.model,
...(observedRoutes ? { observedRoutes } : {}),
};
// Image tools own their provider auth behavior. The text-route artifact
// must not reinterpret image auth as an OpenAI text transport.
const rawEvaluation: ModelAuthAvailabilityEvaluation =
usage.routeScope === "text"
? resolver.evaluateModelAuth(usage.provider, ref)
: {
availability: resolver.resolveProviderAuthAvailability(usage.provider, ref),
routeResolution: null,
};
const routeAuth: StatusProviderRouteAuth = (() => {
if (rawEvaluation.routeResolution?.kind === "incompatible") {
return {
kind: "incompatible",
code: rawEvaluation.routeResolution.code,
message: rawEvaluation.routeResolution.message,
evaluation: rawEvaluation,
usesCodexRuntimeAuth: false,
};
}
const usesCodexRuntimeAuth =
usage.allowCodexRuntimeFallback &&
resolveAgentHarnessPolicy({
provider: usage.provider,
modelId: usage.model,
...(rawEvaluation.selectedRoute
? {
modelApi: rawEvaluation.selectedRoute.api,
modelBaseUrl: rawEvaluation.selectedRoute.baseUrl,
}
: {}),
config: cfg,
agentId: workspaceAgentId,
}).runtime === "codex";
if (
usesCodexRuntimeAuth &&
usage.provider !== OPENAI_PROVIDER_ID &&
usage.provider !== "codex"
) {
return {
kind: "incompatible",
code: "unsupported-codex-runtime-provider",
message: `The Codex runtime does not support provider ${usage.provider}.`,
evaluation: rawEvaluation,
usesCodexRuntimeAuth: false,
};
}
const evaluation = rawEvaluation;
if (evaluation.selectedRoute) {
return {
kind: "route",
route: evaluation.selectedRoute,
evaluation,
usesCodexRuntimeAuth,
};
}
if (
evaluation.routeResolution?.kind === "routes" ||
evaluation.routeResolution?.kind === "indeterminate"
) {
return {
kind: "indeterminate",
evaluation,
usesCodexRuntimeAuth,
};
}
return {
kind: "legacy",
evaluation,
usesCodexRuntimeAuth,
const resolveProviderUses = async (
resolver: ModelAuthAvailabilityResolver,
): Promise<StatusProviderUse[]> =>
await Promise.all(
providerUseRefs.map(async (usage) => {
const observedRoutes = routeSourcesByModel.get(
modelCatalogLogicalKey({ provider: usage.provider, id: usage.model }),
);
const ref = {
modelId: usage.model,
...(observedRoutes ? { observedRoutes } : {}),
};
})();
return {
provider: usage.provider,
model: usage.model,
allowCodexRuntimeFallback: usage.allowCodexRuntimeFallback,
routeAuth,
};
});
let providerUses = resolveProviderUses(authResolver);
// Image tools own their provider auth behavior. The text-route artifact
// must not reinterpret image auth as an OpenAI text transport.
const rawEvaluation: ModelAuthAvailabilityEvaluation =
usage.routeScope === "text"
? resolver.evaluateModelAuth(usage.provider, ref)
: {
availability: resolver.resolveProviderAuthAvailability(usage.provider, ref),
routeResolution: null,
};
const routeAuth: StatusProviderRouteAuth = await (async () => {
if (rawEvaluation.routeResolution?.kind === "incompatible") {
return {
kind: "incompatible",
code: rawEvaluation.routeResolution.code,
message: rawEvaluation.routeResolution.message,
evaluation: rawEvaluation,
usesCodexRuntimeAuth: false,
};
}
const usesCodexRuntimeAuth =
usage.allowCodexRuntimeFallback &&
resolveAgentHarnessPolicy({
provider: usage.provider,
modelId: usage.model,
...(rawEvaluation.selectedRoute
? {
modelApi: rawEvaluation.selectedRoute.api,
modelBaseUrl: rawEvaluation.selectedRoute.baseUrl,
}
: {}),
config: cfg,
agentId: workspaceAgentId,
}).runtime === "codex";
if (
usesCodexRuntimeAuth &&
usage.provider !== OPENAI_PROVIDER_ID &&
usage.provider !== "codex"
) {
return {
kind: "incompatible",
code: "unsupported-codex-runtime-provider",
message: `The Codex runtime does not support provider ${usage.provider}.`,
evaluation: rawEvaluation,
usesCodexRuntimeAuth: false,
};
}
const runtimeAvailability = usesCodexRuntimeAuth
? await resolveCodexRuntimeAvailability(usage.provider)
: undefined;
const evaluation = rawEvaluation;
if (evaluation.selectedRoute) {
return {
kind: "route",
route: evaluation.selectedRoute,
evaluation,
usesCodexRuntimeAuth,
runtimeAvailability,
};
}
if (
evaluation.routeResolution?.kind === "routes" ||
evaluation.routeResolution?.kind === "indeterminate"
) {
return {
kind: "indeterminate",
evaluation,
usesCodexRuntimeAuth,
runtimeAvailability,
};
}
return {
kind: "legacy",
evaluation,
usesCodexRuntimeAuth,
runtimeAvailability,
};
})();
return {
provider: usage.provider,
model: usage.model,
allowCodexRuntimeFallback: usage.allowCodexRuntimeFallback,
routeAuth,
};
}),
);
let providerUses = await resolveProviderUses(authResolver);
const syntheticAuthByProvider = new Map<string, StatusSyntheticAuth>();
const runtimeSyntheticAuthByProvider = new Map<string, StatusSyntheticAuth>();
const cliRuntimeAuthUsages = providerUses
@@ -762,7 +846,7 @@ export async function modelsStatusCommand(
...store,
profiles: { ...store.profiles, ...syntheticProfiles },
});
providerUses = resolveProviderUses(authResolver);
providerUses = await resolveProviderUses(authResolver);
codexRuntimeAuthUsages = providerUses.filter((usage) => usage.routeAuth.usesCodexRuntimeAuth);
}
@@ -894,51 +978,66 @@ export async function modelsStatusCommand(
usages.push(usage);
codexRuntimeUsagesByProvider.set(usage.provider, usages);
}
const runtimeAuthRoutes = Array.from(
new Map([
...Array.from(codexRuntimeUsagesByProvider.entries()).map(([provider, usages]) => {
const representative =
usages.find((usage) => usage.routeAuth.evaluation.availability === true) ?? usages[0];
const effective = resolveRuntimeAuthRouteEffective(
codexProvider,
representative?.routeAuth.evaluation,
);
const availabilities = usages.map((usage) => usage.routeAuth.evaluation.availability);
return [
`${provider}:codex:${codexProvider}`,
{
provider,
runtime: "codex",
authProvider: codexProvider,
status: availabilities.every((availability) => availability === true)
const runtimeAuthRouteEntries: Array<readonly [string, StatusRuntimeAuthRoute]> = [
...Array.from(codexRuntimeUsagesByProvider.entries()).map(([provider, usages]) => {
const representative =
usages.find((usage) => usage.routeAuth.evaluation.availability === true) ?? usages[0];
const effective = resolveRuntimeAuthRouteEffective(
codexProvider,
representative?.routeAuth.evaluation,
);
const availabilities = usages.map((usage) => usage.routeAuth.evaluation.availability);
const authStatus = availabilities.every((availability) => availability === true)
? "usable"
: availabilities.some((availability) => availability === false)
? "missing"
: "indeterminate";
const runtimeAvailability = representative?.routeAuth.runtimeAvailability;
const route: StatusRuntimeAuthRoute =
runtimeAvailability?.status === "unavailable"
? {
provider,
runtime: "codex",
authProvider: codexProvider,
status: "unavailable",
effective,
authStatus,
runtimeStatus: runtimeAvailability.status,
runtimeReason: runtimeAvailability.reason,
runtimeDetail: runtimeAvailability.detail,
runtimePluginIds: runtimeAvailability.ownerPluginIds,
}
: {
provider,
runtime: "codex",
authProvider: codexProvider,
status: authStatus,
effective,
};
return [`${provider}:codex:${codexProvider}`, route] as const;
}),
...cliRuntimeAuthUsages.map((usage) => {
const evaluation = authResolver.evaluateModelAuth(usage.runtime);
const effective = resolveRuntimeAuthRouteEffective(usage.runtime, evaluation);
return [
`${usage.provider}:${usage.runtime}:${usage.runtime}`,
{
provider: usage.provider,
runtime: usage.runtime,
authProvider: usage.runtime,
status:
evaluation.availability === true
? "usable"
: availabilities.some((availability) => availability === false)
: evaluation.availability === false
? "missing"
: "indeterminate",
effective,
},
] as const;
}),
...cliRuntimeAuthUsages.map((usage) => {
const evaluation = authResolver.evaluateModelAuth(usage.runtime);
const effective = resolveRuntimeAuthRouteEffective(usage.runtime, evaluation);
return [
`${usage.provider}:${usage.runtime}:${usage.runtime}`,
{
provider: usage.provider,
runtime: usage.runtime,
authProvider: usage.runtime,
status:
evaluation.availability === true
? "usable"
: evaluation.availability === false
? "missing"
: "indeterminate",
effective,
},
] as const;
}),
]).values(),
effective,
},
] as const;
}),
];
const runtimeAuthRoutes = Array.from(
new Map<string, StatusRuntimeAuthRoute>(runtimeAuthRouteEntries).values(),
).toSorted((a, b) => a.provider.localeCompare(b.provider));
const modelRouteIssues = providerUses.flatMap<StatusModelRouteIssue>((usage) => {
const cliRuntimeAuthProvider = resolveCliRuntimeAuthProvider(usage);
@@ -1172,6 +1271,7 @@ export async function modelsStatusCommand(
) ||
routeAuthHealth.has("missing") ||
routeAuthHealth.has("indeterminate") ||
runtimeAuthRoutes.some((route) => route.status === "unavailable") ||
missingProvidersInUse.length > 0;
const hasExpiring = routeAuthHealth.has("expiring");
if (hasExpiredOrMissing) {
@@ -1410,6 +1510,17 @@ export async function modelsStatusCommand(
runtime.log("");
runtime.log(colorize(rich, theme.heading, "Runtime auth"));
for (const route of runtimeAuthRoutes) {
const runtimeAvailability =
route.status === "unavailable"
? `${formatSeparator()}${formatKeyValue(
"auth",
route.authStatus,
)}${formatSeparator()}${formatKeyValue("runtime", route.runtimeStatus)}${
route.runtimeDetail
? `${formatSeparator()}${colorize(rich, theme.muted, route.runtimeDetail)}`
: ""
}`
: "";
runtime.log(
`- ${theme.heading(route.provider)} via ${colorize(
rich,
@@ -1422,7 +1533,7 @@ export async function modelsStatusCommand(
theme.muted,
route.effective.detail,
)}`,
)}${formatSeparator()}${formatKeyValue("status", route.status)}`,
)}${formatSeparator()}${formatKeyValue("status", route.status)}${runtimeAvailability}`,
);
}
}
+99
View File
@@ -154,6 +154,14 @@ const mocks = vi.hoisted(() => {
loadProviderUsageSummary: vi.fn().mockResolvedValue(undefined),
resolveRuntimeSyntheticAuthProviderRefs: vi.fn().mockReturnValue([]),
resolveProviderSyntheticAuthWithPlugin: vi.fn().mockReturnValue(undefined),
resolveAgentHarnessOwnerPluginIds: vi.fn().mockReturnValue(["codex"]),
runPluginPayloadSmokeCheckForManifestRecords: vi
.fn()
.mockResolvedValue({ checked: ["codex"], failures: [] }),
resolveAgentHarnessRuntimeAvailability: vi.fn().mockReturnValue({
status: "available",
ownerPluginIds: ["codex"],
}),
loadModelCatalog: vi.fn().mockResolvedValue([]),
modelCatalogRouteVariants: undefined as unknown[] | undefined,
openAIModelRouteOverride: undefined as ((params: unknown) => unknown) | undefined,
@@ -276,6 +284,13 @@ vi.mock("../../plugins/synthetic-auth.runtime.js", () => ({
vi.mock("../../plugins/provider-runtime.js", () => ({
resolveProviderSyntheticAuthWithPlugin: mocks.resolveProviderSyntheticAuthWithPlugin,
}));
vi.mock("../../agents/harness/runtime-plugin.js", () => ({
resolveAgentHarnessOwnerPluginIds: mocks.resolveAgentHarnessOwnerPluginIds,
resolveAgentHarnessRuntimeAvailability: mocks.resolveAgentHarnessRuntimeAvailability,
}));
vi.mock("../../cli/update-cli/plugin-payload-validation.js", () => ({
runPluginPayloadSmokeCheckForManifestRecords: mocks.runPluginPayloadSmokeCheckForManifestRecords,
}));
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalogSnapshot: async (...args: unknown[]) => {
const entries = await mocks.loadModelCatalog(...args);
@@ -770,6 +785,90 @@ describe("modelsStatusCommand auth overview", () => {
expect(textRuntime.log.mock.calls.flat().join("\n")).not.toContain("set an API key env var");
});
it("reports usable Codex auth as unavailable when its harness plugin is quarantined", async () => {
const localRuntime = createRuntime();
const textRuntime = createRuntime();
const payloadFailure = {
pluginId: "codex",
installPath: "/private/plugin",
reason: "missing-package-dir" as const,
detail: "missing",
};
mocks.runPluginPayloadSmokeCheckForManifestRecords
.mockResolvedValueOnce({ checked: ["codex"], failures: [payloadFailure] })
.mockResolvedValueOnce({ checked: ["codex"], failures: [payloadFailure] });
const resolveAvailability = (params: {
payloadFailures: Array<{ pluginId: string; reason: string }>;
}) =>
params.payloadFailures.some((failure) => failure.pluginId === "codex")
? {
status: "unavailable",
ownerPluginIds: ["codex", "openai"],
reason: "owner-plugin-degraded",
detail:
'Agent harness "codex" owner plugin "codex" is unavailable (missing-package-dir).',
}
: { status: "available", ownerPluginIds: ["codex", "openai"] };
mocks.resolveAgentHarnessRuntimeAvailability
.mockImplementationOnce(resolveAvailability)
.mockImplementationOnce(resolveAvailability);
await withOpenAIStatusFixture(
{
primary: "openai/gpt-5.5",
profiles: {
"openai:default": {
type: "oauth",
provider: "openai",
access: "oauth-access",
refresh: "oauth-refresh",
expires: Date.now() + 60_000,
},
},
},
async () => {
await modelsStatusCommand({ json: true, check: true }, localRuntime as never);
await modelsStatusCommand({ check: true }, textRuntime as never);
},
);
expect(mocks.runPluginPayloadSmokeCheckForManifestRecords).toHaveBeenCalledWith(
expect.objectContaining({ env: process.env }),
);
expect(mocks.resolveAgentHarnessRuntimeAvailability).toHaveBeenCalledWith(
expect.objectContaining({
runtime: "codex",
provider: "openai",
payloadFailures: [payloadFailure],
payloadCheckedPluginIds: ["codex"],
selectedPluginRootDirs: expect.any(Map),
}),
);
const payload = parseFirstJsonLog(localRuntime);
expect(payload.auth.runtimeAuthRoutes).toEqual([
{
provider: "openai",
runtime: "codex",
authProvider: "openai",
status: "unavailable",
authStatus: "usable",
runtimeStatus: "unavailable",
runtimeReason: "owner-plugin-degraded",
runtimeDetail:
'Agent harness "codex" owner plugin "codex" is unavailable (missing-package-dir).',
runtimePluginIds: ["codex", "openai"],
effective: {
kind: "profiles",
detail: "/tmp/openclaw-agent/auth-profiles.json",
},
},
]);
expect(localRuntime.exit).toHaveBeenCalledWith(1);
expect(textRuntime.exit).toHaveBeenCalledWith(1);
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("status=unavailable");
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("auth=usable");
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("runtime=unavailable");
});
it("evaluates mixed primary and fallback OpenAI routes independently", async () => {
const localRuntime = createRuntime();
await withOpenAIStatusFixture(