src/vs/platform/agentHost/node/claude/claudeModelId.ts

203 LOC · 203 covered · 0 uncovered · 57 ranges · 495 concepts · 34 introducers · 293 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.

1 > /*--------------------------------------------------------------------------------------------- claudeModelId.ts ×9
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 > /**
7 > * Mirror of `extensions/copilot/src/extension/chatSessions/claude/{common,node}/claudeModelId.ts`.
8 > *
9 > * The Claude Agent SDK speaks Anthropic-canonical hyphenated model IDs
10 > * (e.g. `claude-opus-4-6-20251101`). CAPI speaks dotted endpoint IDs
11 > * (`claude-opus-4.6`). The Phase 2 proxy needs bidirectional translation
12 > * at three points: inbound `requestBody.model` (SDK→CAPI), outbound
13 > * `model` fields on streaming events / non-streaming responses (CAPI→SDK),
14 > * and `GET /v1/models` response IDs (CAPI→SDK).
15 > *
16 > * **Keep in sync with the extension copy.** When the model-ID grammar
17 > * changes (new family name, new modifier suffix), update both files.
18 > */
19 >
20 > export interface ParsedClaudeModelId {
21 > readonly name: string;
22 > readonly version: string;
23 > readonly modifiers: string;
24 > toSdkModelId(): string;
25 > toEndpointModelId(): string;
26 > }
27 >
28 > /**
29 > * Known model suffixes that are meaningful variants and should be preserved
30 > * in SDK/endpoint model IDs. Mapped per model family. Date-based suffixes
31 > * (e.g. 20251101) are intentionally excluded — they are build identifiers
32 > * that should not appear in the normalized output.
33 > */
34 > const VALID_SUFFIXES: ReadonlyMap<string, ReadonlySet<string>> = new Map([
35 > ['opus', new Set(['1m'])],
36 > ]);
37 >
38 > const cache = new Map<string, ParsedClaudeModelId | undefined>();
39 >
40 > /**
41 > * Parses a Claude model ID string (SDK or endpoint format) into its components.
42 > * Throws if the model ID is unparseable or not a Claude ID.
43 > *
44 > * Use {@link tryParseClaudeModelId} when the input may not be a valid Claude model ID
45 > * (e.g. model IDs from disk or external sources).
46 > */
47 > export function parseClaudeModelId(modelId: string): ParsedClaudeModelId {
48 > const result = tryParseClaudeModelId(modelId); claudeModelId.ts ×1
49 > if (!result) {
50 > throw new Error(`Unable to parse Claude model ID: '${modelId}'`); claudeModelId.ts ×1
51 > }
52 > return result; claudeModelId.ts ×1
53 > }
55 > /**
56 > * Normalize a Claude model ID to the SDK format (dash-separated version, e.g.
57 > * `claude-haiku-4-5`). The Claude Agent SDK / CLI only recognizes this form;
58 > * given the endpoint format (`claude-haiku-4.5`) it treats the model as unknown
59 > * and falls back to a generic feature set (adaptive thinking + reasoning effort)
60 > * that the model may not support, producing a 400 from CAPI. Unparseable /
61 > * non-Claude IDs pass through unchanged; `undefined` passes through as
62 > * `undefined` so callers can normalize an optional model id in one step.
63 > */
64 > export function toSdkModelId(modelId: string): string;
65 > export function toSdkModelId(modelId: string | undefined): string | undefined;
66 > export function toSdkModelId(modelId: string | undefined): string | undefined {
67 > if (modelId === undefined) { claudeModelId.ts ×2
68 > return undefined; claudeModelId.ts ×1
69 > }
70 > return tryParseClaudeModelId(modelId)?.toSdkModelId() ?? modelId; claudeModelId.ts ×2
71 > }
73 > /**
74 > * Attempts to parse a Claude model ID string (SDK or endpoint format) into its components.
75 > *
76 > * Accepts either format:
77 > * - SDK: `claude-opus-4-5-20251101`, `claude-3-5-sonnet-20241022`, `claude-sonnet-4-20250514`
78 > * - Endpoint: `claude-opus-4.5`, `claude-sonnet-4`, `claude-haiku-3.5`
79 > *
80 > * Returns `undefined` for unparseable or non-Claude IDs.
81 > */
82 > export function tryParseClaudeModelId(modelId: string): ParsedClaudeModelId | undefined {
83 > const cacheKey = modelId.toLowerCase(); claudeModelId.ts ×9
84 > if (cache.has(cacheKey)) {
85 > return cache.get(cacheKey); claudeModelId.ts ×1
86 > }
88 > const result = doParse(cacheKey);
89 > cache.set(cacheKey, result);
90 > return result;
91 > }
93 > const DATE_SUFFIX_RE = /^(?<base>.*)-(?<date>\d{8})$/;
94 >
95 > function doParse(lower: string): ParsedClaudeModelId | undefined { claudeModelId.ts ×9
96 > let dateSuffix = '';
97 > let base = lower;
98 >
99 > const dateMatch = DATE_SUFFIX_RE.exec(lower);
100 > if (dateMatch?.groups) {
101 > base = dateMatch.groups.base; claudeModelId.ts ×1
102 > dateSuffix = dateMatch.groups.date;
103 > }
105 > // Pattern 1: claude-{name}-{major}-{minor}[-{mod}] (e.g. claude-opus-4-5, claude-opus-4-6-1m)
106 > const p1 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)-(?<minor>\d+)(?:-(?<mod>.+))?$/);
107 > if (p1?.groups) {
108 > return makeResult(p1.groups.name, p1.groups.major, p1.groups.minor, joinModifiers(p1.groups.mod, dateSuffix)); claudeModelId.ts ×1
109 > }
111 > // Pattern 2: claude-{major}-{minor}-{name}[-{mod}] (e.g. claude-3-5-sonnet)
112 > const p2 = base.match(/^claude-(?<major>\d+)-(?<minor>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);
113 > if (p2?.groups) { claudeModelId.ts ×9
114 > return makeResult(p2.groups.name, p2.groups.major, p2.groups.minor, joinModifiers(p2.groups.mod, dateSuffix)); claudeModelId.ts ×1
115 > }
117 > // Pattern 3: claude-{name}-{major}.{minor}[-{mod}] (e.g. claude-opus-4.5, claude-opus-4.6-1m)
118 > const p3 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)\.(?<minor>\d+)(?:-(?<mod>.+))?$/);
119 > if (p3?.groups) { claudeModelId.ts ×9
120 > return makeResult(p3.groups.name, p3.groups.major, p3.groups.minor, joinModifiers(p3.groups.mod, dateSuffix)); claudeModelId.ts ×1
121 > }
123 > // Pattern 4: claude-{name}-{major}[-{mod}] (e.g. claude-sonnet-4, claude-sonnet-4-1m)
124 > const p4 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)(?:-(?<mod>.+))?$/);
125 > if (p4?.groups) { claudeModelId.ts ×9
126 > return makeResult(p4.groups.name, p4.groups.major, undefined, joinModifiers(p4.groups.mod, dateSuffix)); claudeModelId.ts ×1
127 > }
129 > // Pattern 5: claude-{major}-{name}[-{mod}] (e.g. claude-3-opus)
130 > const p5 = base.match(/^claude-(?<major>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);
131 > if (p5?.groups) { claudeModelId.ts ×9
132 > return makeResult(p5.groups.name, p5.groups.major, undefined, joinModifiers(p5.groups.mod, dateSuffix)); claudeModelId.ts ×1
133 > }
135 > // Pattern 6: bare model name with no version (e.g. nectarine)
136 > const p6 = base.match(/^(?<name>\w+)$/);
137 > if (p6?.groups) { claudeModelId.ts ×9
138 > return makeBareResult(p6.groups.name); claudeModelId.ts ×2
139 > }
141 > return undefined;
142 > }
144 > function joinModifiers(mod: string | undefined, dateSuffix: string): string { claudeModelId.ts ×4
145 > if (mod && dateSuffix) {
146 > return `${mod}-${dateSuffix}`; claudeModelId.ts ×2
147 > }
148 > return mod || dateSuffix; claudeModelId.ts ×1
151 > function formatModelId(name: string, major: string, minor: string | undefined, versionSep: string, validSuffix: string): string { claudeModelId.ts ×2
152 > const base = minor !== undefined
153 > ? `claude-${name}-${major}${versionSep}${minor}` claudeModelId.ts ×1
154 > : `claude-${name}-${major}`; claudeModelId.ts ×1
155 > return validSuffix ? `${base}-${validSuffix}` : base; claudeModelId.ts ×2
156 > }
158 > function makeBareResult(name: string): ParsedClaudeModelId { claudeModelId.ts ×2
159 > return {
160 > name,
161 > version: '',
162 > modifiers: '',
163 > toSdkModelId: () => name,
164 > toEndpointModelId: () => name,
165 > };
166 > }
168 > function makeResult(name: string, major: string, minor: string | undefined, modifiers: string): ParsedClaudeModelId { claudeModelId.ts ×4
169 > const version = minor !== undefined ? `${major}.${minor}` : major;
170 > const validSuffix = extractValidSuffix(name, modifiers);
171 > return {
172 > name,
173 > version,
174 > modifiers,
175 > toSdkModelId: () => formatModelId(name, major, minor, '-', validSuffix),
176 > toEndpointModelId: () => formatModelId(name, major, minor, '.', validSuffix),
177 > };
178 > }
180 > /**
181 > * Extracts the valid suffix portion from modifiers for a given model family.
182 > * For example, given modifiers '1m-20251101' and family 'opus', returns '1m'.
183 > * Returns an empty string if no valid suffix is found.
184 > */
185 > function extractValidSuffix(name: string, modifiers: string): string { claudeModelId.ts ×4
186 > if (!modifiers) {
187 > return ''; claudeModelId.ts ×1
188 > }
189 > const allowedSuffixes = VALID_SUFFIXES.get(name); claudeModelId.ts ×1
190 > if (!allowedSuffixes) {
191 > return ''; claudeModelId.ts ×1
192 > }
193 > // Check the full modifier string first (e.g. '1m') claudeModelId.ts ×1
194 > if (allowedSuffixes.has(modifiers)) {
195 > return modifiers; claudeModelId.ts ×1
196 > }
197 > // Check the first segment of compound modifiers (e.g. '1m' from '1m-20251101') claudeModelId.ts ×1
198 > const firstSegment = modifiers.split('-')[0];
199 > if (allowedSuffixes.has(firstSegment)) {
200 > return firstSegment; claudeModelId.ts ×2
201 > }
202 > return ''; claudeModelId.ts ×1
203 > }