TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type OpenAI from "openai";2import { type PolyModel, modelKey } from "@/lib/ai/core/types";3import { createOpenAICompatAdapter } from "../shared/openai-compat/factory";4import { MISTRAL_CATALOG, mistralPricingFor, MISTRAL_NON_CHAT } from "./catalog";56/**7 * Mistral AI (La Plateforme) — `https://api.mistral.ai/v1/chat/completions` is OpenAI-shaped.8 * `GET /v1/models` returns rich capability metadata (completion_chat, function_calling, vision,9 * reasoning, max_context_length, aliases, deprecation) which drives the registry.10 * Quirks (docs/provider-research/mistral.md): `random_seed` instead of `seed`, `max_tokens`,11 * tool_choice `any`, Magistral reasoning streamed as `thinking` content chunks, `safe_prompt`.12 */13interface MistralModel {14 id: string;15 name?: string;16 description?: string;17 created?: number;18 max_context_length?: number;19 aliases?: string[];20 deprecation?: string | null;21 deprecation_replacement_model?: string | null;22 default_model_temperature?: number | null;23 type?: string;24 capabilities?: { completion_chat?: boolean; function_calling?: boolean; vision?: boolean; reasoning?: boolean; completion_fim?: boolean; ocr?: boolean; audio?: boolean };25}2627export function normalizeMistralModel(m: MistralModel): PolyModel | null {28 const caps = m.capabilities ?? {};29 if (!caps.completion_chat) return null;30 if (MISTRAL_NON_CHAT.test(m.id)) return null;31 const cat = MISTRAL_CATALOG.get(m.id) ?? MISTRAL_CATALOG.get(m.name ?? "");32 const reasoning = caps.reasoning === true;33 const deprecated = Boolean(m.deprecation) && new Date(m.deprecation!).getTime() < Date.now();34 const isAlias = m.name && m.name !== m.id;35 return {36 key: modelKey("mistral", m.id),37 id: m.id,38 provider: "mistral",39 displayName: cat?.displayName ?? prettyMistral(m.id, m.name),40 family: cat?.family ?? familyOf(m.id),41 capabilities: {42 text: true,43 vision: caps.vision === true,44 audioInput: caps.audio === true,45 audioOutput: false,46 imageGeneration: false,47 video: false,48 reasoning,49 tools: caps.function_calling === true,50 structuredOutput: true,51 streaming: true,52 files: true, // PDF via `document_url` data URI works on every chat model (probed)53 webSearch: false,54 ...(cat?.capabilityOverrides ?? {}),55 },56 limits: { contextTokens: m.max_context_length ?? cat?.limits?.contextTokens, maxOutputTokens: cat?.limits?.maxOutputTokens },57 parameters: {58 temperature: true,59 topP: true,60 topK: false,61 maxTokens: true,62 stop: true,63 seed: true,64 frequencyPenalty: true,65 presencePenalty: true,66 reasoningEffort: reasoning,67 reasoningEffortLevels: reasoning ? (m.id.includes("glm") ? ["none", "minimal", "low", "medium", "high", "xhigh", "max"] : ["none", "high"]) : undefined,68 thinkingBudget: false,69 temperatureRange: { min: 0, max: 1.5 },70 ...(cat?.parameters ?? {}),71 },72 status: deprecated ? "deprecated" : m.deprecation ? "deprecated" : cat?.status ?? (m.id.startsWith("labs-") ? "preview" : "active"),73 pricing: mistralPricingFor(m.id, m.name),74 metadata: {75 ...(cat?.metadata ?? {}),76 aliases: m.aliases ?? [],77 resolvesTo: isAlias ? m.name : undefined,78 description: m.description,79 defaultTemperature: m.default_model_temperature,80 deprecation: m.deprecation,81 replacement: m.deprecation_replacement_model,82 createdAt: m.created ? new Date(m.created * 1000).toISOString() : undefined,83 sortWeight: cat?.sortWeight ?? sortWeightOf(m.id),84 },85 };86}8788function familyOf(id: string): string {89 if (id.startsWith("magistral")) return "Magistral";90 if (id.startsWith("codestral") || id.startsWith("mistral-code")) return "Codestral";91 if (id.startsWith("ministral")) return "Ministral";92 if (id.startsWith("mistral-large")) return "Mistral Large";93 if (id.startsWith("mistral-medium")) return "Mistral Medium";94 if (id.startsWith("mistral-small")) return "Mistral Small";95 if (id.includes("glm")) return "GLM (hosted)";96 if (id.startsWith("labs-")) return "Labs";97 return "Mistral";98}99function sortWeightOf(id: string): number {100 if (id === "mistral-large-latest") return 100;101 if (id === "mistral-medium-latest") return 96;102 if (id === "magistral-medium-latest") return 94;103 if (id === "mistral-small-latest") return 90;104 if (id === "magistral-small-latest") return 88;105 if (id === "codestral-latest") return 84;106 if (id.startsWith("ministral-14b-latest")) return 80;107 if (id.startsWith("ministral-8b-latest")) return 78;108 if (id.startsWith("ministral-3b-latest")) return 76;109 if (id.endsWith("-latest")) return 60;110 return 30;111}112function prettyMistral(id: string, name?: string): string {113 const base = id.replace(/-latest$/, "");114 const words = base.split("-").map((w) => (/^\d/.test(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)));115 const pretty = words.join(" ").replace(/\b(\d+)b\b/i, "$1B");116 return id.endsWith("-latest") ? `${pretty} (latest${name && name !== id ? ` → ${name}` : ""})` : pretty;117}118119async function listMistral(client: OpenAI, apiKey: string, signal?: AbortSignal): Promise<PolyModel[]> {120 void client;121 const res = await fetch("https://api.mistral.ai/v1/models", { headers: { Authorization: `Bearer ${apiKey}` }, signal });122 if (!res.ok) {123 const body = (await res.json().catch(() => ({}))) as { message?: string; detail?: unknown };124 throw Object.assign(new Error(body.message ?? `HTTP ${res.status}`), { status: res.status, error: body, headers: res.headers });125 }126 const data = (await res.json()) as { data: MistralModel[] };127 return data.data.map(normalizeMistralModel).filter((m): m is PolyModel => Boolean(m));128}129130export const mistralAdapter = createOpenAICompatAdapter({131 id: "mistral",132 name: "Mistral AI",133 baseURL: "https://api.mistral.ai/v1",134 keyDocsUrl: "https://console.mistral.ai/api-keys",135 listModels: listMistral,136 useMaxTokens: true,137 // PDFs go in natively as `document_url` data URIs (probed OK, even on Codestral).138 messageOptions: { inlineFiles: true, pdfPart: (data) => ({ type: "document_url", document_url: `data:application/pdf;base64,${data}` }) as unknown as import("openai/resources/chat/completions").ChatCompletionContentPart },139 tweakParams: (params, settings, req) => {140 const p = params as unknown as Record<string, unknown>;141 // Mistral validates bodies strictly (422 on unknown keys): rename `seed` → `random_seed`, never send `max_completion_tokens`.142 if (params.seed !== undefined) {143 p.random_seed = params.seed;144 delete p.seed;145 }146 // tool_choice "required" is spelled "any" on Mistral.147 if (params.tool_choice === "required") p.tool_choice = "any";148 // Hybrid reasoning (Mistral Medium 3.5 / Small 4 / Magistral aliases): off by default, `reasoning_effort: "high"`149 // turns it on and `"none"` keeps it off (docs/provider-research/mistral.md). Third-party GLM accepts the full ladder.150 if (req.modelInfo?.capabilities.reasoning && settings.reasoningEffort) {151 const levels = req.modelInfo.parameters.reasoningEffortLevels ?? ["none", "high"];152 const e = settings.reasoningEffort;153 p.reasoning_effort = levels.includes(e) ? e : e === "minimal" ? (levels.includes("low") ? "low" : "none") : levels.includes("high") ? "high" : levels[levels.length - 1];154 }155 delete p.stream_options; // Mistral returns usage on the final chunk without it156 },157 streamUsage: false,158 refineError: (status, code, message) => {159 if (status === 401) return "INVALID_API_KEY";160 if (status === 422 && /model|not found/i.test(message)) return "MODEL_NOT_FOUND";161 if (status === 422) return "INVALID_PARAMETER";162 if (status === 429 && /quota|capacity|tier/i.test(message)) return "RATE_LIMITED";163 return undefined;164 },165});166