From 45b200cd09be2284b905cda20d6e52a9c2a5d940 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 22 Jun 2026 13:39:48 +0000 Subject: [PATCH] fix(coding-agent): evicted resolved completions session URLs The completions provider stores session state under the request-time resolved base URL, which can differ from the catalog baseUrl for Moonshot, Alibaba Coding Plan, Azure deployments, and similar provider overrides. The model-switch cleanup now evicts the previous provider prefix whenever the switch leaves that completions backend, so those resolved-url keys cannot survive the switch. --- packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/session/agent-session.ts | 10 ++++----- ...on-openai-completions-model-switch.test.ts | 21 ++++++++++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fb2d4f07c..0b6afada5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed `openai-completions` provider session state surviving `/model` switches across different providers or base URLs. `AgentSession.#closeProviderSessionsForModelSwitch` only evicted `openai-codex-responses` and `openai-responses:` keys; entries keyed `openai-completions:::` (cached strict-tools disable scopes and reasoning-effort fallbacks for the old transport) lingered indefinitely. Moving away from an `openai-completions` backend now evicts every cached entry sharing the previous `(provider, baseUrl)` pair, while same-backend model toggles keep their cached state ([#3260](https://github.com/can1357/oh-my-pi/issues/3260)) +- Fixed `openai-completions` provider session state surviving `/model` switches across different providers or base URLs. `AgentSession.#closeProviderSessionsForModelSwitch` only evicted `openai-codex-responses` and `openai-responses:` keys; entries keyed `openai-completions:::` (cached strict-tools disable scopes and reasoning-effort fallbacks for the old transport) lingered indefinitely. Moving away from an `openai-completions` backend now evicts every cached entry for the previous provider, including entries whose base URL was resolved at request time rather than read from the catalog, while same-backend model toggles keep their cached state ([#3260](https://github.com/can1357/oh-my-pi/issues/3260)) ## [16.1.14] - 2026-06-22 diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index b285e6b00..d66f24cc2 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -9026,18 +9026,18 @@ export class AgentSession { providerKeys.add(`openai-responses:${nextModel.provider}`); } - // `openai-completions` sessions are keyed `openai-completions:::` + // `openai-completions` sessions are keyed `openai-completions:::` // and cache backend-specific decisions (strict-tools disable scopes, reasoning-effort - // fallbacks). When the user moves away from that backend — different `api`, `provider`, - // or `baseUrl` — the cached decisions no longer track the active transport. Evict the - // full `(provider, baseUrl)` prefix; same-backend model toggles keep their state. + // fallbacks). The resolved request base URL can differ from the catalog `model.baseUrl` + // (Moonshot env override, Alibaba Coding Plan enterprise URL, Azure deployment URL), + // so evict by provider prefix when the user moves away from that completions backend. let completionsPrefixToEvict: string | undefined; if (currentModel.api === "openai-completions") { const currentScope = `${currentModel.provider}:${currentModel.baseUrl ?? ""}`; const nextScope = nextModel.api === "openai-completions" ? `${nextModel.provider}:${nextModel.baseUrl ?? ""}` : undefined; if (currentScope !== nextScope) { - completionsPrefixToEvict = `openai-completions:${currentScope}:`; + completionsPrefixToEvict = `openai-completions:${currentModel.provider}:`; } } diff --git a/packages/coding-agent/test/agent-session-openai-completions-model-switch.test.ts b/packages/coding-agent/test/agent-session-openai-completions-model-switch.test.ts index 9b71f2f65..5a1253504 100644 --- a/packages/coding-agent/test/agent-session-openai-completions-model-switch.test.ts +++ b/packages/coding-agent/test/agent-session-openai-completions-model-switch.test.ts @@ -92,7 +92,7 @@ describe("AgentSession openai-completions provider session eviction", () => { expect(session.providerSessionState.has(completionsSessionKey(deepseek))).toBe(false); }); - it("evicts every cached entry under the old (provider, baseUrl) prefix", async () => { + it("evicts every cached entry under the old provider prefix", async () => { const deepseekPro = completionsModel("deepseek", "deepseek-v4-pro"); const deepseekFlash = completionsModel("deepseek", "deepseek-v4-flash"); const cerebras = completionsModel("cerebras", "llama3.1-8b"); @@ -119,6 +119,25 @@ describe("AgentSession openai-completions provider session eviction", () => { expect(session.providerSessionState.has(completionsSessionKey(deepseekFlash))).toBe(false); }); + it("evicts entries whose base URL was resolved at request time", async () => { + const moonshot = completionsModel("moonshot", "kimi-k2-thinking"); + const cerebras = completionsModel("cerebras", "llama3.1-8b"); + authStorage.setRuntimeApiKey(cerebras.provider, "cerebras-test-key"); + + session = buildSession(moonshot); + + const closeSpy = vi.fn(); + const resolvedBaseUrlKey = `openai-completions:${moonshot.provider}:https://api.moonshot.cn/v1:${moonshot.id}`; + session.providerSessionState.set(resolvedBaseUrlKey, { + close: closeSpy, + } satisfies ProviderSessionState); + + await session.setModel(cerebras); + + expect(closeSpy).toHaveBeenCalledTimes(1); + expect(session.providerSessionState.has(resolvedBaseUrlKey)).toBe(false); + }); + it("leaves unrelated provider session state untouched", async () => { const deepseek = completionsModel("deepseek", "deepseek-v4-pro"); const cerebras = completionsModel("cerebras", "llama3.1-8b");