From 3693d4a7286c826238457991e83a3d83cb84f940 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Fri, 15 May 2026 16:12:54 -0700 Subject: [PATCH 1/6] Reset utility model cache and force retry when fallback flag is missing --- .../src/platform/endpoint/node/modelMetadataFetcher.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index 6387159fa3a04..5b172f4ab3f45 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -172,6 +172,11 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe public async getCopilotUtilityModel(): Promise { await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this)); + if (!this._copilotUtilityModel && this._familyMap.size > 0) { + // Server returned models but did not flag a chat fallback; force one refresh + // before throwing so we are not stuck on a stale 10-minute cache window. + await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, () => this._fetchModels(true)); + } const resolvedModel = this._copilotUtilityModel; if (!resolvedModel || !isChatModelInformation(resolvedModel)) { throw new Error(await this._getErrorMessage('Unable to resolve Copilot utility chat model (server did not mark a chat fallback model)')); @@ -272,6 +277,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe } this._familyMap.clear(); + this._copilotUtilityModel = undefined; const data: IModelAPIResponse[] = (await response.json()).data; this._requestLogger.logModelListCall(requestId, requestMetadata, data); From 77ba9eaa69d3a532c98a19e1bc22d86f9d5a772a Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Fri, 15 May 2026 17:02:47 -0700 Subject: [PATCH 2/6] Gate utility-model forced retry with a flag and add observability --- .../platform/endpoint/node/modelMetadataFetcher.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index 5b172f4ab3f45..d70cd7a1d6f11 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -82,6 +82,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe private _completionsFamilyMap: Map = new Map(); private _copilotUtilityModel: IModelAPIResponse | undefined; private _lastFetchTime: number = 0; + private _hasForcedUtilityModelRetry: boolean = false; private readonly _taskSingler = new TaskSingler(); private _lastFetchError: any; @@ -106,10 +107,12 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe // Only clear the family map if the copilot token is undefined, as this means the user has logged out and we should clear the models, otherwise we want to keep the old models around until we get a new list if (this._authService.copilotToken === undefined) { this._familyMap.clear(); + this._copilotUtilityModel = undefined; } this._completionsFamilyMap.clear(); this._lastFetchTime = 0; + this._hasForcedUtilityModelRetry = false; })); } @@ -172,9 +175,13 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe public async getCopilotUtilityModel(): Promise { await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this)); - if (!this._copilotUtilityModel && this._familyMap.size > 0) { - // Server returned models but did not flag a chat fallback; force one refresh - // before throwing so we are not stuck on a stale 10-minute cache window. + if (!this._copilotUtilityModel && this._familyMap.size > 0 && !this._hasForcedUtilityModelRetry) { + // Server returned models but did not flag a chat fallback. Force one refresh + // before throwing; gated so a persistent server misconfiguration cannot storm CAPI. + // The flag is cleared on auth change and on fetch failure so a recovered server + // can be retried later. + this._hasForcedUtilityModelRetry = true; + this._logService.warn('Utility model unset after initial fetch; forcing one refresh'); await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, () => this._fetchModels(true)); } const resolvedModel = this._copilotUtilityModel; @@ -301,6 +308,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe this._logService.error(e, `Failed to fetch models (${requestId})`); this._lastFetchError = e; this._lastFetchTime = 0; + this._hasForcedUtilityModelRetry = false; } } From 94dfd62e010dd9bec928a5981620440dfd663261 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Fri, 15 May 2026 17:11:06 -0700 Subject: [PATCH 3/6] Tighten utility-model retry: scope flag reset to logout, compress comment --- .../src/platform/endpoint/node/modelMetadataFetcher.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index d70cd7a1d6f11..3fa4e909a6fc1 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -108,11 +108,11 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe if (this._authService.copilotToken === undefined) { this._familyMap.clear(); this._copilotUtilityModel = undefined; + this._hasForcedUtilityModelRetry = false; } this._completionsFamilyMap.clear(); this._lastFetchTime = 0; - this._hasForcedUtilityModelRetry = false; })); } @@ -176,10 +176,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe public async getCopilotUtilityModel(): Promise { await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this)); if (!this._copilotUtilityModel && this._familyMap.size > 0 && !this._hasForcedUtilityModelRetry) { - // Server returned models but did not flag a chat fallback. Force one refresh - // before throwing; gated so a persistent server misconfiguration cannot storm CAPI. - // The flag is cleared on auth change and on fetch failure so a recovered server - // can be retried later. + // One-shot retry per auth epoch: avoids storming CAPI on persistent server misconfig. this._hasForcedUtilityModelRetry = true; this._logService.warn('Utility model unset after initial fetch; forcing one refresh'); await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, () => this._fetchModels(true)); From 5cad5ed48b41f9f3e8d1956f4ac033feff55cdee Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Fri, 15 May 2026 17:14:13 -0700 Subject: [PATCH 4/6] Reset utility-model retry budget on each successful fetch --- .../copilot/src/platform/endpoint/node/modelMetadataFetcher.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index 3fa4e909a6fc1..1528c15a2d7f5 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -300,6 +300,9 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe familyMap.get(family)?.push(model); } this._lastFetchError = undefined; + if (this._copilotUtilityModel) { + this._hasForcedUtilityModelRetry = false; + } this._onDidModelRefresh.fire(); } catch (e) { this._logService.error(e, `Failed to fetch models (${requestId})`); From bf7088fc819bc7f55b20cfa5b28c4b8a730faac9 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Mon, 18 May 2026 11:25:48 -0700 Subject: [PATCH 5/6] Clear completions family map and gate forced retry on env active --- .../copilot/src/platform/endpoint/node/modelMetadataFetcher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index 1528c15a2d7f5..2b3cec33ee0c1 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -175,7 +175,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe public async getCopilotUtilityModel(): Promise { await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this)); - if (!this._copilotUtilityModel && this._familyMap.size > 0 && !this._hasForcedUtilityModelRetry) { + if (!this._copilotUtilityModel && this._familyMap.size > 0 && !this._hasForcedUtilityModelRetry && this._envService.isActive) { // One-shot retry per auth epoch: avoids storming CAPI on persistent server misconfig. this._hasForcedUtilityModelRetry = true; this._logService.warn('Utility model unset after initial fetch; forcing one refresh'); @@ -281,6 +281,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe } this._familyMap.clear(); + this._completionsFamilyMap.clear(); this._copilotUtilityModel = undefined; const data: IModelAPIResponse[] = (await response.json()).data; From f6e32279f2a134df15902e613c113b24a6fa7a93 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Mon, 18 May 2026 11:35:04 -0700 Subject: [PATCH 6/6] Coalesce concurrent forced-retry callers via TaskSingler factory gate --- .../endpoint/node/modelMetadataFetcher.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts index 2b3cec33ee0c1..7e02612a204c7 100644 --- a/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts +++ b/extensions/copilot/src/platform/endpoint/node/modelMetadataFetcher.ts @@ -175,11 +175,17 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe public async getCopilotUtilityModel(): Promise { await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this)); - if (!this._copilotUtilityModel && this._familyMap.size > 0 && !this._hasForcedUtilityModelRetry && this._envService.isActive) { - // One-shot retry per auth epoch: avoids storming CAPI on persistent server misconfig. - this._hasForcedUtilityModelRetry = true; - this._logService.warn('Utility model unset after initial fetch; forcing one refresh'); - await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, () => this._fetchModels(true)); + if (!this._copilotUtilityModel && this._familyMap.size > 0 && this._envService.isActive) { + // One-shot retry per auth epoch: gated inside the factory so concurrent callers + // coalesce on the same forced fetch via TaskSingler instead of falling through. + await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, async () => { + if (this._hasForcedUtilityModelRetry) { + return; + } + this._hasForcedUtilityModelRetry = true; + this._logService.warn('Utility model unset after initial fetch; forcing one refresh'); + await this._fetchModels(true); + }); } const resolvedModel = this._copilotUtilityModel; if (!resolvedModel || !isChatModelInformation(resolvedModel)) {