mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
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:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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")],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -520,6 +520,9 @@ export function prepareAgentRuntimeAuth(
|
||||
sourcePlan,
|
||||
configuredAuthMode: automaticRouteAuthMode,
|
||||
...(runtimeAuthOwner ? { runtimeAuthOwner } : {}),
|
||||
...(runtimeAuthOwner && configuredProvider === undefined
|
||||
? { allowNativeAuthOnSingleRoute: true }
|
||||
: {}),
|
||||
});
|
||||
if (routeAuthDecision.kind === "deferred") {
|
||||
const plan = buildAgentRuntimeAuthPlan({
|
||||
|
||||
Reference in New Issue
Block a user