UNPKG

manifest

Version:

Self-hosted Manifest LLM router with embedded server, SQLite database, and dashboard

376 lines 14.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ProviderModelFetcherService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProviderModelFetcherService = exports.PROVIDER_CONFIGS = exports.PROVIDER_BLOCKLIST = exports.PROVIDER_NON_CHAT = exports.UNIVERSAL_NON_CHAT_RE = void 0; exports.filterNonChatModels = filterNonChatModels; const common_1 = require("@nestjs/common"); const ollama_1 = require("../common/constants/ollama"); const provider_base_url_1 = require("../routing/provider-base-url"); const qwen_region_1 = require("../routing/qwen-region"); const FETCH_TIMEOUT_MS = 5000; const DEFAULT_CONTEXT_WINDOW = 128000; const ANTHROPIC_DEFAULT_CONTEXT = 200000; const GEMINI_DEFAULT_CONTEXT = 1000000; const MINIMAX_SUBSCRIPTION_MODELS_URL = 'https://api.minimax.io/anthropic/v1/models?limit=100'; function createModelParser(config) { return (body, provider) => { const arr = body?.[config.arrayKey]; if (!Array.isArray(arr)) return []; return arr .filter((m) => config.filter(m)) .map((m) => { const entry = m; const id = config.getId(entry); const ctxVal = config.contextWindow ?? DEFAULT_CONTEXT_WINDOW; return { id, displayName: config.getDisplayName(entry, id), provider, contextWindow: typeof ctxVal === 'function' ? ctxVal(entry) : ctxVal, inputPricePerToken: config.inputPricePerToken ?? null, outputPricePerToken: config.outputPricePerToken ?? null, capabilityReasoning: false, capabilityCode: config.capabilityCode ?? false, qualityScore: config.qualityScore ?? 3, }; }); }; } const parseOpenAI = createModelParser({ arrayKey: 'data', filter: (entry) => typeof entry.id === 'string' && entry.id.length > 0, getId: (entry) => entry.id, getDisplayName: (_entry, id) => id, }); const OPENAI_DATE_SUFFIX_RE = /-\d{4}-\d{2}-\d{2}$/; const OPENAI_RESPONSES_ONLY_RE = /(?:-codex(?!-mini-latest)|^gpt-5[^/]*-pro(?:-|$)|^gpt-image-|^o1-pro|^o4-mini-deep-research)/i; function parseOpenAIDeduped(body, provider) { const filtered = parseOpenAI(body, provider).filter((m) => !OPENAI_RESPONSES_ONLY_RE.test(m.id)); const ids = new Set(filtered.map((m) => m.id)); return filtered.filter((m) => { if (!OPENAI_DATE_SUFFIX_RE.test(m.id)) return true; const alias = m.id.replace(OPENAI_DATE_SUFFIX_RE, ''); return !ids.has(alias); }); } exports.UNIVERSAL_NON_CHAT_RE = /(?:embed|tts|whisper|dall-e|imagen|cogview|wanx|sambert|paraformer|text-embedding|speech-to|voice-|audio-turbo)/i; exports.PROVIDER_NON_CHAT = { openai: /(?:moderation|davinci|babbage|^text-|realtime|-transcribe|^sora|^gpt-3\.5-turbo-instruct|audio|^chatgpt-image|search-api)/i, 'openai-subscription': /(?:moderation|davinci|babbage|^text-|realtime|-transcribe|^sora|audio|^chatgpt-image)/i, gemini: /(?:^aqs-|nano-banana|^deep-research|computer-use|^lyria|^gemini-2\.0-flash-lite$|flash-lite-preview|robotics)/i, mistral: /(?:^mistral-ocr|moderation|voxtral-.*-(?:transcribe|realtime)|^labs-|^mistral-vibe-cli)/i, xai: /(?:imagine|multi-agent)/i, }; exports.PROVIDER_BLOCKLIST = { mistral: new Set([ 'voxtral-mini-2602', ]), zai: new Set([ 'glm-5.1', ]), }; function filterNonChatModels(models, configKey) { const providerFilter = exports.PROVIDER_NON_CHAT[configKey]; const blocklist = exports.PROVIDER_BLOCKLIST[configKey]; return models.filter((m) => { if (exports.UNIVERSAL_NON_CHAT_RE.test(m.id)) return false; if (providerFilter && providerFilter.test(m.id)) return false; if (blocklist && blocklist.has(m.id)) return false; return true; }); } function bearerHeaders(key) { return { Authorization: `Bearer ${key}` }; } const parseMistral = createModelParser({ arrayKey: 'data', filter: (entry) => { if (typeof entry.id !== 'string' || entry.id.length === 0) return false; if (entry.deprecation != null) return false; if (entry.capabilities && entry.capabilities.completion_chat === false) return false; return true; }, getId: (entry) => entry.id, getDisplayName: (_entry, id) => id, }); const parseAnthropic = createModelParser({ arrayKey: 'data', filter: (entry) => typeof entry.id === 'string' && entry.type === 'model', getId: (entry) => entry.id, getDisplayName: (entry, id) => entry.display_name || id, contextWindow: ANTHROPIC_DEFAULT_CONTEXT, }); const GEMINI_VERSION_SUFFIX_RE = /-\d{3}$/; function parseGemini(body, provider) { const models = body?.models; if (!Array.isArray(models)) return []; const parsed = models .filter((m) => { const entry = m; if (typeof entry.name !== 'string') return false; const methods = entry.supportedGenerationMethods; return Array.isArray(methods) && methods.includes('generateContent'); }) .map((m) => { const entry = m; const id = entry.name.replace(/^models\//, ''); return { id, displayName: entry.displayName || id, provider, contextWindow: entry.inputTokenLimit ?? GEMINI_DEFAULT_CONTEXT, inputPricePerToken: null, outputPricePerToken: null, capabilityReasoning: false, capabilityCode: false, qualityScore: 3, }; }); const ids = new Set(parsed.map((m) => m.id)); return parsed.filter((m) => { if (!GEMINI_VERSION_SUFFIX_RE.test(m.id)) return true; const alias = m.id.replace(GEMINI_VERSION_SUFFIX_RE, ''); return !ids.has(alias); }); } function parseOpenRouter(body, provider) { const data = body?.data; if (!Array.isArray(data)) return []; return data .filter((m) => { const entry = m; if (typeof entry.id !== 'string') return false; const output = entry.architecture?.output_modalities?.map((o) => o.toLowerCase()); if (output && output.length > 0 && !output.every((o) => o === 'text')) { return false; } return true; }) .map((m) => { const entry = m; const prompt = entry.pricing?.prompt ? Number(entry.pricing.prompt) : null; const completion = entry.pricing?.completion ? Number(entry.pricing.completion) : null; return { id: entry.id, displayName: entry.name || entry.id, provider, contextWindow: entry.context_length ?? 128000, inputPricePerToken: prompt !== null && Number.isFinite(prompt) && prompt >= 0 ? prompt : null, outputPricePerToken: completion !== null && Number.isFinite(completion) && completion >= 0 ? completion : null, capabilityReasoning: false, capabilityCode: false, qualityScore: 3, }; }); } const parseOllama = createModelParser({ arrayKey: 'models', filter: (entry) => typeof entry.name === 'string', getId: (entry) => entry.name.replace(/:latest$/, ''), getDisplayName: (_entry, id) => id, inputPricePerToken: 0, outputPricePerToken: 0, qualityScore: 2, }); const parseOpenaiSubscription = createModelParser({ arrayKey: 'models', filter: (entry) => typeof entry.slug === 'string' && entry.visibility === 'list', getId: (entry) => entry.slug, getDisplayName: (entry, id) => entry.display_name || id, contextWindow: (entry) => entry.context_window ?? 200000, inputPricePerToken: 0, outputPricePerToken: 0, capabilityCode: true, }); const parseCopilot = createModelParser({ arrayKey: 'data', filter: (entry) => typeof entry.id === 'string' && entry.id.length > 0, getId: (entry) => `copilot/${entry.id}`, getDisplayName: (entry) => entry.id, inputPricePerToken: 0, outputPricePerToken: 0, }); exports.PROVIDER_CONFIGS = { openai: { endpoint: 'https://api.openai.com/v1/models', buildHeaders: bearerHeaders, parse: parseOpenAIDeduped, }, 'openai-subscription': { endpoint: 'https://chatgpt.com/backend-api/codex/models?client_version=0.99.0', buildHeaders: (key) => ({ Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', originator: 'codex_cli_rs', 'user-agent': 'codex_cli_rs/0.0.0 (Unknown 0; unknown) unknown', }), parse: parseOpenaiSubscription, }, deepseek: { endpoint: 'https://api.deepseek.com/models', buildHeaders: bearerHeaders, parse: parseOpenAI, }, mistral: { endpoint: 'https://api.mistral.ai/v1/models', buildHeaders: bearerHeaders, parse: parseMistral, }, moonshot: { endpoint: 'https://api.moonshot.ai/v1/models', buildHeaders: bearerHeaders, parse: parseOpenAI, }, xai: { endpoint: 'https://api.x.ai/v1/models', buildHeaders: bearerHeaders, parse: parseOpenAI, }, minimax: { endpoint: 'https://api.minimaxi.chat/v1/models', buildHeaders: bearerHeaders, parse: parseOpenAI, }, 'minimax-subscription': { endpoint: MINIMAX_SUBSCRIPTION_MODELS_URL, buildHeaders: (key) => ({ Authorization: `Bearer ${key}`, 'anthropic-version': '2023-06-01', }), parse: parseAnthropic, }, qwen: { endpoint: `${(0, qwen_region_1.getQwenCompatibleBaseUrl)('beijing')}/v1/models`, buildHeaders: bearerHeaders, parse: parseOpenAI, }, zai: { endpoint: 'https://open.bigmodel.cn/api/paas/v4/models', buildHeaders: bearerHeaders, parse: parseOpenAI, }, anthropic: { endpoint: 'https://api.anthropic.com/v1/models?limit=100', buildHeaders: (key, authType) => { const headers = { 'anthropic-version': '2023-06-01', }; if (authType === 'subscription') { headers['Authorization'] = `Bearer ${key}`; headers['anthropic-beta'] = 'oauth-2025-04-20'; } else { headers['x-api-key'] = key; } return headers; }, parse: parseAnthropic, }, gemini: { endpoint: (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}`, buildHeaders: () => ({}), parse: parseGemini, }, openrouter: { endpoint: 'https://openrouter.ai/api/v1/models', buildHeaders: () => ({}), parse: parseOpenRouter, }, ollama: { endpoint: `${ollama_1.OLLAMA_HOST}/api/tags`, buildHeaders: () => ({}), parse: parseOllama, }, copilot: { endpoint: 'https://api.githubcopilot.com/models', buildHeaders: (key) => ({ Authorization: `Bearer ${key}`, Accept: 'application/json', 'Editor-Version': 'vscode/1.100.0', 'Editor-Plugin-Version': 'copilot/1.300.0', 'Copilot-Integration-Id': 'vscode-chat', }), parse: parseCopilot, }, }; let ProviderModelFetcherService = ProviderModelFetcherService_1 = class ProviderModelFetcherService { logger = new common_1.Logger(ProviderModelFetcherService_1.name); async fetch(providerId, apiKey, authType, endpointOverride) { let configKey = providerId.toLowerCase(); if (configKey === 'openai' && authType === 'subscription') { configKey = 'openai-subscription'; } else if (configKey === 'minimax' && authType === 'subscription') { configKey = 'minimax-subscription'; } const config = exports.PROVIDER_CONFIGS[configKey]; if (!config) { this.logger.warn(`No fetcher config for provider: ${providerId}`); return []; } let url = typeof config.endpoint === 'function' ? config.endpoint(apiKey) : config.endpoint; if (endpointOverride && configKey === 'minimax-subscription') { const minimaxBaseUrl = (0, provider_base_url_1.normalizeMinimaxSubscriptionBaseUrl)(endpointOverride); if (minimaxBaseUrl) { url = `${minimaxBaseUrl}/v1/models?limit=100`; } else { this.logger.warn('Ignoring invalid MiniMax subscription endpoint override'); } } else if (endpointOverride && configKey === 'qwen') { const qwenBaseUrl = (0, qwen_region_1.normalizeQwenCompatibleBaseUrl)(endpointOverride); if (qwenBaseUrl) { url = `${qwenBaseUrl}/v1/models`; } else { this.logger.warn('Ignoring invalid Qwen endpoint override'); } } const headers = config.buildHeaders(apiKey, authType); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); const res = await fetch(url, { headers, signal: controller.signal, }); clearTimeout(timeout); if (!res.ok) { this.logger.warn(`Provider ${providerId} returned ${res.status} from ${url.replace(apiKey, '***')}`); return []; } const body = await res.json(); return filterNonChatModels(config.parse(body, providerId), configKey); } catch (err) { const message = err instanceof Error ? err.message : String(err); this.logger.warn(`Failed to fetch models from ${providerId}: ${message}`); return []; } } }; exports.ProviderModelFetcherService = ProviderModelFetcherService; exports.ProviderModelFetcherService = ProviderModelFetcherService = ProviderModelFetcherService_1 = __decorate([ (0, common_1.Injectable)() ], ProviderModelFetcherService); //# sourceMappingURL=provider-model-fetcher.service.js.map