Let Codex use newly available account models before catalog refresh (#127394)

* Let Codex run newly available account models without host credentials

* Keep authored provider routes ahead of native account authentication

* Keep native Codex account discovery consistent with model availability

* Preserve advanced reasoning for newly discovered Codex models
This commit is contained in:
pash-openai
2026-08-21 12:30:54 -07:00
committed by GitHub
parent 0b85966072
commit 4573635c77
11 changed files with 171 additions and 4 deletions
+11
View File
@@ -262,6 +262,17 @@ describe("Codex agent harness supports()", () => {
).toEqual({ supported: true, priority: 100 });
});
it("lets explicit Codex model discovery run before auth has been prepared", () => {
expect(
harness.supports({
provider: "openai",
modelId: "gpt-future",
requestedRuntime: "codex",
modelProvider: { requestTransportOverrides: "none" },
}),
).toEqual({ supported: true, priority: 100 });
});
it.each([
{
label: "automatic runtime selection",
+3 -3
View File
@@ -210,9 +210,9 @@ export function createCodexAppServerAgentHarness(
provider === "openai" &&
ctx.requestedRuntime === "codex" &&
Boolean(ctx.modelId?.trim()) &&
preparedAuth?.source === "harness" &&
preparedAuth.mode === undefined &&
preparedAuth.requirement === undefined &&
(preparedAuth === undefined || preparedAuth.source === "harness") &&
preparedAuth?.mode === undefined &&
preparedAuth?.requirement === undefined &&
ctx.modelProvider?.api === undefined &&
ctx.modelProvider?.baseUrl === undefined &&
ctx.modelProvider?.azureApiVersion === undefined &&
@@ -327,6 +327,25 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
maxTokens: OPENAI_CODEX_GPT_54_MAX_TOKENS,
cost: OPENAI_CODEX_GPT_54_MINI_COST,
};
} else if (
ctx.agentRuntimeId === "codex" &&
ctx.authProfileId === undefined &&
ctx.authProfileMode === undefined &&
ctx.providerConfig?.auth === undefined
) {
// Codex owns its account-scoped model catalog. When that catalog is not yet
// available, keep the requested identity intact and let the native runtime
// decide whether the account can actually use it.
templateIds = OPENAI_CODEX_GPT_56_MODEL_IDS;
patch = {
reasoning: true,
input: ["text", "image"],
thinkingLevelMap: OPENAI_CODEX_GPT_56_THINKING_LEVEL_MAP,
compat: {
supportsReasoningEffort: true,
supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
},
};
} else {
return undefined;
}
@@ -365,6 +384,8 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
contextWindow: patch?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
contextTokens: patch?.contextTokens,
maxTokens: patch?.maxTokens ?? DEFAULT_CONTEXT_TOKENS,
...(patch?.thinkingLevelMap ? { thinkingLevelMap: patch.thinkingLevelMap } : {}),
...(patch?.compat ? { compat: patch.compat } : {}),
} as ProviderRuntimeModel)
);
}
+68
View File
@@ -1766,6 +1766,74 @@ describe("buildOpenAIProvider", () => {
).toEqual({ effort: "high", transport: "sse" });
});
it("delegates an unlisted first-party model to its explicitly selected Codex runtime", () => {
const provider = buildOpenAIProvider();
const model = provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-future",
modelRegistry: { find: () => null },
agentRuntimeId: "codex",
} as never);
expect(model).toMatchObject({
provider: "openai",
id: "gpt-future",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max"] },
});
expect(
provider
.resolveThinkingProfile?.({
provider: "openai",
modelId: "gpt-future",
agentRuntime: "codex",
api: model?.api,
compat: model?.compat,
} as never)
?.levels.map((level) => level.id),
).toContain("max");
expect(
provider
.resolveThinkingProfile?.({
provider: "openai",
modelId: "gpt-future",
agentRuntime: "codex",
} as never)
?.levels.map((level) => level.id),
).toEqual(expect.arrayContaining(["xhigh", "max"]));
});
it("does not invent an unlisted model for authored Platform credentials", () => {
const provider = buildOpenAIProvider();
expect(
provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-future",
modelRegistry: { find: () => null },
agentRuntimeId: "codex",
authProfileId: "openai:platform",
authProfileMode: "api_key",
providerConfig: {
auth: "api-key",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
},
} as never),
).toBeUndefined();
expect(
provider
.resolveThinkingProfile?.({
provider: "openai",
modelId: "gpt-future",
agentRuntime: "codex",
api: "openai-responses",
} as never)
?.levels.map((level) => level.id),
).not.toContain("max");
});
it("restores gpt-5.3-codex-spark only through ChatGPT/Codex OAuth routing", () => {
const provider = buildOpenAIProvider();
+9
View File
@@ -110,6 +110,12 @@ function buildOpenAIThinkingProfile(params: {
(agentRuntime === "openclaw" ||
agentRuntime === "auto" ||
(agentRuntime === "codex" && codexSupportsUltra));
const nativeCodexNeedsAccountEffortValidation =
agentRuntime === "codex" &&
params.compat?.supportedReasoningEfforts === undefined &&
(params.api === undefined || params.api === "openai-chatgpt-responses") &&
!matchesExactOrPrefix(params.modelId, params.xhighModelIds) &&
!modelId.startsWith("gpt-5.6");
const defaultLevel = isGpt56Variant ? "medium" : undefined;
const fallbackLevels: ProviderThinkingProfile["levels"] = [
...OPENAI_THINKING_BASE_LEVELS,
@@ -118,6 +124,9 @@ function buildOpenAIThinkingProfile(params: {
: []),
...(supportsMax ? [{ id: "max" as const }] : []),
...(supportsUltra ? [{ id: "ultra" as const }] : []),
...(nativeCodexNeedsAccountEffortValidation
? [{ id: "xhigh" as const }, { id: "max" as const }]
: []),
];
const levels =
agentRuntime === "codex" && resolvedCodexEfforts !== undefined
@@ -875,6 +875,15 @@ describe("createModelAuthAvailabilityResolver", () => {
expect(result).not.toHaveProperty("selectedRoute");
});
it("keeps one unconfigured Codex route indeterminate until native account validation", () => {
expect(
evaluate({
resolution: { ...dualRoutes, routes: [subscriptionRoute] },
syntheticAuthProviderRefs: ["codex"],
}),
).toMatchObject({ availability: undefined, evidence: "synthetic" });
});
it("does not let invalid automatic profile evidence block synthetic Codex ownership", () => {
expect(
evaluate({
+4
View File
@@ -1106,6 +1106,10 @@ export function createModelAuthAvailabilityResolver(
sourcePlan,
configuredAuthMode: automaticRouteAuthMode,
...(syntheticCodexOwnsAuth ? { runtimeAuthOwner: { id: "codex" } } : {}),
...(syntheticCodexOwnsAuth &&
resolveMergedModelProviderConfig(params.cfg, provider) === undefined
? { allowNativeAuthOnSingleRoute: true }
: {}),
});
if (routeAuthDecision.kind === "deferred" && syntheticCodexOwnsAuth) {
return { availability: undefined, routeResolution, evidence: "synthetic" };
+1
View File
@@ -59,6 +59,7 @@ export function selectOpenAIModelRouteAuth(params: {
sourcePlan: ProviderModelAuthSourcePlan;
configuredAuthMode?: string;
runtimeAuthOwner?: { id: string };
allowNativeAuthOnSingleRoute?: boolean;
}) {
return selectProviderModelRouteAuth({ provider: OPENAI_PROVIDER_ID, ...params });
}
@@ -336,6 +336,36 @@ describe("provider model route auth", () => {
});
});
it("lets an explicit native owner authenticate its sole compatible route", () => {
expect(
selectProviderModelRouteAuth({
provider: "openai",
resolution: { ...routes, routes: [routes.routes[1]] },
runtimeAuthOwner: { id: "codex" },
allowNativeAuthOnSingleRoute: true,
sourcePlan: buildProviderModelAuthSourcePlan({ profiles: [] }),
}),
).toEqual({
kind: "deferred",
reason: "runtime-auth-owner",
routeSupport: {
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
},
});
});
it("does not infer native ownership for an explicitly authored single route", () => {
expect(
selectProviderModelRouteAuth({
provider: "openai",
resolution: { ...routes, routes: [routes.routes[1]] },
runtimeAuthOwner: { id: "codex" },
sourcePlan: buildProviderModelAuthSourcePlan({ profiles: [] }),
}),
).toMatchObject({ kind: "rejected", reason: "configured-auth" });
});
it.each([
["Platform", routes.routes[0], profile("openai:chatgpt", "oauth", "ready")],
["subscription", routes.routes[1], profile("openai:platform", "api_key", "ready")],
+12 -1
View File
@@ -285,6 +285,8 @@ export function selectProviderModelRouteAuth(params: {
configuredAuthMode?: string;
/** Explicit native auth owner allowed to defer an otherwise unowned route. */
runtimeAuthOwner?: { id: string };
/** True only when no provider transport or credentials were authored. */
allowNativeAuthOnSingleRoute?: boolean;
}): ProviderModelRouteAuthDecision {
const requiredProfile =
params.sourcePlan.kind === "required" && params.sourcePlan.source.kind === "profile"
@@ -412,7 +414,16 @@ export function selectProviderModelRouteAuth(params: {
const runtimeAuthOwnerIsCompatible =
Boolean(normalizedRuntimeAuthOwner) &&
routeSupport.runtimePolicy.compatibleIds.includes(normalizedRuntimeAuthOwner ?? "");
if (params.resolution.routes.length > 1 && runtimeAuthOwnerIsCompatible && !configuredRoute) {
const hostHasNoCredentialToHonor =
params.allowNativeAuthOnSingleRoute === true &&
params.sourcePlan.kind === "automatic" &&
params.sourcePlan.orderedProfiles.length === 0 &&
params.sourcePlan.fallback === undefined;
if (
runtimeAuthOwnerIsCompatible &&
!configuredRoute &&
(params.resolution.routes.length > 1 || hostHasNoCredentialToHonor)
) {
return { kind: "deferred", reason: "runtime-auth-owner", routeSupport };
}
return reject(
+3
View File
@@ -520,6 +520,9 @@ export function prepareAgentRuntimeAuth(
sourcePlan,
configuredAuthMode: automaticRouteAuthMode,
...(runtimeAuthOwner ? { runtimeAuthOwner } : {}),
...(runtimeAuthOwner && configuredProvider === undefined
? { allowNativeAuthOnSingleRoute: true }
: {}),
});
if (routeAuthDecision.kind === "deferred") {
const plan = buildAgentRuntimeAuthPlan({