@ai-sdk/mistral
Version:
316 lines (288 loc) • 9.04 kB
text/typescript
import {
InvalidArgumentError,
type JSONObject,
type SharedV4Warning,
type TranscriptionModelV4,
} from '@ai-sdk/provider';
import {
resolve,
type Resolvable,
lazySchema,
zodSchema,
combineHeaders,
convertBase64ToUint8Array,
createJsonResponseHandler,
mediaTypeToExtension,
parseProviderOptions,
postFormDataToApi,
serializeModelOptions,
WORKFLOW_DESERIALIZE,
WORKFLOW_SERIALIZE,
type FetchFunction,
type WebSocketConstructor,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { mistralFailedResponseHandler } from './mistral-error';
import { getMistralTranscriptionModelEntry } from './mistral-model-catalog';
import {
mistralTranscriptionModelOptions,
type MistralTranscriptionModelId,
} from './mistral-transcription-model-options';
import { streamMistralTranscription } from './mistral-transcription-stream';
type MistralTranscriptionModelConfig = {
provider: string;
baseURL: string;
headers?: Resolvable<Record<string, string | undefined>>;
fetch?: FetchFunction;
webSocket?: WebSocketConstructor;
_internal?: {
currentDate?: () => Date;
};
};
export class MistralTranscriptionModel implements TranscriptionModelV4 {
readonly specificationVersion = 'v4';
static [WORKFLOW_SERIALIZE](model: MistralTranscriptionModel) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config,
});
}
static [WORKFLOW_DESERIALIZE](options: {
modelId: MistralTranscriptionModelId;
config: MistralTranscriptionModelConfig;
}) {
return new MistralTranscriptionModel(options.modelId, options.config);
}
get provider(): string {
return this.config.provider;
}
constructor(
readonly modelId: MistralTranscriptionModelId,
private readonly config: MistralTranscriptionModelConfig,
) {}
private async getArgs({
audio,
mediaType,
providerOptions,
}: Parameters<TranscriptionModelV4['doGenerate']>[0]) {
const warnings: SharedV4Warning[] = [];
const mistralOptions = await parseProviderOptions({
provider: 'mistral',
providerOptions,
schema: mistralTranscriptionModelOptions,
});
for (const option of [
'targetStreamingDelayMs',
'speakerReferences',
'turnDetection',
] as const) {
if (mistralOptions?.[option] != null) {
warnings.push({
type: 'unsupported',
feature: `providerOptions.mistral.${option}`,
details: 'This option is only supported for realtime transcription.',
});
}
}
if (
!getMistralTranscriptionModelEntry(this.modelId).capabilities
.transcription.languageWithTimestamps &&
mistralOptions?.language != null &&
mistralOptions.timestampGranularities != null
) {
throw new InvalidArgumentError({
argument: 'providerOptions',
message:
'providerOptions.mistral.language cannot be combined with providerOptions.mistral.timestampGranularities',
});
}
if (mistralOptions?.language != null && mistralOptions.returnLanguage) {
throw new InvalidArgumentError({
argument: 'providerOptions',
message:
'providerOptions.mistral.language cannot be combined with providerOptions.mistral.returnLanguage',
});
}
const formData = new FormData();
const blob =
audio instanceof Uint8Array
? new Blob([audio as Uint8Array<ArrayBuffer>])
: new Blob([convertBase64ToUint8Array(audio)]);
formData.append('model', this.modelId);
formData.append(
'file',
new File([blob], 'audio', { type: mediaType }),
`audio.${mediaTypeToExtension(mediaType)}`,
);
if (mistralOptions != null) {
appendFormValue(formData, 'language', mistralOptions.language);
appendFormValue(
formData,
'return_language',
mistralOptions.returnLanguage,
);
appendFormValue(formData, 'temperature', mistralOptions.temperature);
appendFormValue(
formData,
'timestamp_granularities',
mistralOptions.timestampGranularities,
);
appendFormValue(formData, 'diarize', mistralOptions.diarize);
appendFormValue(formData, 'context_bias', mistralOptions.contextBias);
}
return { formData, warnings };
}
async doStream(
options: Parameters<NonNullable<TranscriptionModelV4['doStream']>>[0],
) {
return streamMistralTranscription({
...options,
modelId: this.modelId,
baseURL: this.config.baseURL,
headers: combineHeaders(
await resolve(this.config.headers),
options.headers,
),
webSocket: this.config.webSocket,
currentDate: this.config._internal?.currentDate?.() ?? new Date(),
});
}
async doGenerate(
options: Parameters<TranscriptionModelV4['doGenerate']>[0],
): Promise<Awaited<ReturnType<TranscriptionModelV4['doGenerate']>>> {
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
const { formData, warnings } = await this.getArgs(options);
const {
value: response,
responseHeaders,
rawValue: rawResponse,
} = await postFormDataToApi({
url: `${this.config.baseURL}/audio/transcriptions`,
headers: combineHeaders(
await resolve(this.config.headers),
options.headers,
),
formData,
failedResponseHandler: mistralFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
mistralTranscriptionResponseSchema,
),
abortSignal: options.abortSignal,
fetch: this.config.fetch,
});
const segments =
response.segments?.map(segment => ({
text: segment.text,
startSecond: segment.start,
endSecond: segment.end,
})) ?? [];
const mistralMetadata: JSONObject = {};
if (response.usage != null) {
mistralMetadata.usage = {
...(response.usage.prompt_tokens != null && {
promptTokens: response.usage.prompt_tokens,
}),
...(response.usage.completion_tokens != null && {
completionTokens: response.usage.completion_tokens,
}),
...(response.usage.total_tokens != null && {
totalTokens: response.usage.total_tokens,
}),
...(response.usage.prompt_audio_seconds != null && {
promptAudioSeconds: response.usage.prompt_audio_seconds,
}),
...(response.usage.request_count != null && {
requestCount: response.usage.request_count,
}),
};
}
const providerSegments = response.segments
?.filter(
segment =>
segment.type != null ||
segment.score != null ||
segment.speaker_id != null,
)
.map(segment => ({
text: segment.text,
startSecond: segment.start,
endSecond: segment.end,
...(segment.type != null && { type: segment.type }),
...(segment.score != null && { score: segment.score }),
...(segment.speaker_id != null && {
speakerId: segment.speaker_id,
}),
}));
if (providerSegments != null && providerSegments.length > 0) {
mistralMetadata.segments = providerSegments;
}
return {
text: response.text,
segments,
language: response.language ?? undefined,
durationInSeconds:
response.usage?.prompt_audio_seconds ??
segments.at(-1)?.endSecond ??
undefined,
warnings,
...(response.usage != null && { usage: response.usage }),
response: {
timestamp: currentDate,
modelId: response.model,
headers: responseHeaders,
body: rawResponse,
},
...(Object.keys(mistralMetadata).length > 0 && {
providerMetadata: {
mistral: mistralMetadata,
},
}),
};
}
}
function appendFormValue(
formData: FormData,
key: string,
value: string | number | boolean | string[] | undefined,
): void {
if (value == null) {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
formData.append(key, item);
}
return;
}
formData.append(key, String(value));
}
const mistralTranscriptionResponseSchema = lazySchema(() =>
zodSchema(
z.object({
model: z.string(),
text: z.string(),
language: z.string().nullish(),
segments: z
.array(
z.object({
type: z.literal('transcription_segment').nullish(),
text: z.string(),
start: z.number(),
end: z.number(),
score: z.number().nullish(),
speaker_id: z.string().nullish(),
}),
)
.nullish(),
usage: z
.object({
prompt_tokens: z.number().nullish(),
completion_tokens: z.number().nullish(),
total_tokens: z.number().nullish(),
prompt_audio_seconds: z.number().nullish(),
request_count: z.number().nullish(),
})
.nullish(),
}),
),
);