mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 23:49:51 +00:00
* 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>
208 lines
6.5 KiB
TypeScript
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;
|
|
}
|