diff --git a/packages/coding-agent/.changes/derive-connection-models.md b/packages/coding-agent/.changes/derive-connection-models.md new file mode 100644 index 0000000000..dc0d1874f0 --- /dev/null +++ b/packages/coding-agent/.changes/derive-connection-models.md @@ -0,0 +1 @@ +- Kept available model lists in sync with the current catalog and configured providers. diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index db5abdb5c6..100dbe05fe 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -982,7 +982,6 @@ export class InteractiveMode { private skillCommands = new Map(); private connectionCommands: AgentConnectionSlashCommand[] = []; - private connectionModels: AgentConnectionModel[] = []; private connectionModelCatalog: AgentConnectionModel[] = []; private connectionConfiguredProviders = new Set(); private connectionModelsFetchedAt = 0; @@ -7772,7 +7771,10 @@ export class InteractiveMode { private applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void { this.connectionModelCatalog = [...catalog.models]; this.connectionConfiguredProviders = new Set(catalog.configuredProviders); - this.connectionModels = catalog.models.filter((model) => this.connectionConfiguredProviders.has(model.provider)); + } + + private getAvailableConnectionModels(): AgentConnectionModel[] { + return this.connectionModelCatalog.filter((model) => this.connectionConfiguredProviders.has(model.provider)); } private async getConnectionAvailableModels(): Promise { @@ -7784,11 +7786,11 @@ export class InteractiveMode { const version = this.connectionModelsRefreshVersion; const promise = this.agentConnection.getModelCatalog().then((catalog) => { if (version !== this.connectionModelsRefreshVersion) { - return [...this.connectionModels]; + return this.getAvailableConnectionModels(); } this.applyConnectionModelCatalog(catalog); this.connectionModelsFetchedAt = Date.now(); - return [...this.connectionModels]; + return this.getAvailableConnectionModels(); }); this.connectionModelsRefreshInFlight = { version, promise }; @@ -7839,7 +7841,6 @@ export class InteractiveMode { } private invalidateConnectionModels(): void { - this.connectionModels = []; this.connectionConfiguredProviders = new Set(); this.connectionModelsFetchedAt = 0; this.invalidateConnectionModelRefresh(); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 4ef691e164..8d0c8e8f2a 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -2172,7 +2172,6 @@ describe("InteractiveMode startup onboarding warnings", () => { describe("InteractiveMode model candidates", () => { type ModelCandidatesHarness = { agentConnection: { getModelCatalog: () => Promise }; - connectionModels: AgentConnectionModel[]; connectionModelCatalog: AgentConnectionModel[]; connectionConfiguredProviders: Set; connectionModelsFetchedAt: number; @@ -2180,6 +2179,7 @@ describe("InteractiveMode model candidates", () => { connectionModelsRefreshInFlight: { version: number; promise: Promise } | undefined; getScopedModelState(): AgentConnectionState["scopedModels"]; applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void; + getAvailableConnectionModels(): AgentConnectionModel[]; getConnectionAvailableModels(): Promise; getModelCandidates(): Promise; getScopedModelsFromModelIds( @@ -2201,7 +2201,6 @@ describe("InteractiveMode model candidates", () => { const getModelCatalog = vi.fn(async () => ({ models: [model], configuredProviders: [model.provider] })); const fakeThis: ModelCandidatesHarness = { agentConnection: { getModelCatalog }, - connectionModels: [], connectionModelCatalog: [], connectionConfiguredProviders: new Set(), connectionModelsFetchedAt: 0, @@ -2209,6 +2208,7 @@ describe("InteractiveMode model candidates", () => { connectionModelsRefreshInFlight: undefined, getScopedModelState: () => [], applyConnectionModelCatalog: prototype.applyConnectionModelCatalog, + getAvailableConnectionModels: prototype.getAvailableConnectionModels, getConnectionAvailableModels: prototype.getConnectionAvailableModels, getModelCandidates: prototype.getModelCandidates, getScopedModelsFromModelIds: prototype.getScopedModelsFromModelIds, @@ -2218,7 +2218,7 @@ describe("InteractiveMode model candidates", () => { expect(result).toEqual([model]); expect(getModelCatalog).toHaveBeenCalledTimes(1); - expect(fakeThis.connectionModels).toEqual([model]); + expect(fakeThis.getAvailableConnectionModels()).toEqual([model]); }); test("uses connection state for scoped model candidates", async () => { @@ -2229,7 +2229,6 @@ describe("InteractiveMode model candidates", () => { }); const fakeThis: ModelCandidatesHarness = { agentConnection: { getModelCatalog }, - connectionModels: [], connectionModelCatalog: [], connectionConfiguredProviders: new Set(), connectionModelsFetchedAt: 0, @@ -2237,6 +2236,7 @@ describe("InteractiveMode model candidates", () => { connectionModelsRefreshInFlight: undefined, getScopedModelState: () => [{ model, thinkingLevel: "medium" }], applyConnectionModelCatalog: prototype.applyConnectionModelCatalog, + getAvailableConnectionModels: prototype.getAvailableConnectionModels, getConnectionAvailableModels: prototype.getConnectionAvailableModels, getModelCandidates: prototype.getModelCandidates, getScopedModelsFromModelIds: prototype.getScopedModelsFromModelIds, @@ -2299,7 +2299,6 @@ describe("InteractiveMode model selection persistence", () => { getModelCatalog(): Promise; setModel(provider: string, modelId: string): Promise; }; - connectionModels: AgentConnectionModel[]; connectionModelCatalog: AgentConnectionModel[]; connectionConfiguredProviders: Set; connectionModelsFetchedAt: number; @@ -2321,6 +2320,7 @@ describe("InteractiveMode model selection persistence", () => { getScopedModelState(): AgentConnectionState["scopedModels"]; getCurrentModel(): AgentConnectionModel | undefined; applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void; + getAvailableConnectionModels(): AgentConnectionModel[]; findExactModelMatch(searchTerm: string): Promise; getConnectionAvailableModels(): Promise; getCachedModelCandidates(): AgentConnectionModel[]; @@ -2409,7 +2409,6 @@ describe("InteractiveMode model selection persistence", () => { }), setModel: vi.fn(async () => {}), }; - fakeThis.connectionModels = [...options.connectionModels]; fakeThis.connectionModelCatalog = catalogModels; fakeThis.connectionConfiguredProviders = configuredProviders; fakeThis.connectionModelsFetchedAt = options.connectionModelsFetchedAt ?? 0; @@ -2947,7 +2946,6 @@ describe("InteractiveMode model selection persistence", () => { getResourceSnapshot: vi.fn(async () => ({})), setModel: vi.fn(async () => {}), } as never; - fakeThis.connectionModels = []; fakeThis.connectionModelCatalog = []; fakeThis.connectionConfiguredProviders = new Set(); fakeThis.connectionModelsFetchedAt = 0; @@ -2973,7 +2971,7 @@ describe("InteractiveMode model selection persistence", () => { await expect(staleRefresh).resolves.toEqual([freshModel]); - expect(fakeThis.connectionModels).toEqual([freshModel]); + expect(fakeThis.getAvailableConnectionModels()).toEqual([freshModel]); }); test("keeps the cached model catalog when a catalog refresh fails", async () => { @@ -2997,7 +2995,6 @@ describe("InteractiveMode model selection persistence", () => { getResourceSnapshot: vi.fn(async () => ({})), setModel: vi.fn(async () => {}), } as never; - fakeThis.connectionModels = [cachedModel]; fakeThis.connectionModelCatalog = [cachedModel]; fakeThis.connectionConfiguredProviders = new Set([cachedModel.provider]); fakeThis.connectionModelsFetchedAt = Date.now(); @@ -3015,7 +3012,7 @@ describe("InteractiveMode model selection persistence", () => { await expect(fakeThis.refreshConnectionCatalog()).resolves.toBeUndefined(); expect(fakeThis.connectionCommands).toEqual([]); - expect(fakeThis.connectionModels).toEqual([expect.objectContaining({ id: "fresh" })]); + expect(fakeThis.getAvailableConnectionModels()).toEqual([expect.objectContaining({ id: "fresh" })]); expect(fakeThis.connectionModelsFetchedAt).toBeGreaterThan(0); }); @@ -3223,7 +3220,6 @@ describe("InteractiveMode Prime CLI onboarding", () => { }; type OnboardingFake = OnboardingHarness & { connectionState: AgentConnectionState; - connectionModels: AgentConnectionModel[]; agentConnection: { getAvailableModels?: () => Promise; setModel?: (provider: string, modelId: string) => Promise; @@ -3898,7 +3894,6 @@ describe("InteractiveMode Prime CLI onboarding", () => { function createPrimeCliHarness(shown: boolean): OnboardingFake { const fakeThis = Object.create(InteractiveMode.prototype) as OnboardingFake; fakeThis.connectionState = createConnectionState({ model: primeModel }); - fakeThis.connectionModels = [primeModel]; fakeThis.agentConnection = { getAvailableModels: vi.fn(async () => [primeModel]), }; diff --git a/packages/coding-agent/test/suite/regressions/4575-model-auth-selection.test.ts b/packages/coding-agent/test/suite/regressions/4575-model-auth-selection.test.ts index cd87112ba6..93b41ed17e 100644 --- a/packages/coding-agent/test/suite/regressions/4575-model-auth-selection.test.ts +++ b/packages/coding-agent/test/suite/regressions/4575-model-auth-selection.test.ts @@ -11,7 +11,6 @@ import { createHarness, type Harness } from "../harness.js"; interface ConnectionAuthRefreshHarness { agentConnection: { getModelCatalog(): Promise }; - connectionModels: AgentConnectionModel[]; connectionModelCatalog: AgentConnectionModel[]; connectionConfiguredProviders: Set; connectionModelsFetchedAt: number; @@ -19,6 +18,7 @@ interface ConnectionAuthRefreshHarness { connectionModelsRefreshInFlight: { version: number; promise: Promise } | undefined; invalidateConnectionModels(): void; applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void; + getAvailableConnectionModels(): AgentConnectionModel[]; getConnectionAvailableModels(): Promise; getConnectionModelCatalog(): Promise; refreshConnectionModelsAfterAuthChange(): Promise; @@ -124,7 +124,6 @@ describe("ENG-4575 model authentication", () => { const getModelCatalog = vi.fn(async () => ({ models: [model], configuredProviders: [] })); const fakeThis = Object.create(InteractiveMode.prototype) as ConnectionAuthRefreshHarness; fakeThis.agentConnection = { getModelCatalog }; - fakeThis.connectionModels = [model]; fakeThis.connectionModelCatalog = [model]; fakeThis.connectionConfiguredProviders = new Set([model.provider]); fakeThis.connectionModelsFetchedAt = Date.now(); @@ -135,7 +134,7 @@ describe("ENG-4575 model authentication", () => { expect(getModelCatalog).toHaveBeenCalledOnce(); expect(fakeThis.connectionConfiguredProviders).toEqual(new Set()); - expect(fakeThis.connectionModels).toEqual([]); + expect(fakeThis.getAvailableConnectionModels()).toEqual([]); expect(fakeThis.connectionModelCatalog).toEqual([model]); }); @@ -147,7 +146,6 @@ describe("ENG-4575 model authentication", () => { fakeThis.agentConnection = { getModelCatalog: vi.fn(async () => ({ models: [model], configuredProviders: [] })), }; - fakeThis.connectionModels = []; fakeThis.connectionModelCatalog = []; fakeThis.connectionConfiguredProviders = new Set(); fakeThis.connectionModelsFetchedAt = 0; @@ -155,7 +153,7 @@ describe("ENG-4575 model authentication", () => { fakeThis.connectionModelsRefreshInFlight = undefined; await expect(fakeThis.getConnectionModelCatalog()).resolves.toEqual([model]); - expect(fakeThis.connectionModels).toEqual([]); + expect(fakeThis.getAvailableConnectionModels()).toEqual([]); }); test("uses the full public catalog for scoped-session model autocomplete", async () => {