import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts"; import { resolveWildcardAlias } from "./wildcardRouter.ts"; // Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS) // This prevents the two maps from drifting out of sync const ALIAS_TO_PROVIDER_ID = {}; for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) { if (ALIAS_TO_PROVIDER_ID[alias]) { console.log( `[MODEL] Warning: alias "${alias}" maps to both "${ALIAS_TO_PROVIDER_ID[alias]}" and "${id}". Using "${id}".` ); } ALIAS_TO_PROVIDER_ID[alias] = id; } // Provider-scoped legacy model aliases. Used to normalize provider/model inputs // and keep backward compatibility when upstream IDs change. const PROVIDER_MODEL_ALIASES = { github: { "claude-4.5-opus": "claude-opus-4-5-20251101", "claude-opus-4.5": "claude-opus-4-5-20251101", "gemini-3-pro": "gemini-3-pro-preview", "gemini-3-flash": "gemini-3-flash-preview", "raptor-mini": "oswe-vscode-prime", }, gemini: { "gemini-3.1-pro-preview": "gemini-3.1-pro", "gemini-3-1-pro": "gemini-3.1-pro", }, "gemini-cli": { "gemini-3.1-pro-preview": "gemini-3.1-pro", "gemini-3-1-pro": "gemini-3.1-pro", }, nvidia: { "gpt-oss-120b": "openai/gpt-oss-120b", "nvidia/gpt-oss-120b": "openai/gpt-oss-120b", }, antigravity: {}, }; // Reverse index: modelId -> providerIds that expose this model const MODEL_TO_PROVIDERS = new Map(); for (const [aliasOrId, models] of Object.entries(PROVIDER_MODELS)) { const providerId = ALIAS_TO_PROVIDER_ID[aliasOrId] || aliasOrId; for (const modelEntry of models || []) { const modelId = modelEntry?.id; if (!modelId) continue; const providers = MODEL_TO_PROVIDERS.get(modelId) || []; if (!providers.includes(providerId)) { providers.push(providerId); MODEL_TO_PROVIDERS.set(modelId, providers); } } } /** * Resolve provider alias to provider ID */ export function resolveProviderAlias(aliasOrId) { return ALIAS_TO_PROVIDER_ID[aliasOrId] || aliasOrId; } /** * Resolve provider-specific legacy model alias to canonical model ID. */ function resolveProviderModelAlias(providerOrAlias, modelId) { if (!modelId || typeof modelId !== "string") return modelId; const providerId = resolveProviderAlias(providerOrAlias); const aliases = PROVIDER_MODEL_ALIASES[providerId]; return aliases?.[modelId] || modelId; } /** * Parse model string: "alias/model" or "provider/model" or just alias * Supports [1m] suffix for extended 1M context window (e.g. "claude-sonnet-4-6[1m]") */ export function parseModel(modelStr) { if (!modelStr) { return { provider: null, model: null, isAlias: false, providerAlias: null, extendedContext: false, }; } // Sanitize: reject strings with path traversal or control characters if (/\.\.[\/\\]/.test(modelStr) || /[\x00-\x1f]/.test(modelStr)) { console.log(`[MODEL] Warning: rejected malformed model string: "${modelStr.substring(0, 50)}"`); return { provider: null, model: null, isAlias: false, providerAlias: null, extendedContext: false, }; } // Extract [1m] suffix before parsing provider/model let extendedContext = false; let cleanStr = modelStr; if (cleanStr.endsWith("[1m]")) { extendedContext = true; cleanStr = cleanStr.slice(0, -4); } // Check if standard format: provider/model or alias/model if (cleanStr.includes("/")) { const firstSlash = cleanStr.indexOf("/"); const providerOrAlias = cleanStr.slice(0, firstSlash); const model = cleanStr.slice(firstSlash + 1); const provider = resolveProviderAlias(providerOrAlias); return { provider, model, isAlias: false, providerAlias: providerOrAlias, extendedContext }; } // Alias format (model alias, not provider alias) return { provider: null, model: cleanStr, isAlias: true, providerAlias: null, extendedContext }; } /** * Resolve model alias from aliases object * Format: { "alias": "provider/model" } */ export function resolveModelAliasFromMap(alias, aliases) { if (!aliases) return null; // Check if alias exists const resolved = aliases[alias]; if (!resolved) return null; // Resolved value is "provider/model" format if (typeof resolved === "string" && resolved.includes("/")) { const firstSlash = resolved.indexOf("/"); const providerOrAlias = resolved.slice(0, firstSlash); return { provider: resolveProviderAlias(providerOrAlias), model: resolved.slice(firstSlash + 1), }; } // Or object { provider, model } if (typeof resolved === "object" && resolved.provider && resolved.model) { return { provider: resolveProviderAlias(resolved.provider), model: resolved.model, }; } return null; } /** * Get full model info (parse or resolve) * @param {string} modelStr - Model string * @param {object|function} aliasesOrGetter - Aliases object or async function to get aliases */ export async function getModelInfoCore(modelStr, aliasesOrGetter) { const parsed = parseModel(modelStr); const { extendedContext } = parsed; if (!parsed.isAlias) { const canonicalModel = resolveProviderModelAlias(parsed.provider, parsed.model); return { provider: parsed.provider, model: canonicalModel, extendedContext, }; } // Get aliases (from object or function) const aliases = typeof aliasesOrGetter === "function" ? await aliasesOrGetter() : aliasesOrGetter; // Resolve exact alias const resolved = resolveModelAliasFromMap(parsed.model, aliases); if (resolved) { const canonicalModel = resolveProviderModelAlias(resolved.provider, resolved.model); return { provider: resolved.provider, model: canonicalModel, extendedContext, }; } // T13: Try wildcard alias (glob patterns like "claude-sonnet-*" → "anthropic/claude-sonnet-4-...") if (aliases && typeof aliases === "object") { const aliasEntries = Object.entries(aliases).map(([pattern, target]) => ({ pattern, target })); const wildcardMatch = resolveWildcardAlias(parsed.model, aliasEntries); if (wildcardMatch) { const target = wildcardMatch.target as string; if (target.includes("/")) { const firstSlash = target.indexOf("/"); const providerOrAlias = target.slice(0, firstSlash); const targetModel = target.slice(firstSlash + 1); const provider = resolveProviderAlias(providerOrAlias); const canonicalModel = resolveProviderModelAlias(provider, targetModel); return { provider, model: canonicalModel, extendedContext, wildcardPattern: wildcardMatch.pattern, }; } } } const modelId = parsed.model; const providers = MODEL_TO_PROVIDERS.get(modelId) || []; // Preserve historical behavior: OpenAI stays default when model exists there if (providers.includes("openai")) { return { provider: "openai", model: modelId, extendedContext, }; } const nonOpenAIProviders = providers.filter((p) => p !== "openai"); if (nonOpenAIProviders.length === 1) { const provider = nonOpenAIProviders[0]; const canonicalModel = resolveProviderModelAlias(provider, modelId); return { provider, model: canonicalModel, extendedContext }; } if (nonOpenAIProviders.length > 1) { const aliasesForHint = nonOpenAIProviders.map((p) => PROVIDER_ID_TO_ALIAS[p] || p); const hints = aliasesForHint.slice(0, 2).map((alias) => `${alias}/${modelId}`); const message = `Ambiguous model '${modelId}'. Use provider/model prefix (ex: ${hints.join(" or ")}).`; console.warn(`[MODEL] ${message} Candidates: ${aliasesForHint.join(", ")}`); return { provider: null, model: modelId, errorType: "ambiguous_model", errorMessage: message, candidateProviders: nonOpenAIProviders, candidateAliases: aliasesForHint, }; } // Fallback: infer provider from known model name prefixes before defaulting to openai // FIX #73: Models like claude-haiku-4-5-20251001 sent without provider prefix // would incorrectly route to OpenAI. Use heuristic prefix detection first. if (/^claude-/i.test(modelId)) { // Claude models → Anthropic provider (canonical source for Claude models) return { provider: "anthropic", model: modelId, extendedContext }; } if (/^gemini-/i.test(modelId) || /^gemma-/i.test(modelId)) { // Gemini/Gemma models → Gemini provider return { provider: "gemini", model: modelId, extendedContext }; } // Last resort: treat as openai model return { provider: "openai", model: modelId, extendedContext, }; }