From ff21dc23157c75699f4dd4b98037a190cdccd5c5 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:53:58 +0200 Subject: [PATCH] fix(codex): keep native sessions out of custom provider imports --- docs/custom-providers.md | 2 +- .../providers/codex-app-server-agent.test.ts | 27 ++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 17 +++++++-- ...x-custom-provider-import.local.e2e.test.ts | 36 ++++++++++--------- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/docs/custom-providers.md b/docs/custom-providers.md index 35667ac38b..33b897639c 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -232,7 +232,7 @@ For pay-as-you-go, use `ANTHROPIC_API_KEY` with a standard Model Studio key (`sk Codex talks to OpenAI's Responses API by default. Custom providers that extend `"codex"` can point Codex at any OpenAI-compatible endpoint (OpenRouter, LiteLLM, vLLM, llama.cpp server, an internal gateway, etc.) by setting `OPENAI_BASE_URL` and `OPENAI_API_KEY` in the provider `env`. -Paseo passes those variables through to the Codex app-server process **and** maps them into Codex's thread config under `model_provider` / `model_providers`, because Codex reads provider routing from config rather than from `OPENAI_BASE_URL` alone. The generated Codex provider key is `paseo:`, so a Paseo provider such as `ollama` does not override a Codex built-in. Existing sessions created with the original provider key remain available in Import session. +Paseo passes those variables through to the Codex app-server process **and** maps them into Codex's thread config under `model_provider` / `model_providers`, because Codex reads provider routing from config rather than from `OPENAI_BASE_URL` alone. The generated Codex provider key is `paseo:`, so a Paseo provider such as `ollama` does not override a Codex built-in. Existing custom sessions created with a non-built-in provider key remain available in Import session. Native Codex conversations using a built-in key stay separate from a custom provider of the same name. ### Setup diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index f21802708d..f22494222b 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -6942,6 +6942,33 @@ describe("Codex importable sessions", () => { appServer.assertNoErrors(); }); + test.each(["openai", "ollama", "lmstudio", "amazon-bedrock", "amazon-bedrock-runtime"])( + "a custom provider named %s excludes native built-in sessions", + async (providerId) => { + const appServer = createFakeCodexAppServer({ + "thread/list": providerFilteringThreadListHandler([ + { id: "native-thread", cwd: "/workspace/project-a", modelProvider: providerId }, + { + id: "custom-thread", + cwd: "/workspace/project-a", + modelProvider: `paseo:${providerId}`, + }, + ]), + }); + const provider = createProviderWithFakeAppServer(appServer, { + runtimeSettings: { + env: { OPENAI_BASE_URL: "https://llm.example.test/v1", OPENAI_API_KEY: "test-key" }, + }, + customProvider: { id: providerId, label: "Custom Codex", extends: "codex" }, + }); + + const sessions = await provider.listImportableSessions({ cwd: "/workspace/project-a" }); + + expect(sessions.map((session) => session.providerHandleId)).toEqual(["custom-thread"]); + appServer.assertNoErrors(); + }, + ); + test("stock Codex keeps listing only its own threads", async () => { const appServer = createFakeCodexAppServer({ "thread/list": providerFilteringThreadListHandler(threadsByModelProvider), diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index d1c9bc6b0c..f5b9d4fefe 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -145,6 +145,15 @@ function isCodexAlreadyUnarchivedError(error: unknown, threadId: string): boolea const TURN_START_TIMEOUT_MS = 90 * 1000; const INTERRUPT_TIMEOUT_MS = 2_000; const CODEX_PROVIDER = "codex" as const; +// These native provider identities belong to Codex, not to an older custom +// Paseo provider. Never include them in a custom provider's legacy import scan. +const CODEX_BUILTIN_MODEL_PROVIDER_IDS = new Set([ + "openai", + "ollama", + "lmstudio", + "amazon-bedrock", + "amazon-bedrock-runtime", +]); // Codex treats most app-server client names as the model-request originator. // This reserved Codex name is non-originating, so requests keep Codex's default // CLI identity instead of showing up as Paseo in provider usage logs. @@ -7353,13 +7362,17 @@ export class CodexAppServerAgentClient implements AgentClient { const listLimit = options?.cwd ? Math.max(scanLimit, 50) : scanLimit; // Codex records each thread under the model provider that ran it and, // unless told otherwise, lists only the provider its own config selects. - // Include the original id for sessions created before namespacing. + // Include legacy custom ids, but keep Codex built-in sessions separate. const customProviderConfig = this.customProviderConfig(); const allThreads = await readCodexThreadWindow(client, this.logger, { limit: listLimit, cwd: options?.cwd, ...(customProviderConfig && this.deps.customProvider - ? { modelProviders: [customProviderConfig.model_provider, this.deps.customProvider.id] } + ? { + modelProviders: CODEX_BUILTIN_MODEL_PROVIDER_IDS.has(this.deps.customProvider.id) + ? [customProviderConfig.model_provider] + : [customProviderConfig.model_provider, this.deps.customProvider.id], + } : {}), }); const threads = filterCodexThreadsByCwd(allThreads, options?.cwd); diff --git a/packages/server/src/server/agent/providers/codex-custom-provider-import.local.e2e.test.ts b/packages/server/src/server/agent/providers/codex-custom-provider-import.local.e2e.test.ts index 39f11a8212..7fa57f89eb 100644 --- a/packages/server/src/server/agent/providers/codex-custom-provider-import.local.e2e.test.ts +++ b/packages/server/src/server/agent/providers/codex-custom-provider-import.local.e2e.test.ts @@ -90,12 +90,13 @@ describe("Codex custom provider import list", () => { }); test.each([ - { providerId: "my-codex", seedLegacy: false }, - { providerId: "ollama", seedLegacy: false }, - { providerId: "my-codex", seedLegacy: true }, + { providerId: "my-codex", seedLegacy: false, seedBuiltin: false }, + { providerId: "ollama", seedLegacy: false, seedBuiltin: false }, + { providerId: "my-codex", seedLegacy: true, seedBuiltin: false }, + { providerId: "ollama", seedLegacy: false, seedBuiltin: true }, ])( - "lists and imports sessions for $providerId (legacy session: $seedLegacy)", - async ({ providerId, seedLegacy }) => { + "lists and imports sessions for $providerId (legacy: $seedLegacy, native: $seedBuiltin)", + async ({ providerId, seedLegacy, seedBuiltin }) => { const codexHome = mkdtempSync(path.join(os.tmpdir(), "codex-custom-import-home-")); const cwd = mkdtempSync(path.join(os.tmpdir(), "codex-custom-import-cwd-")); cleanups.push(() => rmSync(codexHome, { recursive: true, force: true })); @@ -104,11 +105,12 @@ describe("Codex custom provider import list", () => { cleanups.push(() => closeServer(server)); const logger = createTestLogger(); - if (seedLegacy) { - // Recreate the config emitted before namespacing with the real app-server. + if (seedLegacy || seedBuiltin) { + // Seed an old custom session or a native built-in session with real Codex. const environment = { ...process.env, CODEX_HOME: codexHome, + CODEX_OSS_BASE_URL: baseUrl, OPENAI_API_KEY: "test-key", }; const legacy = new CodexAppServerAgentSession( @@ -120,15 +122,17 @@ describe("Codex custom provider import list", () => { environment, customCodexConfig: { model_provider: providerId, - model_providers: { - [providerId]: { - name: "My Codex", - base_url: baseUrl, - wire_api: "responses", - env_key: "OPENAI_API_KEY", - requires_openai_auth: false, - }, - }, + model_providers: seedBuiltin + ? {} + : { + [providerId]: { + name: "My Codex", + base_url: baseUrl, + wire_api: "responses", + env_key: "OPENAI_API_KEY", + requires_openai_auth: false, + }, + }, }, }, );