From 32e0a7cb16bad97b781f99d4ecea57abc1d03ef5 Mon Sep 17 00:00:00 2001 From: backryun Date: Fri, 1 May 2026 20:36:24 +0900 Subject: [PATCH] Add Grok 4.3 + Add Xiaomi Mimo TTS provider (#1837) Integrated into release/v3.7.8 --- open-sse/config/audioRegistry.ts | 12 ++ open-sse/config/providerRegistry.ts | 53 ++++----- open-sse/handlers/audioSpeech.ts | 106 +++++++++++++++++- .../dashboard/cache/media/MediaPageClient.tsx | 34 +++++- src/shared/constants/modelSpecs.ts | 8 -- src/shared/constants/providers.ts | 3 +- tests/unit/audio-speech-handler.test.ts | 68 +++++++++++ tests/unit/chatgpt-web.test.ts | 28 ++--- tests/unit/executor-codex.test.ts | 6 - tests/unit/t12-pricing-updates.test.ts | 1 - .../unit/t31-t33-t34-t38-model-specs.test.ts | 1 - tests/unit/xiaomi-mimo-provider.test.ts | 32 ++++-- 12 files changed, 280 insertions(+), 72 deletions(-) diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index 9841fc7399..f453dd6236 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -262,6 +262,18 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { id: "generative", name: "Polly Generative" }, ], }, + "xiaomi-mimo": { + id: "xiaomi-mimo", + baseUrl: "https://api.xiaomimimo.com/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + format: "xiaomi-mimo-tts", + models: [ + { id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS" }, + { id: "mimo-v2.5-tts-voicedesign", name: "MiMo V2.5 Voice Design" }, + { id: "mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 Voice Clone" }, + ], + }, }; /** diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index d648bbd473..5ac00eee42 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -228,17 +228,7 @@ const CHAT_OPENAI_COMPAT_MODELS: Record = { codestral: buildModels(["codestral-2405", "codestral-latest"]), upstage: buildModels(["solar-pro", "solar-mini", "solar-docvision", "solar-embedding-1-large"]), maritalk: buildModels(["sabia-3", "sabia-3-small"]), - "xiaomi-mimo": buildModels([ - "mimo-v2.5-pro", - "mimo-v2.5", - "mimo-v2.5-tts", - "mimo-v2.5-tts-voiceclone", - "mimo-v2.5-tts-voicedesign", - "mimo-v2-pro", - "mimo-v2-omni", - "mimo-v2-tts", - "mimo-v2-flash", - ]), + "xiaomi-mimo": buildModels(["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-omni", "mimo-v2-flash"]), "inference-net": buildModels([ "meta-llama/Llama-3.3-70B-Instruct", "deepseek-ai/DeepSeek-R1", @@ -398,7 +388,6 @@ export const REGISTRY: Record = { { id: "gpt-5.5-medium", name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5", name: "GPT 5.5", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-low", name: "GPT 5.5 (Low)", ...GPT_5_5_CODEX_CAPABILITIES }, - { id: "gpt-5.5-mini", name: "GPT 5.5 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.4", name: "GPT 5.4", targetFormat: "openai-responses" }, { id: "gpt-5.4-mini", name: "GPT 5.4 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, @@ -1094,6 +1083,7 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "bearer", models: [ + { id: "grok-4.3", name: "Grok 4.3" }, { id: "grok-4.20-multi-agent-0309", name: "Grok 4.20 Multi Agent" }, { id: "grok-4.20-0309-reasoning", name: "Grok 4.20 Reasoning" }, { id: "grok-4.20-0309-non-reasoning", name: "Grok 4.20" }, @@ -1111,19 +1101,18 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "cookie", models: [ - { id: "gpt-5.3-instant", name: "GPT-5.3 Instant" }, - { id: "gpt-5.3", name: "GPT-5.3" }, - { id: "gpt-5.3-mini", name: "GPT-5.3 Mini" }, - { id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking" }, - { id: "gpt-5.4-thinking", name: "GPT-5.4 Thinking" }, - { id: "gpt-5.4-thinking-mini", name: "GPT-5.4 Thinking Mini" }, - { id: "gpt-5.2-instant", name: "GPT-5.2 Instant" }, - { id: "gpt-5.2", name: "GPT-5.2" }, - { id: "gpt-5.2-thinking", name: "GPT-5.2 Thinking" }, - { id: "gpt-5.1", name: "GPT-5.1" }, - { id: "gpt-5", name: "GPT-5" }, - { id: "gpt-5-mini", name: "GPT-5 Mini" }, - { id: "o3", name: "o3" }, + { id: "gpt-5.5-pro", name: "GPT-5.5 Pro" }, //pro tier only + { id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking" }, //plus, pro tier + { id: "gpt-5.4-pro", name: "GPT-5.4 Pro" }, //pro tier only + { id: "gpt-5.4-thinking", name: "GPT-5.4 Thinking" }, //plus, pro tier + { id: "gpt-5.4-thinking-mini", name: "GPT-5.4 Thinking Mini" }, //free-login only + { id: "gpt-5.3", name: "GPT-5.3" }, //free, free-login, plus, pro tier + { id: "gpt-5.3-mini", name: "GPT-5.3 Mini" }, //limit fallback + { id: "gpt-5.2-pro", name: "GPT-5.2 Pro" }, //pro tier only + { id: "gpt-5.2-thinking", name: "GPT-5.2 Thinking" }, //plus ~ tier + { id: "gpt-5.2-instant", name: "GPT-5.2 Instant" }, //plus ~ tier + { id: "o3", name: "o3" }, //plus ~ tier + { id: "gpt-4-5", name: "GPT-4.5" }, //pro tier only ], }, @@ -2056,6 +2045,20 @@ export const REGISTRY: Record = { ], passthroughModels: true, }, + // Free tier: 50 RPM, 500,000 TPM + "nous-research": { + id: "nous-research", + alias: "nous", + format: "openai", + executor: "default", + baseUrl: "https://inference-api.nousresearch.com/v1", + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "Hermes-4-405B", name: "Hermes 4 7B (Nous Research)" }, + { id: "Hermes-4-70B", name: "Hermes 4 70B (Nous Research)" }, + ], + }, }; // ── Generator Functions ─────────────────────────────────────────────────── diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index a730f456dc..d4d224531f 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -106,6 +106,53 @@ function resolveAwsPollyBaseUrl(providerSpecificData, region) { return stripTrailingSlashes(baseUrl.replace(/\/v1\/speech\/?$/i, "")); } +function getProviderSpecificData(credentials) { + return credentials?.providerSpecificData && + typeof credentials.providerSpecificData === "object" && + !Array.isArray(credentials.providerSpecificData) + ? credentials.providerSpecificData + : {}; +} + +function normalizeXiaomiMimoSpeechUrl(baseUrl) { + const configured = getStringValue(baseUrl) || "https://api.xiaomimimo.com/v1"; + const normalized = stripTrailingSlashes(configured).replace(/\/chat\/completions$/i, ""); + return `${normalized}/chat/completions`; +} + +function normalizeXiaomiMimoMimeType(format) { + switch (getStringValue(format)?.toLowerCase()) { + case undefined: + case null: + case "mp3": + case "audio/mp3": + case "audio/mpeg": + return "audio/mpeg"; + case "wav": + case "audio/wav": + return "audio/wav"; + default: + return null; + } +} + +function getXiaomiMimoAudioData(data) { + const messageAudio = data?.choices?.[0]?.message?.audio; + const directAudio = data?.audio || data?.output_audio; + const firstDataItem = Array.isArray(data?.data) ? data.data[0] : null; + + return ( + getStringValue(messageAudio?.data) || + getStringValue(messageAudio?.b64_json) || + getStringValue(directAudio?.data) || + getStringValue(directAudio?.b64_json) || + getStringValue(firstDataItem?.b64_json) || + getStringValue(firstDataItem?.audio) || + getStringValue(data?.audioContent) || + getStringValue(data?.audio_content) + ); +} + function normalizeAwsPollyEngine(modelId) { const engine = getStringValue(modelId) || "standard"; return ["standard", "neural", "long-form", "generative"].includes(engine) ? engine : "standard"; @@ -467,6 +514,59 @@ async function handleAwsPollySpeech(providerConfig, body, modelId, token, creden return audioStreamResponse(res, outputFormat === "pcm" ? "audio/pcm" : "audio/mpeg"); } +/** + * Xiaomi MiMo TTS uses chat/completions with an audio config instead of OpenAI's /audio/speech + * request body. + */ +async function handleXiaomiMimoSpeech(providerConfig, body, modelId, token, credentials) { + const providerSpecificData = getProviderSpecificData(credentials); + const url = normalizeXiaomiMimoSpeechUrl(providerSpecificData.baseUrl || providerConfig.baseUrl); + const audioMimeType = normalizeXiaomiMimoMimeType(body.response_format); + if (!audioMimeType) { + return errorResponse(400, "Xiaomi MiMo TTS supports response_format mp3 or wav only"); + } + + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...buildAuthHeaders(providerConfig, token), + }, + body: JSON.stringify({ + model: modelId, + messages: [{ role: "assistant", content: body.input }], + audio: { + format: audioMimeType, + voice: body.voice || getStringValue(providerSpecificData.defaultVoice) || "mimo_default", + }, + }), + }); + + if (!res.ok) { + return upstreamErrorResponse(res, await res.text()); + } + + const contentType = res.headers.get("content-type") || ""; + if (contentType.startsWith("audio/")) { + return audioStreamResponse(res, audioMimeType); + } + + const data = await res.json(); + const audioBase64 = getXiaomiMimoAudioData(data); + if (!audioBase64) { + return errorResponse(502, "Xiaomi MiMo TTS response did not contain audio data"); + } + + const audioBuffer = Uint8Array.from(atob(audioBase64), (c) => c.charCodeAt(0)); + return new Response(audioBuffer, { + status: 200, + headers: { + ...CORS_HEADERS, + "Content-Type": audioMimeType, + }, + }); +} + /** * Handle Coqui TTS (local, no auth) * POST {baseUrl} with { text, speaker_id } → WAV audio @@ -556,7 +656,7 @@ export async function handleAudioSpeech({ if (!providerConfig) { return errorResponse( 400, - `No speech provider found for model "${body.model}". Use format provider/model. Available: openai, hyperbolic, deepgram, nvidia, elevenlabs, huggingface, inworld, cartesia, playht, aws-polly, coqui, tortoise, qwen` + `No speech provider found for model "${body.model}". Use format provider/model. Available: openai, hyperbolic, deepgram, nvidia, elevenlabs, huggingface, inworld, cartesia, playht, aws-polly, xiaomi-mimo, coqui, tortoise, qwen` ); } @@ -605,6 +705,10 @@ export async function handleAudioSpeech({ return handleAwsPollySpeech(providerConfig, body, modelId, token, credentials); } + if (providerConfig.format === "xiaomi-mimo-tts") { + return handleXiaomiMimoSpeech(providerConfig, body, modelId, token, credentials); + } + if (providerConfig.format === "coqui") { return handleCoquiSpeech(providerConfig, body); } diff --git a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx index 917b94077d..c731722ff2 100644 --- a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx @@ -70,7 +70,7 @@ const MODALITY_CONFIG: Record< placeholder: "Hello! Welcome to OmniRoute, your intelligent AI gateway...", color: "from-green-500 to-teal-500", textLabel: "Text", - needsCredentials: ["openai", "elevenlabs", "deepgram"], + needsCredentials: ["openai", "elevenlabs", "deepgram", "xiaomi-mimo"], }, transcription: { icon: "mic", @@ -183,6 +183,15 @@ const PROVIDER_MODELS: Record< models: [{ id: "huggingface/espnet/kan-bayashi_ljspeech_vits", name: "VITS LJSpeech" }], }, { id: "qwen", name: "Qwen", models: [{ id: "qwen/qwen3-tts", name: "Qwen3 TTS" }] }, + { + id: "xiaomi-mimo", + name: "Xiaomi MiMo", + models: [ + { id: "xiaomi-mimo/mimo-v2.5-tts", name: "MiMo V2.5 TTS" }, + { id: "xiaomi-mimo/mimo-v2.5-tts-voicedesign", name: "MiMo V2.5 Voice Design" }, + { id: "xiaomi-mimo/mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 Voice Clone" }, + ], + }, ], transcription: [ { @@ -271,9 +280,26 @@ const VOICE_PRESETS: Record = { { id: "Eva", label: "Eva (EN)" }, { id: "Marcus", label: "Marcus (EN)" }, ], + "xiaomi-mimo": [ + { id: "冰糖", label: "冰糖 (Chinese Female)" }, + { id: "茉莉", label: "茉莉 (Chinese Female)" }, + { id: "苏打", label: "苏打 (Chinese Male)" }, + { id: "白桦", label: "白桦 (Chinese Male)" }, + { id: "Mia", label: "Mia (English Female)" }, + { id: "Chloe", label: "Chloe (English Female)" }, + { id: "Milo", label: "Milo (English Male)" }, + { id: "Dean", label: "Dean (English Male)" }, + ], }; const SPEECH_FORMATS = ["mp3", "wav", "opus", "flac", "pcm"]; +const SPEECH_FORMATS_BY_PROVIDER: Record = { + "xiaomi-mimo": ["mp3", "wav"], +}; + +function getSpeechFormats(providerId: string): string[] { + return SPEECH_FORMATS_BY_PROVIDER[providerId] || SPEECH_FORMATS; +} function getVoiceList(providerId: string) { return VOICE_PRESETS[providerId] ?? VOICE_PRESETS.default; @@ -485,6 +511,7 @@ export default function MediaPageClient() { setSelectedModel(firstModel); if (tab === "speech") { setSpeechVoice(getVoiceList(firstProvider?.id ?? "")[0]?.id ?? "alloy"); + setSpeechFormat(getSpeechFormats(firstProvider?.id ?? "")[0] ?? "mp3"); } }; @@ -495,6 +522,8 @@ export default function MediaPageClient() { setSelectedModel(firstModel); if (activeTab === "speech") { setSpeechVoice(getVoiceList(providerId)[0]?.id ?? "alloy"); + const formats = getSpeechFormats(providerId); + setSpeechFormat((current) => (formats.includes(current) ? current : (formats[0] ?? "mp3"))); } }; @@ -649,6 +678,7 @@ export default function MediaPageClient() { const config = MODALITY_CONFIG[activeTab]; const voiceList = getVoiceList(selectedProvider); + const currentSpeechFormats = getSpeechFormats(selectedProvider); const isTopazImageFlow = activeTab === "image" && selectedProvider === "topaz"; const isGenerateDisabled = loading || @@ -765,7 +795,7 @@ export default function MediaPageClient() { onChange={(e) => setSpeechFormat(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" > - {SPEECH_FORMATS.map((f) => ( + {currentSpeechFormats.map((f) => ( diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 5f33af4a50..079b6e04b7 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -18,14 +18,6 @@ export interface ModelSpec { } export const MODEL_SPECS: Record = { - "gpt-5.5-mini": { - maxOutputTokens: 128000, - contextWindow: 400000, - supportsThinking: true, - supportsTools: true, - supportsVision: true, - }, - "gpt-5.5": { maxOutputTokens: 128000, contextWindow: 1050000, diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 6113263ef4..2f68c7cc0b 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1239,7 +1239,8 @@ export const APIKEY_PROVIDERS = { "Use your Nous Portal API key. OmniRoute targets the official OpenAI-compatible inference endpoint at https://inference-api.nousresearch.com/v1.", apiHint: "Nous exposes an OpenAI-compatible /v1 surface with a large remote /models catalog. The /chat/completions endpoint requires a valid API key for programmatic inference.", - passthroughModels: true, + hasFree: true, + freeNote: "Free tier: 50 RPM, 500,000 TPM — no credit card", }, petals: { id: "petals", diff --git a/tests/unit/audio-speech-handler.test.ts b/tests/unit/audio-speech-handler.test.ts index bdfe41b7bb..0cd3f39ead 100644 --- a/tests/unit/audio-speech-handler.test.ts +++ b/tests/unit/audio-speech-handler.test.ts @@ -267,6 +267,74 @@ test("handleAudioSpeech signs AWS Polly synthesize requests with SigV4", async ( } }); +test("handleAudioSpeech maps Xiaomi MiMo TTS to chat completions audio payload", async () => { + const originalFetch = globalThis.fetch; + let captured; + + globalThis.fetch = async (url, options = {}) => { + captured = { + url: String(url), + headers: options.headers, + body: JSON.parse(String(options.body || "{}")), + }; + + return new Response( + JSON.stringify({ + choices: [{ message: { audio: { data: "AQID" } } }], + }), + { + status: 200, + headers: { "content-type": "application/json" }, + } + ); + }; + + try { + const response = await handleAudioSpeech({ + body: { + model: "xiaomi-mimo/mimo-v2.5-tts", + input: "mimo text", + voice: "default_zh", + response_format: "wav", + }, + credentials: { + apiKey: "xm-key", + providerSpecificData: { + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + }, + }, + }); + + assert.equal(captured.url, "https://token-plan-sgp.xiaomimimo.com/v1/chat/completions"); + assert.equal(captured.headers.Authorization, "Bearer xm-key"); + assert.deepEqual(captured.body, { + model: "mimo-v2.5-tts", + messages: [{ role: "assistant", content: "mimo text" }], + audio: { format: "audio/wav", voice: "default_zh" }, + }); + assert.equal(response.status, 200); + assert.equal(response.headers.get("content-type"), "audio/wav"); + assert.deepEqual(Array.from(new Uint8Array(await response.arrayBuffer())), [1, 2, 3]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("handleAudioSpeech rejects unsupported Xiaomi MiMo TTS audio formats", async () => { + const response = await handleAudioSpeech({ + body: { + model: "xiaomi-mimo/mimo-v2.5-tts", + input: "mimo text", + response_format: "opus", + }, + credentials: { apiKey: "xm-key" }, + }); + const payload = (await response.json()) as any; + + assert.equal(response.status, 400); + assert.equal(payload.error.message, "Xiaomi MiMo TTS supports response_format mp3 or wav only"); +}); + test("handleAudioSpeech requires credentials for authenticated providers", async () => { const response = await handleAudioSpeech({ body: { diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index a6976a7fed..dc401b20e1 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -1078,7 +1078,7 @@ test("Request: payload has correct ChatGPT shape", async () => { // ─── Provider registry ────────────────────────────────────────────────────── -test("Provider registry: chatgpt-web exposes the full ChatGPT Plus model catalog", async () => { +test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catalog", async () => { const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); const entry = getRegistryEntry("chatgpt-web"); assert.ok(entry, "chatgpt-web should be in the registry"); @@ -1087,25 +1087,22 @@ test("Provider registry: chatgpt-web exposes the full ChatGPT Plus model catalog assert.equal(entry.authHeader, "cookie"); const ids = (entry.models || []).map((m) => m.id); - // Mirrors /backend-api/models for a Plus account (no "research" or - // "agent-mode" — those are specialty surfaces, not chat models). - for (const id of [ - "gpt-5.3-instant", - "gpt-5.3", - "gpt-5.3-mini", + // Mirrors /backend-api/models for ChatGPT Web. Retired GPT-5/GPT-5.1 + // entries should stay out of this list. + assert.deepEqual(ids, [ + "gpt-5.5-pro", "gpt-5.5-thinking", + "gpt-5.4-pro", "gpt-5.4-thinking", "gpt-5.4-thinking-mini", - "gpt-5.2-instant", - "gpt-5.2", + "gpt-5.3", + "gpt-5.3-mini", + "gpt-5.2-pro", "gpt-5.2-thinking", - "gpt-5.1", - "gpt-5", - "gpt-5-mini", + "gpt-5.2-instant", "o3", - ]) { - assert.ok(ids.includes(id), `registry should list ${id}`); - } + "gpt-4-5", + ]); }); test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT slugs", async () => { @@ -1113,7 +1110,6 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT const m = installMockFetch(); try { const cases: Array<[string, string]> = [ - ["gpt-5.3-instant", "gpt-5-3-instant"], ["gpt-5.3", "gpt-5-3"], ["gpt-5.5-thinking", "gpt-5-5-thinking"], ["gpt-5.4-thinking-mini", "gpt-5-4-t-mini"], diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 323c27ec39..22336d58b2 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -88,12 +88,6 @@ test("Codex helper functions isolate rate-limit scopes and parse quota headers", }), true ); - assert.equal( - isCodexResponsesWebSocketRequired("gpt-5.5-mini", { - providerSpecificData: { codexTransport: "websocket" }, - }), - true - ); // Without codexTransport setting, defaults to HTTP (false) assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-xhigh", {}), false); assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-medium", {}), false); diff --git a/tests/unit/t12-pricing-updates.test.ts b/tests/unit/t12-pricing-updates.test.ts index bf0f0cfe81..1b7b17cad4 100644 --- a/tests/unit/t12-pricing-updates.test.ts +++ b/tests/unit/t12-pricing-updates.test.ts @@ -39,7 +39,6 @@ test("T12: codex catalog includes GPT 5.5 entries", () => { const codexModels = new Map(REGISTRY.codex.models.map((m) => [m.id, m])); assert.ok(codexModels.has("gpt-5.5"), "missing codex/gpt-5.5"); assert.ok(codexModels.has("gpt-5.5-medium"), "missing codex/gpt-5.5-medium"); - assert.ok(codexModels.has("gpt-5.5-mini"), "missing codex/gpt-5.5-mini"); assert.equal(codexModels.get("gpt-5.5")?.name, "GPT 5.5"); assert.equal(codexModels.get("gpt-5.5-medium")?.name, "GPT 5.5 (Medium)"); assert.equal(codexModels.get("gpt-5.5")?.contextLength, 1050000); diff --git a/tests/unit/t31-t33-t34-t38-model-specs.test.ts b/tests/unit/t31-t33-t34-t38-model-specs.test.ts index 893c1f0a1d..e088acda46 100644 --- a/tests/unit/t31-t33-t34-t38-model-specs.test.ts +++ b/tests/unit/t31-t33-t34-t38-model-specs.test.ts @@ -56,7 +56,6 @@ test("T34: max output tokens are capped by model spec", () => { test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", () => { assert.equal(getModelSpec("gpt-5.5").contextWindow, 1050000); assert.equal(getModelSpec("gpt-5.5-high").maxOutputTokens, 128000); - assert.equal(getModelSpec("gpt-5.5-mini").contextWindow, 400000); assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object"); assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 65535); assert.equal(getModelSpec("gemini-3-pro-preview").maxOutputTokens, 65535); diff --git a/tests/unit/xiaomi-mimo-provider.test.ts b/tests/unit/xiaomi-mimo-provider.test.ts index 4491ea634e..a74d5ebdba 100644 --- a/tests/unit/xiaomi-mimo-provider.test.ts +++ b/tests/unit/xiaomi-mimo-provider.test.ts @@ -2,6 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { REGISTRY } from "../../open-sse/config/providerRegistry.ts"; +import { getAllAudioModels, getSpeechProvider } from "../../open-sse/config/audioRegistry.ts"; import { DefaultExecutor } from "../../open-sse/executors/default.ts"; import { createProviderSchema, @@ -16,17 +17,26 @@ test("xiaomi-mimo registry uses the current default base URL and MiMo V2.5 + V2 assert.equal(entry.baseUrl, "https://api.xiaomimimo.com/v1"); assert.deepEqual( entry.models.map((model) => model.id), - [ - "mimo-v2.5-pro", - "mimo-v2.5", - "mimo-v2.5-tts", - "mimo-v2.5-tts-voiceclone", - "mimo-v2.5-tts-voicedesign", - "mimo-v2-pro", - "mimo-v2-omni", - "mimo-v2-tts", - "mimo-v2-flash", - ] + ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-omni", "mimo-v2-flash"] + ); +}); + +test("xiaomi-mimo TTS models are registered in the audio speech registry", () => { + const provider = getSpeechProvider("xiaomi-mimo"); + + assert.ok(provider, "xiaomi-mimo should exist in speech registry"); + assert.equal(provider.id, "xiaomi-mimo"); + assert.equal(provider.authType, "apikey"); + assert.equal(provider.authHeader, "bearer"); + assert.equal(provider.format, "xiaomi-mimo-tts"); + assert.deepEqual( + provider.models.map((model) => model.id), + ["mimo-v2.5-tts", "mimo-v2.5-tts-voicedesign", "mimo-v2.5-tts-voiceclone"] + ); + assert.ok( + getAllAudioModels().some( + (model) => model.id === "xiaomi-mimo/mimo-v2.5-tts" && model.subtype === "speech" + ) ); });