Skip to content

Commit f4cfa01

Browse files
funmerlinVACIncsteipete
authored
fix(openai): route compaction through Codex auth provider (#86408)
* fix(openai): route compaction through codex auth provider Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> * fix(openai): honor default responses compaction threshold Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> * fix(openai): preserve codex runtime routing * docs(changelog): note Codex routing fix --------- Co-authored-by: Merlin <258679497+funmerlin@users.noreply.github.com> Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 5dccba7 commit f4cfa01

9 files changed

Lines changed: 501 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
3434
- Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.
3535
- Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.
3636
- Agents/Codex: allow Codex app-server runs to bootstrap from `CODEX_API_KEY` or `OPENAI_API_KEY` when no Codex auth profile is configured.
37+
- Agents/Codex: keep selected Codex runtime routing on OpenAI-Codex while preserving direct OpenAI API-key compaction fallback. (#86408) Thanks @funmerlin and @VACInc.
3738
- Agent transcript: include OpenClaw agent session logs when finding local transcript candidates.
3839
- Crabbox: bootstrap raw AWS macOS shell commands wrapped in absolute `time` paths so RSS probes can run Node and pnpm on fresh macOS runners.
3940
- Crabbox: bootstrap raw AWS macOS shell commands even when setup statements precede Node or pnpm usage.

src/agents/openai-codex-routing.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
listOpenAIAuthProfileProvidersForAgentRuntime,
55
modelSelectionShouldEnsureCodexPlugin,
66
openAIProviderUsesCodexRuntimeByDefault,
7+
resolveOpenAICompactionRuntimeProvider,
78
resolveOpenAIRuntimeProviderForPi,
89
resolveSelectedOpenAIPiRuntimeProvider,
910
} from "./openai-codex-routing.js";
@@ -151,7 +152,7 @@ describe("OpenAI Codex routing policy", () => {
151152
).toEqual(["openai-codex"]);
152153
});
153154

