From 1dbfa78dac6ab05aecfcb345f029076cdfad705e Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 24 Apr 2026 16:58:03 -0400 Subject: [PATCH] Fix runtime provider override leakage across source handoff --- packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/config/model-registry.ts | 1 + .../model-registry-runtime-provider.test.ts | 48 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 48646205e..38ab79c4b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -25,7 +25,7 @@ - Fixed edit streaming preview updates to cancel obsolete in-flight computations and avoid rendering stale previews as args change - Fixed Mermaid fenced markdown rendering in assistant messages on terminals without image protocol support ([#650](https://github.com/can1357/oh-my-pi/issues/650)) - Fixed SQLite `read` helper queries to reject `where=` clauses with SQL control syntax that could override the structured selector's pagination; raw SQL remains available through `q=SELECT ...` -- Fixed `models` provider transport overrides so `headers`-only entries apply without requiring `baseUrl`, including runtime `registerProvider()` overrides that now persist across `refresh()` / `refreshProvider()` and preserve existing `baseUrl` on subsequent headers-only updates +- Fixed `models` provider transport overrides so `headers`-only entries apply without requiring `baseUrl`, including runtime `registerProvider()` overrides that now persist across `refresh()` / `refreshProvider()`, preserve existing `baseUrl` on subsequent headers-only updates, and clear stale transport overrides when a provider is re-registered under a different extension source ## [14.2.0] - 2026-04-23 diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index b01fd0a3f..fa10f0897 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -2035,6 +2035,7 @@ export class ModelRegistry { if (previousProviders && previousProviders.size === 0) { this.#runtimeProvidersBySource.delete(previousSourceId); } + this.#runtimeProviderOverrides.delete(providerName); } const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set(); sourceProviders.add(providerName); diff --git a/packages/coding-agent/test/model-registry-runtime-provider.test.ts b/packages/coding-agent/test/model-registry-runtime-provider.test.ts index ca8411e4e..c6e3f684c 100644 --- a/packages/coding-agent/test/model-registry-runtime-provider.test.ts +++ b/packages/coding-agent/test/model-registry-runtime-provider.test.ts @@ -382,6 +382,54 @@ describe("ModelRegistry runtime provider registration", () => { expect(modelAfterProviderRefresh?.headers?.[runtimeHeader]).toBe("runtime-header"); }); + test("provider source handoff does not retain previous source transport overrides", async () => { + const registry = new ModelRegistry(authStorage, modelsJsonPath); + const providerName = "shared-runtime-provider"; + const leakedHeader = "X-Old-Source-Header"; + const sourceBBaseUrl = "https://source-b.example.com/v1"; + + registry.registerProvider( + providerName, + { + baseUrl: "https://source-a.example.com/v1", + apiKey: "KEY_A", + api: "openai-completions", + models: [{ ...baseModel, id: "model-a" }], + }, + "ext://a", + ); + registry.registerProvider( + providerName, + { baseUrl: "https://override-a.example.com/v1", headers: { [leakedHeader]: "from-source-a" } }, + "ext://a", + ); + registry.registerProvider( + providerName, + { + baseUrl: sourceBBaseUrl, + apiKey: "KEY_B", + api: "openai-completions", + models: [{ ...baseModel, id: "model-b" }], + }, + "ext://b", + ); + + expect(registry.find(providerName, "model-a")).toBeUndefined(); + const modelAfterHandoff = registry.find(providerName, "model-b"); + expect(modelAfterHandoff?.baseUrl).toBe(sourceBBaseUrl); + expect(modelAfterHandoff?.headers?.[leakedHeader]).toBeUndefined(); + + await registry.refresh("offline"); + const modelAfterRefresh = registry.find(providerName, "model-b"); + expect(modelAfterRefresh?.baseUrl).toBe(sourceBBaseUrl); + expect(modelAfterRefresh?.headers?.[leakedHeader]).toBeUndefined(); + + await registry.refreshProvider(providerName, "offline"); + const modelAfterProviderRefresh = registry.find(providerName, "model-b"); + expect(modelAfterProviderRefresh?.baseUrl).toBe(sourceBBaseUrl); + expect(modelAfterProviderRefresh?.headers?.[leakedHeader]).toBeUndefined(); + }); + test("multiple extension providers survive refresh independently", async () => { const registry = new ModelRegistry(authStorage, modelsJsonPath);