Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/claudeModelId.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 type { ParsedClaudeModelId } from '../common/claudeModelId';
7
8
/**
9
* Known model suffixes that are meaningful variants and should be preserved
10
* in SDK/endpoint model IDs. Mapped per model family. Date-based suffixes
11
* (e.g. 20251101) are intentionally excluded — they are build identifiers
12
* that should not appear in the normalized output.
13
*/
14
const VALID_SUFFIXES: ReadonlyMap<string, ReadonlySet<string>> = new Map([
15
['opus', new Set(['1m'])],
16
]);
17
18
const cache = new Map<string, ParsedClaudeModelId | undefined>();
19
20
/**
21
* Parses a Claude model ID string (SDK or endpoint format) into its components.
22
* Throws if the model ID is unparseable or not a Claude ID.
23
*
24
* Use {@link tryParseClaudeModelId} when the input may not be a valid Claude model ID
25
* (e.g. model IDs from disk or external sources).
26
*/
27
export function parseClaudeModelId(modelId: string): ParsedClaudeModelId {
28
const result = tryParseClaudeModelId(modelId);
29
if (!result) {
30
throw new Error(`Unable to parse Claude model ID: '${modelId}'`);
31
}
32
return result;
33
}
34
35
/**
36
* Attempts to parse a Claude model ID string (SDK or endpoint format) into its components.
37
*
38
* Accepts either format:
39
* - SDK: `claude-opus-4-5-20251101`, `claude-3-5-sonnet-20241022`, `claude-sonnet-4-20250514`
40
* - Endpoint: `claude-opus-4.5`, `claude-sonnet-4`, `claude-haiku-3.5`
41
*
42
* Returns `undefined` for unparseable or non-Claude IDs.
43
*/
44
export function tryParseClaudeModelId(modelId: string): ParsedClaudeModelId | undefined {
45
const cacheKey = modelId.toLowerCase();
46
if (cache.has(cacheKey)) {
47
return cache.get(cacheKey);
48
}
49
50
const result = doParse(cacheKey);
51
cache.set(cacheKey, result);
52
return result;
53
}
54
55
const DATE_SUFFIX_RE = /^(?<base>.*)-(?<date>\d{8})$/;
56
57
function doParse(lower: string): ParsedClaudeModelId | undefined {
58
let dateSuffix = '';
59
let base = lower;
60
61
const dateMatch = DATE_SUFFIX_RE.exec(lower);
62
if (dateMatch?.groups) {
63
base = dateMatch.groups.base;
64
dateSuffix = dateMatch.groups.date;
65
}
66
67
// Pattern 1: claude-{name}-{major}-{minor}[-{mod}] (e.g. claude-opus-4-5, claude-opus-4-6-1m)
68
const p1 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)-(?<minor>\d+)(?:-(?<mod>.+))?$/);
69
if (p1?.groups) {
70
return makeResult(p1.groups.name, p1.groups.major, p1.groups.minor, joinModifiers(p1.groups.mod, dateSuffix));
71
}
72
73
// Pattern 2: claude-{major}-{minor}-{name}[-{mod}] (e.g. claude-3-5-sonnet)
74
const p2 = base.match(/^claude-(?<major>\d+)-(?<minor>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);
75
if (p2?.groups) {
76
return makeResult(p2.groups.name, p2.groups.major, p2.groups.minor, joinModifiers(p2.groups.mod, dateSuffix));
77
}
78
79
// Pattern 3: claude-{name}-{major}.{minor}[-{mod}] (e.g. claude-opus-4.5, claude-opus-4.6-1m)
80
const p3 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)\.(?<minor>\d+)(?:-(?<mod>.+))?$/);
81
if (p3?.groups) {
82
return makeResult(p3.groups.name, p3.groups.major, p3.groups.minor, joinModifiers(p3.groups.mod, dateSuffix));
83
}
84
85
// Pattern 4: claude-{name}-{major}[-{mod}] (e.g. claude-sonnet-4, claude-sonnet-4-1m)
86
const p4 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)(?:-(?<mod>.+))?$/);
87
if (p4?.groups) {
88
return makeResult(p4.groups.name, p4.groups.major, undefined, joinModifiers(p4.groups.mod, dateSuffix));
89
}
90
91
// Pattern 5: claude-{major}-{name}[-{mod}] (e.g. claude-3-opus)
92
const p5 = base.match(/^claude-(?<major>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);
93
if (p5?.groups) {
94
return makeResult(p5.groups.name, p5.groups.major, undefined, joinModifiers(p5.groups.mod, dateSuffix));
95
}
96
97
// Pattern 6: bare model name with no version (e.g. nectarine)
98
const p6 = base.match(/^(?<name>\w+)$/);
99
if (p6?.groups) {
100
return makeBareResult(p6.groups.name);
101
}
102
103
return undefined;
104
}
105
106
function joinModifiers(mod: string | undefined, dateSuffix: string): string {
107
if (mod && dateSuffix) {
108
return `${mod}-${dateSuffix}`;
109
}
110
return mod || dateSuffix;
111
}
112
113
function formatModelId(name: string, major: string, minor: string | undefined, versionSep: string, validSuffix: string): string {
114
const base = minor !== undefined
115
? `claude-${name}-${major}${versionSep}${minor}`
116
: `claude-${name}-${major}`;
117
return validSuffix ? `${base}-${validSuffix}` : base;
118
}
119
120
function makeBareResult(name: string): ParsedClaudeModelId {
121
return {
122
name,
123
version: '',
124
modifiers: '',
125
toSdkModelId: () => name,
126
toEndpointModelId: () => name,
127
};
128
}
129
130
function makeResult(name: string, major: string, minor: string | undefined, modifiers: string): ParsedClaudeModelId {
131
const version = minor !== undefined ? `${major}.${minor}` : major;
132
const validSuffix = extractValidSuffix(name, modifiers);
133
return {
134
name,
135
version,
136
modifiers,
137
toSdkModelId: () => formatModelId(name, major, minor, '-', validSuffix),
138
toEndpointModelId: () => formatModelId(name, major, minor, '.', validSuffix),
139
};
140
}
141
142
/**
143
* Extracts the valid suffix portion from modifiers for a given model family.
144
* For example, given modifiers '1m-20251101' and family 'opus', returns '1m'.
145
* Returns an empty string if no valid suffix is found.
146
*/
147
function extractValidSuffix(name: string, modifiers: string): string {
148
if (!modifiers) {
149
return '';
150
}
151
const allowedSuffixes = VALID_SUFFIXES.get(name);
152
if (!allowedSuffixes) {
153
return '';
154
}
155
// Check the full modifier string first (e.g. '1m')
156
if (allowedSuffixes.has(modifiers)) {
157
return modifiers;
158
}
159
// Check the first segment of compound modifiers (e.g. '1m' from '1m-20251101')
160
const firstSegment = modifiers.split('-')[0];
161
if (allowedSuffixes.has(firstSegment)) {
162
return firstSegment;
163
}
164
return '';
165
}
166
167