154-
it("routes openai provider to openai-codex when harness runtime is codex", () => {
155+
it("routes selected OpenAI Codex runtime through OpenAI-Codex even before auth is configured", () => {
155156
expect(
156157
resolveSelectedOpenAIPiRuntimeProvider({
157158
provider: "openai",
@@ -160,6 +161,68 @@ describe("OpenAI Codex routing policy", () => {
160161
).toBe("openai-codex");
161162
});
162163

164+
it("routes OpenAI compaction to OpenAI-Codex when Codex auth order is configured", () => {
165+
expect(
166+
resolveOpenAICompactionRuntimeProvider({
167+
provider: "openai",
168+
harnessRuntime: "codex",
169+
config: {
170+
auth: {
171+
order: {
172+
"openai-codex": ["openai-codex:work"],
173+
},
174+
},
175+
},
176+
}),
177+
).toBe("openai-codex");
178+
});
179+
180+
it("routes OpenAI compaction to OpenAI-Codex when a Codex auth profile is configured", () => {
181+
expect(
182+
resolveOpenAICompactionRuntimeProvider({
183+
provider: "openai",
184+
harnessRuntime: "codex",
185+
config: {
186+
auth: {
187+
profiles: {
188+
work: {
189+
provider: "openai-codex",
190+
mode: "oauth",
191+
},
192+
},
193+
},
194+
},
195+
}),
196+
).toBe("openai-codex");
197+
});
198+
199+
it("routes OpenAI compaction to OpenAI-Codex when OpenAI auth order selects Codex", () => {
200+
const config = {
201+
auth: {
202+
order: {
203+
openai: ["openai-codex:work"],
204+
},
205+
},
206+
} satisfies OpenClawConfig;
207+
208+
expect(
209+
resolveOpenAICompactionRuntimeProvider({
210+
provider: "openai",
211+
harnessRuntime: "codex",
212+
config,
213+
}),
214+
).toBe("openai-codex");
215+
});
216+
217+
it("keeps OpenAI compaction on OpenAI when only direct API-key auth is implied", () => {
218+
expect(
219+
resolveOpenAICompactionRuntimeProvider({
220+
provider: "openai",
221+
harnessRuntime: "codex",
222+
}),
223+
).toBe("openai");
224+
});
225+
163226
it("does not route non-OpenAI providers when runtime is codex", () => {
164227
expect(
165228
resolveSelectedOpenAIPiRuntimeProvider({

src/agents/openai-codex-routing.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig
9292
return hasOpenAICodexAuthProfileOverride(firstProfile);
9393
}
9494

95+
function configuredOpenAICodexAuthProfileExists(config: OpenClawConfig | undefined): boolean {
96+
if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) {
97+
return false;
98+
}
99+
const configuredCodexOrder = findNormalizedProviderValue(
100+
config?.auth?.order,
101+
OPENAI_CODEX_PROVIDER_ID,
102+
);
103+
if (
104+
configuredCodexOrder?.some(
105+
(profileId) => typeof profileId === "string" && profileId.trim().length > 0,
106+
) === true
107+
) {
108+
return true;
109+
}
110+
return Object.values(config?.auth?.profiles ?? {}).some(
111+
(profile) => normalizeProviderId(profile?.provider ?? "") === OPENAI_CODEX_PROVIDER_ID,
112+
);
113+
}
114+
95115
export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
96116
provider: string;
97117
harnessRuntime?: string;
@@ -194,6 +214,37 @@ export function resolveSelectedOpenAIPiRuntimeProvider(params: {
194214
: params.provider;
195215
}
196216

217+
export function resolveOpenAICompactionRuntimeProvider(params: {
218+
provider: string;
219+
harnessRuntime?: string;
220+
agentHarnessId?: string;
221+
authProfileProvider?: string;
222+
authProfileId?: string;
223+
config?: OpenClawConfig;
224+
workspaceDir?: string;
225+
}): string {
226+
if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) {
227+
return OPENAI_CODEX_PROVIDER_ID;
228+
}
229+
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
230+
if (!isOpenAIProvider(params.provider)) {
231+
return params.provider;
232+
}
233+
if (
234+
runtime === "codex" &&
235+
(hasOpenAICodexAuthProfileOverride(params.authProfileId) ||
236+
configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) ||
237+
configuredOpenAICodexAuthProfileExists(params.config))
238+
) {
239+
return OPENAI_CODEX_PROVIDER_ID;
240+
}
241+
return runtime === "pi" &&
242+
!params.authProfileId?.trim() &&
243+
configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)
244+
? OPENAI_CODEX_PROVIDER_ID
245+
: params.provider;
246+
}
247+
197248
export function resolveContextConfigProviderForRuntime(params: {
198249
provider: string;
199250
runtimeId?: string;

src/agents/pi-embedded-runner/compact.hooks.harness.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export const resolveSessionAgentIdsMock = vi.fn(() => ({
7878
sessionAgentId: "main",
7979
}));
8080
export const estimateTokensMock = vi.fn((_message?: unknown) => 10);
81+
export const resolveAgentHarnessPolicyMock = vi.fn(() => ({ runtime: "pi" }));
82+
export const resolveContextWindowInfoMock = vi.fn(() => ({ tokens: 128_000 }));
8183
function createDefaultSessionMessages(): unknown[] {
8284
return [
8385
{ role: "user", content: "hello", timestamp: 1 },
@@ -313,6 +315,10 @@ export function resetCompactHooksHarnessMocks(): void {
313315
authStorage: { setRuntimeApiKey: vi.fn() },
314316
modelRegistry: {},
315317
});
318+
resolveAgentHarnessPolicyMock.mockReset();
319+
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "pi" });
320+
resolveContextWindowInfoMock.mockReset();
321+
resolveContextWindowInfoMock.mockReturnValue({ tokens: 128_000 });
316322

317323
sessionCompactImpl.mockReset();
318324
sessionCompactImpl.mockResolvedValue({
@@ -381,7 +387,7 @@ export async function loadCompactHooksHarness(): Promise<{
381387

382388
vi.doMock("../harness/selection.js", () => ({
383389
maybeCompactAgentHarnessSession: maybeCompactAgentHarnessSessionMock,
384-
resolveAgentHarnessPolicy: vi.fn(() => ({ runtime: "pi" })),
390+
resolveAgentHarnessPolicy: resolveAgentHarnessPolicyMock,
385391
}));
386392

387393
vi.doMock("../../plugins/provider-runtime.js", () => ({
@@ -537,7 +543,7 @@ export async function loadCompactHooksHarness(): Promise<{
537543
}));
538544

539545
vi.doMock("../context-window-guard.js", () => ({
540-
resolveContextWindowInfo: vi.fn(() => ({ tokens: 128_000 })),
546+
resolveContextWindowInfo: resolveContextWindowInfoMock,
541547
}));
542548

543549
vi.doMock("../bootstrap-files.js", () => ({

src/agents/pi-embedded-runner/compact.hooks.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
listRegisteredPluginAgentPromptGuidanceMock,
1313
loadCompactHooksHarness,
1414
maybeCompactAgentHarnessSessionMock,
15+
resolveAgentHarnessPolicyMock,
1516
registerProviderStreamForModelMock,
17+
resolveContextWindowInfoMock,
1618
resolveContextEngineMock,
1719
resolveEmbeddedAgentStreamFnMock,
1820
resolveMemorySearchConfigMock,
@@ -441,6 +443,115 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
441443
}
442444
});
443445

446+
it("routes OpenAI compaction through the selected Codex runtime provider before auth", async () => {
447+
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
448+
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
449+
model: { provider, api: "responses", id: modelId, input: [] },
450+
error: null,
451+
authStorage: { setRuntimeApiKey: vi.fn() },
452+
modelRegistry: {},
453+
}));
454+
455+
const result = await compactEmbeddedPiSessionDirect({
456+
sessionId: "session-1",
457+
sessionKey: TEST_SESSION_KEY,
458+
sessionFile: "/tmp/session.jsonl",
459+
workspaceDir: "/tmp/workspace",
460+
provider: "openai",
461+
model: "gpt-5.5",
462+
config: {
463+
models: {
464+
providers: {
465+
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
466+
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
467+
},
468+
},
469+
auth: {
470+
order: {
471+
"openai-codex": ["openai-codex:work"],
472+
},
473+
},
474+
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
475+
} as never,
476+
});
477+
478+
expect(result.ok).toBe(true);
479+
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
480+
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
481+
});
482+
483+
it("preserves direct OpenAI API-key compaction when no Codex auth is configured", async () => {
484+
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
485+
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
486+
model: { provider, api: "responses", id: modelId, input: [] },
487+
error: null,
488+
authStorage: { setRuntimeApiKey: vi.fn() },
489+
modelRegistry: {},
490+
}));
491+
492+
const result = await compactEmbeddedPiSessionDirect({
493+
sessionId: "session-1",
494+
sessionKey: TEST_SESSION_KEY,
495+
sessionFile: "/tmp/session.jsonl",
496+
workspaceDir: "/tmp/workspace",
497+
provider: "openai",
498+
model: "gpt-5.5",
499+
config: {
500+
models: {
501+
providers: {
502+
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
503+
},
504+
},
505+
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
506+
} as never,
507+
});
508+
509+
expect(result.ok).toBe(true);
510+
expect(mockCallArg(resolveModelMock)).toBe("openai");
511+
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
512+
});
513+
514+
it("uses the persisted Codex runtime for compaction context windows", async () => {
515+
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "pi" });
516+
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
517+
model: { provider, api: "responses", id: modelId, input: [], contextWindow: 1_000_000 },
518+
error: null,
519+
authStorage: { setRuntimeApiKey: vi.fn() },
520+
modelRegistry: {},
521+
}));
522+
523+
const result = await compactEmbeddedPiSessionDirect({
524+
sessionId: "session-1",
525+
sessionKey: TEST_SESSION_KEY,
526+
sessionFile: "/tmp/session.jsonl",
527+
workspaceDir: "/tmp/workspace",
528+
provider: "openai",
529+
model: "gpt-5.5",
530+
agentHarnessId: "codex",
531+
config: {
532+
models: {
533+
providers: {
534+
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
535+
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
536+
},
537+
},
538+
auth: {
539+
order: {
540+
"openai-codex": ["openai-codex:work"],
541+
},
542+
},
543+
agents: { defaults: { embeddedHarness: { runtime: "pi" } } },
544+
} as never,
545+
});
546+
547+
expect(result.ok).toBe(true);
548+
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
549+
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
550+
provider: "openai-codex",
551+
modelId: "gpt-5.5",
552+
});
553+
});
554+
444555
it("keeps compaction fallback selection ephemeral", async () => {
445556
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
446557
model: { provider, api: "responses", id: modelId, input: [] },

0 commit comments

Comments
 (0)