diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 38ab79c4b..fb692f7c4 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()`, preserve existing `baseUrl` on subsequent headers-only updates, and clear stale transport overrides when a provider is re-registered under a different extension source +- 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, clear stale transport overrides when a provider is re-registered under a different extension source, and keep runtime transport headers authoritative when `modelOverrides` set overlapping header keys ## [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 fa10f0897..b1c34e0a2 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -886,9 +886,8 @@ export class ModelRegistry { const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays); // Merge runtime extension models so they survive refresh() cycles const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays); - const withRuntimeProviderOverrides = this.#applyRuntimeProviderOverrides(combined); - - this.#models = this.#applyModelOverrides(withRuntimeProviderOverrides, this.#modelOverrides); + const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides); + this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides); this.#rebuildCanonicalIndex(); } @@ -1179,8 +1178,8 @@ export class ModelRegistry { const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays); // Merge runtime extension models so they survive online discovery completion const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays); - const withRuntimeProviderOverrides = this.#applyRuntimeProviderOverrides(combined); - this.#models = this.#applyModelOverrides(withRuntimeProviderOverrides, this.#modelOverrides); + const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides); + this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides); this.#rebuildCanonicalIndex(); } 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 c6e3f684c..80e059be2 100644 --- a/packages/coding-agent/test/model-registry-runtime-provider.test.ts +++ b/packages/coding-agent/test/model-registry-runtime-provider.test.ts @@ -295,6 +295,49 @@ describe("ModelRegistry runtime provider registration", () => { expect(registry.find("runtime-provider", modelId)).toBeUndefined(); }); + test("runtime headers override modelOverrides headers across refresh cycles", async () => { + const initialRegistry = new ModelRegistry(authStorage, modelsJsonPath); + const targetModel = initialRegistry.getAll().find(model => model.provider === "anthropic"); + if (!targetModel) throw new Error("Expected bundled anthropic model"); + + const modelId = targetModel.id; + const sharedHeader = "X-Shared-Provider-Model-Header"; + const configHeaderValue = "config-header"; + const runtimeHeaderValue = "runtime-header"; + + fs.writeFileSync( + modelsJsonPath, + JSON.stringify({ + providers: { + anthropic: { + modelOverrides: { + [modelId]: { headers: { [sharedHeader]: configHeaderValue } }, + }, + }, + }, + }), + ); + + const registry = new ModelRegistry(authStorage, modelsJsonPath); + expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(configHeaderValue); + + registry.registerProvider( + "anthropic", + { headers: { [sharedHeader]: runtimeHeaderValue } }, + "ext://runtime", + ); + expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(runtimeHeaderValue); + + await registry.refresh("offline"); + expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(runtimeHeaderValue); + + await registry.refreshProvider("anthropic", "offline"); + expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(runtimeHeaderValue); + + registry.clearSourceRegistrations("ext://runtime"); + expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(configHeaderValue); + }); + test("extension-registered API keys survive refresh cycle for auth resolution", async () => { const registry = new ModelRegistry(authStorage, modelsJsonPath);