Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class RefreshableMockEndpointProvider implements IEndpointProvider {
getChatEndpoint(): Promise<IChatEndpoint> {
throw new Error('Not implemented');
}
getChatEndpointDuringProviderResolution(): Promise<IChatEndpoint> {
throw new Error('Not implemented');
}
getEmbeddingsEndpoint(): Promise<any> {
throw new Error('Not implemented');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib
for (const family of aliasFamilies) {
let endpoint: IChatEndpoint | undefined;
try {
endpoint = await this._endpointProvider.getChatEndpoint(family);
endpoint = await this._endpointProvider.getChatEndpointDuringProviderResolution(family);
Comment thread
rwoll marked this conversation as resolved.
} catch (err) {
this._logService.warn(`[LanguageModelAccess] Failed to resolve utility alias '${family}': ${err}`);
continue;
Expand Down Expand Up @@ -444,7 +444,16 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib
}
const aliasEndpoint = this._utilityAliasEndpoints.get(model.id);
if (aliasEndpoint) {
return aliasEndpoint;
// Re-resolve via the full `getChatEndpoint` path so non-copilot
// overrides that were deferred during provider resolution (to avoid
// re-entering the language model service) are picked up on first
// use. Fall back to the cached endpoint if resolution fails.
try {
return await this._endpointProvider.getChatEndpoint(model.id as ChatEndpointFamily);
} catch (err) {
this._logService.warn(`[LanguageModelAccess] Failed to re-resolve utility alias '${model.id}'; using endpoint cached during provider resolution. Error: ${err}`);
return aliasEndpoint;
}
}
return this._chatEndpoints.find(e => e.model === model.id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,10 @@ multiple lines.
return this._endpoint as unknown as IChatEndpoint;
}

async getChatEndpointDuringProviderResolution(): Promise<IChatEndpoint> {
return this._endpoint as unknown as IChatEndpoint;
}

async getEmbeddingsEndpoint(): Promise<any> {
throw new Error('Not implemented');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,25 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP
return modelMetadata ? this.getOrCreateChatEndpointInstance(modelMetadata) : this.getChatEndpoint('copilot-utility');
}

/**
* Like {@link getChatEndpoint} but safe to call during the copilot LM
* provider's `provideLanguageModelChatInfo` callback. Non-copilot
* utility overrides that require `lm.selectChatModels` are skipped
* (they would deadlock) and resolved lazily on first use instead.
*/
async getChatEndpointDuringProviderResolution(family: ChatEndpointFamily): Promise<IChatEndpoint> {
return this._resolveUtilityFamily(family, /* duringProviderResolution */ true);
Comment thread
rwoll marked this conversation as resolved.
}

/**
* Resolves an internal utility family (`copilot-utility-small` /
* `copilot-utility`) to a concrete `CopilotChatEndpoint`. The model
* selection for each family lives in the corresponding resolver
* class so callers don't need to know which CAPI family backs each
* purpose.

*/
private async _resolveUtilityFamily(family: ChatEndpointFamily): Promise<IChatEndpoint> {
const override = await this._resolveUtilityOverride(family);
private async _resolveUtilityFamily(family: ChatEndpointFamily, duringProviderResolution?: boolean): Promise<IChatEndpoint> {
const override = await this._resolveUtilityOverride(family, duringProviderResolution);
if (override) {
return override;
}
Expand All @@ -154,8 +163,12 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP
* Returns `undefined` if no override is configured, if the value is
* malformed, if no matching model is currently available, or if the
* lookup throws.
*
* @param duringProviderResolution When true, skip non-copilot overrides
* to avoid re-entering the language model service (which deadlocks).
* Non-copilot overrides are resolved lazily on first use instead.
*/
private async _resolveUtilityOverride(family: ChatEndpointFamily): Promise<IChatEndpoint | undefined> {
private async _resolveUtilityOverride(family: ChatEndpointFamily, duringProviderResolution?: boolean): Promise<IChatEndpoint | undefined> {
let configKey: string;
if (family === 'copilot-utility-small') {
configKey = ProductionEndpointProvider.UTILITY_SMALL_MODEL_CONFIG_KEY;
Expand Down Expand Up @@ -211,6 +224,16 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP
return this.getOrCreateChatEndpointInstance(modelMetadata);
}

// Non-copilot overrides require `lm.selectChatModels` which re-enters
// the language model service. When called during provider resolution
// (i.e. from _registerUtilityAliasModels inside provideLanguageModelChatInfo),
// this deadlocks because the copilot vendor's sequencer slot is held.
// Skip here and resolve lazily in _resolveUtilityFamily on first use.
if (duringProviderResolution) {
this._logService.trace(`[ProductionEndpointProvider] Deferring non-copilot ${configKey} override '${raw}' (resolving during provider callback).`);
return undefined;
}

let models: readonly LanguageModelChat[];
try {
models = await lm.selectChatModels({ vendor, id });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class MockEndpointProvider implements IEndpointProvider {
constructor(private readonly endpoint: IChatEndpoint) { }
readonly onDidModelsRefresh = Event.None;
async getChatEndpoint(): Promise<IChatEndpoint> { return this.endpoint; }
async getChatEndpointDuringProviderResolution(): Promise<IChatEndpoint> { return this.endpoint; }
async getEmbeddingsEndpoint(): Promise<never> { throw new Error('not implemented'); }
async getAllChatEndpoints(): Promise<IChatEndpoint[]> { return [this.endpoint]; }
async getAllCompletionModels(): Promise<never[]> { return []; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,48 @@ suite('ProductionEndpointProvider — utility model overrides', () => {
const endpoint = await endpointProvider.getChatEndpoint('copilot-utility');
assert.strictEqual(endpoint.model, 'copilot-utility');
});

test('getChatEndpointDuringProviderResolution defers non-copilot override without calling lm.selectChatModels', async () => {
setFetcher([makeChatModel('copilot-utility')]);
const selectStub = sandbox.stub(lm, 'selectChatModels').resolves([
makeFakeLanguageModelChat({ vendor: 'anthropic', id: 'claude-haiku-4.5' }),
]);
await configService.setNonExtensionConfig('chat.utilityModel', 'anthropic/claude-haiku-4.5');

const endpoint = await endpointProvider.getChatEndpointDuringProviderResolution('copilot-utility');
assert.strictEqual(selectStub.callCount, 0, 'lm.selectChatModels must not be called during provider resolution');
// The deferred path returns the default copilot-utility endpoint;
// the non-copilot override is resolved lazily on first regular use.
assert.strictEqual(endpoint.model, 'copilot-utility');
});

test('getChatEndpointDuringProviderResolution still applies copilot-vendor overrides (no lm re-entry)', async () => {
setFetcher([makeChatModel('copilot-utility'), makeChatModel('gpt-4o-mini')]);
const selectStub = sandbox.stub(lm, 'selectChatModels').resolves([]);
await configService.setNonExtensionConfig('chat.utilityModel', 'copilot/gpt-4o-mini');

const endpoint = await endpointProvider.getChatEndpointDuringProviderResolution('copilot-utility');
assert.strictEqual(selectStub.callCount, 0, 'copilot-vendor overrides resolve via the model fetcher, not lm.selectChatModels');
assert.ok(endpoint instanceof CopilotChatEndpoint);
assert.strictEqual(endpoint.model, 'gpt-4o-mini');
});

test('non-copilot override deferred during provider resolution is resolved on subsequent getChatEndpoint call', async () => {
setFetcher([makeChatModel('copilot-utility')]);
const fakeModel = makeFakeLanguageModelChat({ vendor: 'anthropic', id: 'claude-haiku-4.5' });
const selectStub = sandbox.stub(lm, 'selectChatModels').resolves([fakeModel]);
await configService.setNonExtensionConfig('chat.utilityModel', 'anthropic/claude-haiku-4.5');

// First call simulates being invoked from inside `provideLanguageModelChatInfo`:
// it must not re-enter the language model service.
const deferred = await endpointProvider.getChatEndpointDuringProviderResolution('copilot-utility');
assert.strictEqual(selectStub.callCount, 0);
assert.strictEqual(deferred.model, 'copilot-utility');

// Subsequent regular resolution must apply the override.
const resolved = await endpointProvider.getChatEndpoint('copilot-utility');
assert.strictEqual(selectStub.callCount, 1);
assert.ok(resolved instanceof ExtensionContributedChatEndpoint);
assert.strictEqual(resolved.model, 'claude-haiku-4.5');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ export function createMockEndpointProvider(modelFamily: string): IEndpointProvid
supportsPrediction: true,
showInModelPicker: true,
} as IChatEndpoint),
getChatEndpointDuringProviderResolution: async () => ({
family: modelFamily,
model: 'test-model',
maxOutputTokens: 1000,
supportsToolCalls: true,
supportsVision: true,
supportsPrediction: true,
showInModelPicker: true,
} as IChatEndpoint),
getAllChatEndpoints: async () => [],
getAllCompletionModels: async () => [],
getEmbeddingsEndpoint: async () => ({
Expand Down
1 change: 1 addition & 0 deletions extensions/copilot/src/lib/node/chatLibMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ class NullEndpointProvider implements IEndpointProvider {
async getAllCompletionModels(): Promise<[]> { return []; }
async getAllChatEndpoints(): Promise<[]> { return []; }
async getChatEndpoint(): Promise<never> { throw new Error('not implemented'); }
async getChatEndpointDuringProviderResolution(): Promise<never> { throw new Error('not implemented'); }
async getEmbeddingsEndpoint(): Promise<never> { throw new Error('not implemented'); }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ class TestEndpointProvider implements IEndpointProvider {
throw new Error('Method not implemented.');
}

async getChatEndpointDuringProviderResolution(family: ChatEndpointFamily): Promise<IChatEndpoint> {
return this.getChatEndpoint(family);
}

async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise<IEmbeddingsEndpoint> {
throw new Error('Method not implemented.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ export interface IEndpointProvider {
*/
getChatEndpoint(requestOrFamily: LanguageModelChat | ChatRequest | ChatEndpointFamily): Promise<IChatEndpoint>;

/**
* Like {@link getChatEndpoint} but safe to call during the copilot LM
* provider's `provideLanguageModelChatInfo` callback. Non-copilot utility
* overrides that require `lm.selectChatModels` are deferred to avoid
* re-entering the language model service (which deadlocks).
*/
getChatEndpointDuringProviderResolution(family: ChatEndpointFamily): Promise<IChatEndpoint>;
Comment thread
rwoll marked this conversation as resolved.

/**
* Get the CAPI embedding endpoint information
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ export class TestEndpointProvider implements IEndpointProvider {
return await this.getChatEndpointInfo(this.gpt4oMiniModelToRunAgainst ?? CHAT_MODEL.GPT4OMINI, await this._modelLabChatModelMetadata, await this._prodChatModelMetadata);
}
}
async getChatEndpointDuringProviderResolution(family: ChatEndpointFamily): Promise<IChatEndpoint> {
const endpoint = await this.getChatEndpoint(family);
if (!endpoint) {
throw new Error(`Unrecognized chat endpoint family ${family}`);
}
return endpoint;
}
async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise<IEmbeddingsEndpoint> {
const id = LEGACY_EMBEDDING_MODEL_ID.TEXT3SMALL;
const modelInformation: IEmbeddingModelInformation = {
Expand Down
Loading