Skip to content

Commit 3ce9224

Browse files
authored
fix: load Codex for selectable OpenAI agent models
Treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair.\n\nPR: #81591
1 parent 97ed9b2 commit 3ce9224

7 files changed

Lines changed: 229 additions & 35 deletions

CHANGELOG.md

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

1111
### Fixes
1212

13+
- Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly.
1314
- Sessions/status: classify ACP spawn-child sessions as `kind: "spawn-child"` instead of `"direct"` in `openclaw sessions` and status output; extract the duplicated session-kind classifier into a shared helper (`src/sessions/classify-session-kind.ts`) so both surfaces stay in sync. Fixes catalog #19. (#79544)
1415
- Telegram: delete tool-progress-only draft bubbles before rotating to the real answer, preventing orphaned progress messages in streamed replies.
1516
- Codex app-server: keep per-agent `CODEX_HOME` isolation without rewriting `HOME` by default, so Codex-run subprocesses can still find normal user-home config, tokens, and CLI state unless the launch explicitly overrides `HOME`. Thanks @pashpashpash.

src/agents/harness-runtimes.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,86 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import { collectConfiguredAgentHarnessRuntimes } from "./harness-runtimes.js";
44

55
describe("collectConfiguredAgentHarnessRuntimes", () => {
6+
it("requires Codex for selectable default OpenAI agent models", () => {
7+
const config = {
8+
agents: {
9+
defaults: {
10+
model: { primary: "anthropic/claude-sonnet-4-6" },
11+
models: {
12+
"openai/gpt-5.5": {},
13+
},
14+
},
15+
},
16+
} as OpenClawConfig;
17+
18+
expect(collectConfiguredAgentHarnessRuntimes(config, {}, { includeEnvRuntime: false })).toEqual(
19+
["codex"],
20+
);
21+
});
22+
23+
it("requires Codex for selectable per-agent OpenAI models", () => {
24+
const config = {
25+
agents: {
26+
defaults: {
27+
model: { primary: "anthropic/claude-sonnet-4-6" },
28+
},
29+
list: [
30+
{
31+
id: "worker",
32+
models: {
33+
"openai/gpt-5.5": {},
34+
},
35+
},
36+
],
37+
},
38+
} as OpenClawConfig;
39+
40+
expect(collectConfiguredAgentHarnessRuntimes(config, {}, { includeEnvRuntime: false })).toEqual(
41+
["codex"],
42+
);
43+
});
44+
45+
it("respects explicit Pi runtime policy on selectable OpenAI agent models", () => {
46+
const config = {
47+
agents: {
48+
defaults: {
49+
model: { primary: "anthropic/claude-sonnet-4-6" },
50+
models: {
51+
"openai/gpt-5.5": { agentRuntime: { id: "pi" } },
52+
},
53+
},
54+
},
55+
} as OpenClawConfig;
56+
57+
expect(collectConfiguredAgentHarnessRuntimes(config, {}, { includeEnvRuntime: false })).toEqual(
58+
[],
59+
);
60+
});
61+
62+
it("does not infer Codex for custom OpenAI-compatible base URLs", () => {
63+
const config = {
64+
models: {
65+
providers: {
66+
openai: {
67+
baseUrl: "https://openai-compatible.example.test/v1",
68+
models: [],
69+
},
70+
},
71+
},
72+
agents: {
73+
defaults: {
74+
models: {
75+
"openai/gpt-5.5": {},
76+
},
77+
},
78+
},
79+
} as OpenClawConfig;
80+
81+
expect(collectConfiguredAgentHarnessRuntimes(config, {}, { includeEnvRuntime: false })).toEqual(
82+
[],
83+
);
84+
});
85+
686
it("ignores malformed agents.list while scanning best-effort config", () => {
787
const config = {
888
agents: {

src/agents/harness-runtimes.ts

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
33
import { isRecord } from "../utils.js";
4-
import { resolveModelRuntimePolicy } from "./model-runtime-policy.js";
5-
import { modelSelectionShouldEnsureCodexPlugin } from "./openai-codex-routing.js";
4+
import { resolveAgentHarnessPolicy } from "./harness/policy.js";
65
import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js";
76
import { normalizeProviderId } from "./provider-id.js";
87

@@ -38,6 +37,12 @@ function listAgentModelRefs(value: unknown): string[] {
3837
return refs;
3938
}
4039

40+
function pushAgentModelRefs(refs: string[], value: unknown): void {
41+
for (const ref of listAgentModelRefs(value)) {
42+
refs.push(ref);
43+
}
44+
}
45+
4146
function parseConfiguredModelRef(
4247
value: unknown,
4348
): { provider: string; modelId: string } | undefined {
@@ -55,21 +60,23 @@ function parseConfiguredModelRef(
5560
};
5661
}
5762

58-
function hasOpenAIModelRef(config: OpenClawConfig, value: unknown, agentId?: string): boolean {
59-
return listAgentModelRefs(value).some((ref) => {
60-
if (!modelSelectionShouldEnsureCodexPlugin({ model: ref, config })) {
61-
return false;
62-
}
63-
const parsed = parseConfiguredModelRef(ref);
64-
const policy = resolveModelRuntimePolicy({
65-
config,
66-
provider: parsed?.provider,
67-
modelId: parsed?.modelId,
68-
agentId,
69-
});
70-
const runtime = normalizeRuntimeId(policy.policy?.id);
71-
return !runtime || runtime === "auto" || runtime === "codex";
63+
function resolveConfiguredModelHarnessRuntime(params: {
64+
config: OpenClawConfig;
65+
modelRef: string;
66+
agentId?: string;
67+
}): string | undefined {
68+
const parsed = parseConfiguredModelRef(params.modelRef);
69+
if (!parsed) {
70+
return undefined;
71+
}
72+
const policy = resolveAgentHarnessPolicy({
73+
config: params.config,
74+
provider: parsed.provider,
75+
modelId: parsed.modelId,
76+
agentId: params.agentId,
7277
});
78+
const runtime = normalizeRuntimeId(policy.runtime);
79+
return runtime && runtime !== "auto" && runtime !== "pi" ? runtime : undefined;
7380
}
7481

7582
function pushConfiguredModelRuntimeIds(config: OpenClawConfig, runtimes: Set<string>): void {
@@ -104,7 +111,44 @@ function pushConfiguredModelRuntimeIds(config: OpenClawConfig, runtimes: Set<str
104111
pushModelMapRuntimeIds(config.agents?.defaults?.models);
105112
const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
106113
for (const agent of agents) {
107-
pushModelMapRuntimeIds(agent.models);
114+
pushModelMapRuntimeIds(isRecord(agent) ? agent.models : undefined);
115+
}
116+
}
117+
118+
function pushConfiguredAgentModelRuntimeIds(config: OpenClawConfig, runtimes: Set<string>): void {
119+
const pushModelRefs = (modelRefs: string[], agentId?: string) => {
120+
for (const modelRef of modelRefs) {
121+
const runtime = resolveConfiguredModelHarnessRuntime({ config, modelRef, agentId });
122+
if (runtime) {
123+
runtimes.add(runtime);
124+
}
125+
}
126+
};
127+
const pushModelMapRefs = (models: unknown, agentId?: string) => {
128+
if (!isRecord(models)) {
129+
return;
130+
}
131+
pushModelRefs(Object.keys(models), agentId);
132+
};
133+
134+
const defaultsModel = config.agents?.defaults?.model;
135+
const defaultsModelRefs: string[] = [];
136+
pushAgentModelRefs(defaultsModelRefs, defaultsModel);
137+
pushModelRefs(defaultsModelRefs);
138+
pushModelMapRefs(config.agents?.defaults?.models);
139+
140+
if (!Array.isArray(config.agents?.list)) {
141+
return;
142+
}
143+
for (const agent of config.agents.list) {
144+
if (!isRecord(agent)) {
145+
continue;
146+
}
147+
const agentId = typeof agent.id === "string" ? agent.id : undefined;
148+
const selectedModelRefs: string[] = [];
149+
pushAgentModelRefs(selectedModelRefs, agent.model ?? defaultsModel);
150+
pushModelRefs(selectedModelRefs, agentId);
151+
pushModelMapRefs(agent.models, agentId);
108152
}
109153
}
110154

@@ -136,11 +180,6 @@ export function collectConfiguredAgentHarnessRuntimes(
136180
const runtimes = new Set<string>();
137181
const includeEnvRuntime = options.includeEnvRuntime ?? true;
138182
const includeLegacyAgentRuntimes = options.includeLegacyAgentRuntimes ?? true;
139-
const pushCodexForOpenAIModel = (model: unknown, agentId?: string) => {
140-
if (hasOpenAIModelRef(config, model, agentId)) {
141-
runtimes.add("codex");
142-
}
143-
};
144183

145184
if (includeEnvRuntime) {
146185
const envRuntime = normalizeRuntimeId(env.OPENCLAW_AGENT_RUNTIME);
@@ -152,19 +191,7 @@ export function collectConfiguredAgentHarnessRuntimes(
152191
if (includeLegacyAgentRuntimes) {
153192
pushLegacyAgentRuntimeIds(config, runtimes);
154193
}
155-
const defaultsModel = config.agents?.defaults?.model;
156-
pushCodexForOpenAIModel(defaultsModel);
157-
if (Array.isArray(config.agents?.list)) {
158-
for (const agent of config.agents.list) {
159-
if (!isRecord(agent)) {
160-
continue;
161-
}
162-
pushCodexForOpenAIModel(
163-
agent.model ?? defaultsModel,
164-
typeof agent.id === "string" ? agent.id : undefined,
165-
);
166-
}
167-
}
194+
pushConfiguredAgentModelRuntimeIds(config, runtimes);
168195

169196
return [...runtimes].toSorted((left, right) => left.localeCompare(right));
170197
}

src/commands/doctor/shared/missing-configured-plugin-install.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,20 @@ describe("repairMissingConfiguredPluginInstalls", () => {
12881288
},
12891289
{},
12901290
],
1291+
[
1292+
"default selectable OpenAI agent model",
1293+
{
1294+
agents: {
1295+
defaults: {
1296+
model: { primary: "anthropic/claude-sonnet-4-6" },
1297+
models: {
1298+
"openai/gpt-5.5": {},
1299+
},
1300+
},
1301+
},
1302+
},
1303+
{},
1304+
],
12911305
[
12921306
"agent model runtime policy",
12931307
{

src/commands/doctor/shared/release-configured-plugin-installs.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,27 @@ describe("configured plugin install release step", () => {
202202
expect(result.channelIds).toStrictEqual([]);
203203
});
204204

205+
it("collects Codex from selectable OpenAI agent models even without integration discovery", async () => {
206+
const { collectReleaseConfiguredPluginIds } =
207+
await import("./release-configured-plugin-installs.js");
208+
const result = collectReleaseConfiguredPluginIds({
209+
cfg: {
210+
agents: {
211+
defaults: {
212+
model: { primary: "anthropic/claude-sonnet-4-6" },
213+
models: {
214+
"openai/gpt-5.5": {},
215+
},
216+
},
217+
},
218+
},
219+
env: {},
220+
});
221+
222+
expect(result.pluginIds).toEqual(["codex"]);
223+
expect(result.channelIds).toStrictEqual([]);
224+
});
225+
205226
it("collects external web search and ACP runtime plugins from config-only usage", async () => {
206227
const { collectReleaseConfiguredPluginIds } =
207228
await import("./release-configured-plugin-installs.js");

src/config/plugin-auto-enable.core.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,41 @@ describe("applyPluginAutoEnable core", () => {
675675
]);
676676
});
677677

678+
it("auto-enables Codex when OpenAI is a selectable default agent model", () => {
679+
const result = applyPluginAutoEnable({
680+
config: {
681+
agents: {
682+
defaults: {
683+
model: { primary: "anthropic/claude-sonnet-4-6" },
684+
models: {
685+
"openai/gpt-5.5": {},
686+
},
687+
},
688+
},
689+
plugins: {
690+
allow: ["openai"],
691+
entries: {
692+
openai: { enabled: true },
693+
},
694+
},
695+
},
696+
env,
697+
manifestRegistry: makeRegistry([
698+
{ id: "openai", channels: [], providers: ["openai", "openai-codex"] },
699+
{
700+
id: "codex",
701+
channels: [],
702+
providers: ["codex"],
703+
activation: { onAgentHarnesses: ["codex"] },
704+
},
705+
]),
706+
});
707+
708+
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
709+
expect(result.config.plugins?.allow).toEqual(["openai", "codex"]);
710+
expect(result.changes).toEqual(["codex agent runtime configured, enabled automatically."]);
711+
});
712+
678713
it("auto-enables an opt-in plugin when a provider runtime is configured", () => {
679714
const result = applyPluginAutoEnable({
680715
config: {

src/plugins/channel-plugin-ids.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,22 @@ describe("resolveGatewayStartupPluginIds", () => {
14451445
});
14461446
});
14471447

1448+
it("includes Codex when OpenAI is a selectable default agent model", () => {
1449+
expectStartupPluginIdsCase({
1450+
config: {
1451+
agents: {
1452+
defaults: {
1453+
model: { primary: "anthropic/claude-sonnet-4-6" },
1454+
models: {
1455+
"openai/gpt-5.5": {},
1456+
},
1457+
},
1458+
},
1459+
} as OpenClawConfig,
1460+
expected: ["demo-channel", "browser", "codex", "memory-core"],
1461+
});
1462+
});
1463+
14481464
it("does not include Codex when an OpenAI model is manually pinned to PI", () => {
14491465
expectStartupPluginIdsCase({
14501466
config: {

0 commit comments

Comments
 (0)