Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeModels.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import type * as vscode from 'vscode';7import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';8import { ILogService } from '../../../../platform/log/common/logService';9import { IChatEndpoint } from '../../../../platform/networking/common/networking';10import { formatPricingLabel, getModelCapabilitiesDescription } from '../../../conversation/common/languageModelAccess';11import { createServiceIdentifier } from '../../../../util/common/services';12import { Emitter } from '../../../../util/vs/base/common/event';13import { Disposable } from '../../../../util/vs/base/common/lifecycle';14import type { ParsedClaudeModelId } from '../common/claudeModelId';15import { tryParseClaudeModelId } from './claudeModelId';16import { EffortLevel } from '@anthropic-ai/claude-agent-sdk';1718export const CLAUDE_REASONING_EFFORT_PROPERTY = 'reasoningEffort';1920export interface IClaudeCodeModels {21readonly _serviceBrand: undefined;22/**23* Resolves a Claude endpoint for the given requested model ID.24* Falls back to the fallback model ID if the requested model doesn't match,25* then to the newest Sonnet, newest Haiku, or any Claude endpoint.26* Returns `undefined` if no Claude endpoint can be found.27*/28resolveEndpoint(requestedModel: ParsedClaudeModelId | string | undefined, fallbackModelId: ParsedClaudeModelId | undefined): Promise<IChatEndpoint | undefined>;2930/**31* Resolves the reasoning effort level for the given requested model ID and requested reasoning effort.32*/33resolveReasoningEffort(requestedModel: ParsedClaudeModelId | string | undefined, requestedReasoningEffort: string | undefined): Promise<EffortLevel | undefined>;3435/**36* Registers a LanguageModelChatProvider so that Claude models appear in37* VS Code's built-in model picker for the claude-code session type.38*/39registerLanguageModelChatProvider(lm: typeof vscode['lm']): void;40}4142export const IClaudeCodeModels = createServiceIdentifier<IClaudeCodeModels>('IClaudeCodeModels');4344export class ClaudeCodeModels extends Disposable implements IClaudeCodeModels {45declare _serviceBrand: undefined;46private _cachedEndpoints: Promise<IChatEndpoint[]> | undefined;47private readonly _onDidChange = this._register(new Emitter<void>());4849constructor(50@IEndpointProvider private readonly endpointProvider: IEndpointProvider,51@ILogService private readonly logService: ILogService,52) {53super();54this._register(this.endpointProvider.onDidModelsRefresh(() => {55this._cachedEndpoints = undefined;56this._onDidChange.fire();57}));58}5960public registerLanguageModelChatProvider(lm: typeof vscode['lm']): void {61const provider: vscode.LanguageModelChatProvider = {62onDidChangeLanguageModelChatInformation: this._onDidChange.event,63provideLanguageModelChatInformation: async (_options, _token) => {64return this._provideLanguageModelChatInfo();65},66provideLanguageModelChatResponse: async (_model, _messages, _options, _progress, _token) => {67// Implemented via chat participants.68},69provideTokenCount: async (_model, _text, _token) => {70// Token counting is not currently supported for the claude provider.71return 0;72}73};74this._register(lm.registerLanguageModelChatProvider('claude-code', provider));7576void this._getEndpoints().then(() => this._onDidChange.fire());77}7879private _getEndpoints(): Promise<IChatEndpoint[]> {80if (!this._cachedEndpoints) {81this._cachedEndpoints = this._fetchAvailableEndpoints();82}83return this._cachedEndpoints;84}8586private async _provideLanguageModelChatInfo(): Promise<vscode.LanguageModelChatInformation[]> {87const endpoints = await this._getEndpoints();88return endpoints.map(endpoint => {89const multiplier = endpoint.multiplier === undefined ? undefined : `${endpoint.multiplier}x`;90const tooltip: string | undefined = getModelCapabilitiesDescription(endpoint);91return {92id: endpoint.model,93name: endpoint.name,94family: endpoint.family,95version: endpoint.version,96maxInputTokens: endpoint.modelMaxPromptTokens,97maxOutputTokens: endpoint.maxOutputTokens,98pricing: multiplier ?? (endpoint.tokenPricing ? formatPricingLabel(endpoint.tokenPricing) : undefined),99multiplierNumeric: endpoint.multiplier,100tooltip,101isUserSelectable: true,102configurationSchema: buildConfigurationSchema(endpoint),103capabilities: {104imageInput: endpoint.supportsVision,105toolCalling: endpoint.supportsToolCalls,106editTools: endpoint.supportedEditTools ? [...endpoint.supportedEditTools] : undefined,107},108targetChatSessionType: 'claude-code'109};110});111}112113public async resolveReasoningEffort(requestedModel: ParsedClaudeModelId | string | undefined, requestedReasoningEffort: string | undefined): Promise<EffortLevel | undefined> {114const endpoint = await this.resolveEndpoint(requestedModel, undefined);115return pickReasoningEffort(endpoint, requestedReasoningEffort);116}117118public async resolveEndpoint(requestedModel: ParsedClaudeModelId | string | undefined, fallbackModelId: ParsedClaudeModelId | undefined): Promise<IChatEndpoint | undefined> {119const endpoints = await this._getEndpoints();120121// 1. Exact match for the requested model122if (requestedModel) {123let parsedModel: ParsedClaudeModelId | undefined;124if (typeof requestedModel === 'string') {125parsedModel = tryParseClaudeModelId(requestedModel);126} else {127parsedModel = requestedModel;128}129const mappedModel = parsedModel?.toEndpointModelId() ?? requestedModel;130const exact = endpoints.find(e => e.family === mappedModel || e.model === mappedModel);131if (exact) {132return exact;133}134}135136// 2. Exact match for the fallback model from session state137if (fallbackModelId) {138const fallback = endpoints.find(e => e.model === fallbackModelId.toEndpointModelId());139if (fallback) {140return fallback;141}142}143144// 3. Newest Sonnet (endpoints are sorted by name descending)145const sonnet = endpoints.find(e => e.family?.includes('sonnet') || e.model.includes('sonnet'));146if (sonnet) {147return sonnet;148}149150// 4. Newest Haiku151const haiku = endpoints.find(e => e.family?.includes('haiku') || e.model.includes('haiku'));152if (haiku) {153return haiku;154}155156// 5. Any model (these are already only Anthropic models)157return endpoints[0];158}159160private async _fetchAvailableEndpoints(): Promise<IChatEndpoint[]> {161try {162const endpoints = await this.endpointProvider.getAllChatEndpoints();163164// Filter for Anthropic models that are available in the model picker165// and use the Messages API (required for Claude Code)166const claudeEndpoints = endpoints.filter(e =>167e.supportsToolCalls &&168e.showInModelPicker &&169e.modelProvider === 'Anthropic' &&170e.apiType === 'messages'171);172173if (claudeEndpoints.length === 0) {174this.logService.trace('[ClaudeCodeModels] No Anthropic models with Messages API found');175return [];176}177178return claudeEndpoints.sort((a, b) => b.name.localeCompare(a.name));179} catch (ex) {180this.logService.error(`[ClaudeCodeModels] Failed to fetch models`, ex);181return [];182}183}184}185186const SUPPORTED_EFFORT_LEVELS: EffortLevel[] = ['low', 'medium', 'high'];187188export function isEffortLevel(value: string): value is EffortLevel {189return SUPPORTED_EFFORT_LEVELS.includes(value as EffortLevel);190}191192/**193* Formats a Claude endpoint for display in the chat response footer.194* Mirrors the Codex CLI's `formatModelDetails` for visual parity across providers.195*/196export function formatClaudeModelDetails(endpoint: IChatEndpoint): string {197return `${endpoint.name}${endpoint.multiplier ? ` • ${endpoint.multiplier}x` : ''}`;198}199200/**201* Picks the reasoning effort to use for an endpoint given a requested level.202*/203export function pickReasoningEffort(endpoint: IChatEndpoint | undefined, requestedReasoningEffort: string | undefined): EffortLevel | undefined {204if (!endpoint || !endpoint.supportsReasoningEffort || endpoint.supportsReasoningEffort.length === 0) {205return undefined;206}207if (requestedReasoningEffort && isEffortLevel(requestedReasoningEffort) && endpoint.supportsReasoningEffort.includes(requestedReasoningEffort)) {208return requestedReasoningEffort;209}210if (endpoint.supportsReasoningEffort.length === 1 && isEffortLevel(endpoint.supportsReasoningEffort[0])) {211return endpoint.supportsReasoningEffort[0];212}213return undefined;214}215216function buildConfigurationSchema(endpoint: IChatEndpoint): vscode.LanguageModelConfigurationSchema | undefined {217const effortLevels = endpoint.supportsReasoningEffort?.filter(218(level): level is typeof SUPPORTED_EFFORT_LEVELS[number] =>219(SUPPORTED_EFFORT_LEVELS as readonly string[]).includes(level)220);221if (!effortLevels) {222return;223}224225const defaultEffort = effortLevels.includes('high') ? 'high' : undefined;226227return {228properties: {229[CLAUDE_REASONING_EFFORT_PROPERTY]: {230type: 'string',231title: l10n.t('Thinking Effort'),232enum: effortLevels,233enumItemLabels: effortLevels.map(level => level.charAt(0).toUpperCase() + level.slice(1)),234enumDescriptions: effortLevels.map(level => {235switch (level) {236case 'low': return l10n.t('Faster responses with less reasoning');237case 'medium': return l10n.t('Balanced reasoning and speed');238case 'high': return l10n.t('Greater reasoning depth but slower');239}240}),241default: defaultEffort,242group: 'navigation',243}244}245};246}247248249