From ec75643a0904dded9fbdbd17287b7d7c1e4185b9 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sun, 8 Mar 2026 09:32:21 -0400 Subject: [PATCH] Models: scope implicit codex baseUrl override --- ...dels-config.providers.openai-codex.test.ts | 48 +++++++++++++++++++ src/agents/models-config.providers.ts | 2 + src/agents/models-config.ts | 28 ++++++++--- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/agents/models-config.providers.openai-codex.test.ts b/src/agents/models-config.providers.openai-codex.test.ts index 20df80e1885..596f858b9c0 100644 --- a/src/agents/models-config.providers.openai-codex.test.ts +++ b/src/agents/models-config.providers.openai-codex.test.ts @@ -56,6 +56,7 @@ describe("openai-codex implicit provider", () => { api: "openai-codex-responses", models: [], }); + expect(providers?.["openai-codex"]).not.toHaveProperty("apiKey"); }); }); }); @@ -104,4 +105,51 @@ describe("openai-codex implicit provider", () => { }); }); }); + + it("preserves an existing baseUrl for explicit openai-codex config without oauth synthesis", async () => { + await withModelsTempHome(async () => { + await withTempEnv(MODELS_CONFIG_IMPLICIT_ENV_VARS, async () => { + unsetEnv(MODELS_CONFIG_IMPLICIT_ENV_VARS); + const agentDir = resolveOpenClawAgentDir(); + await fs.mkdir(agentDir, { recursive: true }); + await fs.writeFile( + path.join(agentDir, "models.json"), + JSON.stringify( + { + providers: { + "openai-codex": { + baseUrl: "https://chatgpt.com/backend-api", + api: "openai-codex-responses", + models: [], + }, + }, + }, + null, + 2, + ), + "utf8", + ); + + await ensureOpenClawModelsJson({ + models: { + mode: "merge", + providers: { + "openai-codex": { + api: "openai-codex-responses", + models: [], + }, + }, + }, + }); + + const parsed = await readGeneratedModelsJson<{ + providers: Record; + }>(); + expect(parsed.providers["openai-codex"]).toMatchObject({ + baseUrl: "https://chatgpt.com/backend-api", + api: "openai-codex-responses", + }); + }); + }); + }); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index eac89af3501..9232e45ae85 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -1000,6 +1000,8 @@ function buildOpenAICodexProvider(): ProviderConfig { return { baseUrl: OPENAI_CODEX_BASE_URL, api: "openai-codex-responses", + // Like Copilot, Codex resolves OAuth credentials from auth-profiles at + // runtime, so the snapshot only needs the canonical API surface. models: [], }; } diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index c67c00549d3..89a791747cc 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -142,10 +142,16 @@ async function readJson(pathname: string): Promise { async function resolveProvidersForModelsJson(params: { cfg: OpenClawConfig; agentDir: string; -}): Promise> { +}): Promise<{ + providers: Record; + authoritativeImplicitBaseUrlProviders: ReadonlySet; +}> { const { cfg, agentDir } = params; const explicitProviders = cfg.models?.providers ?? {}; const implicitProviders = await resolveImplicitProviders({ agentDir, explicitProviders }); + const authoritativeImplicitBaseUrlProviders = new Set( + [...AUTHORITATIVE_IMPLICIT_BASEURL_PROVIDERS].filter((key) => Boolean(implicitProviders[key])), + ); const providers: Record = mergeProviders({ implicit: implicitProviders, explicit: explicitProviders, @@ -163,7 +169,7 @@ async function resolveProvidersForModelsJson(params: { if (implicitCopilot && !providers["github-copilot"]) { providers["github-copilot"] = implicitCopilot; } - return providers; + return { providers, authoritativeImplicitBaseUrlProviders }; } function mergeWithExistingProviderSecrets(params: { @@ -171,9 +177,15 @@ function mergeWithExistingProviderSecrets(params: { existingProviders: Record[string]>; secretRefManagedProviders: ReadonlySet; explicitBaseUrlProviders: ReadonlySet; + authoritativeImplicitBaseUrlProviders: ReadonlySet; }): Record { - const { nextProviders, existingProviders, secretRefManagedProviders, explicitBaseUrlProviders } = - params; + const { + nextProviders, + existingProviders, + secretRefManagedProviders, + explicitBaseUrlProviders, + authoritativeImplicitBaseUrlProviders, + } = params; const mergedProviders: Record = {}; for (const [key, entry] of Object.entries(existingProviders)) { mergedProviders[key] = entry; @@ -199,7 +211,7 @@ function mergeWithExistingProviderSecrets(params: { preserved.apiKey = existing.apiKey; } if ( - !AUTHORITATIVE_IMPLICIT_BASEURL_PROVIDERS.has(key) && + !authoritativeImplicitBaseUrlProviders.has(key) && !explicitBaseUrlProviders.has(key) && typeof existing.baseUrl === "string" && existing.baseUrl @@ -217,6 +229,7 @@ async function resolveProvidersForMode(params: { providers: Record; secretRefManagedProviders: ReadonlySet; explicitBaseUrlProviders: ReadonlySet; + authoritativeImplicitBaseUrlProviders: ReadonlySet; }): Promise> { if (params.mode !== "merge") { return params.providers; @@ -234,6 +247,7 @@ async function resolveProvidersForMode(params: { existingProviders, secretRefManagedProviders: params.secretRefManagedProviders, explicitBaseUrlProviders: params.explicitBaseUrlProviders, + authoritativeImplicitBaseUrlProviders: params.authoritativeImplicitBaseUrlProviders, }); } @@ -300,7 +314,8 @@ export async function ensureOpenClawModelsJson( // through the full loadConfig() pipeline which applies these. applyConfigEnvVars(cfg); - const providers = await resolveProvidersForModelsJson({ cfg, agentDir }); + const { providers, authoritativeImplicitBaseUrlProviders } = + await resolveProvidersForModelsJson({ cfg, agentDir }); if (Object.keys(providers).length === 0) { return { agentDir, wrote: false }; @@ -331,6 +346,7 @@ export async function ensureOpenClawModelsJson( providers: normalizedProviders, secretRefManagedProviders, explicitBaseUrlProviders, + authoritativeImplicitBaseUrlProviders, }); const next = `${JSON.stringify({ providers: mergedProviders }, null, 2)}\n`; const existingRaw = await readRawFile(targetPath);