diff --git a/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts b/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts index c5a2484761..c1ef838b6b 100644 --- a/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts +++ b/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts @@ -1,85 +1,5 @@ -const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; -const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; - -function normalizeFamily(value: string | null | undefined) { - return typeof value === "string" ? value.trim() : ""; -} - -function splitActualModelId(modelId: string) { - const trimmedModelId = modelId.trim(); - const slashIndex = trimmedModelId.indexOf("/"); - if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { - return null; - } - - return { - providerPrefix: trimmedModelId.slice(0, slashIndex), - providerModelId: trimmedModelId.slice(slashIndex + 1), - }; -} - -function extractTierSuffix(modelId: string) { - const match = modelId.match(TIER_SUFFIX_PATTERN); - return match?.[1] || ""; -} - -function stripTierSuffix(modelId: string) { - return modelId.replace(TIER_SUFFIX_PATTERN, ""); -} - -function isFamilyFirstEligibleFamily(family: string) { - const normalized = family.toLowerCase(); - return normalized.startsWith("gpt-") || normalized.startsWith("claude"); -} - -export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { - return actualModelId; - } - - const parts = splitActualModelId(actualModelId); - if (!parts) { - return actualModelId; - } - - const tierSuffix = extractTierSuffix(parts.providerModelId); - const providerModelBase = stripTierSuffix(parts.providerModelId); - if (providerModelBase !== normalizedFamily) { - return actualModelId; - } - - return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; -} - -export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { - const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!trimmedModelId) { - return trimmedModelId; - } - - const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); - if (!match) { - return trimmedModelId; - } - - const [, family, providerPrefix, serviceTier] = match; - const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; - return `${providerPrefix}/${family}${tierSuffix}`; -} - -export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - const candidates = new Set([actualModelId]); - const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); - if (publishedModelId !== actualModelId) { - candidates.add(publishedModelId); - } - - if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { - const tierSuffix = extractTierSuffix(actualModelId); - candidates.add(`${normalizedFamily}${tierSuffix}`); - } - - return [...candidates]; -} \ No newline at end of file +export { + getFamilyFirstModelCandidates, + getFamilyFirstPublishedModelId, + resolveFamilyFirstPublishedModelId, +} from "@/lib/vscode/familyFirstModelIds"; diff --git a/src/app/api/v1/vscode/[token]/modelPresentation.ts b/src/app/api/v1/vscode/[token]/modelPresentation.ts index 510467d5dc..be09ed3bf6 100644 --- a/src/app/api/v1/vscode/[token]/modelPresentation.ts +++ b/src/app/api/v1/vscode/[token]/modelPresentation.ts @@ -1,177 +1,5 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { - getCanonicalModelMetadata, - type CanonicalModelMetadata, -} from "@/lib/modelMetadataRegistry"; -import { - getVscodeServiceTierVariantSuffix, - parseVscodeServiceTierVariantModelId, - supportsVscodeServiceTierVariants, -} from "@/app/api/v1/vscode/[token]/serviceTierVariants"; -import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/[token]/reasoningMetadata"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds"; -type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -const PROVIDER_NAME_OVERRIDES: Record = { - codex: "Codex", - cx: "Codex", - github: "GitHub", - gh: "GitHub", - gemini: "Gemini", -}; - -function tokenize(value: string) { - return value - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((part) => part.trim()) - .filter((part) => part.length >= 4); -} - -function getProviderPrefix(metadata: CanonicalModelMetadata | null) { - const providerKey = metadata?.providerAlias || metadata?.provider || ""; - if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { - return PROVIDER_NAME_OVERRIDES[providerKey]; - } - - const providerLabel = metadata?.providerLabel?.trim() || null; - if (!providerLabel) { - return null; - } - if (/codex/i.test(providerLabel)) { - return "Codex"; - } - if (/github/i.test(providerLabel)) { - return "GitHub"; - } - if (/gemini/i.test(providerLabel)) { - return "Gemini"; - } - - return providerLabel; -} - -function normalizeDisplayNameBranding(displayName: string) { - return displayName - .replace(/^OpenAI\s+Codex\b/i, "Codex") - .replace(/^GitHub\s+Copilot\b/i, "GitHub") - .trim(); -} - -function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { - if (!providerPrefix) { - return displayName; - } - - const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); -} - -function looksLikeTechnicalModelName(value: string) { - return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value); -} - -function humanizeModelIdentifier(modelId: string) { - const identifier = (modelId.split("/").pop() || modelId).trim(); - if (!identifier) { - return identifier; - } - - return identifier - .split(/[-_]+/) - .filter(Boolean) - .map((part) => { - if (/^gpt$/i.test(part)) return "GPT"; - if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part; - if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase(); - return part.charAt(0).toUpperCase() + part.slice(1); - }) - .join(" "); -} - -function resolveFriendlyBaseDisplayName( - rawModelId: string, - metadata: CanonicalModelMetadata | null, - fallbackValue: string -) { - const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim()); - if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) { - return normalizedFallback; - } - - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const providerModelId = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - fallbackValue; - - return humanizeModelIdentifier(providerModelId); -} - -function prefixDisplayName(displayName: string, providerPrefix: string | null) { - const normalizedProviderPrefix = providerPrefix?.trim() || null; - const normalizedDisplayName = normalizeDisplayNameBranding(displayName); - - if (!normalizedProviderPrefix) return normalizedDisplayName; - - const providerTokens = tokenize(normalizedProviderPrefix); - if (providerTokens.length === 0) return normalizedDisplayName; - - const displayNameLower = normalizedDisplayName.toLowerCase(); - if (providerTokens.some((token) => displayNameLower.includes(token))) { - return normalizedDisplayName; - } - - return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); -} - -export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const provider = parsed.provider || model.owned_by || undefined; - const providerModel = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - model.root || - model.id || - model.name || - undefined; - - return providerModel && provider - ? getCanonicalModelMetadata({ provider, model: providerModel }) - : providerModel - ? getCanonicalModelMetadata({ model: providerModel }) - : null; -} - -export function getVscodeModelDisplayName(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); - const metadata = resolveVscodeModelMetadata(model); - const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; - const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); - const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); - return shouldShowTierSuffix - ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` - : prefixedDisplayName; -} - -export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { - const metadata = resolveVscodeModelMetadata(model); - return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; -} +export { + getVscodeModelDisplayName, + getVscodeModelGroupingKey, + resolveVscodeModelMetadata, +} from "@/lib/vscode/modelPresentation"; diff --git a/src/app/api/v1/vscode/[token]/reasoningMetadata.ts b/src/app/api/v1/vscode/[token]/reasoningMetadata.ts index e7996a8883..3153457309 100644 --- a/src/app/api/v1/vscode/[token]/reasoningMetadata.ts +++ b/src/app/api/v1/vscode/[token]/reasoningMetadata.ts @@ -1,175 +1,13 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; -import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/[token]/serviceTierVariants"; - -export type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; - capabilities?: Record; - supportsReasoningEffort?: string[]; - supportedReasoningEfforts?: string[]; - supports_reasoning_effort?: string[]; - defaultReasoningEffort?: string; - default_reasoning_effort?: string; -}; - -const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; -const DEFAULT_REASONING_EFFORT = "none"; -const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); - -export type VscodeModelConfigSchema = { - type: "object"; - properties: { - reasoningEffort: { - type: "string"; - title: string; - description: string; - default: string; - enum: string[]; - enumLabels: string[]; - enumDescriptions: string[]; - }; - }; -}; - -export function getCatalogModelName(model: VscodeCatalogModel) { - return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); -} - -function normalizeReasoningEffortValue(value: string) { - const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, ""); - if (normalized === "xhigh") return "xhigh"; - if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; - return undefined; -} - -function getNativeReasoningEffortValues(model: VscodeCatalogModel) { - const candidates = [ - model.supportsReasoningEffort, - model.supportedReasoningEfforts, - model.supports_reasoning_effort, - ]; - - for (const candidate of candidates) { - if (!Array.isArray(candidate) || candidate.length === 0) { - continue; - } - - const normalized = Array.from( - new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean)) - ) as string[]; - - if (normalized.length > 0) { - return normalized; - } - } - - return undefined; -} - -export function isReasoningCapableModel(model: VscodeCatalogModel) { - return ( - model.capabilities?.reasoning === true || - model.capabilities?.thinking === true || - (getNativeReasoningEffortValues(model)?.length || 0) > 0 - ); -} - -export function getReasoningEffortValues(model: VscodeCatalogModel) { - const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); - if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { - return nativeReasoningEffortValues; - } - - if (!isReasoningCapableModel(model)) return undefined; - - const modelId = getCatalogModelName(model); - const parsed = parseModel(modelId, ""); - const providerId = parsed.provider || model.owned_by || ""; - const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; - const values = ["none", "low", "medium", "high"]; - - if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { - values.push("xhigh"); - } - - return values; -} - -export function formatReasoningEffortLabel(level: string) { - if (level === "xhigh") return "XHigh"; - return level.charAt(0).toUpperCase() + level.slice(1); -} - -function describeReasoningEffort(level: string) { - switch (level) { - case "none": - return "Disables extra reasoning effort."; - case "low": - return "Uses a light amount of reasoning."; - case "medium": - return "Uses a balanced amount of reasoning."; - case "high": - return "Uses an extended amount of reasoning."; - case "xhigh": - return "Uses the maximum available reasoning effort."; - default: - return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; - } -} - -export function buildSupportedReasoningEfforts( - supportedValues: string[] -): string[] { - return [...supportedValues]; -} - -export function inferSelectedReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - const modelId = getCatalogModelName(model); - const match = modelId.match(EFFORT_SUFFIX_PATTERN); - if (!match) return undefined; - - const selected = match[1]?.toLowerCase(); - if (!selected) return undefined; - if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) { - return undefined; - } - - return selected; -} - -export function getReasoningVariantBaseModelId(modelId: string) { - return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); -} - -export function getDefaultReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; -} - -export function buildReasoningConfigSchema( - supportedValues: string[], - defaultReasoningEffort: string -): VscodeModelConfigSchema { - return { - type: "object", - properties: { - reasoningEffort: { - type: "string", - title: "Reasoning effort", - description: "Controls how much reasoning effort the model uses.", - default: defaultReasoningEffort, - enum: supportedValues, - enumLabels: supportedValues.map(formatReasoningEffortLabel), - enumDescriptions: supportedValues.map(describeReasoningEffort), - }, - }, - }; -} +export { + buildReasoningConfigSchema, + buildSupportedReasoningEfforts, + formatReasoningEffortLabel, + getCatalogModelName, + getDefaultReasoningEffort, + getReasoningEffortValues, + getReasoningVariantBaseModelId, + inferSelectedReasoningEffort, + isReasoningCapableModel, + type VscodeCatalogModel, + type VscodeModelConfigSchema, +} from "@/lib/vscode/reasoningMetadata"; diff --git a/src/app/api/v1/vscode/[token]/serviceTierVariants.ts b/src/app/api/v1/vscode/[token]/serviceTierVariants.ts index 8e69f224c3..08bfb6ef3b 100644 --- a/src/app/api/v1/vscode/[token]/serviceTierVariants.ts +++ b/src/app/api/v1/vscode/[token]/serviceTierVariants.ts @@ -1,190 +1,12 @@ -import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; -import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds"; - -const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; -const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; - -export type VscodeServiceTierModelLike = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { - baseModelId: string; - serviceTier?: ServiceTierId; -} { - const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!rawModelId) { - return { baseModelId: "" }; - } - - const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); - if (!match) { - return { baseModelId: rawModelId }; - } - - const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); - const serviceTier = normalizeServiceTierId(match[1]); - return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; -} - -export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { - return parseVscodeServiceTierVariantModelId(modelId).baseModelId; -} - -export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { - return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); -} - -export function getVscodeServiceTierVariantModelId( - baseModelId: string, - serviceTier: ServiceTierId -): string { - if (serviceTier === "standard") { - return baseModelId; - } - return `${baseModelId}__tier_${serviceTier}`; -} - -function getRawModelId(model: VscodeServiceTierModelLike): string { - return (model.id || model.name || model.root || "").trim(); -} - -function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { - const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; - if (owner) { - return owner; - } - const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; - return prefix; -} - -function supportsCodexServiceTierModel(baseModelId: string): boolean { - const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); - if (!normalizedModel) { - return false; - } - - return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { - const normalizedCandidate = candidate.trim().toLowerCase(); - return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate); - }); -} - -export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - return false; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const provider = getModelProvider(model, baseModelId); - if (provider !== "codex" && provider !== "cx") { - return false; - } - - return supportsCodexServiceTierModel(baseModelId); -} - -function cloneModelIdentifiers( - model: T, - modelId: string -): T { - return { - ...model, - ...(model.id ? { id: modelId } : {}), - ...(model.name ? { name: modelId } : {}), - ...(model.root ? { root: modelId } : {}), - }; -} - -export function expandVscodeServiceTierModels(models: T[]): T[] { - const expanded: T[] = []; - - for (const model of models) { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - expanded.push(model); - continue; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); - expanded.push(baseModel as T); - - if (!supportsVscodeServiceTierVariants(model)) { - continue; - } - - for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { - expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier))); - } - } - - return expanded; -} - -export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { - if (serviceTier === "priority") { - return "Fast"; - } - if (serviceTier === "flex") { - return "Flex"; - } - return "Default"; -} - -export function resolveVscodeServiceTierRequest(body: Record): Record { - const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; - if (!rawModelId) { - return body; - } - - const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - - const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); - if (!serviceTier) { - if (resolvedModelId === rawModelId) { - return body; - } - - return { - ...body, - model: resolvedModelId, - }; - } - - return { - ...body, - model: baseModelId, - ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), - }; -} - -export async function rewriteVscodeServiceTierRequest(request: Request): Promise { - if (request.method !== "POST") { - return request; - } - - const body = await request.clone().json().catch(() => null); - if (!body || typeof body !== "object" || Array.isArray(body)) { - return request; - } - - const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); - if (rewrittenBody === body) { - return request; - } - - const headers = new Headers(request.headers); - headers.delete("content-length"); - - return new Request(request.url, { - method: request.method, - headers, - body: JSON.stringify(rewrittenBody), - }); -} +export { + expandVscodeServiceTierModels, + getVscodeServiceTierVariantModelId, + getVscodeServiceTierVariantSuffix, + isVscodeServiceTierVariantModelId, + parseVscodeServiceTierVariantModelId, + resolveVscodeServiceTierRequest, + rewriteVscodeServiceTierRequest, + stripVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, + type VscodeServiceTierModelLike, +} from "@/lib/vscode/serviceTierVariants"; diff --git a/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts b/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts index c5a2484761..c1ef838b6b 100644 --- a/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts +++ b/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts @@ -1,85 +1,5 @@ -const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; -const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; - -function normalizeFamily(value: string | null | undefined) { - return typeof value === "string" ? value.trim() : ""; -} - -function splitActualModelId(modelId: string) { - const trimmedModelId = modelId.trim(); - const slashIndex = trimmedModelId.indexOf("/"); - if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { - return null; - } - - return { - providerPrefix: trimmedModelId.slice(0, slashIndex), - providerModelId: trimmedModelId.slice(slashIndex + 1), - }; -} - -function extractTierSuffix(modelId: string) { - const match = modelId.match(TIER_SUFFIX_PATTERN); - return match?.[1] || ""; -} - -function stripTierSuffix(modelId: string) { - return modelId.replace(TIER_SUFFIX_PATTERN, ""); -} - -function isFamilyFirstEligibleFamily(family: string) { - const normalized = family.toLowerCase(); - return normalized.startsWith("gpt-") || normalized.startsWith("claude"); -} - -export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { - return actualModelId; - } - - const parts = splitActualModelId(actualModelId); - if (!parts) { - return actualModelId; - } - - const tierSuffix = extractTierSuffix(parts.providerModelId); - const providerModelBase = stripTierSuffix(parts.providerModelId); - if (providerModelBase !== normalizedFamily) { - return actualModelId; - } - - return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; -} - -export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { - const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!trimmedModelId) { - return trimmedModelId; - } - - const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); - if (!match) { - return trimmedModelId; - } - - const [, family, providerPrefix, serviceTier] = match; - const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; - return `${providerPrefix}/${family}${tierSuffix}`; -} - -export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - const candidates = new Set([actualModelId]); - const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); - if (publishedModelId !== actualModelId) { - candidates.add(publishedModelId); - } - - if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { - const tierSuffix = extractTierSuffix(actualModelId); - candidates.add(`${normalizedFamily}${tierSuffix}`); - } - - return [...candidates]; -} \ No newline at end of file +export { + getFamilyFirstModelCandidates, + getFamilyFirstPublishedModelId, + resolveFamilyFirstPublishedModelId, +} from "@/lib/vscode/familyFirstModelIds"; diff --git a/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts b/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts index ca632e6902..be09ed3bf6 100644 --- a/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts +++ b/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts @@ -1,177 +1,5 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { - getCanonicalModelMetadata, - type CanonicalModelMetadata, -} from "@/lib/modelMetadataRegistry"; -import { - getVscodeServiceTierVariantSuffix, - parseVscodeServiceTierVariantModelId, - supportsVscodeServiceTierVariants, -} from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants"; -import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds"; -type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -const PROVIDER_NAME_OVERRIDES: Record = { - codex: "Codex", - cx: "Codex", - github: "GitHub", - gh: "GitHub", - gemini: "Gemini", -}; - -function tokenize(value: string) { - return value - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((part) => part.trim()) - .filter((part) => part.length >= 4); -} - -function getProviderPrefix(metadata: CanonicalModelMetadata | null) { - const providerKey = metadata?.providerAlias || metadata?.provider || ""; - if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { - return PROVIDER_NAME_OVERRIDES[providerKey]; - } - - const providerLabel = metadata?.providerLabel?.trim() || null; - if (!providerLabel) { - return null; - } - if (/codex/i.test(providerLabel)) { - return "Codex"; - } - if (/github/i.test(providerLabel)) { - return "GitHub"; - } - if (/gemini/i.test(providerLabel)) { - return "Gemini"; - } - - return providerLabel; -} - -function normalizeDisplayNameBranding(displayName: string) { - return displayName - .replace(/^OpenAI\s+Codex\b/i, "Codex") - .replace(/^GitHub\s+Copilot\b/i, "GitHub") - .trim(); -} - -function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { - if (!providerPrefix) { - return displayName; - } - - const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); -} - -function looksLikeTechnicalModelName(value: string) { - return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value); -} - -function humanizeModelIdentifier(modelId: string) { - const identifier = (modelId.split("/").pop() || modelId).trim(); - if (!identifier) { - return identifier; - } - - return identifier - .split(/[-_]+/) - .filter(Boolean) - .map((part) => { - if (/^gpt$/i.test(part)) return "GPT"; - if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part; - if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase(); - return part.charAt(0).toUpperCase() + part.slice(1); - }) - .join(" "); -} - -function resolveFriendlyBaseDisplayName( - rawModelId: string, - metadata: CanonicalModelMetadata | null, - fallbackValue: string -) { - const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim()); - if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) { - return normalizedFallback; - } - - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const providerModelId = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - fallbackValue; - - return humanizeModelIdentifier(providerModelId); -} - -function prefixDisplayName(displayName: string, providerPrefix: string | null) { - const normalizedProviderPrefix = providerPrefix?.trim() || null; - const normalizedDisplayName = normalizeDisplayNameBranding(displayName); - - if (!normalizedProviderPrefix) return normalizedDisplayName; - - const providerTokens = tokenize(normalizedProviderPrefix); - if (providerTokens.length === 0) return normalizedDisplayName; - - const displayNameLower = normalizedDisplayName.toLowerCase(); - if (providerTokens.some((token) => displayNameLower.includes(token))) { - return normalizedDisplayName; - } - - return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); -} - -export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const provider = parsed.provider || model.owned_by || undefined; - const providerModel = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - model.root || - model.id || - model.name || - undefined; - - return providerModel && provider - ? getCanonicalModelMetadata({ provider, model: providerModel }) - : providerModel - ? getCanonicalModelMetadata({ model: providerModel }) - : null; -} - -export function getVscodeModelDisplayName(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); - const metadata = resolveVscodeModelMetadata(model); - const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; - const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); - const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); - return shouldShowTierSuffix - ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` - : prefixedDisplayName; -} - -export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { - const metadata = resolveVscodeModelMetadata(model); - return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; -} +export { + getVscodeModelDisplayName, + getVscodeModelGroupingKey, + resolveVscodeModelMetadata, +} from "@/lib/vscode/modelPresentation"; diff --git a/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts b/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts index 431bc25a4c..3153457309 100644 --- a/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts +++ b/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts @@ -1,175 +1,13 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; -import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants"; - -export type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; - capabilities?: Record; - supportsReasoningEffort?: string[]; - supportedReasoningEfforts?: string[]; - supports_reasoning_effort?: string[]; - defaultReasoningEffort?: string; - default_reasoning_effort?: string; -}; - -const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; -const DEFAULT_REASONING_EFFORT = "none"; -const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); - -export type VscodeModelConfigSchema = { - type: "object"; - properties: { - reasoningEffort: { - type: "string"; - title: string; - description: string; - default: string; - enum: string[]; - enumLabels: string[]; - enumDescriptions: string[]; - }; - }; -}; - -export function getCatalogModelName(model: VscodeCatalogModel) { - return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); -} - -function normalizeReasoningEffortValue(value: string) { - const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, ""); - if (normalized === "xhigh") return "xhigh"; - if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; - return undefined; -} - -function getNativeReasoningEffortValues(model: VscodeCatalogModel) { - const candidates = [ - model.supportsReasoningEffort, - model.supportedReasoningEfforts, - model.supports_reasoning_effort, - ]; - - for (const candidate of candidates) { - if (!Array.isArray(candidate) || candidate.length === 0) { - continue; - } - - const normalized = Array.from( - new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean)) - ) as string[]; - - if (normalized.length > 0) { - return normalized; - } - } - - return undefined; -} - -export function isReasoningCapableModel(model: VscodeCatalogModel) { - return ( - model.capabilities?.reasoning === true || - model.capabilities?.thinking === true || - (getNativeReasoningEffortValues(model)?.length || 0) > 0 - ); -} - -export function getReasoningEffortValues(model: VscodeCatalogModel) { - const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); - if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { - return nativeReasoningEffortValues; - } - - if (!isReasoningCapableModel(model)) return undefined; - - const modelId = getCatalogModelName(model); - const parsed = parseModel(modelId, ""); - const providerId = parsed.provider || model.owned_by || ""; - const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; - const values = ["none", "low", "medium", "high"]; - - if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { - values.push("xhigh"); - } - - return values; -} - -export function formatReasoningEffortLabel(level: string) { - if (level === "xhigh") return "XHigh"; - return level.charAt(0).toUpperCase() + level.slice(1); -} - -function describeReasoningEffort(level: string) { - switch (level) { - case "none": - return "Disables extra reasoning effort."; - case "low": - return "Uses a light amount of reasoning."; - case "medium": - return "Uses a balanced amount of reasoning."; - case "high": - return "Uses an extended amount of reasoning."; - case "xhigh": - return "Uses the maximum available reasoning effort."; - default: - return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; - } -} - -export function buildSupportedReasoningEfforts( - supportedValues: string[] -): string[] { - return [...supportedValues]; -} - -export function inferSelectedReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - const modelId = getCatalogModelName(model); - const match = modelId.match(EFFORT_SUFFIX_PATTERN); - if (!match) return undefined; - - const selected = match[1]?.toLowerCase(); - if (!selected) return undefined; - if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) { - return undefined; - } - - return selected; -} - -export function getReasoningVariantBaseModelId(modelId: string) { - return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); -} - -export function getDefaultReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; -} - -export function buildReasoningConfigSchema( - supportedValues: string[], - defaultReasoningEffort: string -): VscodeModelConfigSchema { - return { - type: "object", - properties: { - reasoningEffort: { - type: "string", - title: "Reasoning effort", - description: "Controls how much reasoning effort the model uses.", - default: defaultReasoningEffort, - enum: supportedValues, - enumLabels: supportedValues.map(formatReasoningEffortLabel), - enumDescriptions: supportedValues.map(describeReasoningEffort), - }, - }, - }; -} +export { + buildReasoningConfigSchema, + buildSupportedReasoningEfforts, + formatReasoningEffortLabel, + getCatalogModelName, + getDefaultReasoningEffort, + getReasoningEffortValues, + getReasoningVariantBaseModelId, + inferSelectedReasoningEffort, + isReasoningCapableModel, + type VscodeCatalogModel, + type VscodeModelConfigSchema, +} from "@/lib/vscode/reasoningMetadata"; diff --git a/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts b/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts index 538354a839..08bfb6ef3b 100644 --- a/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts +++ b/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts @@ -1,190 +1,12 @@ -import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; -import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds"; - -const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; -const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; - -export type VscodeServiceTierModelLike = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { - baseModelId: string; - serviceTier?: ServiceTierId; -} { - const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!rawModelId) { - return { baseModelId: "" }; - } - - const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); - if (!match) { - return { baseModelId: rawModelId }; - } - - const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); - const serviceTier = normalizeServiceTierId(match[1]); - return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; -} - -export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { - return parseVscodeServiceTierVariantModelId(modelId).baseModelId; -} - -export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { - return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); -} - -export function getVscodeServiceTierVariantModelId( - baseModelId: string, - serviceTier: ServiceTierId -): string { - if (serviceTier === "standard") { - return baseModelId; - } - return `${baseModelId}__tier_${serviceTier}`; -} - -function getRawModelId(model: VscodeServiceTierModelLike): string { - return (model.id || model.name || model.root || "").trim(); -} - -function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { - const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; - if (owner) { - return owner; - } - const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; - return prefix; -} - -function supportsCodexServiceTierModel(baseModelId: string): boolean { - const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); - if (!normalizedModel) { - return false; - } - - return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { - const normalizedCandidate = candidate.trim().toLowerCase(); - return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate); - }); -} - -export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - return false; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const provider = getModelProvider(model, baseModelId); - if (provider !== "codex" && provider !== "cx") { - return false; - } - - return supportsCodexServiceTierModel(baseModelId); -} - -function cloneModelIdentifiers( - model: T, - modelId: string -): T { - return { - ...model, - ...(model.id ? { id: modelId } : {}), - ...(model.name ? { name: modelId } : {}), - ...(model.root ? { root: modelId } : {}), - }; -} - -export function expandVscodeServiceTierModels(models: T[]): T[] { - const expanded: T[] = []; - - for (const model of models) { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - expanded.push(model); - continue; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); - expanded.push(baseModel as T); - - if (!supportsVscodeServiceTierVariants(model)) { - continue; - } - - for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { - expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier))); - } - } - - return expanded; -} - -export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { - if (serviceTier === "priority") { - return "Fast"; - } - if (serviceTier === "flex") { - return "Flex"; - } - return "Default"; -} - -export function resolveVscodeServiceTierRequest(body: Record): Record { - const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; - if (!rawModelId) { - return body; - } - - const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - - const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); - if (!serviceTier) { - if (resolvedModelId === rawModelId) { - return body; - } - - return { - ...body, - model: resolvedModelId, - }; - } - - return { - ...body, - model: baseModelId, - ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), - }; -} - -export async function rewriteVscodeServiceTierRequest(request: Request): Promise { - if (request.method !== "POST") { - return request; - } - - const body = await request.clone().json().catch(() => null); - if (!body || typeof body !== "object" || Array.isArray(body)) { - return request; - } - - const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); - if (rewrittenBody === body) { - return request; - } - - const headers = new Headers(request.headers); - headers.delete("content-length"); - - return new Request(request.url, { - method: request.method, - headers, - body: JSON.stringify(rewrittenBody), - }); -} +export { + expandVscodeServiceTierModels, + getVscodeServiceTierVariantModelId, + getVscodeServiceTierVariantSuffix, + isVscodeServiceTierVariantModelId, + parseVscodeServiceTierVariantModelId, + resolveVscodeServiceTierRequest, + rewriteVscodeServiceTierRequest, + stripVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, + type VscodeServiceTierModelLike, +} from "@/lib/vscode/serviceTierVariants"; diff --git a/src/lib/vscode/familyFirstModelIds.ts b/src/lib/vscode/familyFirstModelIds.ts new file mode 100644 index 0000000000..051707ab49 --- /dev/null +++ b/src/lib/vscode/familyFirstModelIds.ts @@ -0,0 +1,92 @@ +const FAMILY_FIRST_MODEL_PATTERN = + /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; +const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; + +function normalizeFamily(value: string | null | undefined) { + return typeof value === "string" ? value.trim() : ""; +} + +function splitActualModelId(modelId: string) { + const trimmedModelId = modelId.trim(); + const slashIndex = trimmedModelId.indexOf("/"); + if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { + return null; + } + + return { + providerPrefix: trimmedModelId.slice(0, slashIndex), + providerModelId: trimmedModelId.slice(slashIndex + 1), + }; +} + +function extractTierSuffix(modelId: string) { + const match = modelId.match(TIER_SUFFIX_PATTERN); + return match?.[1] || ""; +} + +function stripTierSuffix(modelId: string) { + return modelId.replace(TIER_SUFFIX_PATTERN, ""); +} + +function isFamilyFirstEligibleFamily(family: string) { + const normalized = family.toLowerCase(); + return normalized.startsWith("gpt-") || normalized.startsWith("claude"); +} + +export function getFamilyFirstPublishedModelId( + actualModelId: string, + family: string | null | undefined +) { + const normalizedFamily = normalizeFamily(family); + if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { + return actualModelId; + } + + const parts = splitActualModelId(actualModelId); + if (!parts) { + return actualModelId; + } + + const tierSuffix = extractTierSuffix(parts.providerModelId); + const providerModelBase = stripTierSuffix(parts.providerModelId); + if (providerModelBase !== normalizedFamily) { + return actualModelId; + } + + return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; +} + +export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { + const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; + if (!trimmedModelId) { + return trimmedModelId; + } + + const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); + if (!match) { + return trimmedModelId; + } + + const [, family, providerPrefix, serviceTier] = match; + const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; + return `${providerPrefix}/${family}${tierSuffix}`; +} + +export function getFamilyFirstModelCandidates( + actualModelId: string, + family: string | null | undefined +) { + const normalizedFamily = normalizeFamily(family); + const candidates = new Set([actualModelId]); + const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); + if (publishedModelId !== actualModelId) { + candidates.add(publishedModelId); + } + + if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { + const tierSuffix = extractTierSuffix(actualModelId); + candidates.add(`${normalizedFamily}${tierSuffix}`); + } + + return [...candidates]; +} diff --git a/src/lib/vscode/modelPresentation.ts b/src/lib/vscode/modelPresentation.ts new file mode 100644 index 0000000000..980f8722c9 --- /dev/null +++ b/src/lib/vscode/modelPresentation.ts @@ -0,0 +1,132 @@ +import { parseModel } from "@omniroute/open-sse/services/model"; +import { + getCanonicalModelMetadata, + type CanonicalModelMetadata, +} from "@/lib/modelMetadataRegistry"; +import { resolveFamilyFirstPublishedModelId } from "@/lib/vscode/familyFirstModelIds"; +import { getReasoningVariantBaseModelId } from "@/lib/vscode/reasoningMetadata"; +import { + getVscodeServiceTierVariantSuffix, + parseVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, +} from "@/lib/vscode/serviceTierVariants"; + +type VscodeCatalogModel = { + id?: string; + name?: string; + root?: string; + owned_by?: string; +}; + +const PROVIDER_NAME_OVERRIDES: Record = { + codex: "Codex", + cx: "Codex", + github: "GitHub", + gh: "GitHub", + gemini: "Gemini", +}; + +function tokenize(value: string) { + return value + .toLowerCase() + .split(/[^a-z0-9]+/i) + .map((part) => part.trim()) + .filter((part) => part.length >= 4); +} + +function getProviderPrefix(metadata: CanonicalModelMetadata | null) { + const providerKey = metadata?.providerAlias || metadata?.provider || ""; + if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { + return PROVIDER_NAME_OVERRIDES[providerKey]; + } + + const providerLabel = metadata?.providerLabel?.trim() || null; + if (!providerLabel) { + return null; + } + if (/codex/i.test(providerLabel)) { + return "Codex"; + } + if (/github/i.test(providerLabel)) { + return "GitHub"; + } + if (/gemini/i.test(providerLabel)) { + return "Gemini"; + } + + return providerLabel; +} + +function normalizeDisplayNameBranding(displayName: string) { + return displayName + .replace(/^OpenAI\s+Codex\b/i, "Codex") + .replace(/^GitHub\s+Copilot\b/i, "GitHub") + .trim(); +} + +function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { + if (!providerPrefix) { + return displayName; + } + + const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); +} + +function prefixDisplayName(displayName: string, providerPrefix: string | null) { + const normalizedProviderPrefix = providerPrefix?.trim() || null; + const normalizedDisplayName = normalizeDisplayNameBranding(displayName); + + if (!normalizedProviderPrefix) return normalizedDisplayName; + + const providerTokens = tokenize(normalizedProviderPrefix); + if (providerTokens.length === 0) return normalizedDisplayName; + + const displayNameLower = normalizedDisplayName.toLowerCase(); + if (providerTokens.some((token) => displayNameLower.includes(token))) { + return normalizedDisplayName; + } + + return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); +} + +export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { + const rawModelId = model.id || model.root || model.name || ""; + const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); + const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); + const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); + const parsed = parseModel(canonicalBaseModelId, ""); + const provider = parsed.provider || model.owned_by || undefined; + const providerModel = + parsed.model || + (canonicalBaseModelId.includes("/") + ? canonicalBaseModelId.split("/").slice(1).join("/") + : canonicalBaseModelId) || + model.root || + model.id || + model.name || + undefined; + + return providerModel && provider + ? getCanonicalModelMetadata({ provider, model: providerModel }) + : providerModel + ? getCanonicalModelMetadata({ model: providerModel }) + : null; +} + +export function getVscodeModelDisplayName(model: VscodeCatalogModel) { + const rawModelId = model.id || model.root || model.name || ""; + const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); + const metadata = resolveVscodeModelMetadata(model); + const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; + const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); + const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); + return shouldShowTierSuffix + ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` + : prefixedDisplayName; +} + +export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { + const metadata = resolveVscodeModelMetadata(model); + return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; +} diff --git a/src/lib/vscode/reasoningMetadata.ts b/src/lib/vscode/reasoningMetadata.ts new file mode 100644 index 0000000000..60c1d30c1f --- /dev/null +++ b/src/lib/vscode/reasoningMetadata.ts @@ -0,0 +1,183 @@ +import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; +import { parseModel } from "@omniroute/open-sse/services/model"; +import { stripVscodeServiceTierVariantModelId } from "@/lib/vscode/serviceTierVariants"; + +export type VscodeCatalogModel = { + id?: string; + name?: string; + root?: string; + owned_by?: string; + capabilities?: Record; + supportsReasoningEffort?: string[]; + supportedReasoningEfforts?: string[]; + supports_reasoning_effort?: string[]; + defaultReasoningEffort?: string; + default_reasoning_effort?: string; +}; + +const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; +const DEFAULT_REASONING_EFFORT = "none"; +const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); + +export type VscodeModelConfigSchema = { + type: "object"; + properties: { + reasoningEffort: { + type: "string"; + title: string; + description: string; + default: string; + enum: string[]; + enumLabels: string[]; + enumDescriptions: string[]; + }; + }; +}; + +export function getCatalogModelName(model: VscodeCatalogModel) { + return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); +} + +function normalizeReasoningEffortValue(value: string) { + const normalized = value + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, ""); + if (normalized === "xhigh") return "xhigh"; + if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; + return undefined; +} + +function getNativeReasoningEffortValues(model: VscodeCatalogModel) { + const candidates = [ + model.supportsReasoningEffort, + model.supportedReasoningEfforts, + model.supports_reasoning_effort, + ]; + + for (const candidate of candidates) { + if (!Array.isArray(candidate) || candidate.length === 0) { + continue; + } + + const normalized = Array.from( + new Set( + candidate + .map((value) => + typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined + ) + .filter(Boolean) + ) + ) as string[]; + + if (normalized.length > 0) { + return normalized; + } + } + + return undefined; +} + +export function isReasoningCapableModel(model: VscodeCatalogModel) { + return ( + model.capabilities?.reasoning === true || + model.capabilities?.thinking === true || + (getNativeReasoningEffortValues(model)?.length || 0) > 0 + ); +} + +export function getReasoningEffortValues(model: VscodeCatalogModel) { + const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); + if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { + return nativeReasoningEffortValues; + } + + if (!isReasoningCapableModel(model)) return undefined; + + const modelId = getCatalogModelName(model); + const parsed = parseModel(modelId, ""); + const providerId = parsed.provider || model.owned_by || ""; + const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; + const values = ["none", "low", "medium", "high"]; + + if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { + values.push("xhigh"); + } + + return values; +} + +export function formatReasoningEffortLabel(level: string) { + if (level === "xhigh") return "XHigh"; + return level.charAt(0).toUpperCase() + level.slice(1); +} + +function describeReasoningEffort(level: string) { + switch (level) { + case "none": + return "Disables extra reasoning effort."; + case "low": + return "Uses a light amount of reasoning."; + case "medium": + return "Uses a balanced amount of reasoning."; + case "high": + return "Uses an extended amount of reasoning."; + case "xhigh": + return "Uses the maximum available reasoning effort."; + default: + return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; + } +} + +export function buildSupportedReasoningEfforts(supportedValues: string[]): string[] { + return [...supportedValues]; +} + +export function inferSelectedReasoningEffort( + model: VscodeCatalogModel, + supportedValues?: string[] +) { + const modelId = getCatalogModelName(model); + const match = modelId.match(EFFORT_SUFFIX_PATTERN); + if (!match) return undefined; + + const selected = match[1]?.toLowerCase(); + if (!selected) return undefined; + if ( + Array.isArray(supportedValues) && + supportedValues.length > 0 && + !supportedValues.includes(selected) + ) { + return undefined; + } + + return selected; +} + +export function getReasoningVariantBaseModelId(modelId: string) { + return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); +} + +export function getDefaultReasoningEffort(model: VscodeCatalogModel, supportedValues?: string[]) { + return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; +} + +export function buildReasoningConfigSchema( + supportedValues: string[], + defaultReasoningEffort: string +): VscodeModelConfigSchema { + return { + type: "object", + properties: { + reasoningEffort: { + type: "string", + title: "Reasoning effort", + description: "Controls how much reasoning effort the model uses.", + default: defaultReasoningEffort, + enum: supportedValues, + enumLabels: supportedValues.map(formatReasoningEffortLabel), + enumDescriptions: supportedValues.map(describeReasoningEffort), + }, + }, + }; +} diff --git a/src/lib/vscode/serviceTierVariants.ts b/src/lib/vscode/serviceTierVariants.ts new file mode 100644 index 0000000000..d5032127bc --- /dev/null +++ b/src/lib/vscode/serviceTierVariants.ts @@ -0,0 +1,202 @@ +import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; +import { resolveFamilyFirstPublishedModelId } from "@/lib/vscode/familyFirstModelIds"; +import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; + +const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; +const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; + +export type VscodeServiceTierModelLike = { + id?: string; + name?: string; + root?: string; + owned_by?: string; +}; + +export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { + baseModelId: string; + serviceTier?: ServiceTierId; +} { + const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; + if (!rawModelId) { + return { baseModelId: "" }; + } + + const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); + if (!match) { + return { baseModelId: rawModelId }; + } + + const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); + const serviceTier = normalizeServiceTierId(match[1]); + return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; +} + +export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { + return parseVscodeServiceTierVariantModelId(modelId).baseModelId; +} + +export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { + return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); +} + +export function getVscodeServiceTierVariantModelId( + baseModelId: string, + serviceTier: ServiceTierId +): string { + if (serviceTier === "standard") { + return baseModelId; + } + return `${baseModelId}__tier_${serviceTier}`; +} + +function getRawModelId(model: VscodeServiceTierModelLike): string { + return (model.id || model.name || model.root || "").trim(); +} + +function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { + const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; + if (owner) { + return owner; + } + const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; + return prefix; +} + +function supportsCodexServiceTierModel(baseModelId: string): boolean { + const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); + if (!normalizedModel) { + return false; + } + + return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { + const normalizedCandidate = candidate.trim().toLowerCase(); + return ( + normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate) + ); + }); +} + +export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { + const rawModelId = getRawModelId(model); + if (!rawModelId) { + return false; + } + + const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); + const provider = getModelProvider(model, baseModelId); + if (provider !== "codex" && provider !== "cx") { + return false; + } + + return supportsCodexServiceTierModel(baseModelId); +} + +function cloneModelIdentifiers(model: T, modelId: string): T { + return { + ...model, + ...(model.id ? { id: modelId } : {}), + ...(model.name ? { name: modelId } : {}), + ...(model.root ? { root: modelId } : {}), + }; +} + +export function expandVscodeServiceTierModels( + models: T[] +): T[] { + const expanded: T[] = []; + + for (const model of models) { + const rawModelId = getRawModelId(model); + if (!rawModelId) { + expanded.push(model); + continue; + } + + const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); + const baseModel = + rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); + expanded.push(baseModel as T); + + if (!supportsVscodeServiceTierVariants(model)) { + continue; + } + + for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { + expanded.push( + cloneModelIdentifiers( + baseModel as T, + getVscodeServiceTierVariantModelId(baseModelId, serviceTier) + ) + ); + } + } + + return expanded; +} + +export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { + if (serviceTier === "priority") { + return "Fast"; + } + if (serviceTier === "flex") { + return "Flex"; + } + return "Default"; +} + +export function resolveVscodeServiceTierRequest( + body: Record +): Record { + const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; + if (!rawModelId) { + return body; + } + + const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); + + const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); + if (!serviceTier) { + if (resolvedModelId === rawModelId) { + return body; + } + + return { + ...body, + model: resolvedModelId, + }; + } + + return { + ...body, + model: baseModelId, + ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), + }; +} + +export async function rewriteVscodeServiceTierRequest(request: Request): Promise { + if (request.method !== "POST") { + return request; + } + + const body = await request + .clone() + .json() + .catch(() => null); + if (!body || typeof body !== "object" || Array.isArray(body)) { + return request; + } + + const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); + if (rewrittenBody === body) { + return request; + } + + const headers = new Headers(request.headers); + headers.delete("content-length"); + + return new Request(request.url, { + method: request.method, + headers, + body: JSON.stringify(rewrittenBody), + }); +} diff --git a/tests/unit/vscode-shared-metadata.test.ts b/tests/unit/vscode-shared-metadata.test.ts new file mode 100644 index 0000000000..9d8e1d0047 --- /dev/null +++ b/tests/unit/vscode-shared-metadata.test.ts @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const familyFirstModelIds = + await import("../../src/app/api/v1/vscode/[token]/familyFirstModelIds.ts"); +const rawFamilyFirstModelIds = + await import("../../src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts"); +const serviceTierVariants = + await import("../../src/app/api/v1/vscode/[token]/serviceTierVariants.ts"); +const rawServiceTierVariants = + await import("../../src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts"); +const reasoningMetadata = await import("../../src/app/api/v1/vscode/[token]/reasoningMetadata.ts"); +const rawReasoningMetadata = + await import("../../src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts"); + +test("vscode raw and tokenized family-first helpers share behavior", () => { + assert.equal( + familyFirstModelIds.resolveFamilyFirstPublishedModelId("gpt-5.4__provider_cx__tier_priority"), + "cx/gpt-5.4__tier_priority" + ); + assert.deepEqual( + rawFamilyFirstModelIds.getFamilyFirstModelCandidates("cx/gpt-5.4__tier_flex", "gpt-5.4"), + familyFirstModelIds.getFamilyFirstModelCandidates("cx/gpt-5.4__tier_flex", "gpt-5.4") + ); +}); + +test("vscode raw and tokenized service tier helpers share behavior", () => { + const tokenizedPayload = serviceTierVariants.resolveVscodeServiceTierRequest({ + model: "gpt-5.4__provider_cx__tier_flex", + }); + const rawPayload = rawServiceTierVariants.resolveVscodeServiceTierRequest({ + model: "gpt-5.4__provider_cx__tier_flex", + }); + + assert.deepEqual(rawPayload, tokenizedPayload); + assert.deepEqual( + serviceTierVariants.expandVscodeServiceTierModels([ + { id: "cx/gpt-5.4", name: "cx/gpt-5.4", owned_by: "codex" }, + ]), + rawServiceTierVariants.expandVscodeServiceTierModels([ + { id: "cx/gpt-5.4", name: "cx/gpt-5.4", owned_by: "codex" }, + ]) + ); +}); + +test("vscode raw and tokenized reasoning helpers share behavior", () => { + const reasoningModel = { + id: "openai/gpt-5-high", + owned_by: "openai", + capabilities: { reasoning: true }, + }; + const supportedValues = reasoningMetadata.getReasoningEffortValues(reasoningModel); + + assert.deepEqual(supportedValues, rawReasoningMetadata.getReasoningEffortValues(reasoningModel)); + assert.equal( + reasoningMetadata.inferSelectedReasoningEffort(reasoningModel, supportedValues), + "high" + ); + assert.deepEqual( + reasoningMetadata.buildReasoningConfigSchema(["none", "high"], "high"), + rawReasoningMetadata.buildReasoningConfigSchema(["none", "high"], "high") + ); +});