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