basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { AuthType, type Config } from '@qwen-code/qwen-code-core';8import { z } from 'zod';9 10/**11 * ACP model IDs are represented as `${modelId}(${authType})` in the ACP protocol.12 *13 * NOTE: The VSCode webview side mirrors this encoding contract in14 * `packages/vscode-ide-companion/src/webview/utils/discontinuedModel.ts` to15 * detect discontinued Qwen OAuth registry models without changing the wire16 * format. If the encoding here evolves (new authTypes, runtime prefix changes,17 * etc.), update that file too.18 */19export function formatAcpModelId(modelId: string, authType: AuthType): string {20 return `${modelId}(${authType})`;21}22 23export function sanitizeProviderBaseUrl(baseUrl: string): string {24 const scheme = baseUrl.match(/^[A-Za-z][A-Za-z\d+.-]*:\/\//);25 if (!scheme) {26 return baseUrl;27 }28 29 const authorityStart = scheme[0].length;30 const stripAt = (at: number) =>31 `${baseUrl.slice(0, authorityStart)}${baseUrl.slice(at + 1)}`;32 const authorityEnd = findAuthorityEnd(baseUrl, authorityStart);33 const authorityAt = baseUrl34 .slice(authorityStart, authorityEnd)35 .lastIndexOf('@');36 const authorityAtIndex =37 authorityAt === -1 ? -1 : authorityStart + authorityAt;38 39 try {40 const parsed = new URL(baseUrl);41 if (parsed.username || parsed.password) {42 return authorityAtIndex >= authorityStart43 ? stripAt(authorityAtIndex)44 : baseUrl;45 }46 return baseUrl;47 } catch {48 if (authorityAtIndex >= authorityStart) {49 return stripAt(authorityAtIndex);50 }51 52 const fallbackAt = findUnescapedUserInfoFallbackAt(53 baseUrl,54 authorityStart,55 authorityEnd,56 );57 return fallbackAt === -1 ? baseUrl : stripAt(fallbackAt);58 }59}60 61function findUnescapedUserInfoFallbackAt(62 baseUrl: string,63 authorityStart: number,64 authorityEnd: number,65): number {66 const at = baseUrl.lastIndexOf('@');67 if (at < authorityStart || authorityEnd >= at) {68 return -1;69 }70 71 const colon = baseUrl.indexOf(':', authorityStart);72 if (colon === -1 || colon > authorityEnd) {73 return -1;74 }75 76 const portCandidate = baseUrl.slice(colon + 1, authorityEnd);77 return /^\d+$/.test(portCandidate) ? -1 : at;78}79 80function findAuthorityEnd(baseUrl: string, authorityStart: number): number {81 const slash = baseUrl.indexOf('/', authorityStart);82 const query = baseUrl.indexOf('?', authorityStart);83 const hash = baseUrl.indexOf('#', authorityStart);84 let end = baseUrl.length;85 if (slash !== -1) end = Math.min(end, slash);86 if (query !== -1) end = Math.min(end, query);87 if (hash !== -1) end = Math.min(end, hash);88 return end;89}90 91/**92 * Extracts the base model id from an ACP model id string.93 *94 * If the string ends with `(...)`, the suffix is removed; otherwise returns the95 * trimmed input as-is.96 */97export function parseAcpBaseModelId(value: string): string {98 const trimmed = value.trim();99 const closeIdx = trimmed.lastIndexOf(')');100 const openIdx = trimmed.lastIndexOf('(');101 if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {102 return trimmed.slice(0, openIdx);103 }104 return trimmed;105}106 107/**108 * Parses an ACP model option string into `{ modelId, authType? }`.109 *110 * Supports the following formats:111 * - `${modelId}(${authType})` - Standard registry model (e.g., "gpt-4(USE_OPENAI)")112 * - `${snapshotId}(${authType})` - Runtime model snapshot (e.g., "$runtime|USE_OPENAI|gpt-4(USE_OPENAI)")113 * where snapshotId is in format `$runtime|${authType}|${modelId}`114 * - Plain model ID - Returns as-is with no authType115 *116 * If the string ends with `(...)` and `...` is a valid `AuthType`, returns both;117 * otherwise returns the trimmed input as `modelId` only.118 */119export function parseAcpModelOption(input: string): {120 modelId: string;121 authType?: AuthType;122} {123 const trimmed = input.trim();124 const closeIdx = trimmed.lastIndexOf(')');125 const openIdx = trimmed.lastIndexOf('(');126 if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {127 const maybeModelId = trimmed.slice(0, openIdx);128 const maybeAuthType = trimmed.slice(openIdx + 1, closeIdx);129 const parsedAuthType = z.nativeEnum(AuthType).safeParse(maybeAuthType);130 if (parsedAuthType.success) {131 return { modelId: maybeModelId, authType: parsedAuthType.data };132 }133 }134 return { modelId: trimmed };135}136 137/**138 * Whether a bare `modelId` resolves to the SAME provider identity as the active139 * content generator — same auth type, base URL, and credential env key.140 *141 * A per-turn inline `modelOverride` reuses the active provider's endpoint and142 * credentials and only swaps the model id; it cannot rebuild baseUrl/envKey for143 * a different provider. Any consumer that applies a `submit_prompt` result's144 * `modelOverride` must gate on this so an override naming a same-id model owned145 * by a different provider (or a different auth type) is never silently sent to146 * the active endpoint/account — even if a future (or untrusted) slash command147 * produces the override instead of the validated `/model` command. `modelId` is148 * the bare id without any `(authType)` suffix.149 */150export function isInlineModelOverrideAllowed(151 config: Config,152 modelId: string,153): boolean {154 const contentGeneratorConfig = config.getContentGeneratorConfig();155 const authType = contentGeneratorConfig?.authType;156 if (!authType) {157 return false;158 }159 const activeBaseUrl = contentGeneratorConfig.baseUrl;160 const activeEnvKey = contentGeneratorConfig.apiKeyEnvKey;161 return config162 .getAvailableModelsForAuthType(authType)163 .filter((m) => !m.fastOnly && !m.voiceOnly)164 .some(165 (m) =>166 m.id === modelId &&167 (m.baseUrl ?? undefined) === (activeBaseUrl ?? undefined) &&168 (m.envKey ?? undefined) === (activeEnvKey ?? undefined),169 );170}171 