@ai-sdk/mistral
Version:
492 lines (463 loc) • 15.7 kB
text/typescript
import type {
ModelCapabilities,
ModelCapability,
ModelEntry,
ProviderOptionDefinition,
ProviderOptionDefinitions,
} from '@ai-sdk/provider';
import type {
MistralChatModelId,
MistralLanguageModelChatOptions,
} from './mistral-chat-language-model-options';
import type { MistralEmbeddingModelOptions } from './mistral-embedding-model-options';
import type { MistralSpeechModelOptions } from './mistral-speech-model-options';
import type { MistralTranscriptionModelOptions } from './mistral-transcription-model-options';
export const mistralProviderOptions = {
language: {
safePrompt: { description: 'Inject a safety prompt. Chat API only.' },
documentImageLimit: {
description: 'Maximum document images. Chat API only.',
},
documentPageLimit: {
description: 'Maximum document pages. Chat API only.',
},
structuredOutputs: {
description: 'Whether to send a JSON response schema.',
},
strictJsonSchema: {
description: 'Whether to use strict JSON schema validation.',
},
parallelToolCalls: {
description: 'Allow parallel function calls. Chat API only.',
},
promptCacheKey: { description: 'Cache affinity key. Chat API only.' },
reasoningEffort: {
description: 'Reasoning effort for configurable reasoning models.',
},
} satisfies Record<
keyof MistralLanguageModelChatOptions,
ProviderOptionDefinition
>,
embedding: {
metadata: { description: 'Metadata attached to the embedding request.' },
outputDimension: { description: 'Output embedding dimensions.' },
outputDtype: { description: 'Output embedding data type.' },
} satisfies Record<
keyof MistralEmbeddingModelOptions,
ProviderOptionDefinition
>,
transcription: {
language: { description: 'Language of the audio.' },
returnLanguage: { description: 'Return the detected language.' },
temperature: { description: 'Sampling temperature.' },
timestampGranularities: {
description: 'Timestamp granularities to return.',
},
diarize: { description: 'Identify speakers in the transcription.' },
contextBias: {
description: 'Vocabulary and context to guide transcription.',
},
targetStreamingDelayMs: {
description: 'Target transcription delay in milliseconds. Realtime only.',
},
speakerReferences: {
description: 'Named audio clips for known speakers. Realtime only.',
},
turnDetection: {
description: 'Turn detection settings. Realtime only.',
},
} satisfies Record<
keyof MistralTranscriptionModelOptions,
ProviderOptionDefinition
>,
speech: {
refAudio: {
description: 'Reference audio for voice cloning, overriding voice.',
},
expressiveness: { description: 'Speech expressiveness, from 1 to 5.' },
phonemeHints: { description: 'Pronunciation hints as IPA phonemes.' },
} satisfies Record<keyof MistralSpeechModelOptions, ProviderOptionDefinition>,
} as const satisfies ProviderOptionDefinitions;
type ReasoningEffort = NonNullable<
MistralLanguageModelChatOptions['reasoningEffort']
>;
type ReasoningCapabilities = {
support: boolean;
efforts: readonly ReasoningEffort[];
};
type LanguageCapabilities = NonNullable<ModelCapabilities['language']> & {
reasoning: ReasoningCapabilities;
endpoints: {
conversation: {
reasoning: ReasoningCapabilities;
};
};
};
export type MistralLanguageModelEntry = {
capabilities: { language: LanguageCapabilities };
providerOptions: {
language: Record<keyof MistralLanguageModelChatOptions, ModelCapability>;
};
};
type TranscriptionCapabilities = NonNullable<
ModelCapabilities['transcription']
> & {
languageWithTimestamps: boolean;
timestamps: ModelCapability;
diarization: ModelCapability;
contextBias: ModelCapability;
};
type SpeechCapabilities = NonNullable<ModelCapabilities['speech']> & {
savedVoices: ModelCapability;
referenceAudio: ModelCapability;
nonStreaming: boolean;
outputFormats: readonly string[];
};
type MistralModelEntry = ModelEntry & {
providerOptions?: {
[Section in keyof typeof mistralProviderOptions]?: Record<
keyof (typeof mistralProviderOptions)[Section],
ModelCapability
>;
};
capabilities: ModelCapabilities & {
language?: LanguageCapabilities;
transcription?: TranscriptionCapabilities;
speech?: SpeechCapabilities;
};
};
// Preserve the provider page's documented model set and image-input exceptions.
const languageModelDefinitions = {
'labs-leanstral-1-5': true,
'magistral-medium-latest': true,
'magistral-small-latest': true,
'ministral-14b-latest': true,
'ministral-8b-latest': true,
'mistral-large-latest': true,
'mistral-medium-latest': true,
'mistral-small-latest': true,
'voxtral-small-latest': false,
'zai-glm-5-2': false,
// Known IDs without declared capabilities retain the custom-model fallback.
'codestral-2508': undefined,
'codestral-latest': undefined,
'glm-5-2': undefined,
'labs-leanstral-1-5-1': undefined,
'ministral-14b-2512': undefined,
'ministral-3b-2512': undefined,
'ministral-3b-latest': undefined,
'ministral-8b-2512': undefined,
'mistral-code-fim-latest': undefined,
'mistral-code-latest': undefined,
'mistral-large-2512': undefined,
'mistral-large-4': undefined,
'mistral-large-4-0': undefined,
'mistral-medium': undefined,
'mistral-medium-2604': undefined,
'mistral-medium-3': undefined,
'mistral-medium-3-5': undefined,
'mistral-medium-3.5': undefined,
'mistral-small-2603': undefined,
'mistral-vibe-cli-fast': undefined,
'mistral-vibe-cli-latest': undefined,
'mistral-vibe-cli-with-tools': undefined,
'voxtral-small-2507': undefined,
'zai-glm-5': undefined,
'zai-glm-5-3': undefined,
'zai-glm-5-3-flash': undefined,
'zai-glm-latest': undefined,
} satisfies Record<string, boolean | undefined>;
// Known non-reasoning models from https://api.mistral.ai/v1/models
// (2026-10-06). Unknown IDs inherit current reasoning configuration support.
const modelsWithoutReasoningEffort = new Set<MistralChatModelId>([
'codestral-2508',
'codestral-latest',
'ministral-14b-2512',
'ministral-14b-latest',
'ministral-3b-2512',
'ministral-3b-latest',
'ministral-8b-2512',
'ministral-8b-latest',
'mistral-code-fim-latest',
'mistral-code-latest',
'mistral-large-latest',
'mistral-large-2512',
'voxtral-small-2507',
'voxtral-small-latest',
]);
// GLM-5.3 models accept only low, high, and max reasoning effort and cannot
// disable reasoning. Omitting reasoning_effort uses max.
// https://docs.mistral.ai/studio/conversations/reasoning (2026-10-26)
const modelsWithLowHighMaxReasoningEffort = new Set<MistralChatModelId>([
'zai-glm-5',
'zai-glm-5-3',
'zai-glm-5-3-flash',
'zai-glm-latest',
]);
// Mistral Large 4 Chat models accept low, medium, and high reasoning effort
// and cannot disable reasoning (see #24350).
const modelsWithLowMediumHighReasoningEffort = new Set<MistralChatModelId>([
'mistral-large-4',
'mistral-large-4-0',
]);
// https://api.mistral.ai/v1/models (2026-10-06)
const reasoningEffortModelIds = new Set<MistralChatModelId>([
'glm-5-2',
'labs-leanstral-1-5',
'labs-leanstral-1-5-1',
'magistral-medium-latest',
'magistral-small-latest',
'mistral-large-4',
'mistral-large-4-0',
'mistral-medium',
'mistral-medium-2604',
'mistral-medium-3',
'mistral-medium-3-5',
'mistral-medium-3.5',
'mistral-medium-latest',
'mistral-small-2603',
'mistral-small-latest',
'mistral-vibe-cli-fast',
'mistral-vibe-cli-latest',
'mistral-vibe-cli-with-tools',
'zai-glm-5-2',
]);
function reasoningCapabilities(
efforts: readonly ReasoningEffort[],
): ReasoningCapabilities {
return { support: efforts.length > 0, efforts };
}
function createLanguageModelEntry(
modelId: string,
image?: boolean,
): MistralLanguageModelEntry {
const reasoning = reasoningCapabilities(
modelsWithoutReasoningEffort.has(modelId)
? []
: modelsWithLowHighMaxReasoningEffort.has(modelId)
? ['low', 'high', 'max']
: modelsWithLowMediumHighReasoningEffort.has(modelId)
? ['low', 'medium', 'high']
: ['none', 'high'],
);
const conversationReasoning = reasoningCapabilities(
reasoningEffortModelIds.has(modelId) ? ['none', 'high'] : [],
);
const chatOnly = { condition: 'Only supported on the Chat API.' };
return {
capabilities: {
language: {
inputs: { image: image ?? 'unknown', audio: 'unknown' },
objectGeneration: image != null ? true : 'unknown',
tools: {
usage: image != null ? true : 'unknown',
streaming: image != null ? true : 'unknown',
},
reasoning,
sampling: { temperature: 'unknown', topP: 'unknown', logprobs: false },
endpoints: { conversation: { reasoning: conversationReasoning } },
},
},
providerOptions: {
language: declareProviderOptions(
mistralProviderOptions.language,
image != null ? true : 'unknown',
{
safePrompt: chatOnly,
documentImageLimit: chatOnly,
documentPageLimit: chatOnly,
parallelToolCalls: chatOnly,
promptCacheKey: chatOnly,
reasoningEffort:
reasoning.support && !conversationReasoning.support
? chatOnly
: reasoning.support,
},
),
},
};
}
function createTranscriptionModelEntry(modelId: string, declared: boolean) {
// Only these legacy IDs reject pinned language combined with timestamps.
// Future and custom IDs keep the existing permissive request behavior.
const languageWithTimestamps = ![
'voxtral-mini-2507',
'voxtral-mini-2602',
'voxtral-mini-latest',
].includes(modelId);
const support = declared ? true : 'unknown';
const capabilities: TranscriptionCapabilities = {
transcription: support,
streaming: false,
duration: support,
segments: support,
language: support,
languageWithTimestamps,
timestamps: support,
diarization: support,
contextBias: support,
};
return {
capabilities: { transcription: capabilities },
providerOptions: {
transcription: declareProviderOptions(
mistralProviderOptions.transcription,
support,
{
targetStreamingDelayMs: false,
speakerReferences: false,
turnDetection: false,
returnLanguage: {
condition: 'Only supported when language is not pinned.',
},
timestampGranularities: languageWithTimestamps
? support
: {
condition: 'Only supported when language is not pinned.',
},
},
),
},
};
}
function createSpeechModelEntry(declared: boolean) {
const support = declared ? true : 'unknown';
const capabilities: SpeechCapabilities = {
instructions: false,
savedVoices: support,
referenceAudio: support,
nonStreaming: true,
outputFormats: ['pcm', 'wav', 'mp3', 'flac', 'opus'],
};
return {
capabilities: { speech: capabilities },
providerOptions: {
speech: declareProviderOptions(
mistralProviderOptions.speech,
support,
{},
),
},
};
}
function createEmbeddingModelEntry(
defaultDimensions: number,
customDimensions: boolean,
) {
return {
capabilities: { embedding: { defaultDimensions, customDimensions } },
providerOptions: {
embedding: declareProviderOptions(
mistralProviderOptions.embedding,
true,
{
outputDimension: customDimensions,
outputDtype: customDimensions,
},
),
},
};
}
const mistralEmbeddingModelEntries = {
'mistral-embed': createEmbeddingModelEntry(1024, false),
// https://docs.mistral.ai/studio/knowledge-rag/embeddings/code_embeddings
'codestral-embed': createEmbeddingModelEntry(1536, true),
// Known IDs without declared capabilities retain the custom-model fallback.
'codestral-embed-2505': undefined,
'mistral-embed-2312': undefined,
};
const mistralSpeechModelEntries = {
'voxtral-mini-tts-3-0': createSpeechModelEntry(true),
'voxtral-mini-tts-2603': createSpeechModelEntry(true),
// Known IDs without declared capabilities retain the custom-model fallback.
'voxtral-mini-tts-3': undefined,
'voxtral-mini-tts-latest': undefined,
};
const mistralTranscriptionModelEntries = {
'voxtral-mini-transcribe-3-0': createTranscriptionModelEntry(
'voxtral-mini-transcribe-3-0',
true,
),
'voxtral-small-transcribe-3-0': createTranscriptionModelEntry(
'voxtral-small-transcribe-3-0',
true,
),
'voxtral-mini-latest': createTranscriptionModelEntry(
'voxtral-mini-latest',
true,
),
// Known IDs without declared capabilities retain the custom-model fallback.
'voxtral-mini-2602': undefined,
'voxtral-mini-transcribe-3': undefined,
'voxtral-mini-transcribe-latest': undefined,
'voxtral-mini-transcribe-realtime-3': undefined,
'voxtral-mini-transcribe-realtime-3-0': undefined,
'voxtral-small-transcribe-3': undefined,
'voxtral-small-transcribe-latest': undefined,
};
export const mistralModelCatalog: Readonly<Record<string, MistralModelEntry>> =
Object.fromEntries(
Object.entries({
...Object.fromEntries(
Object.entries(languageModelDefinitions)
.filter(([, definition]) => definition != null)
.map(([modelId, image]) => [
modelId,
createLanguageModelEntry(modelId, image),
]),
),
...mistralTranscriptionModelEntries,
...mistralSpeechModelEntries,
...mistralEmbeddingModelEntries,
}).flatMap(([modelId, entry]) =>
entry != null ? [[modelId, entry] as const] : [],
),
);
function findModelEntry(modelId: string) {
return Object.prototype.hasOwnProperty.call(mistralModelCatalog, modelId)
? mistralModelCatalog[modelId]
: undefined;
}
export function getMistralLanguageModelEntry(
modelId: string,
): MistralLanguageModelEntry {
const entry = findModelEntry(modelId);
return entry?.capabilities.language != null &&
entry.providerOptions?.language != null
? {
capabilities: { language: entry.capabilities.language },
providerOptions: { language: entry.providerOptions.language },
}
: createLanguageModelEntry(modelId);
}
export function getMistralTranscriptionModelEntry(modelId: string) {
const capabilities = findModelEntry(modelId)?.capabilities.transcription;
return capabilities != null
? { capabilities: { transcription: capabilities } }
: createTranscriptionModelEntry(modelId, false);
}
export function getMistralSpeechModelEntry(modelId: string) {
const capabilities = findModelEntry(modelId)?.capabilities.speech;
return capabilities != null
? { capabilities: { speech: capabilities } }
: createSpeechModelEntry(false);
}
function declareProviderOptions<Option extends string>(
definitions: Readonly<Record<Option, ProviderOptionDefinition>>,
support: ModelCapability,
exceptions: Partial<Record<Option, ModelCapability>>,
): Record<Option, ModelCapability> {
return Object.fromEntries(
(Object.keys(definitions) as Option[]).map(option => [
option,
exceptions[option] ?? support,
]),
) as Record<Option, ModelCapability>;
}
/** Known model IDs, derived from the declarations used to build the catalog. */
export type MistralModelIds = {
language: keyof typeof languageModelDefinitions;
embedding: keyof typeof mistralEmbeddingModelEntries;
speech: keyof typeof mistralSpeechModelEntries;
transcription: keyof typeof mistralTranscriptionModelEntries;
};