Files
openclaw/src/agents/openai-codex-routing.ts
ragesaq 58f1db1bc8 Fix OpenAI Codex runtime provider routing (#82864)
* fix: route Codex OpenAI runtime through Codex provider

* docs: add Codex routing evidence collection

* fix(agents): bootstrap OAuth credentials for Codex harness with openai/* model refs

When a plugin harness (e.g. Codex) owns its transport but the runtime
plan resolved to openai-codex via agentRuntime.id: codex, the auth
profile store was left empty because pluginHarnessOwnsTransport short-
circuited initializeAuthProfile(). This caused 'No API key found for
openai-codex' at runtime even though the OAuth profile existed in OpenClaw's
store.

- Add pluginHarnessNeedsOpenClawAuthBootstrap flag when harness owns
transport but the provider is openai-codex and the API is openai-codex-
responses
- Populate authStore and attemptAuthProfileStore from OpenClaw's profile
store in this case
- Run initializeAuthProfile() to forward the OAuth token into the harness
- Update overflow-compaction tests to expect 'openai-codex' provider
  and add dedicated test for OAuth bootstrap path

* fix(agents): refresh Codex OAuth credentials on profile rotation

---------

Co-authored-by: PsiClawOps <267826480+PsiClawOps@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 07:06:18 +01:00

208 lines
6.5 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js";
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js";
export const OPENAI_PROVIDER_ID = "openai";
export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
function isOfficialOpenAIBaseUrl(baseUrl: unknown): boolean {
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
return true;
}
try {
const url = new URL(baseUrl.trim());
return (
url.protocol === "https:" &&
url.hostname.toLowerCase() === "api.openai.com" &&
(url.pathname === "" ||
url.pathname === "/" ||
url.pathname === "/v1" ||
url.pathname === "/v1/")
);
} catch {
return false;
}
}
function openAIProviderUsesCustomBaseUrl(config: OpenClawConfig | undefined): boolean {
return !isOfficialOpenAIBaseUrl(config?.models?.providers?.openai?.baseUrl);
}
export function isOpenAIProvider(provider: string | undefined): boolean {
return normalizeProviderId(provider ?? "") === OPENAI_PROVIDER_ID;
}
export function isOpenAICodexProvider(provider: string | undefined): boolean {
return normalizeProviderId(provider ?? "") === OPENAI_CODEX_PROVIDER_ID;
}
export function openAIProviderUsesCodexRuntimeByDefault(params: {
provider?: string;
config?: OpenClawConfig;
}): boolean {
return isOpenAIProvider(params.provider) && !openAIProviderUsesCustomBaseUrl(params.config);
}
export function parseModelRefProvider(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const slashIndex = value.trim().indexOf("/");
if (slashIndex <= 0) {
return undefined;
}
return normalizeProviderId(value.trim().slice(0, slashIndex));
}
export function modelRefUsesOpenAIProvider(value: unknown): boolean {
return parseModelRefProvider(value) === OPENAI_PROVIDER_ID;
}
export function modelSelectionShouldEnsureCodexPlugin(params: {
model?: string;
config?: OpenClawConfig;
}): boolean {
const provider = parseModelRefProvider(params.model);
if (provider === OPENAI_CODEX_PROVIDER_ID) {
return true;
}
return provider === OPENAI_PROVIDER_ID && !openAIProviderUsesCustomBaseUrl(params.config);
}
export function hasOpenAICodexAuthProfileOverride(value: unknown): boolean {
return (
typeof value === "string" &&
normalizeOptionalLowercaseString(value)?.startsWith(`${OPENAI_CODEX_PROVIDER_ID}:`) === true
);
}
function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig | undefined) {
if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) {
return false;
}
const configuredOpenAIOrder = findNormalizedProviderValue(
config?.auth?.order,
OPENAI_PROVIDER_ID,
);
const firstProfile = configuredOpenAIOrder?.find(
(profileId) => typeof profileId === "string" && profileId.trim().length > 0,
);
return hasOpenAICodexAuthProfileOverride(firstProfile);
}
export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
provider: string;
harnessRuntime?: string;
agentHarnessId?: string;
authProfileProvider?: string;
authProfileId?: string;
config?: OpenClawConfig;
workspaceDir?: string;
}): boolean {
if (!isOpenAIProvider(params.provider)) {
return false;
}
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
if (runtime !== "pi") {
return false;
}
if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) {
return false;
}
const aliasLookupParams = {
config: params.config,
workspaceDir: params.workspaceDir,
};
const authProfileProvider = resolveProviderIdForAuth(
params.authProfileProvider ?? params.authProfileId?.split(":", 1)[0] ?? "",
aliasLookupParams,
);
return authProfileProvider === OPENAI_CODEX_PROVIDER_ID;
}
export function listOpenAIAuthProfileProvidersForAgentRuntime(params: {
provider: string;
harnessRuntime?: string;
agentHarnessId?: string;
config?: OpenClawConfig;
}): string[] {
if (!isOpenAIProvider(params.provider)) {
return [params.provider];
}
const runtime = normalizeEmbeddedAgentRuntime(
normalizeExplicitRuntimePin(params.agentHarnessId) ?? params.harnessRuntime,
);
if (runtime === "codex") {
return [OPENAI_CODEX_PROVIDER_ID];
}
if (runtime === "pi") {
if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) {
return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID];
}
return [OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID];
}
return [params.provider];
}
function normalizeExplicitRuntimePin(value: unknown): string | undefined {
if (typeof value !== "string" || !value.trim()) {
return undefined;
}
const runtime = normalizeEmbeddedAgentRuntime(value);
return runtime === "auto" || runtime === "default" ? undefined : runtime;
}
export function resolveOpenAIRuntimeProviderForPi(params: {
provider: string;
harnessRuntime?: string;
agentHarnessId?: string;
authProfileProvider?: string;
authProfileId?: string;
config?: OpenClawConfig;
workspaceDir?: string;
}): string {
return shouldRouteOpenAIPiThroughCodexAuthProvider(params)
? OPENAI_CODEX_PROVIDER_ID
: params.provider;
}
export function resolveSelectedOpenAIPiRuntimeProvider(params: {
provider: string;
harnessRuntime?: string;
agentHarnessId?: string;
authProfileProvider?: string;
authProfileId?: string;
config?: OpenClawConfig;
workspaceDir?: string;
}): string {
if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) {
return OPENAI_CODEX_PROVIDER_ID;
}
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
if (!isOpenAIProvider(params.provider)) {
return params.provider;
}
if (runtime === "codex") {
return OPENAI_CODEX_PROVIDER_ID;
}
return runtime === "pi" &&
!params.authProfileId?.trim() &&
configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)
? OPENAI_CODEX_PROVIDER_ID
: params.provider;
}
export function resolveContextConfigProviderForRuntime(params: {
provider: string;
runtimeId?: string;
}): string {
const provider = normalizeProviderId(params.provider);
const runtimeId = normalizeEmbeddedAgentRuntime(params.runtimeId);
if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") {
return OPENAI_CODEX_PROVIDER_ID;
}
return params.provider;
}