Add Grok 4.3 + Add Xiaomi Mimo TTS provider (#1837)

Integrated into release/v3.7.8
This commit is contained in:
backryun
2026-05-01 20:36:24 +09:00
committed by GitHub
parent 4ac29c4d9b
commit 32e0a7cb16
12 changed files with 280 additions and 72 deletions

View File

@@ -262,6 +262,18 @@ export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
{ 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" },
],
},
};
/**

View File

@@ -228,17 +228,7 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
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<string, RegistryEntry> = {
{ 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<string, RegistryEntry> = {
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<string, RegistryEntry> = {
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<string, RegistryEntry> = {
],
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 ───────────────────────────────────────────────────

View File

@@ -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);
}

View File

@@ -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<string, { id: string; label: string }[]> = {
{ 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<string, string[]> = {
"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) => (
<option key={f} value={f}>
{f}
</option>

View File

@@ -18,14 +18,6 @@ export interface ModelSpec {
}
export const MODEL_SPECS: Record<string, ModelSpec> = {
"gpt-5.5-mini": {
maxOutputTokens: 128000,
contextWindow: 400000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
},
"gpt-5.5": {
maxOutputTokens: 128000,
contextWindow: 1050000,

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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"],

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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"
)
);
});