mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
fix(models): normalize media endpoint metadata (#11397)
Validado em lote combinado (batch-0824g) contra o tip de release/v3.8.50: typecheck:core limpo, gates estáticos OK, 62/62 testes focados passando (endpoint/parser/schema/static-model + catálogo). Canonicaliza metadados de endpoint legados (video/audio) para IDs específicos por operação, mantendo compatibilidade retroativa via `normalizeModelSupportedEndpoints` (valores antigos `audio`/`video` continuam válidos como entrada e são normalizados na escrita). Obrigado pela contribuição, primeira PR bem-vinda!
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/shared/components";
|
||||
import {
|
||||
normalizeModelSupportedEndpoints,
|
||||
type ModelSupportedEndpoint,
|
||||
} from "@/shared/constants/modelSupportedEndpoints";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import {
|
||||
buildCompatMap,
|
||||
@@ -51,6 +55,29 @@ function targetFormatLabel(value: string, t: (key: string) => string): string {
|
||||
return key ? t(key) : value;
|
||||
}
|
||||
|
||||
const MODEL_ENDPOINT_OPTIONS: ModelSupportedEndpoint[] = [
|
||||
"chat",
|
||||
"embeddings",
|
||||
"rerank",
|
||||
"images",
|
||||
"videos",
|
||||
"audio-speech",
|
||||
"audio-transcriptions",
|
||||
];
|
||||
|
||||
function endpointLabel(endpoint: ModelSupportedEndpoint, t: (key: string) => string): string {
|
||||
const labels: Partial<Record<ModelSupportedEndpoint, string>> = {
|
||||
chat: `💬 ${t("supportedEndpointChat")}`,
|
||||
embeddings: `📐 ${t("supportedEndpointEmbeddings")}`,
|
||||
rerank: providerText(t, "rerankEndpoint", "Rerank"),
|
||||
images: `🖼️ ${t("supportedEndpointImages")}`,
|
||||
videos: "🎬 Video",
|
||||
"audio-speech": `🔊 ${t("audioSpeech")}`,
|
||||
"audio-transcriptions": `🎙️ ${t("audioTranscriptions")}`,
|
||||
};
|
||||
return labels[endpoint] || endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* #4125: parse the free-text "Context Window Override" field. Blank → no override
|
||||
* (`value: null`, not an error). A non-empty value must be a positive whole number of
|
||||
@@ -232,7 +259,7 @@ export default function CustomModelsSection({
|
||||
setEditingApiFormat(model.apiFormat || "chat-completions");
|
||||
setEditingEndpoints(
|
||||
Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length
|
||||
? model.supportedEndpoints
|
||||
? normalizeModelSupportedEndpoints(model.supportedEndpoints)
|
||||
: ["chat"]
|
||||
);
|
||||
setEditingTargetFormat(model.targetFormat || "");
|
||||
@@ -428,6 +455,7 @@ export default function CustomModelsSection({
|
||||
<option value="audio-transcriptions">{t("audioTranscriptions")}</option>
|
||||
<option value="audio-speech">{t("audioSpeech")}</option>
|
||||
<option value="images-generations">{t("imagesGenerations")}</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
@@ -454,7 +482,7 @@ export default function CustomModelsSection({
|
||||
{t("supportedEndpointsLabel")}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{["chat", "embeddings", "rerank", "images", "audio"].map((ep) => (
|
||||
{MODEL_ENDPOINT_OPTIONS.map((ep) => (
|
||||
<label
|
||||
key={ep}
|
||||
className="flex items-center gap-1.5 text-xs text-text-main cursor-pointer"
|
||||
@@ -471,15 +499,7 @@ export default function CustomModelsSection({
|
||||
}}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
{ep === "chat"
|
||||
? `💬 ${t("supportedEndpointChat")}`
|
||||
: ep === "embeddings"
|
||||
? `📐 ${t("supportedEndpointEmbeddings")}`
|
||||
: ep === "rerank"
|
||||
? providerText(t, "rerankEndpoint", "Rerank")
|
||||
: ep === "images"
|
||||
? `🖼️ ${t("supportedEndpointImages")}`
|
||||
: `🔊 ${t("supportedEndpointAudio")}`}
|
||||
{endpointLabel(ep, t)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -594,6 +614,22 @@ export default function CustomModelsSection({
|
||||
{`🔊 ${t("audioShortLabel")}`}
|
||||
</span>
|
||||
)}
|
||||
{(model.supportedEndpoints?.includes("videos") ||
|
||||
model.supportedEndpoints?.includes("video")) && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-red-500/15 text-red-400 font-medium">
|
||||
🎬 Video
|
||||
</span>
|
||||
)}
|
||||
{model.supportedEndpoints?.includes("audio-speech") && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/15 text-green-400 font-medium">
|
||||
{`🔊 ${t("audioSpeech")}`}
|
||||
</span>
|
||||
)}
|
||||
{model.supportedEndpoints?.includes("audio-transcriptions") && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-cyan-500/15 text-cyan-400 font-medium">
|
||||
{`🎙️ ${t("audioTranscriptions")}`}
|
||||
</span>
|
||||
)}
|
||||
{anyNormalizeCompatBadge(model.id!, customMap, overrideMap) && (
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-full bg-slate-500/15 text-slate-400 font-medium"
|
||||
@@ -639,6 +675,7 @@ export default function CustomModelsSection({
|
||||
<option value="audio-transcriptions">{t("audioTranscriptions")}</option>
|
||||
<option value="audio-speech">{t("audioSpeech")}</option>
|
||||
<option value="images-generations">{t("imagesGenerations")}</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-[11rem] shrink-0 min-w-0">
|
||||
@@ -697,7 +734,7 @@ export default function CustomModelsSection({
|
||||
{t("supportedEndpointsLabel")}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-x-2 sm:gap-x-3 gap-y-1 min-w-0">
|
||||
{["chat", "embeddings", "rerank", "images", "audio"].map((ep) => (
|
||||
{MODEL_ENDPOINT_OPTIONS.map((ep) => (
|
||||
<label
|
||||
key={ep}
|
||||
className="flex items-center gap-1.5 text-xs text-text-main cursor-pointer whitespace-nowrap"
|
||||
@@ -716,15 +753,7 @@ export default function CustomModelsSection({
|
||||
}}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
{ep === "chat"
|
||||
? `💬 ${t("supportedEndpointChat")}`
|
||||
: ep === "embeddings"
|
||||
? `📐 ${t("supportedEndpointEmbeddings")}`
|
||||
: ep === "rerank"
|
||||
? providerText(t, "rerankEndpoint", "Rerank")
|
||||
: ep === "images"
|
||||
? `🖼️ ${t("supportedEndpointImages")}`
|
||||
: `🔊 ${t("supportedEndpointAudio")}`}
|
||||
{endpointLabel(ep, t)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
import { createModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot";
|
||||
import { getModelsDevPricing, getSyncedCapability } from "@/lib/modelsDevSync";
|
||||
import { getModelSpec } from "@/shared/constants/modelSpecs";
|
||||
import { classifyModelSupportedEndpoints } from "@/shared/constants/modelSupportedEndpoints";
|
||||
import { getModelsCatalogPrefixMode } from "@/shared/utils/featureFlags";
|
||||
import { buildReservedPrefixes, selectCompatibleNodeForPrefix } from "@/lib/providerNodePrefixes";
|
||||
import { applyCatalogPostFilters, finalizeCatalogResponse } from "./catalogResponse";
|
||||
@@ -1152,18 +1153,15 @@ async function buildUnifiedModelsResponseCore(
|
||||
const aliasId = `${alias}/${displayModelId}`;
|
||||
const endpoints = Array.isArray(sm.supportedEndpoints) ? sm.supportedEndpoints : ["chat"];
|
||||
const apiFormat = typeof sm.apiFormat === "string" ? sm.apiFormat : "chat-completions";
|
||||
let modelType: string | undefined;
|
||||
if (endpoints.includes("embeddings")) modelType = "embedding";
|
||||
else if (endpoints.includes("rerank")) modelType = "rerank";
|
||||
else if (endpoints.includes("images")) modelType = "image";
|
||||
else if (endpoints.includes("audio")) modelType = "audio";
|
||||
const classification = classifyModelSupportedEndpoints(endpoints);
|
||||
const modelType = classification.type;
|
||||
// Same owned_by the alias/canonical entries below will carry — computed once
|
||||
// so the effort_tiers exclusion (codex/glm/kimi) and the entries agree.
|
||||
const syncedOwnedBy = resolvePublicOwnerId(providerId, canonicalProviderId);
|
||||
const syncedFields = {
|
||||
...(modelType ? { type: modelType } : {}),
|
||||
...(apiFormat !== "chat-completions" ? { api_format: apiFormat } : {}),
|
||||
...(modelType === "audio" ? { subtype: "transcription" } : {}),
|
||||
...(classification.subtype ? { subtype: classification.subtype } : {}),
|
||||
...(sm.inputTokenLimit ? { context_length: sm.inputTokenLimit } : {}),
|
||||
...(typeof sm.outputTokenLimit === "number"
|
||||
? { max_output_tokens: sm.outputTokenLimit }
|
||||
@@ -1604,11 +1602,8 @@ async function buildUnifiedModelsResponseCore(
|
||||
: ["chat"];
|
||||
const apiFormat =
|
||||
typeof model.apiFormat === "string" ? model.apiFormat : "chat-completions";
|
||||
let modelType: string | undefined;
|
||||
if (endpoints.includes("embeddings")) modelType = "embedding";
|
||||
else if (endpoints.includes("rerank")) modelType = "rerank";
|
||||
else if (endpoints.includes("images")) modelType = "image";
|
||||
else if (endpoints.includes("audio")) modelType = "audio";
|
||||
const classification = classifyModelSupportedEndpoints(endpoints);
|
||||
const modelType = classification.type;
|
||||
if (
|
||||
modelType &&
|
||||
hasEquivalentSpecialtyModel(canonicalProviderId, modelId, modelType, aliasId)
|
||||
@@ -1631,6 +1626,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
parent: null,
|
||||
custom: true,
|
||||
...(modelType ? { type: modelType } : {}),
|
||||
...(classification.subtype ? { subtype: classification.subtype } : {}),
|
||||
...(apiFormat !== "chat-completions" ? { api_format: apiFormat } : {}),
|
||||
...(endpoints.length > 1 || !endpoints.includes("chat")
|
||||
? { supported_endpoints: endpoints }
|
||||
|
||||
@@ -190,7 +190,8 @@ export async function addCustomModel(
|
||||
| "rerank"
|
||||
| "audio-transcriptions"
|
||||
| "audio-speech"
|
||||
| "images-generations" = "chat-completions",
|
||||
| "images-generations"
|
||||
| "video" = "chat-completions",
|
||||
supportedEndpoints: string[] = ["chat"],
|
||||
// #2905: optional per-model wire format override (e.g. "claude" for an
|
||||
// opencode-go custom model). When unset, routing falls back to the provider
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
*
|
||||
* Each model's `supportedGenerationMethods` is mapped to OmniRoute endpoints:
|
||||
* - generateContent / generateAnswer → "chat"
|
||||
* - predictLongRunning → "video" (Veo video generation)
|
||||
* - predict → "images" (Imagen image generation)
|
||||
* - predictLongRunning → "videos" (Veo video generation)
|
||||
* - embedContent → "embeddings"
|
||||
* - bidiGenerateContent → "audio" (Live real-time audio)
|
||||
*
|
||||
* Model-id heuristics ensure Veo models remain in the video bucket.
|
||||
* Model-id heuristics refine the long-running bucket because Google exposes both
|
||||
* Imagen and Veo via long-running methods on the same endpoint:
|
||||
* - id contains "veo" → ensure "videos"
|
||||
* - id contains "imagen" → force "images" (never "videos")
|
||||
*
|
||||
* Note: `gemini-*-image` models (e.g. gemini-3-pro-image) generate images via the
|
||||
* regular `generateContent` path, so they stay "chat" (image output is a chat
|
||||
@@ -21,7 +25,8 @@
|
||||
const METHOD_TO_ENDPOINT: Record<string, string> = {
|
||||
generateContent: "chat",
|
||||
embedContent: "embeddings",
|
||||
predictLongRunning: "video",
|
||||
predict: "images",
|
||||
predictLongRunning: "videos",
|
||||
bidiGenerateContent: "audio",
|
||||
generateAnswer: "chat",
|
||||
};
|
||||
@@ -63,9 +68,14 @@ export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] {
|
||||
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
|
||||
const lowerId = id.toLowerCase();
|
||||
|
||||
// Keep Veo models in the video bucket even when the method list is incomplete.
|
||||
// Google exposes Imagen (image) and Veo (video) via long-running methods; the
|
||||
// method alone can't always distinguish them, so refine by model id.
|
||||
if (lowerId.includes("veo")) {
|
||||
endpoints.add("video");
|
||||
endpoints.add("videos");
|
||||
}
|
||||
if (lowerId.includes("imagen")) {
|
||||
endpoints.delete("videos");
|
||||
endpoints.add("images");
|
||||
}
|
||||
|
||||
if (endpoints.size === 0) endpoints.add("chat");
|
||||
|
||||
@@ -221,7 +221,7 @@ export function getStaticModelsForProvider(provider: string): LocalCatalogModel[
|
||||
if (speechProvider) {
|
||||
appendModels(speechProvider.models, {
|
||||
apiFormat: "audio",
|
||||
supportedEndpoints: ["audio"],
|
||||
supportedEndpoints: ["audio-speech"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export function getStaticModelsForProvider(provider: string): LocalCatalogModel[
|
||||
if (transcriptionProvider) {
|
||||
appendModels(transcriptionProvider.models, {
|
||||
apiFormat: "audio",
|
||||
supportedEndpoints: ["audio"],
|
||||
supportedEndpoints: ["audio-transcriptions"],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
54
src/shared/constants/modelSupportedEndpoints.ts
Normal file
54
src/shared/constants/modelSupportedEndpoints.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export const MODEL_SUPPORTED_ENDPOINT_VALUES = [
|
||||
"chat",
|
||||
"embeddings",
|
||||
"rerank",
|
||||
"images",
|
||||
"videos",
|
||||
"audio-speech",
|
||||
"audio-transcriptions",
|
||||
"images-generations",
|
||||
// Persisted legacy values remain valid input and normalize on write/edit.
|
||||
"video",
|
||||
"audio",
|
||||
] as const;
|
||||
|
||||
export type ModelSupportedEndpoint = (typeof MODEL_SUPPORTED_ENDPOINT_VALUES)[number];
|
||||
|
||||
export function normalizeModelSupportedEndpoints(endpoints: readonly string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
const add = (endpoint: string) => {
|
||||
if (!normalized.includes(endpoint)) normalized.push(endpoint);
|
||||
};
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
if (endpoint === "video") {
|
||||
add("videos");
|
||||
} else if (endpoint === "audio") {
|
||||
add("audio-speech");
|
||||
add("audio-transcriptions");
|
||||
} else {
|
||||
add(endpoint);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function classifyModelSupportedEndpoints(endpoints: readonly string[]): {
|
||||
type?: "embedding" | "rerank" | "image" | "video" | "audio";
|
||||
subtype?: "speech" | "transcription";
|
||||
} {
|
||||
if (endpoints.includes("embeddings")) return { type: "embedding" };
|
||||
if (endpoints.includes("rerank")) return { type: "rerank" };
|
||||
if (endpoints.includes("images")) return { type: "image" };
|
||||
if (endpoints.includes("videos") || endpoints.includes("video")) return { type: "video" };
|
||||
|
||||
const supportsSpeech = endpoints.includes("audio-speech");
|
||||
const supportsTranscription =
|
||||
endpoints.includes("audio-transcriptions") || endpoints.includes("audio");
|
||||
if (!supportsSpeech && !supportsTranscription) return {};
|
||||
if (supportsSpeech && !supportsTranscription) return { type: "audio", subtype: "speech" };
|
||||
if (supportsTranscription && !supportsSpeech) {
|
||||
return { type: "audio", subtype: "transcription" };
|
||||
}
|
||||
return { type: "audio" };
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints";
|
||||
import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize";
|
||||
import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
|
||||
import {
|
||||
MODEL_SUPPORTED_ENDPOINT_VALUES,
|
||||
normalizeModelSupportedEndpoints,
|
||||
} from "@/shared/constants/modelSupportedEndpoints";
|
||||
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
|
||||
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
|
||||
import {
|
||||
@@ -238,22 +242,12 @@ export const providerModelMutationSchema = z.object({
|
||||
"audio-transcriptions",
|
||||
"audio-speech",
|
||||
"images-generations",
|
||||
"video",
|
||||
])
|
||||
.default("chat-completions"),
|
||||
supportedEndpoints: z
|
||||
.array(
|
||||
z.enum([
|
||||
"chat",
|
||||
"embeddings",
|
||||
"rerank",
|
||||
"images",
|
||||
"audio",
|
||||
"audio-transcriptions",
|
||||
"audio-speech",
|
||||
"images-generations",
|
||||
"videos",
|
||||
])
|
||||
)
|
||||
.array(z.enum(MODEL_SUPPORTED_ENDPOINT_VALUES))
|
||||
.transform(normalizeModelSupportedEndpoints)
|
||||
.default(["chat"]),
|
||||
// #2905: optional per-model wire format override for custom models (e.g. a
|
||||
// custom opencode-go model that must use the Anthropic Messages shape).
|
||||
|
||||
@@ -82,11 +82,11 @@ test("parseGeminiModelsList maps embedContent and bidiGenerateContent", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList maps Veo predictLongRunning models to the video endpoint", () => {
|
||||
test("parseGeminiModelsList maps Veo predictLongRunning models to the videos endpoint", () => {
|
||||
const models = parseGeminiModelsList(SAMPLE);
|
||||
const veo = models.find((m) => m.id === "veo-3.0-generate-001");
|
||||
assert.ok(veo, "veo-3.0-generate-001 should be present");
|
||||
assert.deepEqual(veo!.supportedEndpoints, ["video"]);
|
||||
assert.deepEqual(veo!.supportedEndpoints, ["videos"]);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList defaults to chat and tolerates empty/missing input", () => {
|
||||
|
||||
58
tests/unit/model-supported-endpoints.test.ts
Normal file
58
tests/unit/model-supported-endpoints.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
classifyModelSupportedEndpoints,
|
||||
MODEL_SUPPORTED_ENDPOINT_VALUES,
|
||||
normalizeModelSupportedEndpoints,
|
||||
} from "../../src/shared/constants/modelSupportedEndpoints.ts";
|
||||
|
||||
test("normalizes legacy video and audio metadata to operation-specific endpoint ids", () => {
|
||||
assert.deepEqual(normalizeModelSupportedEndpoints(["chat", "video", "audio"]), [
|
||||
"chat",
|
||||
"videos",
|
||||
"audio-speech",
|
||||
"audio-transcriptions",
|
||||
]);
|
||||
});
|
||||
|
||||
test("deduplicates canonical endpoint ids while preserving order", () => {
|
||||
assert.deepEqual(
|
||||
normalizeModelSupportedEndpoints([
|
||||
"videos",
|
||||
"video",
|
||||
"audio-speech",
|
||||
"audio",
|
||||
"audio-transcriptions",
|
||||
]),
|
||||
["videos", "audio-speech", "audio-transcriptions"]
|
||||
);
|
||||
});
|
||||
|
||||
test("exports operation-specific values accepted by model metadata", () => {
|
||||
assert.ok(MODEL_SUPPORTED_ENDPOINT_VALUES.includes("videos"));
|
||||
assert.ok(MODEL_SUPPORTED_ENDPOINT_VALUES.includes("audio-speech"));
|
||||
assert.ok(MODEL_SUPPORTED_ENDPOINT_VALUES.includes("audio-transcriptions"));
|
||||
});
|
||||
|
||||
test("preserves endpoint ids introduced by external discovery", () => {
|
||||
assert.deepEqual(normalizeModelSupportedEndpoints(["responses", "video"]), [
|
||||
"responses",
|
||||
"videos",
|
||||
]);
|
||||
});
|
||||
|
||||
test("classifies operation-specific media endpoints for the model catalog", () => {
|
||||
assert.deepEqual(classifyModelSupportedEndpoints(["videos"]), { type: "video" });
|
||||
assert.deepEqual(classifyModelSupportedEndpoints(["audio-speech"]), {
|
||||
type: "audio",
|
||||
subtype: "speech",
|
||||
});
|
||||
assert.deepEqual(classifyModelSupportedEndpoints(["audio-transcriptions"]), {
|
||||
type: "audio",
|
||||
subtype: "transcription",
|
||||
});
|
||||
assert.deepEqual(classifyModelSupportedEndpoints(["audio-speech", "audio-transcriptions"]), {
|
||||
type: "audio",
|
||||
});
|
||||
});
|
||||
16
tests/unit/provider-model-endpoint-schema.test.ts
Normal file
16
tests/unit/provider-model-endpoint-schema.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { providerModelMutationSchema } from "../../src/shared/validation/schemas/provider.ts";
|
||||
|
||||
test("provider model mutations accept video and persist canonical operation endpoints", () => {
|
||||
const parsed = providerModelMutationSchema.parse({
|
||||
provider: "example",
|
||||
modelId: "media-model",
|
||||
apiFormat: "video",
|
||||
supportedEndpoints: ["video", "audio"],
|
||||
});
|
||||
|
||||
assert.equal(parsed.apiFormat, "video");
|
||||
assert.deepEqual(parsed.supportedEndpoints, ["videos", "audio-speech", "audio-transcriptions"]);
|
||||
});
|
||||
20
tests/unit/static-model-operation-endpoints.test.ts
Normal file
20
tests/unit/static-model-operation-endpoints.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels.ts";
|
||||
|
||||
test("speech-only static models advertise the speech operation", () => {
|
||||
const models = getStaticModelsForProvider("elevenlabs") || [];
|
||||
|
||||
assert.ok(models.length > 0);
|
||||
assert.ok(models.every((model) => model.supportedEndpoints?.includes("audio-speech")));
|
||||
assert.ok(models.every((model) => !model.supportedEndpoints?.includes("audio")));
|
||||
});
|
||||
|
||||
test("transcription-only static models advertise the transcription operation", () => {
|
||||
const models = getStaticModelsForProvider("gladia") || [];
|
||||
|
||||
assert.ok(models.length > 0);
|
||||
assert.ok(models.every((model) => model.supportedEndpoints?.includes("audio-transcriptions")));
|
||||
assert.ok(models.every((model) => !model.supportedEndpoints?.includes("audio")));
|
||||
});
|
||||
Reference in New Issue
Block a user