Skip to content

Commit 1fb096f

Browse files
hclsysclaudeshakkernerd
authored
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>
1 parent 9b1bde2 commit 1fb096f

5 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- 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.
910
- 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.
1011
- 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.
1112
- 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.

src/agents/model-suppression.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function resolveBuiltInModelSuppressionFromManifest(params: {
1111
id?: string | null;
1212
baseUrl?: string | null;
1313
config?: OpenClawConfig;
14+
unconditionalOnly?: boolean;
1415
}) {
1516
const provider = normalizeProviderId(params.provider ?? "");
1617
const modelId = normalizeLowercaseStringOrEmpty(params.id);
@@ -22,6 +23,7 @@ function resolveBuiltInModelSuppressionFromManifest(params: {
2223
id: modelId,
2324
...(params.config ? { config: params.config } : {}),
2425
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
26+
unconditionalOnly: params.unconditionalOnly,
2527
env: process.env,
2628
});
2729
}
@@ -61,6 +63,20 @@ export function shouldSuppressBuiltInModel(params: {
6163
return resolveBuiltInModelSuppression(params)?.suppress ?? false;
6264
}
6365

66+
// Checks only unconditional suppressions (no `when` clause). Used for inline
67+
// model entries where user configuration may override conditional suppressions
68+
// (e.g. custom endpoint overrides) but not absolute provider capability blocks.
69+
export function shouldUnconditionallySuppress(params: {
70+
provider?: string | null;
71+
id?: string | null;
72+
config?: OpenClawConfig;
73+
}): boolean {
74+
return (
75+
resolveBuiltInModelSuppressionFromManifest({ ...params, unconditionalOnly: true })?.suppress ??
76+
false
77+
);
78+
}
79+
6480
export function buildSuppressedBuiltInModelError(params: {
6581
provider?: string | null;
6682
id?: string | null;

src/agents/pi-embedded-runner/model.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ vi.mock("../model-suppression.js", () => {
6969
isQwenCodingPlanBaseUrl(baseUrl ?? resolveConfiguredQwenBaseUrl(config))
7070
);
7171
},
72+
shouldUnconditionallySuppress: ({ provider, id }: { provider?: string; id?: string }) => {
73+
if (
74+
(provider === "openai" ||
75+
provider === "azure-openai-responses" ||
76+
provider === "openai-codex") &&
77+
id?.trim().toLowerCase() === "gpt-5.3-codex-spark"
78+
) {
79+
return true;
80+
}
81+
if (provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini") {
82+
return true;
83+
}
84+
return false;
85+
},
7286
buildSuppressedBuiltInModelError: ({
7387
provider,
7488
id,
@@ -355,6 +369,34 @@ describe("resolveModel", () => {
355369
);
356370
});
357371

372+
it("#74451: suppresses explicitly configured openai-codex/gpt-5.4-mini despite inline entry", () => {
373+
const cfg = {
374+
models: {
375+
providers: {
376+
"openai-codex": {
377+
api: "openai-codex-responses",
378+
models: [
379+
{
380+
id: "gpt-5.4-mini",
381+
name: "GPT-5.4 mini",
382+
api: "openai-codex-responses",
383+
contextWindow: 400_000,
384+
maxTokens: 128_000,
385+
},
386+
],
387+
},
388+
},
389+
},
390+
} as unknown as OpenClawConfig;
391+
392+
const result = resolveModelForTest("openai-codex", "gpt-5.4-mini", "/tmp/agent", cfg);
393+
394+
expect(result.model).toBeUndefined();
395+
expect(result.error).toBe(
396+
"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.",
397+
);
398+
});
399+
358400
it("normalizes Google fallback baseUrls for custom providers", () => {
359401
const cfg = {
360402
models: {

src/agents/pi-embedded-runner/model.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { findNormalizedProviderValue, normalizeProviderId } from "../model-selec
2525
import {
2626
buildSuppressedBuiltInModelError,
2727
shouldSuppressBuiltInModel,
28+
shouldUnconditionallySuppress,
2829
} from "../model-suppression.js";
2930
import { isLegacyModelsAddCodexMetadataModel } from "../openai-codex-models-add-legacy.js";
3031
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
@@ -614,6 +615,14 @@ function resolveExplicitModelWithRegistry(params: {
614615
modelId,
615616
});
616617
if (inlineMatch?.api) {
618+
// Unconditional suppressions (no `when` clause) represent absolute provider
619+
// capability blocks that cannot be overridden by inline user configuration.
620+
// Conditional suppressions (e.g. baseUrlHosts-gated qwen restrictions) are
621+
// intentionally bypassable when the user has explicitly configured the model.
622+
// (#74451)
623+
if (shouldUnconditionallySuppress({ provider, id: modelId, config: cfg })) {
624+
return { kind: "suppressed" };
625+
}
617626
const resolvedParams = mergeConfiguredRuntimeModelParams({
618627
cfg,
619628
provider,

src/plugins/manifest-model-suppression.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export function buildManifestBuiltInModelSuppressionResolver(params: {
118118
provider?: string | null;
119119
id?: string | null;
120120
baseUrl?: string | null;
121+
unconditionalOnly?: boolean;
121122
}) => {
122123
const provider = normalizeLowercaseStringOrEmpty(input.provider);
123124
const modelId = normalizeLowercaseStringOrEmpty(input.id);
@@ -128,6 +129,7 @@ export function buildManifestBuiltInModelSuppressionResolver(params: {
128129
const suppression = suppressions.find(
129130
(entry) =>
130131
entry.mergeKey === mergeKey &&
132+
(!input.unconditionalOnly || !entry.when) &&
131133
manifestSuppressionMatchesConditions({
132134
suppression: entry,
133135
provider,
@@ -163,6 +165,7 @@ export function resolveManifestBuiltInModelSuppression(params: {
163165
workspaceDir?: string;
164166
env?: NodeJS.ProcessEnv;
165167
baseUrl?: string | null;
168+
unconditionalOnly?: boolean;
166169
}) {
167170
const resolver = buildManifestBuiltInModelSuppressionResolver({
168171
config: params.config,
@@ -173,5 +176,6 @@ export function resolveManifestBuiltInModelSuppression(params: {
173176
provider: params.provider,
174177
id: params.id,
175178
baseUrl: params.baseUrl,
179+
unconditionalOnly: params.unconditionalOnly,
176180
});
177181
}

0 commit comments

Comments
 (0)