mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Add Grok 4.3 + Add Xiaomi Mimo TTS provider (#1837)
Integrated into release/v3.7.8
This commit is contained in:
@@ -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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user