Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/claudeModelId.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 type { ParsedClaudeModelId } from '../common/claudeModelId';67/**8* Known model suffixes that are meaningful variants and should be preserved9* in SDK/endpoint model IDs. Mapped per model family. Date-based suffixes10* (e.g. 20251101) are intentionally excluded — they are build identifiers11* that should not appear in the normalized output.12*/13const VALID_SUFFIXES: ReadonlyMap<string, ReadonlySet<string>> = new Map([14['opus', new Set(['1m'])],15]);1617const cache = new Map<string, ParsedClaudeModelId | undefined>();1819/**20* Parses a Claude model ID string (SDK or endpoint format) into its components.21* Throws if the model ID is unparseable or not a Claude ID.22*23* Use {@link tryParseClaudeModelId} when the input may not be a valid Claude model ID24* (e.g. model IDs from disk or external sources).25*/26export function parseClaudeModelId(modelId: string): ParsedClaudeModelId {27const result = tryParseClaudeModelId(modelId);28if (!result) {29throw new Error(`Unable to parse Claude model ID: '${modelId}'`);30}31return result;32}3334/**35* Attempts to parse a Claude model ID string (SDK or endpoint format) into its components.36*37* Accepts either format:38* - SDK: `claude-opus-4-5-20251101`, `claude-3-5-sonnet-20241022`, `claude-sonnet-4-20250514`39* - Endpoint: `claude-opus-4.5`, `claude-sonnet-4`, `claude-haiku-3.5`40*41* Returns `undefined` for unparseable or non-Claude IDs.42*/43export function tryParseClaudeModelId(modelId: string): ParsedClaudeModelId | undefined {44const cacheKey = modelId.toLowerCase();45if (cache.has(cacheKey)) {46return cache.get(cacheKey);47}4849const result = doParse(cacheKey);50cache.set(cacheKey, result);51return result;52}5354const DATE_SUFFIX_RE = /^(?<base>.*)-(?<date>\d{8})$/;5556function doParse(lower: string): ParsedClaudeModelId | undefined {57let dateSuffix = '';58let base = lower;5960const dateMatch = DATE_SUFFIX_RE.exec(lower);61if (dateMatch?.groups) {62base = dateMatch.groups.base;63dateSuffix = dateMatch.groups.date;64}6566// Pattern 1: claude-{name}-{major}-{minor}[-{mod}] (e.g. claude-opus-4-5, claude-opus-4-6-1m)67const p1 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)-(?<minor>\d+)(?:-(?<mod>.+))?$/);68if (p1?.groups) {69return makeResult(p1.groups.name, p1.groups.major, p1.groups.minor, joinModifiers(p1.groups.mod, dateSuffix));70}7172// Pattern 2: claude-{major}-{minor}-{name}[-{mod}] (e.g. claude-3-5-sonnet)73const p2 = base.match(/^claude-(?<major>\d+)-(?<minor>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);74if (p2?.groups) {75return makeResult(p2.groups.name, p2.groups.major, p2.groups.minor, joinModifiers(p2.groups.mod, dateSuffix));76}7778// Pattern 3: claude-{name}-{major}.{minor}[-{mod}] (e.g. claude-opus-4.5, claude-opus-4.6-1m)79const p3 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)\.(?<minor>\d+)(?:-(?<mod>.+))?$/);80if (p3?.groups) {81return makeResult(p3.groups.name, p3.groups.major, p3.groups.minor, joinModifiers(p3.groups.mod, dateSuffix));82}8384// Pattern 4: claude-{name}-{major}[-{mod}] (e.g. claude-sonnet-4, claude-sonnet-4-1m)85const p4 = base.match(/^claude-(?<name>\w+)-(?<major>\d+)(?:-(?<mod>.+))?$/);86if (p4?.groups) {87return makeResult(p4.groups.name, p4.groups.major, undefined, joinModifiers(p4.groups.mod, dateSuffix));88}8990// Pattern 5: claude-{major}-{name}[-{mod}] (e.g. claude-3-opus)91const p5 = base.match(/^claude-(?<major>\d+)-(?<name>\w+)(?:-(?<mod>.+))?$/);92if (p5?.groups) {93return makeResult(p5.groups.name, p5.groups.major, undefined, joinModifiers(p5.groups.mod, dateSuffix));94}9596// Pattern 6: bare model name with no version (e.g. nectarine)97const p6 = base.match(/^(?<name>\w+)$/);98if (p6?.groups) {99return makeBareResult(p6.groups.name);100}101102return undefined;103}104105function joinModifiers(mod: string | undefined, dateSuffix: string): string {106if (mod && dateSuffix) {107return `${mod}-${dateSuffix}`;108}109return mod || dateSuffix;110}111112function formatModelId(name: string, major: string, minor: string | undefined, versionSep: string, validSuffix: string): string {113const base = minor !== undefined114? `claude-${name}-${major}${versionSep}${minor}`115: `claude-${name}-${major}`;116return validSuffix ? `${base}-${validSuffix}` : base;117}118119function makeBareResult(name: string): ParsedClaudeModelId {120return {121name,122version: '',123modifiers: '',124toSdkModelId: () => name,125toEndpointModelId: () => name,126};127}128129function makeResult(name: string, major: string, minor: string | undefined, modifiers: string): ParsedClaudeModelId {130const version = minor !== undefined ? `${major}.${minor}` : major;131const validSuffix = extractValidSuffix(name, modifiers);132return {133name,134version,135modifiers,136toSdkModelId: () => formatModelId(name, major, minor, '-', validSuffix),137toEndpointModelId: () => formatModelId(name, major, minor, '.', validSuffix),138};139}140141/**142* Extracts the valid suffix portion from modifiers for a given model family.143* For example, given modifiers '1m-20251101' and family 'opus', returns '1m'.144* Returns an empty string if no valid suffix is found.145*/146function extractValidSuffix(name: string, modifiers: string): string {147if (!modifiers) {148return '';149}150const allowedSuffixes = VALID_SUFFIXES.get(name);151if (!allowedSuffixes) {152return '';153}154// Check the full modifier string first (e.g. '1m')155if (allowedSuffixes.has(modifiers)) {156return modifiers;157}158// Check the first segment of compound modifiers (e.g. '1m' from '1m-20251101')159const firstSegment = modifiers.split('-')[0];160if (allowedSuffixes.has(firstSegment)) {161return firstSegment;162}163return '';164}165166167