Skip to content

Commit 8267492

Browse files
committed
fix: honor Codex auth order for OpenAI PI
1 parent 16e5d66 commit 8267492

19 files changed

Lines changed: 450 additions & 26 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc.
4444
- Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn.
4545
- Agents/Codex: fall back to the embedded PI runner when OpenAI's implicit Codex harness preference cannot find a registered Codex plugin, preventing OpenAI-compatible gateway requests from failing with an unregistered harness error. Fixes #82437.
46+
- Agents/OpenAI: honor `openai-codex:*` entries placed ahead of API-key backups in `auth.order.openai` for explicit OpenAI PI runs, and accept `models auth login --provider openai-codex --device-code` for headless sign-in. Fixes #82521.
4647
- CLI/channels: install missing externalized same-id channel plugins during `channels add --channel <id>`, so recovery for WhatsApp and other externalized stock channels does not require a separate `plugins enable` step. Fixes #82533.
4748
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant.
4849
- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance.

src/agents/agent-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,7 @@ async function agentCommandInternal(
894894
const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({
895895
provider: providerForAuthProfileValidation,
896896
harnessRuntime: validationHarnessPolicy.runtime,
897+
config: cfg,
897898
}).map((candidateProvider) =>
898899
resolveProviderIdForAuth(candidateProvider, { config: cfg, workspaceDir }),
899900
);

src/agents/auth-profiles/order.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,40 @@ describe("resolveAuthProfileOrder", () => {
337337
expect(order).toEqual(["openai-codex:personal", "openai:default"]);
338338
});
339339

340+
it("keeps Codex profiles listed in the friendly OpenAI order for Codex auth", async () => {
341+
const store: AuthProfileStore = {
342+
version: 1,
343+
profiles: {
344+
"openai-codex:personal": {
345+
type: "oauth",
346+
provider: "openai-codex",
347+
access: "access",
348+
refresh: "refresh",
349+
expires: Date.now() + 60_000,
350+
},
351+
"openai:backup": {
352+
type: "api_key",
353+
provider: "openai",
354+
key: "sk-platform",
355+
},
356+
},
357+
};
358+
359+
const order = resolveAuthProfileOrder({
360+
cfg: {
361+
auth: {
362+
order: {
363+
openai: ["openai-codex:personal", "openai:backup"],
364+
},
365+
},
366+
},
367+
store,
368+
provider: "openai-codex",
369+
});
370+
371+
expect(order).toEqual(["openai-codex:personal", "openai:backup"]);
372+
});
373+
340374
it("keeps direct OpenAI Codex auth order ahead of the friendly OpenAI alias", async () => {
341375
const store: AuthProfileStore = {
342376
version: 1,

src/agents/btw.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ async function resolveRuntimeModel(params: {
256256
agentId: params.agentId,
257257
sessionKey: params.sessionKey,
258258
}).runtime,
259+
config: params.cfg,
259260
}),
260261
agentDir: params.agentDir,
261262
sessionEntry: params.sessionEntry,

src/agents/command/attempt-execution.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,11 @@ export function runAgentAttempt(params: {
463463
config: params.cfg,
464464
workspaceDir: params.workspaceDir,
465465
});
466+
const embeddedPiHarnessOverride =
467+
requestedAgentHarnessId ??
468+
(agentHarnessPolicy.runtime === "pi" && embeddedPiProvider !== params.providerOverride
469+
? "pi"
470+
: undefined);
466471
if (!isRawModelRun && isCliProvider(cliExecutionProvider, params.cfg)) {
467472
const cliSessionBinding = getCliSessionBinding(params.sessionEntry, cliExecutionProvider);
468473
const resolveReusableCliSessionBinding = async () => {
@@ -609,7 +614,8 @@ export function runAgentAttempt(params: {
609614
sessionFile: params.sessionFile,
610615
workspaceDir: params.workspaceDir,
611616
config: params.cfg,
612-
agentHarnessId: requestedAgentHarnessId,
617+
agentHarnessId: embeddedPiHarnessOverride,
618+
agentHarnessRuntimeOverride: embeddedPiHarnessOverride,
613619
skillsSnapshot: params.skillsSnapshot,
614620
prompt: effectivePrompt,
615621
images: params.isFallbackRetry ? undefined : params.opts.images,

src/agents/model-auth-label.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
resolveAuthProfileDisplayLabel,
88
resolveAuthProfileOrder,
99
} from "./auth-profiles.js";
10+
import { isStoredCredentialCompatibleWithAuthProvider } from "./auth-profiles/order.js";
1011
import {
1112
readClaudeCliCredentialsCached,
1213
readCodexCliCredentialsCached,
@@ -21,6 +22,7 @@ export function resolveModelAuthLabel(params: {
2122
agentDir?: string;
2223
workspaceDir?: string;
2324
includeExternalProfiles?: boolean;
25+
acceptedProviderIds?: readonly string[];
2426
}): string | undefined {
2527
const resolvedProvider = params.provider?.trim();
2628
if (!resolvedProvider) {
@@ -39,17 +41,37 @@ export function resolveModelAuthLabel(params: {
3941
}),
4042
});
4143
const profileOverride = params.sessionEntry?.authProfileOverride?.trim();
42-
const order = resolveAuthProfileOrder({
43-
cfg: params.cfg,
44-
store,
45-
provider: providerKey,
46-
preferredProfile: profileOverride,
47-
});
44+
const acceptedProviderKeys = [
45+
...new Set(
46+
[...(params.acceptedProviderIds ?? []).map(normalizeProviderId), providerKey].filter(Boolean),
47+
),
48+
];
49+
const order = [
50+
...new Set(
51+
acceptedProviderKeys.flatMap((acceptedProvider) =>
52+
resolveAuthProfileOrder({
53+
cfg: params.cfg,
54+
store,
55+
provider: acceptedProvider,
56+
preferredProfile: profileOverride,
57+
}),
58+
),
59+
),
60+
];
4861
const candidates = [profileOverride, ...order].filter(Boolean) as string[];
4962

5063
for (const profileId of candidates) {
5164
const profile = store.profiles[profileId];
52-
if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
65+
if (
66+
!profile ||
67+
!acceptedProviderKeys.some((acceptedProvider) =>
68+
isStoredCredentialCompatibleWithAuthProvider({
69+
cfg: params.cfg,
70+
provider: acceptedProvider,
71+
credential: profile,
72+
}),
73+
)
74+
) {
5375
continue;
5476
}
5577
const label = resolveAuthProfileDisplayLabel({

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
modelSelectionShouldEnsureCodexPlugin,
66
openAIProviderUsesCodexRuntimeByDefault,
77
resolveOpenAIRuntimeProviderForPi,
8+
resolveSelectedOpenAIPiRuntimeProvider,
89
} from "./openai-codex-routing.js";
910

1011
describe("OpenAI Codex routing policy", () => {
@@ -51,6 +52,96 @@ describe("OpenAI Codex routing policy", () => {
5152
).toBe("openai-codex");
5253
});
5354

55+
it("keeps explicit OpenAI PI Codex auth order ahead of API-key backups", () => {
56+
const config = {
57+
auth: {
58+
order: {
59+
openai: ["openai-codex:work", "openai:backup"],
60+
},
61+
},
62+
} satisfies OpenClawConfig;
63+
64+
expect(
65+
listOpenAIAuthProfileProvidersForAgentRuntime({
66+
provider: "openai",
67+
harnessRuntime: "pi",
68+
config,
69+
}),
70+
).toEqual(["openai-codex", "openai"]);
71+
expect(
72+
resolveSelectedOpenAIPiRuntimeProvider({
73+
provider: "openai",
74+
harnessRuntime: "pi",
75+
config,
76+
}),
77+
).toBe("openai-codex");
78+
expect(
79+
resolveOpenAIRuntimeProviderForPi({
80+
provider: "openai",
81+
harnessRuntime: "pi",
82+
config,
83+
}),
84+
).toBe("openai");
85+
});
86+
87+
it("keeps explicit OpenAI PI API-key auth order ahead of Codex backups", () => {
88+
const config = {
89+
auth: {
90+
order: {
91+
openai: ["openai:backup", "openai-codex:work"],
92+
},
93+
},
94+
} satisfies OpenClawConfig;
95+
96+
expect(
97+
listOpenAIAuthProfileProvidersForAgentRuntime({
98+
provider: "openai",
99+
harnessRuntime: "pi",
100+
config,
101+
}),
102+
).toEqual(["openai", "openai-codex"]);
103+
expect(
104+
resolveSelectedOpenAIPiRuntimeProvider({
105+
provider: "openai",
106+
harnessRuntime: "pi",
107+
config,
108+
}),
109+
).toBe("openai");
110+
});
111+
112+
it("does not route custom OpenAI-compatible PI configs through Codex auth order", () => {
113+
const config = {
114+
models: {
115+
providers: {
116+
openai: {
117+
baseUrl: "https://proxy.example.test/v1",
118+
models: [],
119+
},
120+
},
121+
},
122+
auth: {
123+
order: {
124+
openai: ["openai-codex:work", "openai:backup"],
125+
},
126+
},
127+
} satisfies OpenClawConfig;
128+
129+
expect(
130+
listOpenAIAuthProfileProvidersForAgentRuntime({
131+
provider: "openai",
132+
harnessRuntime: "pi",
133+
config,
134+
}),
135+
).toEqual(["openai", "openai-codex"]);
136+
expect(
137+
resolveSelectedOpenAIPiRuntimeProvider({
138+
provider: "openai",
139+
harnessRuntime: "pi",
140+
config,
141+
}),
142+
).toBe("openai");
143+
});
144+
54145
it("validates Codex harness auth through the Codex provider contract", () => {
55146
expect(
56147
listOpenAIAuthProfileProvidersForAgentRuntime({

src/agents/openai-codex-routing.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
33
import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js";
44
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
5-
import { normalizeProviderId } from "./provider-id.js";
5+
import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js";
66

77
export const OPENAI_PROVIDER_ID = "openai";
88
export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
@@ -78,6 +78,20 @@ export function hasOpenAICodexAuthProfileOverride(value: unknown): boolean {
7878
);
7979
}
8080

81+
function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig | undefined) {
82+
if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) {
83+
return false;
84+
}
85+
const configuredOpenAIOrder = findNormalizedProviderValue(
86+
config?.auth?.order,
87+
OPENAI_PROVIDER_ID,
88+
);
89+
const firstProfile = configuredOpenAIOrder?.find(
90+
(profileId) => typeof profileId === "string" && profileId.trim().length > 0,
91+
);
92+
return hasOpenAICodexAuthProfileOverride(firstProfile);
93+
}
94+
8195
export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
8296
provider: string;
8397
harnessRuntime?: string;
@@ -87,16 +101,16 @@ export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
87101
config?: OpenClawConfig;
88102
workspaceDir?: string;
89103
}): boolean {
90-
if (
91-
!isOpenAIProvider(params.provider) ||
92-
!hasOpenAICodexAuthProfileOverride(params.authProfileId)
93-
) {
104+
if (!isOpenAIProvider(params.provider)) {
94105
return false;
95106
}
96107
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
97108
if (runtime !== "pi") {
98109
return false;
99110
}
111+
if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) {
112+
return false;
113+
}
100114
const aliasLookupParams = {
101115
config: params.config,
102116
workspaceDir: params.workspaceDir,
@@ -112,6 +126,7 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: {
112126
provider: string;
113127
harnessRuntime?: string;
114128
agentHarnessId?: string;
129+
config?: OpenClawConfig;
115130
}): string[] {
116131
if (!isOpenAIProvider(params.provider)) {
117132
return [params.provider];
@@ -123,6 +138,9 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: {
123138
return [OPENAI_CODEX_PROVIDER_ID];
124139
}
125140
if (runtime === "pi") {
141+
if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) {
142+
return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID];
143+
}
126144
return [OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID];
127145
}
128146
return [params.provider];
@@ -150,6 +168,27 @@ export function resolveOpenAIRuntimeProviderForPi(params: {
150168
: params.provider;
151169
}
152170

171+
export function resolveSelectedOpenAIPiRuntimeProvider(params: {
172+
provider: string;
173+
harnessRuntime?: string;
174+
agentHarnessId?: string;
175+
authProfileProvider?: string;
176+
authProfileId?: string;
177+
config?: OpenClawConfig;
178+
workspaceDir?: string;
179+
}): string {
180+
if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) {
181+
return OPENAI_CODEX_PROVIDER_ID;
182+
}
183+
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
184+
return isOpenAIProvider(params.provider) &&
185+
runtime === "pi" &&
186+
!params.authProfileId?.trim() &&
187+
configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)
188+
? OPENAI_CODEX_PROVIDER_ID
189+
: params.provider;
190+
}
191+
153192
export function resolveContextConfigProviderForRuntime(params: {
154193
provider: string;
155194
runtimeId?: string;

0 commit comments

Comments
 (0)