From 5205f369bc70be09d9cc9c118b398607b0efdd24 Mon Sep 17 00:00:00 2001 From: kmiit Date: Wed, 6 May 2026 03:39:10 +0800 Subject: [PATCH] feat(providers): add Xiaomi Mimo (#1760) --- README.md | 1 + docs/README.fr.md | 1 + docs/README.ja-JP.md | 1 + docs/README.ko-KR.md | 1 + docs/README.ru-RU.md | 1 + docs/README.vi.md | 1 + docs/README.zh-CN.md | 1 + packages/i18n/src/locales/en/settings.yaml | 3 + packages/i18n/src/locales/es/settings.yaml | 3 + packages/i18n/src/locales/fr/settings.yaml | 3 + packages/i18n/src/locales/ja/settings.yaml | 3 + packages/i18n/src/locales/ko/settings.yaml | 3 + packages/i18n/src/locales/ru/settings.yaml | 3 + packages/i18n/src/locales/vi/settings.yaml | 3 + .../i18n/src/locales/zh-Hans/settings.yaml | 3 + .../i18n/src/locales/zh-Hant/settings.yaml | 3 + .../providers/speech/mimo-audio-speech.vue | 250 ++++++++++++++ .../mimo-audio-transcription.vue | 162 +++++++++ .../src/libs/providers/providers/index.ts | 1 + .../libs/providers/providers/mimo/index.ts | 54 +++ packages/stage-ui/src/stores/providers.ts | 317 ++++++++++++++++++ 21 files changed, 818 insertions(+) create mode 100644 packages/stage-pages/src/pages/settings/providers/speech/mimo-audio-speech.vue create mode 100644 packages/stage-pages/src/pages/settings/providers/transcription/mimo-audio-transcription.vue create mode 100644 packages/stage-ui/src/libs/providers/providers/mimo/index.ts diff --git a/README.md b/README.md index d899f91af..fb512a196 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html) (PR welcome) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) (PR welcome) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## Sub-projects Born from This Project diff --git a/docs/README.fr.md b/docs/README.fr.md index 221a147b7..04f7ac034 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -319,6 +319,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html) (PR bienvenue) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) (PR bienvenue) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## Sous-projets issus de ce projet diff --git a/docs/README.ja-JP.md b/docs/README.ja-JP.md index dc267fc20..830b1ab45 100644 --- a/docs/README.ja-JP.md +++ b/docs/README.ja-JP.md @@ -322,6 +322,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html)(PR歓迎) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI)(PR歓迎) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## このプロジェクトから生まれたサブプロジェクト diff --git a/docs/README.ko-KR.md b/docs/README.ko-KR.md index fb9d89fe1..e394123df 100644 --- a/docs/README.ko-KR.md +++ b/docs/README.ko-KR.md @@ -355,6 +355,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html) (PR 환영) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) (PR 환영) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## 이 프로젝트에서 탄생한 하위 프로젝트 diff --git a/docs/README.ru-RU.md b/docs/README.ru-RU.md index 0ffea08ce..1c857eefa 100644 --- a/docs/README.ru-RU.md +++ b/docs/README.ru-RU.md @@ -319,6 +319,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html) (PR welcome) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) (PR welcome) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## Подпроекты, родившиеся из этого проекта diff --git a/docs/README.vi.md b/docs/README.vi.md index 72834dd26..2501b821e 100644 --- a/docs/README.vi.md +++ b/docs/README.vi.md @@ -316,6 +316,7 @@ npx bumpp --no-commit --no-tag - [x] [Tencent Cloud](https://cloud.tencent.com/document/product/1729) - [ ] [Sparks](https://www.xfyun.cn/doc/spark/Web.html) (PR welcome) - [ ] [Volcano Engine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) (PR welcome) +- [x] [Xiaomi Mimo](https://platform.xiaomimimo.com) ## Các dự án phụ khác diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index e584cfb64..5bca3e7b9 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -317,6 +317,7 @@ npx bumpp --no-commit --no-tag - [x] [腾讯混元](https://cloud.tencent.com/document/product/1729) - [ ] [讯飞星火](https://www.xfyun.cn/doc/spark/Web.html) - [ ] [火山引擎(豆包)](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI) +- [x] [小米 Mimo](https://platform.xiaomimimo.com) ## 从这个项目诞生的子项目 diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index b77b72eec..954dd0cc9 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -1160,6 +1160,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/es/settings.yaml b/packages/i18n/src/locales/es/settings.yaml index a6edee314..3366199d5 100644 --- a/packages/i18n/src/locales/es/settings.yaml +++ b/packages/i18n/src/locales/es/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/fr/settings.yaml b/packages/i18n/src/locales/fr/settings.yaml index 7dd4f54dc..9deace6b5 100644 --- a/packages/i18n/src/locales/fr/settings.yaml +++ b/packages/i18n/src/locales/fr/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/ja/settings.yaml b/packages/i18n/src/locales/ja/settings.yaml index 5de419c26..b021872f9 100644 --- a/packages/i18n/src/locales/ja/settings.yaml +++ b/packages/i18n/src/locales/ja/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/ko/settings.yaml b/packages/i18n/src/locales/ko/settings.yaml index 522021897..df310f24c 100644 --- a/packages/i18n/src/locales/ko/settings.yaml +++ b/packages/i18n/src/locales/ko/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/ru/settings.yaml b/packages/i18n/src/locales/ru/settings.yaml index e2d08bf0e..64de38edf 100644 --- a/packages/i18n/src/locales/ru/settings.yaml +++ b/packages/i18n/src/locales/ru/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: Minimax.io title: MiniMax речь + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: Mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/vi/settings.yaml b/packages/i18n/src/locales/vi/settings.yaml index b283755f2..cb5a099f3 100644 --- a/packages/i18n/src/locales/vi/settings.yaml +++ b/packages/i18n/src/locales/vi/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: Xiaomi MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 79fa2bc7c..f4ab587da 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: 小米 MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/i18n/src/locales/zh-Hant/settings.yaml b/packages/i18n/src/locales/zh-Hant/settings.yaml index 824dbe2ec..818c29ff2 100644 --- a/packages/i18n/src/locales/zh-Hant/settings.yaml +++ b/packages/i18n/src/locales/zh-Hant/settings.yaml @@ -1110,6 +1110,9 @@ pages: minimax-speech: description: minimax.io title: MiniMax Speech + mimo: + description: api.xiaomimimo.com + title: 小米 MiMo mistral: description: mistral.ai title: Mistral diff --git a/packages/stage-pages/src/pages/settings/providers/speech/mimo-audio-speech.vue b/packages/stage-pages/src/pages/settings/providers/speech/mimo-audio-speech.vue new file mode 100644 index 000000000..8396b099d --- /dev/null +++ b/packages/stage-pages/src/pages/settings/providers/speech/mimo-audio-speech.vue @@ -0,0 +1,250 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/packages/stage-pages/src/pages/settings/providers/transcription/mimo-audio-transcription.vue b/packages/stage-pages/src/pages/settings/providers/transcription/mimo-audio-transcription.vue new file mode 100644 index 000000000..97b03ca21 --- /dev/null +++ b/packages/stage-pages/src/pages/settings/providers/transcription/mimo-audio-transcription.vue @@ -0,0 +1,162 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts index e40569714..fbcc8774b 100644 --- a/packages/stage-ui/src/libs/providers/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/providers/index.ts @@ -26,6 +26,7 @@ import './mistral-ai' import './moonshot-ai' import './modelscope' import './ollama' +import './mimo' import './cloudflare-workers-ai' import './azure-ai-foundry' import './official' diff --git a/packages/stage-ui/src/libs/providers/providers/mimo/index.ts b/packages/stage-ui/src/libs/providers/providers/mimo/index.ts new file mode 100644 index 000000000..6d40c5807 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/mimo/index.ts @@ -0,0 +1,54 @@ +import { createXiaomi } from '@xsai-ext/providers/create' +import { z } from 'zod' + +import { ProviderValidationCheck } from '../../types' +import { createOpenAICompatibleValidators } from '../../validators' +import { defineProvider } from '../registry' + +const mimoConfigSchema = z.object({ + apiKey: z + .string('API Key'), + baseUrl: z + .string('Base URL') + .optional() + .default('https://api.xiaomimimo.com/v1/'), +}) + +type MimoConfig = z.input + +export const providerMimo = defineProvider({ + id: 'mimo', + order: 4, + name: 'Xiaomi MiMo', + nameLocalize: ({ t }) => t('settings.pages.providers.provider.mimo.title'), + description: 'api.xiaomimimo.com', + descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.mimo.description'), + tasks: ['chat'], + icon: 'i-simple-icons:xiaomi', + + createProviderConfig: ({ t }) => mimoConfigSchema.extend({ + apiKey: mimoConfigSchema.shape.apiKey.meta({ + labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'), + descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'), + placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'), + type: 'password', + }), + baseUrl: mimoConfigSchema.shape.baseUrl.meta({ + labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'), + descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'), + placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'), + }), + }), + createProvider(config) { + return createXiaomi(config.apiKey, config.baseUrl) + }, + + validationRequiredWhen(config) { + return !!config.apiKey?.trim() + }, + validators: { + ...createOpenAICompatibleValidators({ + checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions], + }), + }, +}) diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index 13793dbd1..a948beff6 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -1555,6 +1555,173 @@ export const useProvidersStore = defineStore('providers', () => { }, }, 'openrouter-audio-speech': buildOpenRouterAudioSpeechProvider(v => baseUrlValidator.value(v)), + 'mimo-audio-speech': { + id: 'mimo-audio-speech', + category: 'speech', + tasks: ['text-to-speech'], + nameKey: 'settings.pages.providers.provider.mimo.title', + name: 'Xiaomi MiMo', + descriptionKey: 'settings.pages.providers.provider.mimo.description', + description: 'api.xiaomimimo.com', + icon: 'i-simple-icons:xiaomi', + defaultOptions: () => ({ + baseUrl: 'https://api.xiaomimimo.com/v1/', + model: 'mimo-v2.5-tts', + voice: 'mimo_default', + format: 'wav', + }), + createProvider: async (config) => { + const apiKey = (config.apiKey as string)?.trim() ?? '' + const baseUrl = ((config.baseUrl as string) || 'https://api.xiaomimimo.com/v1/').replace(/\/+$/, '') + const defaultModel = (config.model as string) || 'mimo-v2.5-tts' + const defaultVoice = (config.voice as string) || 'mimo_default' + const defaultFormat = (config.format as string) || 'wav' + + const provider: SpeechProvider = { + speech: () => ({ + baseURL: `${baseUrl}/`, + model: defaultModel, + fetch: async (_input: RequestInfo | URL, init?: RequestInit) => { + if (!init?.body || typeof init.body !== 'string') { + throw new Error('Invalid request body') + } + + const body = JSON.parse(init.body) + const text = body.input as string + const modelId = (body.model as string) || defaultModel + const format = (body.response_format as string) || defaultFormat + const stylePrompt = typeof body.style_prompt === 'string' + ? body.style_prompt.trim() + : typeof config.stylePrompt === 'string' + ? config.stylePrompt.trim() + : '' + const voiceSample = typeof body.voice_sample === 'string' + ? body.voice_sample.trim() + : typeof config.voiceSample === 'string' + ? config.voiceSample.trim() + : '' + + const userPrompt = modelId === 'mimo-v2.5-tts-voiceclone' + ? stylePrompt + : stylePrompt || 'Use a natural, clear speaking style.' + + const audio: Record = { format } + if (modelId === 'mimo-v2.5-tts-voiceclone') { + if (!voiceSample) { + throw new Error('MiMo voice clone requires a base64 audio sample in data URI format.') + } + audio.voice = voiceSample + } + else if (modelId === 'mimo-v2.5-tts') { + audio.voice = (body.voice as string) || defaultVoice + } + + if (modelId === 'mimo-v2.5-tts-voicedesign' && !stylePrompt) { + throw new Error('MiMo voice design requires a style prompt in the user message.') + } + + const response = await fetch(`${baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': apiKey, + }, + body: JSON.stringify({ + model: modelId, + messages: [ + { role: 'user', content: userPrompt }, + { role: 'assistant', content: text }, + ], + audio, + }), + }) + + if (!response.ok || !response.body) { + throw new Error(`MiMo TTS request failed: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + const audioBase64 = data?.choices?.[0]?.message?.audio?.data + if (!audioBase64) { + throw new Error('MiMo TTS response missing audio data') + } + + const binaryString = atob(audioBase64) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + const contentType = format === 'wav' ? 'audio/wav' : format === 'mp3' ? 'audio/mpeg' : `audio/${format}` + return new Response(bytes.buffer, { + status: 200, + headers: { 'Content-Type': contentType }, + }) + }, + }), + } + return provider + }, + capabilities: { + listModels: async () => [ + { + id: 'mimo-v2.5-tts', + name: 'MiMo v2.5 TTS', + provider: 'mimo-audio-speech', + description: 'Preset voice synthesis with the built-in MiMo voice list', + contextLength: 0, + deprecated: false, + }, + { + id: 'mimo-v2.5-tts-voicedesign', + name: 'MiMo v2.5 TTS Voice Design', + provider: 'mimo-audio-speech', + description: 'Design a new voice from a natural language description', + contextLength: 0, + deprecated: false, + }, + { + id: 'mimo-v2.5-tts-voiceclone', + name: 'MiMo v2.5 TTS Voice Clone', + provider: 'mimo-audio-speech', + description: 'Clone a voice from a base64-encoded audio sample', + contextLength: 0, + deprecated: false, + }, + ], + listVoices: async () => [ + { id: 'mimo_default', name: 'MiMo-默认', provider: 'mimo-audio-speech', gender: 'female', languages: [{ code: 'en', title: 'English' }, { code: 'zh', title: 'Chinese' }] }, + { id: '冰糖', name: '冰糖', provider: 'mimo-audio-speech', gender: 'female', languages: [{ code: 'zh', title: 'Chinese' }] }, + { id: '茉莉', name: '茉莉', provider: 'mimo-audio-speech', gender: 'female', languages: [{ code: 'zh', title: 'Chinese' }] }, + { id: '苏打', name: '苏打', provider: 'mimo-audio-speech', gender: 'male', languages: [{ code: 'zh', title: 'Chinese' }] }, + { id: '白桦', name: '白桦', provider: 'mimo-audio-speech', gender: 'male', languages: [{ code: 'zh', title: 'Chinese' }] }, + { id: 'Mia', name: 'Mia', provider: 'mimo-audio-speech', gender: 'female', languages: [{ code: 'en', title: 'English' }] }, + { id: 'Chloe', name: 'Chloe', provider: 'mimo-audio-speech', gender: 'female', languages: [{ code: 'en', title: 'English' }] }, + { id: 'Milo', name: 'Milo', provider: 'mimo-audio-speech', gender: 'male', languages: [{ code: 'en', title: 'English' }] }, + { id: 'Dean', name: 'Dean', provider: 'mimo-audio-speech', gender: 'male', languages: [{ code: 'en', title: 'English' }] }, + ], + }, + validators: { + chatPingCheckAvailable: false, + validateProviderConfig: (config) => { + const errors = [ + !config.apiKey && new Error('API key is required.'), + !config.baseUrl && new Error('Base URL is required.'), + ].filter(Boolean) + + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res + } + + return { + errors, + reason: errors.map(e => (e as Error).message).join(', ') || '', + valid: !!config.apiKey && !!config.baseUrl, + } + }, + }, + }, 'comet-api-speech': buildOpenAICompatibleProvider({ id: 'comet-api-speech', name: 'CometAPI Speech', @@ -1587,6 +1754,156 @@ export const useProvidersStore = defineStore('providers', () => { ), validation: [ProviderValidationCheck.ModelList], }), + 'mimo-audio-transcription': { + id: 'mimo-audio-transcription', + category: 'transcription', + tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'], + nameKey: 'settings.pages.providers.provider.mimo.title', + name: 'Xiaomi MiMo', + descriptionKey: 'settings.pages.providers.provider.mimo.description', + description: 'api.xiaomimimo.com', + icon: 'i-simple-icons:xiaomi', + defaultOptions: () => ({ + baseUrl: 'https://api.xiaomimimo.com/v1/', + model: 'mimo-v2-omni', + }), + createProvider: async (config) => { + const apiKey = (config.apiKey as string)?.trim() ?? '' + const rawBaseUrl = `${((config.baseUrl as string) || 'https://api.xiaomimimo.com/v1/').replace(/\/+$/, '')}/` + const defaultModel = (config.model as string) || 'mimo-v2-omni' + + const provider: TranscriptionProvider = { + transcription: model => ({ + baseURL: rawBaseUrl, + model: model || defaultModel, + headers: {}, + fetch: async (_input: RequestInfo | URL, init?: RequestInit) => { + const formData = init?.body as FormData + const file = formData?.get('file') as Blob | null + const modelName = (formData?.get('model') as string) || defaultModel + + if (!file) { + throw new Error('No audio file provided for transcription.') + } + + // Read the file as base64 data URI (works with both Blob and File) + const base64DataUri: string = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(new Error('Failed to read audio file')) + reader.readAsDataURL(file) + }) + + // Extract format and base64 data from data URI + // data:audio/wav;base64,UklGR... + const mimeType = base64DataUri.split(';')[0]?.split(':')[1] || 'audio/wav' + const formatFromMime = mimeType.split('/')[1] || 'wav' + const base64Data = base64DataUri.split(',')[1] + + // Map MIME sub-type to MiMo supported audio format + const audioFormat = formatFromMime === 'webm' + ? 'webm' + : formatFromMime === 'mp4' + ? 'mp4' + : formatFromMime === 'mpeg' || formatFromMime === 'mp3' + ? 'mp3' + : 'wav' + + // MiMo audio understanding uses chat completions with input_audio, + // not a dedicated transcription endpoint + const response = await fetch(`${rawBaseUrl}chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': apiKey, + }, + body: JSON.stringify({ + model: modelName, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Transcribe the audio content.' }, + { + type: 'input_audio', + input_audio: { + data: base64Data, + format: audioFormat, + }, + }, + ], + }, + ], + }), + }) + + if (!response.ok) { + const errorBody = await response.text().catch(() => '') + throw new Error( + `MiMo transcription failed: ${response.status} ${response.statusText}${errorBody ? ` — ${errorBody}` : ''}`, + ) + } + + const data = await response.json() + const text = data?.choices?.[0]?.message?.content || '' + + // Return in OpenAI transcription response format { text: "..." } + return new Response(JSON.stringify({ text }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }, + }), + } + + return provider + }, + capabilities: { + listModels: async () => [ + { + id: 'mimo-v2-omni', + name: 'MiMo V2 Omni', + provider: 'mimo-audio-transcription', + description: 'Omni-modal model with native audio understanding and speech-to-text', + contextLength: 256000, + deprecated: false, + }, + { + id: 'mimo-v2.5', + name: 'MiMo V2.5', + provider: 'mimo-audio-transcription', + description: 'Latest omni-modal model with audio understanding, 1M context', + contextLength: 1_000_000, + deprecated: false, + }, + ], + }, + transcriptionFeatures: { + supportsGenerate: true, + supportsStreamOutput: false, + supportsStreamInput: false, + }, + validators: { + chatPingCheckAvailable: false, + validateProviderConfig: (config) => { + const errors = [ + !config.apiKey && new Error('API key is required.'), + !config.baseUrl && new Error('Base URL is required.'), + ].filter(Boolean) + + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res + } + + return { + errors, + reason: errors.map(e => (e as Error).message).join(', ') || '', + valid: !!config.apiKey && !!config.baseUrl, + } + }, + }, + }, 'player2-speech': { id: 'player2-speech', category: 'speech',