src/vs/workbench/contrib/chat/common/modelSelection.ts
382 LOC · 325 covered · 57 uncovered · 79 ranges · 39 concepts · 35 introducers · 16 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
modelSelection.ts ×10
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService, isLanguageModelVendorAbsenceConclusive } from './languageModels.js';
import { isAgentHostTarget } from './chatSessionsService.js';
export type ModelIdentifierResolution =
| { readonly kind: 'notRequested' }
| { readonly kind: 'pending'; readonly identifier: string }
| { readonly kind: 'available'; readonly model: ILanguageModelChatMetadataAndIdentifier }
| { readonly kind: 'unavailable'; readonly identifier: string };
export interface IModelVendorResolution {
hasLiveModels(vendor: string): boolean;
hasResolved(vendor: string): boolean;
}
/** Resolves a requested model identifier against the current model catalog. */
export function resolveModelIdentifier(
identifier: string | undefined,
isAbsenceConclusive: boolean,
): ModelIdentifierResolution {
if (!identifier) {
}
const model = models.find(model => model.identifier === identifier);
if (model) {
}
return isAbsenceConclusive
? { kind: 'unavailable', identifier }
: { kind: 'pending', identifier };
}
/** Resolves a model identifier using vendor-level catalog readiness. */
export function resolveModelIdentifierFromCatalog(
identifier: string | undefined,
vendorResolution: IModelVendorResolution,
): ModelIdentifierResolution {
if (!identifier) {
return { kind: 'notRequested' };
}
const separator = identifier.search(/[/:]/);
const vendor = separator === -1 ? undefined : identifier.substring(0, separator);
const hasLive = vendor ? vendorResolution.hasLiveModels(vendor) : false;
// Agent-host vendors publish their models asynchronously after the agent host connects, so an
// empty (not-yet-populated) list is transient: keep the remembered/restored model `pending`
// (wait) rather than `unavailable` (give up). Once the vendor HAS live models, an absent model
// is genuinely gone, so stay conclusive. This grace is scoped to restore *resolution* only —
// cache-retention (`mergeModelsWithCache`) and send-availability keep treating a resolved-empty
// list as authoritative. The vendor id equals the session type for agent-host models, so
// `isAgentHostTarget` classifies it directly.
const isAbsenceConclusive = !vendor || (isLanguageModelVendorAbsenceConclusive(
vendor,
hasLive,
vendorResolution.hasResolved(vendor),
) && (hasLive || !isAgentHostTarget(vendor)));
return resolveModelIdentifier(models, identifier, isAbsenceConclusive);
}
export function getRegisteredLanguageModels(languageModelsService: Pick<ILanguageModelsService, 'getLanguageModelIds' | 'lookupLanguageModel'>): ILanguageModelChatMetadataAndIdentifier[] {
return languageModelsService.getLanguageModelIds()
.map(identifier => {
const metadata = languageModelsService.lookupLanguageModel(identifier);
return metadata ? { identifier, metadata } : undefined;
})
.filter(model => model !== undefined);
}
export function resolveModelIdentifierFromLanguageModels(
models: readonly ILanguageModelChatMetadataAndIdentifier[],
identifier: string | undefined,
languageModelsService: Pick<ILanguageModelsService, 'hasResolvedVendor'>,
allModels: readonly ILanguageModelChatMetadataAndIdentifier[],
): ModelIdentifierResolution {
const liveVendors = new Set(allModels.map(model => model.metadata.vendor));
return resolveModelIdentifierFromCatalog(models, identifier, {
hasLiveModels: vendor => liveVendors.has(vendor),
hasResolved: vendor => languageModelsService.hasResolvedVendor(vendor),
});
}
const AUTO_MODEL_ID = 'auto';
function compareModelVersions(a: string | undefined, b: string | undefined): number {
modelSelection.ts ×4
const rawA = a ?? '';
const rawB = b ?? '';
const segmentsA = rawA.match(/\d+/g)?.map(Number) ?? [];
const segmentsB = rawB.match(/\d+/g)?.map(Number) ?? [];
const length = Math.max(segmentsA.length, segmentsB.length);
for (let index = 0; index < length; index++) {
const numberA = segmentsA[index] ?? 0;
const numberB = segmentsB[index] ?? 0;
if (numberA !== numberB) {
return numberA - numberB;
}
}
return rawA.localeCompare(rawB);
}
/** Resolves a configured model id, family, or `auto` value against a model pool. */
export function resolveConfiguredModel(
models: readonly ILanguageModelChatMetadataAndIdentifier[],
): ILanguageModelChatMetadataAndIdentifier | undefined {
const value = configuredValue?.trim().toLowerCase();
if (!value) {
}
if (value === AUTO_MODEL_ID) {
return models.find(model => model.metadata.id?.trim().toLowerCase() === AUTO_MODEL_ID);
modelSelection.ts ×4
}
const byId = models.find(model => model.metadata.id?.trim().toLowerCase() === value);
if (byId) {
}
const family = models.filter(model => model.metadata.family?.trim().toLowerCase() === value);
return family.length > 0
? family.reduce((latest, candidate) => compareModelVersions(candidate.metadata.version, latest.metadata.version) > 0 ? candidate : latest)
modelSelection.ts ×4
export const enum ModelSelectionReason {
ConfiguredDefault = 'configuredDefault',
FirstAvailable = 'firstAvailable',
NoModels = 'noModels',
ProgrammaticSelection = 'programmaticSelection',
Remembered = 'remembered',
RemovedModelFallback = 'removedModelFallback',
SessionRestore = 'sessionRestore',
NewChatRepush = 'newChatRepush',
UserSelection = 'userSelection',
}
export type ModelSelectionApplyReason = Exclude<ModelSelectionReason, ModelSelectionReason.NoModels>;
export function isAuthoritativeModelSelectionReason(reason: ModelSelectionApplyReason | undefined): boolean {
|| reason === ModelSelectionReason.SessionRestore
|| reason === ModelSelectionReason.UserSelection;
}
export interface IPendingModelSelection {
readonly reference: string;
}
export type InitialModelSelectionResult =
| { readonly kind: 'none' }
| { readonly kind: 'pending'; readonly selection: IPendingModelSelection }
| { readonly kind: 'apply'; readonly model: ILanguageModelChatMetadataAndIdentifier; readonly reason: ModelSelectionApplyReason };
export interface IInitialModelSelectionInput {
readonly configuredModel: ILanguageModelChatMetadataAndIdentifier | undefined;
readonly desiredModelResolution: ModelIdentifierResolution;
readonly desiredReason: ModelSelectionReason.SessionRestore | ModelSelectionReason.Remembered;
readonly fallbackModel: ILanguageModelChatMetadataAndIdentifier | undefined;
readonly fallbackReason: ModelSelectionReason.FirstAvailable | ModelSelectionReason.RemovedModelFallback;
}
/** Applies the shared configured, desired, pending, then fallback precedence. */
export function resolveInitialModelSelection(input: IInitialModelSelectionInput): InitialModelSelectionResult {
return { kind: 'apply', model: input.configuredModel, reason: ModelSelectionReason.ConfiguredDefault };
modelSelection.ts ×1
}
return { kind: 'apply', model: input.desiredModelResolution.model, reason: input.desiredReason };
modelSelection.ts ×2
}
return { kind: 'pending', selection: { reference: input.desiredModelResolution.identifier } };
modelSelection.ts ×2
}
? { kind: 'apply', model: input.fallbackModel, reason: input.fallbackReason }
: { kind: 'none' };
export type ModelSelectionEffect =
| { readonly kind: 'none' }
| { readonly kind: 'clear'; readonly reason: ModelSelectionReason.NoModels | ModelSelectionReason.SessionRestore }
| { readonly kind: 'apply'; readonly model: ILanguageModelChatMetadataAndIdentifier; readonly reason: ModelSelectionApplyReason };
export type IModelSelectionSessionContext =
| { readonly kind: 'none' }
| {
readonly kind: 'untitled' | 'existing';
readonly key: string;
readonly chatKey: string | undefined;
readonly modelId: string | undefined;
};
export interface IModelSelectionModelsContext {
readonly available: readonly ILanguageModelChatMetadataAndIdentifier[];
readonly configuredModel: string | undefined;
readonly rememberedModelId: string | undefined;
readonly desiredModelResolution: ModelIdentifierResolution;
readonly fallbackModel: ILanguageModelChatMetadataAndIdentifier | undefined;
}
export interface IModelSelectionMemory {
readonly sessionKey: string | undefined;
readonly lastPushedChatKey: string | undefined;
readonly currentModel: ILanguageModelChatMetadataAndIdentifier | undefined;
readonly currentReason: ModelSelectionApplyReason | undefined;
}
export interface IModelSelectionTransitionInput {
readonly session: IModelSelectionSessionContext;
readonly models: IModelSelectionModelsContext;
readonly previous: IModelSelectionMemory;
}
export interface IModelSelectionTransitionResult {
readonly currentModel: ILanguageModelChatMetadataAndIdentifier | undefined;
readonly currentReason: ModelSelectionApplyReason | undefined;
readonly pendingSelection: IPendingModelSelection | undefined;
readonly effect: ModelSelectionEffect;
readonly sessionKey: string | undefined;
readonly lastPushedChatKey: string | undefined;
}
export function transitionModelSelection(input: IModelSelectionTransitionInput): IModelSelectionTransitionResult {
const sessionKey = session.kind === 'none' ? undefined : session.key;
const chatKey = session.kind === 'none' ? undefined : session.chatKey;
const sessionModelId = session.kind === 'none' ? undefined : session.modelId;
const sessionChanged = sessionKey !== previous.sessionKey;
const currentModel = sessionChanged ? undefined : previous.currentModel;
const currentReason = sessionChanged ? undefined : previous.currentReason;
const sessionModel = sessionModelId ? models.available.find(model => model.identifier === sessionModelId) : undefined;
const fallbackModel = models.available.find(model => model.identifier === models.rememberedModelId) ?? models.fallbackModel;
const newConversation = session.kind === 'untitled' && !sessionChanged && chatKey !== previous.lastPushedChatKey;
const automaticSelection = currentReason === ModelSelectionReason.ConfiguredDefault
|| currentReason === ModelSelectionReason.FirstAvailable
|| currentReason === ModelSelectionReason.NewChatRepush;
|| (!newConversation && (!sessionModelId || automaticSelection) && !isAuthoritativeModelSelectionReason(currentReason)))
modelSelection.ts ×1
if (chatKey === previous.lastPushedChatKey && currentReason === ModelSelectionReason.ConfiguredDefault && currentModel?.identifier === configuredModel.identifier) {
modelSelection.ts ×2
return { currentModel, currentReason, pendingSelection: undefined, effect: { kind: 'none' }, sessionKey, lastPushedChatKey: previous.lastPushedChatKey };
modelSelection.ts ×1
}
return applyResult(sessionKey, chatKey, configuredModel, ModelSelectionReason.ConfiguredDefault);
modelSelection.ts ×2
}
if (session.kind === 'existing' && models.desiredModelResolution.kind === 'pending') {
modelSelection.ts ×11
currentModel: undefined,
currentReason: undefined,
pendingSelection: { reference: models.desiredModelResolution.identifier },
effect: currentModel ? { kind: 'clear', reason: ModelSelectionReason.SessionRestore } : { kind: 'none' },
sessionKey,
lastPushedChatKey: chatKey,
};
}
currentModel: sessionModel,
currentReason: ModelSelectionReason.SessionRestore,
pendingSelection: undefined,
effect: { kind: 'none' },
sessionKey,
lastPushedChatKey: chatKey,
};
}
configuredModel,
desiredModelResolution: models.desiredModelResolution,
desiredReason: sessionModelId ? ModelSelectionReason.SessionRestore : ModelSelectionReason.Remembered,
fallbackModel,
fallbackReason: ModelSelectionReason.FirstAvailable,
});
if (initial.kind === 'pending') {
return { currentModel: undefined, currentReason: undefined, pendingSelection: initial.selection, effect: { kind: 'none' }, sessionKey, lastPushedChatKey: previous.lastPushedChatKey };
modelSelection.ts ×1
}
return applyResult(sessionKey, chatKey, initial.model, initial.reason);
}
}
if (models.available.length === 0) {
currentModel: undefined,
currentReason: undefined,
pendingSelection: undefined,
effect: currentModel ? { kind: 'clear', reason: ModelSelectionReason.NoModels } : { kind: 'none' },
sessionKey,
lastPushedChatKey: previous.lastPushedChatKey,
};
}
if (session.kind === 'existing') {
return {
currentModel: sessionModel,
currentReason: ModelSelectionReason.SessionRestore,
pendingSelection: undefined,
effect: { kind: 'none' },
sessionKey,
lastPushedChatKey: chatKey,
};
}
return applyResult(sessionKey, chatKey, fallbackModel, sessionModelId ? ModelSelectionReason.RemovedModelFallback : ModelSelectionReason.FirstAvailable);
}
const currentModelAvailable = !!currentModel && models.available.some(model => model.identifier === currentModel.identifier);
return {
currentModel: undefined,
currentReason: undefined,
pendingSelection: { reference: models.desiredModelResolution.identifier },
effect: { kind: 'clear', reason: ModelSelectionReason.SessionRestore },
sessionKey,
lastPushedChatKey: previous.lastPushedChatKey,
};
}
return applyResult(sessionKey, chatKey, fallbackModel, ModelSelectionReason.RemovedModelFallback);
}
return {
currentModel: undefined,
currentReason: undefined,
pendingSelection: undefined,
effect: { kind: 'clear', reason: ModelSelectionReason.NoModels },
sessionKey,
lastPushedChatKey: previous.lastPushedChatKey,
};
}
if (session.kind === 'untitled' && currentModel && currentReason === ModelSelectionReason.FirstAvailable) {
modelSelection.ts ×11
const initial = resolveInitialModelSelection({
configuredModel,
desiredModelResolution: models.desiredModelResolution,
desiredReason: ModelSelectionReason.Remembered,
fallbackModel,
fallbackReason: ModelSelectionReason.FirstAvailable,
});
if (initial.kind === 'pending') {
return { currentModel: undefined, currentReason: undefined, pendingSelection: initial.selection, effect: { kind: 'clear', reason: ModelSelectionReason.SessionRestore }, sessionKey, lastPushedChatKey: previous.lastPushedChatKey };
}
if (initial.kind === 'apply' && initial.model.identifier !== currentModel.identifier) {
return applyResult(sessionKey, chatKey, initial.model, initial.reason);
}
}
if (sessionModel && currentModel && sessionModel.identifier !== currentModel.identifier) {
modelSelection.ts ×11
return { currentModel: sessionModel, currentReason: ModelSelectionReason.SessionRestore, pendingSelection: undefined, effect: { kind: 'none' }, sessionKey, lastPushedChatKey: chatKey };
}
if (session.kind === 'untitled' && chatKey !== previous.lastPushedChatKey && currentModel && models.available.some(model => model.identifier === currentModel.identifier)) {
modelSelection.ts ×11
return applyResult(sessionKey, chatKey, currentModel, ModelSelectionReason.NewChatRepush);
modelSelection.ts ×2
}
return { currentModel, currentReason, pendingSelection: undefined, effect: { kind: 'none' }, sessionKey, lastPushedChatKey: previous.lastPushedChatKey };
}
sessionKey: string | undefined,
chatKey: string | undefined,
model: ILanguageModelChatMetadataAndIdentifier,
reason: ModelSelectionApplyReason,
): IModelSelectionTransitionResult {
return { currentModel: model, currentReason: reason, pendingSelection: undefined, effect: { kind: 'apply', model, reason }, sessionKey, lastPushedChatKey: chatKey };
}