fix(models): unconditionally suppress stale openai-codex/gpt-5.4-mini inline entries (#74451) (#74655)

* fix(models): block stale openai-codex/gpt-5.4-mini inline entries via unconditional suppression (#74451)

Suppress explicitly user-configured openai-codex/gpt-5.4-mini inline entries
so a stale models config written by `openclaw doctor --fix` cannot bypass the
manifest capability block and cause repeated assistant-turn failures when the
runtime switches to that model on ChatGPT-backed Codex accounts.

Adds `unconditionalOnly` flag to `buildManifestBuiltInModelSuppressionResolver`
and a `shouldUnconditionallySuppress` helper. Inside `resolveExplicitModelWithRegistry`,
inline matches are now gated on unconditional suppressions (no `when` clause)
before returning. Conditional suppressions such as the qwen Coding Plan endpoint
guard remain bypassable by explicit user configuration, preserving the existing
`resolves explicitly configured qwen3.6-plus before Coding Plan built-in suppression`
behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(changelog): add missing reporter attribution for #74451 models suppression fix

* docs: credit codex mini suppression contributors

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
This commit is contained in:
hcl
2026-04-30 09:02:23 +08:00
committed by GitHub
parent 9b1bde2561
commit 1fb096f0e6
5 changed files with 72 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Models: suppress explicitly configured openai-codex/gpt-5.4-mini inline entries so a stale models config written by `openclaw doctor --fix` cannot bypass the manifest capability block and cause repeated assistant-turn failures when the runtime switches to that model on ChatGPT-backed Codex accounts. Conditional suppressions (e.g. qwen Coding Plan endpoint guards) remain bypassable by explicit user configuration. (#74451) Thanks @0xCyda, @hclsys, and @Marvae.
- Dependencies: refresh workspace runtime, plugin, and tooling packages, including ACP, Pi, AWS SDK, TypeBox, pnpm, oxlint, oxfmt, jsdom, pdfjs, ciao, and tokenjuice, while keeping patched ACP behavior and lint gates current. Thanks @mariozechner.
- Messages/queue: make `steer` drain all pending Pi steering messages at the next model boundary, keep legacy one-at-a-time steering as `queue`, and add a dedicated steering queue docs page. Thanks @vincentkoc.
- Messages/queue: default active-run queueing to `steer` with a 500ms followup fallback debounce, and document the queue modes, precedence, and drop policies on the command queue page. Thanks @vincentkoc.

View File

@@ -11,6 +11,7 @@ function resolveBuiltInModelSuppressionFromManifest(params: {
id?: string | null;
baseUrl?: string | null;
config?: OpenClawConfig;
unconditionalOnly?: boolean;
}) {
const provider = normalizeProviderId(params.provider ?? "");
const modelId = normalizeLowercaseStringOrEmpty(params.id);
@@ -22,6 +23,7 @@ function resolveBuiltInModelSuppressionFromManifest(params: {
id: modelId,
...(params.config ? { config: params.config } : {}),
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
unconditionalOnly: params.unconditionalOnly,
env: process.env,
});
}
@@ -61,6 +63,20 @@ export function shouldSuppressBuiltInModel(params: {
return resolveBuiltInModelSuppression(params)?.suppress ?? false;
}
// Checks only unconditional suppressions (no `when` clause). Used for inline
// model entries where user configuration may override conditional suppressions
// (e.g. custom endpoint overrides) but not absolute provider capability blocks.
export function shouldUnconditionallySuppress(params: {
provider?: string | null;
id?: string | null;
config?: OpenClawConfig;
}): boolean {
return (
resolveBuiltInModelSuppressionFromManifest({ ...params, unconditionalOnly: true })?.suppress ??
false
);
}
export function buildSuppressedBuiltInModelError(params: {
provider?: string | null;
id?: string | null;

View File

@@ -69,6 +69,20 @@ vi.mock("../model-suppression.js", () => {
isQwenCodingPlanBaseUrl(baseUrl ?? resolveConfiguredQwenBaseUrl(config))
);
},
shouldUnconditionallySuppress: ({ provider, id }: { provider?: string; id?: string }) => {
if (
(provider === "openai" ||
provider === "azure-openai-responses" ||
provider === "openai-codex") &&
id?.trim().toLowerCase() === "gpt-5.3-codex-spark"
) {
return true;
}
if (provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini") {
return true;
}
return false;
},
buildSuppressedBuiltInModelError: ({
provider,
id,
@@ -355,6 +369,34 @@ describe("resolveModel", () => {
);
});
it("#74451: suppresses explicitly configured openai-codex/gpt-5.4-mini despite inline entry", () => {
const cfg = {
models: {
providers: {
"openai-codex": {
api: "openai-codex-responses",
models: [
{
id: "gpt-5.4-mini",
name: "GPT-5.4 mini",
api: "openai-codex-responses",
contextWindow: 400_000,
maxTokens: 128_000,
},
],
},
},
},
} as unknown as OpenClawConfig;
const result = resolveModelForTest("openai-codex", "gpt-5.4-mini", "/tmp/agent", cfg);
expect(result.model).toBeUndefined();
expect(result.error).toBe(
"Unknown model: openai-codex/gpt-5.4-mini. gpt-5.4-mini is not supported by the OpenAI Codex OAuth route. Use openai/gpt-5.4-mini with an OpenAI API key or openai-codex/gpt-5.5 with Codex OAuth.",
);
});
it("normalizes Google fallback baseUrls for custom providers", () => {
const cfg = {
models: {

View File

@@ -25,6 +25,7 @@ import { findNormalizedProviderValue, normalizeProviderId } from "../model-selec
import {
buildSuppressedBuiltInModelError,
shouldSuppressBuiltInModel,
shouldUnconditionallySuppress,
} from "../model-suppression.js";
import { isLegacyModelsAddCodexMetadataModel } from "../openai-codex-models-add-legacy.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
@@ -614,6 +615,14 @@ function resolveExplicitModelWithRegistry(params: {
modelId,
});
if (inlineMatch?.api) {
// Unconditional suppressions (no `when` clause) represent absolute provider
// capability blocks that cannot be overridden by inline user configuration.
// Conditional suppressions (e.g. baseUrlHosts-gated qwen restrictions) are
// intentionally bypassable when the user has explicitly configured the model.
// (#74451)
if (shouldUnconditionallySuppress({ provider, id: modelId, config: cfg })) {
return { kind: "suppressed" };
}
const resolvedParams = mergeConfiguredRuntimeModelParams({
cfg,
provider,

View File

@@ -118,6 +118,7 @@ export function buildManifestBuiltInModelSuppressionResolver(params: {
provider?: string | null;
id?: string | null;
baseUrl?: string | null;
unconditionalOnly?: boolean;
}) => {
const provider = normalizeLowercaseStringOrEmpty(input.provider);
const modelId = normalizeLowercaseStringOrEmpty(input.id);
@@ -128,6 +129,7 @@ export function buildManifestBuiltInModelSuppressionResolver(params: {
const suppression = suppressions.find(
(entry) =>
entry.mergeKey === mergeKey &&
(!input.unconditionalOnly || !entry.when) &&
manifestSuppressionMatchesConditions({
suppression: entry,
provider,
@@ -163,6 +165,7 @@ export function resolveManifestBuiltInModelSuppression(params: {
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
baseUrl?: string | null;
unconditionalOnly?: boolean;
}) {
const resolver = buildManifestBuiltInModelSuppressionResolver({
config: params.config,
@@ -173,5 +176,6 @@ export function resolveManifestBuiltInModelSuppression(params: {
provider: params.provider,
id: params.id,
baseUrl: params.baseUrl,
unconditionalOnly: params.unconditionalOnly,
});
}