infomaniak-ai-provider
Version:
The Infomaniak provider for the [AI SDK](https://ai-sdk.dev/docs/introduction) contains language model support for the [Infomaniak AI Tools API](https://www.infomaniak.com/en/hosting/ai-services/open-source-models).
388 lines (383 loc) • 12.6 kB
JavaScript
// src/infomaniak-provider.ts
import {
OpenAICompatibleChatLanguageModel,
OpenAICompatibleEmbeddingModel,
OpenAICompatibleImageModel
} from "@ai-sdk/openai-compatible";
import { loadApiKey, loadSetting } from "@ai-sdk/provider-utils";
// src/transcription/infomaniak-transcription-model.ts
import {
combineHeaders,
convertBase64ToUint8Array,
createJsonResponseHandler,
getFromApi,
parseProviderOptions,
postFormDataToApi
} from "@ai-sdk/provider-utils";
import { z as z3 } from "zod/v4";
// src/infomaniak-error.ts
import { createJsonErrorResponseHandler } from "@ai-sdk/provider-utils";
import { z } from "zod/v4";
var infomaniakErrorDataSchema = z.object({
result: z.string(),
error: z.object({
code: z.string(),
description: z.string(),
errors: z.array(z.any())
}).optional()
});
var infomaniakFailedResponseHandler = createJsonErrorResponseHandler({
errorSchema: infomaniakErrorDataSchema,
errorToMessage: (data) => {
var _a;
return ((_a = data.error) == null ? void 0 : _a.description) || data.result;
}
});
// src/transcription/infomaniak-transcription-options.ts
import { z as z2 } from "zod/v4";
var infomaniakTranscriptionProviderOptions = z2.object({
/**
* Only if timestamp_granularities[]:word is True, merge these punctuation symbols with the previous word
*/
appendPunctuations: z2.array(z2.string()).optional(),
/**
* Defines the maximum duration for an active segment in sec. For subtitle tasks, it's recommended to set this to a short duration (5-10 seconds) to avoid long sentences.
*/
chunkLength: z2.number().min(2).max(30).optional(),
/**
* Subtitle task. Underline each word as it is spoken in srt and vtt output formats (requires timestamp_granularities[]:word)
*/
highlightWords: z2.boolean().optional(),
/**
* The language of the input audio in ISO-639-1 format.
*/
language: z2.string().optional(),
/**
* Subtitle task. The maximum number of lines in a segment in srt and vtt output formats (requires timestamp_granularities[]:word)
*/
maxLineCount: z2.number().min(1).max(1e3).optional(),
/**
* Subtitle task. The maximum number of characters in a line before breaking the line in srt and vtt output formats (requires timestamp_granularities[]:word)
*/
maxLineWidth: z2.number().min(1).max(1e3).optional(),
/**
* Subtitle task. The maximum number of words in a segment (requires timestamp_granularities[]:word)
*/
maxWordsPerLine: z2.number().min(1).max(1e3).optional(),
/**
* If the no_speech probability is higher than this value AND the average log probability over sampled tokens is below log_prob_threshold, consider the segment as silent.
*/
noSpeechThreshold: z2.number().optional(),
/**
* Only if timestamp_granularities[]:word is True, merge these punctuation symbols with the next word
*/
prependPunctuations: z2.array(z2.string()).optional(),
/**
* An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.
*/
prompt: z2.string().optional(),
/**
* The format of the transcript output
* @default 'json'
*/
responseFormat: z2.enum(["text", "json", "srt", "verbose_json", "vtt"]).default("json").optional(),
/**
* The timestamp granularities to populate for this transcription.
* @default ['segment']
*/
timestampGranularities: z2.array(z2.enum(["word", "segment"])).default(["segment"]).optional()
});
// src/transcription/infomaniak-transcription-model.ts
var languageMap = {
afrikaans: "af",
arabic: "ar",
armenian: "hy",
azerbaijani: "az",
belarusian: "be",
bosnian: "bs",
bulgarian: "bg",
catalan: "ca",
chinese: "zh",
croatian: "hr",
czech: "cs",
danish: "da",
dutch: "nl",
english: "en",
estonian: "et",
finnish: "fi",
french: "fr",
galician: "gl",
german: "de",
greek: "el",
hebrew: "he",
hindi: "hi",
hungarian: "hu",
icelandic: "is",
indonesian: "id",
italian: "it",
japanese: "ja",
kannada: "kn",
kazakh: "kk",
korean: "ko",
latvian: "lv",
lithuanian: "lt",
macedonian: "mk",
malay: "ms",
marathi: "mr",
maori: "mi",
nepali: "ne",
norwegian: "no",
persian: "fa",
polish: "pl",
portuguese: "pt",
romanian: "ro",
russian: "ru",
serbian: "sr",
slovak: "sk",
slovenian: "sl",
spanish: "es",
swahili: "sw",
swedish: "sv",
tagalog: "tl",
tamil: "ta",
thai: "th",
turkish: "tr",
ukrainian: "uk",
urdu: "ur",
vietnamese: "vi",
welsh: "cy"
};
var infomaniakTranscriptionResponseSchema = z3.object({
batch_id: z3.uuid()
});
var infomaniakBatchResponseSchema = z3.object({
status: z3.enum(["pending", "success", "error"]),
url: z3.url().nullable(),
file_name: z3.string().nullable(),
file_size: z3.number().nullable(),
data: z3.json().nullable()
});
var infomaniakTranscriptionDataSchema = z3.object({
duration: z3.number().nullish(),
language: z3.string().nullish(),
segments: z3.array(z3.object({
avg_logprob: z3.number(),
compression_ratio: z3.number(),
end: z3.number(),
id: z3.number(),
no_speech_prob: z3.number().min(0).max(1),
seek: z3.number(),
start: z3.number(),
temperature: z3.number().min(0).max(1),
text: z3.string(),
tokens: z3.array(z3.number())
})).nullish(),
task: z3.string().nullish(),
text: z3.string(),
words: z3.array(z3.object({
end: z3.number(),
start: z3.number(),
word: z3.string()
})).nullish()
});
var InfomaniakTranscriptionModel = class {
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
}
modelId;
config;
specificationVersion = "v3";
get provider() {
return this.config.provider;
}
async getArgs({
audio,
mediaType,
providerOptions
}) {
const warnings = [];
const infomaniakOptions = await parseProviderOptions({
provider: "infomaniak",
providerOptions,
schema: infomaniakTranscriptionProviderOptions
});
const formData = new FormData();
const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]);
formData.append("model", this.modelId);
formData.append("file", new File([blob], "audio", { type: mediaType }));
if (infomaniakOptions) {
const transcriptionModelOptions = {
append_punctuations: infomaniakOptions.appendPunctuations,
chunk_length: infomaniakOptions.chunkLength,
highlight_words: infomaniakOptions.highlightWords,
language: infomaniakOptions.language,
max_line_count: infomaniakOptions.maxLineCount,
max_line_width: infomaniakOptions.maxLineWidth,
max_words_per_line: infomaniakOptions.maxWordsPerLine,
no_speech_threshold: infomaniakOptions.noSpeechThreshold,
prepend_punctuations: infomaniakOptions.prependPunctuations,
prompt: infomaniakOptions.prompt,
response_format: "verbose_json",
// always use verbose_json to get correct data shape
timestamp_granularities: infomaniakOptions.timestampGranularities || ["segment"]
};
for (const [key, value] of Object.entries(transcriptionModelOptions)) {
if (value != null) {
if (Array.isArray(value)) {
value.forEach((item) => formData.append(`${key}[]`, String(item)));
} else {
formData.append(key, String(value));
}
}
}
}
return {
formData,
warnings
};
}
async pollForResults(batchId, options, maxRetries = 60, delayMs = 2e3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const url = this.config.url({
path: `/results/${batchId}`
});
const { value: response, responseHeaders } = await getFromApi({
url: url.replace("/openai", ""),
// remove the /openai part, maybe remove it from the base anyway
headers: combineHeaders(this.config.headers(), options.headers),
failedResponseHandler: infomaniakFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
infomaniakBatchResponseSchema
),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
if (response.status === "success" && response.data) {
const parsedData = infomaniakTranscriptionDataSchema.parse(
JSON.parse(response.data)
);
return {
result: parsedData,
responseHeaders,
rawResponse: JSON.stringify(response)
};
}
if (attempt < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
throw new Error(`Transcription timed out after ${maxRetries} attempts`);
}
async doGenerate(options) {
var _a, _b, _c, _d;
const currentDate = ((_b = (_a = this.config._internal) == null ? void 0 : _a.currentDate) == null ? void 0 : _b.call(_a)) ?? /* @__PURE__ */ new Date();
const { formData, warnings } = await this.getArgs(options);
const { value: batchResponse } = await postFormDataToApi({
url: this.config.url({
path: "/audio/transcriptions"
}),
headers: combineHeaders(this.config.headers(), options.headers),
formData,
failedResponseHandler: infomaniakFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
infomaniakTranscriptionResponseSchema
),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
const { result, responseHeaders, rawResponse } = await this.pollForResults(
batchResponse.batch_id,
options
);
const language = result.language != null && result.language in languageMap ? languageMap[result.language] : void 0;
return {
text: result.text,
segments: ((_c = result.segments) == null ? void 0 : _c.map((segment) => ({
text: segment.text,
startSecond: segment.start,
endSecond: segment.end
}))) ?? ((_d = result.words) == null ? void 0 : _d.map((word) => ({
text: word.word,
startSecond: word.start,
endSecond: word.end
}))) ?? [],
language,
durationInSeconds: result.duration ?? void 0,
warnings,
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders,
body: rawResponse
},
providerMetadata: {
infomaniak: {
batchId: batchResponse.batch_id
}
}
};
}
};
// src/infomaniak-provider.ts
function createInfomaniak(options = {}) {
const getProductId = () => loadSetting({
environmentVariableName: "INFOMANIAK_PRODUCT_ID",
settingName: "productId",
description: "Infomaniak product ID",
settingValue: options.productId
});
const getApiUrl = () => `https://api.infomaniak.com/2/ai/${getProductId()}/openai/v1`;
const getHeaders = () => ({
Authorization: `Bearer ${loadApiKey({
apiKey: options.apiKey,
environmentVariableName: "INFOMANIAK_API_KEY",
description: "Infomaniak API key"
})}`,
...options.headers
});
const getCommonModelConfig = (modelType) => ({
provider: `infomaniak.${modelType}`,
url: ({ path }) => {
const url = new URL(`${getApiUrl()}${path}`);
return url.toString();
},
headers: getHeaders,
fetch: options.fetch
});
const createChatModel = (modelId) => {
return new OpenAICompatibleChatLanguageModel(
modelId,
{ ...getCommonModelConfig("chat"), includeUsage: options.includeUsage ?? false }
);
};
const createTextEmbeddingModel = (modelId) => {
return new OpenAICompatibleEmbeddingModel(
modelId,
{
...getCommonModelConfig("text_embedding"),
url: ({ path }) => {
const url = new URL(`${getApiUrl()}/v1${path}`);
return url.toString();
}
}
);
};
const createImageModel = (modelId) => new OpenAICompatibleImageModel(modelId, getCommonModelConfig("image"));
const createTranscriptionModel = (modelId) => new InfomaniakTranscriptionModel(modelId, {
...getCommonModelConfig("transcription")
});
const provider = (modelId) => createChatModel(modelId);
provider.chatModel = createChatModel;
provider.languageModel = createChatModel;
provider.textEmbeddingModel = createTextEmbeddingModel;
provider.imageModel = createImageModel;
provider.transcription = createTranscriptionModel;
return provider;
}
var infomaniak = createInfomaniak();
export {
createInfomaniak,
infomaniak
};
//# sourceMappingURL=index.mjs.map