feat(dashboard): derive media serviceKinds from registries (surface MiniMax + media catalog) (#4212)

Integrated into release/v3.8.29 (round 7). Media serviceKinds derived from backend registries (single source of truth) unioned with declared kinds; listing + detail pages use the same resolver (no 'listed but 404'). Surfaces MiniMax (tts/video/music) + ~48 previously-invisible media providers. Validated locally: 6/6 tests + typecheck:core clean.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-18 20:19:00 -03:00
committed by GitHub
parent 12124aaad8
commit cfc2bc707b
4 changed files with 181 additions and 6 deletions

View File

@@ -0,0 +1,71 @@
/**
* Media serviceKinds — derived from the backend registries.
*
* The dashboard's `/dashboard/media-providers/[kind]` pages list a provider
* under a media kind when the provider supports it. Historically this relied on
* a hand-maintained `serviceKinds` array on every provider in providers.ts,
* which had drifted badly: ~48 providers were wired into the audio/video/music/
* image/embedding registries (backend works) but declared no `serviceKinds`, so
* they never appeared in the UI.
*
* This module makes the registries the single source of truth: a provider that
* appears as a key in a media registry supports that kind, full stop. The UI
* derives membership from here instead of duplicating it by hand, so adding a
* provider to a registry automatically surfaces it — no second edit, no drift.
*
* Kinds without a backing registry (imageToText, webSearch, webFetch, llm) are
* still declared explicitly via `serviceKinds` on the provider entry; callers
* union the two sources.
*/
import {
AUDIO_TRANSCRIPTION_PROVIDERS,
AUDIO_SPEECH_PROVIDERS,
} from "./audioRegistry.ts";
import { VIDEO_PROVIDERS } from "./videoRegistry.ts";
import { MUSIC_PROVIDERS } from "./musicRegistry.ts";
import { IMAGE_PROVIDERS } from "./imageRegistry.ts";
import { EMBEDDING_PROVIDERS } from "./embeddingRegistry.ts";
/** Media kinds whose provider membership is defined by a backend registry. */
export const MEDIA_KIND_REGISTRIES = {
stt: AUDIO_TRANSCRIPTION_PROVIDERS,
tts: AUDIO_SPEECH_PROVIDERS,
video: VIDEO_PROVIDERS,
music: MUSIC_PROVIDERS,
image: IMAGE_PROVIDERS,
embedding: EMBEDDING_PROVIDERS,
} as const satisfies Record<string, Record<string, unknown>>;
export type RegistryMediaKind = keyof typeof MEDIA_KIND_REGISTRIES;
export const REGISTRY_MEDIA_KINDS: readonly RegistryMediaKind[] = Object.freeze(
Object.keys(MEDIA_KIND_REGISTRIES) as RegistryMediaKind[]
);
/**
* Media serviceKinds a provider supports, derived from the backend registries.
* Returns the kinds (e.g. `["tts","video","music"]`) for which `providerId`
* appears as a registry key. Empty array if the provider serves no media.
*/
export function getRegistryMediaKinds(providerId: string): RegistryMediaKind[] {
const kinds: RegistryMediaKind[] = [];
for (const kind of REGISTRY_MEDIA_KINDS) {
if (Object.prototype.hasOwnProperty.call(MEDIA_KIND_REGISTRIES[kind], providerId)) {
kinds.push(kind);
}
}
return kinds;
}
/**
* Full set of serviceKinds for a provider: the explicitly declared ones (llm,
* web*, imageToText) unioned with the media kinds derived from the registries.
*/
export function resolveProviderServiceKinds(
providerId: string,
declared: readonly string[] | undefined
): string[] {
const set = new Set<string>(declared ?? []);
for (const kind of getRegistryMediaKinds(providerId)) set.add(kind);
return [...set];
}

View File

@@ -1,6 +1,7 @@
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { resolveProviderServiceKinds } from "@omniroute/open-sse/config/mediaServiceKinds.ts";
import type { MediaKind } from "../../components/mediaKinds";
import { MEDIA_KINDS } from "../../components/mediaKinds";
import MediaProviderPageClient from "./MediaProviderPageClient";
@@ -42,9 +43,14 @@ export default async function MediaProviderDetailPage({ params }: PageProps) {
notFound();
}
// Validate that the provider declares this kind
const serviceKinds = provider.serviceKinds as string[] | undefined;
if (!Array.isArray(serviceKinds) || !serviceKinds.includes(validKind)) {
// Validate that the provider supports this kind — declared serviceKinds unioned
// with the media kinds derived from the backend registries (must mirror the
// listing filter in ../page.tsx, otherwise a listed provider would 404 on click).
const serviceKinds = resolveProviderServiceKinds(
provider.id,
provider.serviceKinds as string[] | undefined
);
if (!serviceKinds.includes(validKind)) {
notFound();
}

View File

@@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { resolveProviderServiceKinds } from "@omniroute/open-sse/config/mediaServiceKinds.ts";
import type { MediaKind } from "../components/mediaKinds";
import { MEDIA_KINDS } from "../components/mediaKinds";
import MediaProviderKindNav from "../components/MediaProviderKindNav";
@@ -28,10 +29,14 @@ export default async function MediaProviderKindPage({ params }: PageProps) {
const validKind = kind as MediaKind;
const t = await getTranslations("media");
// Filter providers that support this kind
// Filter providers that support this kind. Membership is the union of the
// explicitly declared serviceKinds and the media kinds derived from the
// backend registries (audio/video/music/image/embedding) — see
// resolveProviderServiceKinds. This keeps the UI in lockstep with the backend
// without hand-maintaining serviceKinds on every media provider.
const matchingProviders = Object.values(AI_PROVIDERS).filter((p) => {
const serviceKinds = (p as Record<string, unknown>).serviceKinds as string[] | undefined;
return Array.isArray(serviceKinds) && serviceKinds.includes(validKind);
const entry = p as Record<string, unknown> & { id: string; serviceKinds?: string[] };
return resolveProviderServiceKinds(entry.id, entry.serviceKinds).includes(validKind);
});
const kindLabel = t(`kinds.${validKind}`);

View File

@@ -0,0 +1,93 @@
import test from "node:test";
import assert from "node:assert/strict";
// Regression guard: media providers surface in the dashboard via registry-derived
// serviceKinds.
//
// Root cause this fixes: `/dashboard/media-providers/[kind]` listed a provider only
// if it hand-declared `serviceKinds` in providers.ts. ~48 providers were wired into
// the audio/video/music/image/embedding registries (backend works) but declared no
// serviceKinds, so every media page was empty. The fix derives media membership from
// the registries (single source of truth) and unions it with declared serviceKinds.
//
// MiniMax was the flagged case: its international endpoint serves TTS/video/music, the
// China variant (minimax-cn) has no media registry entries.
const { getRegistryMediaKinds, resolveProviderServiceKinds, REGISTRY_MEDIA_KINDS } =
await import("../../open-sse/config/mediaServiceKinds.ts");
const { AI_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
test("minimax (international) derives tts/video/music from the registries", () => {
const kinds = getRegistryMediaKinds("minimax").sort();
assert.deepEqual(kinds, ["music", "tts", "video"]);
});
test("minimax-cn derives no media kinds (China endpoint has no media registry entries)", () => {
assert.deepEqual(getRegistryMediaKinds("minimax-cn"), []);
});
test("representative media providers derive the expected kinds", () => {
// Each pair: provider id -> at least these kinds must be derived.
const cases: Record<string, string[]> = {
elevenlabs: ["tts"],
deepgram: ["stt", "tts"],
suno: ["music"],
udio: ["music"],
runwayml: ["video"],
openai: ["embedding", "image", "stt", "tts"],
cohere: ["embedding", "stt"],
};
for (const [id, expected] of Object.entries(cases)) {
const derived = getRegistryMediaKinds(id);
for (const kind of expected) {
assert.ok(derived.includes(kind as never), `${id} should derive ${kind}; got ${derived.join(",")}`);
}
}
});
test("resolveProviderServiceKinds unions declared kinds with derived media kinds", () => {
// A provider with a declared non-media kind keeps it AND gains derived media kinds.
const merged = resolveProviderServiceKinds("minimax", ["llm"]);
for (const expected of ["llm", "tts", "video", "music"]) {
assert.ok(merged.includes(expected), `expected ${expected} in ${merged.join(",")}`);
}
// De-dupes when declared overlaps derived.
const veo = resolveProviderServiceKinds("veoaifree-web", ["video"]);
assert.equal(veo.filter((k) => k === "video").length, 1);
});
test("media listing filter surfaces minimax where the old declared-only filter missed it", () => {
const cfg = AI_PROVIDERS as Record<string, { id: string; serviceKinds?: string[] }>;
// OLD behavior (the bug): filter by declared serviceKinds only.
const oldListFor = (kind: string) =>
Object.values(cfg)
.filter((p) => (p.serviceKinds ?? []).includes(kind))
.map((p) => p.id);
// NEW behavior (the fix): union declared with registry-derived kinds — mirrors page.tsx.
const newListFor = (kind: string) =>
Object.values(cfg)
.filter((p) => resolveProviderServiceKinds(p.id, p.serviceKinds).includes(kind))
.map((p) => p.id);
for (const kind of ["tts", "video", "music"]) {
assert.ok(!oldListFor(kind).includes("minimax"), `precondition (bug): old filter missed minimax under ${kind}`);
assert.ok(newListFor(kind).includes("minimax"), `fix: minimax now listed under ${kind}`);
assert.ok(!newListFor(kind).includes("minimax-cn"), `minimax-cn must not appear under ${kind}`);
}
// The fix is systemic, not minimax-only: many providers were invisible before.
assert.ok(oldListFor("tts").length < newListFor("tts").length, "fix surfaces additional tts providers");
});
test("derived kinds are always within the known media-kind set", () => {
for (const id of Object.keys(AI_PROVIDERS)) {
for (const kind of getRegistryMediaKinds(id)) {
assert.ok(
(REGISTRY_MEDIA_KINDS as readonly string[]).includes(kind),
`${id} derived unknown kind ${kind}`
);
}
}
});