fix(modality-bridge): harden audio catalog and response header

This commit is contained in:
diegosouzapw
2026-08-08 11:25:22 -03:00
parent ebe04e06d3
commit 49560f4ea3
7 changed files with 93 additions and 15 deletions

View File

@@ -114,6 +114,7 @@ export default function ModalityBridgeAudioTab() {
placeholder={t("modalityBridgeAudioModelAuto")}
allowEmpty
modelFilter={isSttModel}
modelSource="catalog"
onChange={(value) => void update({ modalityBridgeAudioModel: value })}
className="text-sm"
/>

View File

@@ -38,6 +38,7 @@ export async function GET(request: Request) {
id: model.id,
name: model.name || model.root || model.id,
type: model.type || "chat",
...(typeof model.subtype === "string" ? { subtype: model.subtype } : {}),
custom: model.custom === true,
...(model.free === true ? { free: true } : {}),
...(model.capabilities ? { capabilities: model.capabilities } : {}),

View File

@@ -48,6 +48,12 @@ interface GuardrailMetaEntry {
meta?: Record<string, unknown> | null;
}
function headerModelToken(value: unknown): string {
return String(value ?? "unknown")
.slice(0, 200)
.replace(/[^A-Za-z0-9._~/-]/g, "_");
}
/** Response header value for a describe-bridged request; null when untouched. */
export function buildModalityBridgeHeader(results: GuardrailMetaEntry[]): string | null {
const segments: string[] = [];
@@ -59,7 +65,7 @@ export function buildModalityBridgeHeader(results: GuardrailMetaEntry[]): string
!meta.rerouted
) {
segments.push(
`image->text;model=${String(meta.visionModel ?? "unknown")};parts=${meta.imagesProcessed}`
`image->text;model=${headerModelToken(meta.visionModel)};parts=${meta.imagesProcessed}`
);
}
if (
@@ -68,7 +74,7 @@ export function buildModalityBridgeHeader(results: GuardrailMetaEntry[]): string
!meta.rerouted
) {
segments.push(
`audio->text;model=${String(meta.sttModel ?? "unknown")};parts=${meta.clipsProcessed}`
`audio->text;model=${headerModelToken(meta.sttModel)};parts=${meta.clipsProcessed}`
);
}
}

View File

@@ -25,6 +25,8 @@ export interface ModelSelectFieldProps {
allowEmpty?: boolean;
/** Optional catalog predicate, e.g. restrict the picker to STT models. */
modelFilter?: (model: ApiModel) => boolean;
/** Model API to read. The unified catalog includes specialty audio/video surfaces. */
modelSource?: "available" | "catalog";
className?: string;
}
@@ -33,6 +35,35 @@ interface FetchState {
options: { value: string; label: string }[];
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function readCatalogModels(value: unknown): ApiModel[] {
const catalog = asRecord(asRecord(value)?.catalog);
if (!catalog) return [];
const models: ApiModel[] = [];
for (const [provider, rawBucket] of Object.entries(catalog)) {
const bucket = asRecord(rawBucket);
if (!Array.isArray(bucket?.models)) continue;
for (const rawModel of bucket.models) {
const model = asRecord(rawModel);
const id = typeof model?.id === "string" ? model.id : "";
if (!id) continue;
const providerPrefix = `${provider}/`;
models.push({
provider,
model: id.startsWith(providerPrefix) ? id.slice(providerPrefix.length) : id,
fullModel: id.startsWith(providerPrefix) ? id : `${providerPrefix}${id}`,
type: typeof model?.type === "string" ? model.type : undefined,
subtype: typeof model?.subtype === "string" ? model.subtype : undefined,
});
}
}
return models;
}
/**
* hidePaid-aware model picker (#6540). Loads options from `GET /api/models`
* (already filters by `hidePaidModels`) instead of a static catalog. Falls
@@ -51,17 +82,24 @@ export default function ModelSelectField({
allowCustom = true,
allowEmpty = false,
modelFilter,
modelSource = "available",
className,
}: ModelSelectFieldProps) {
const [state, setState] = useState<FetchState>({ status: "loading", options: [] });
useEffect(() => {
let cancelled = false;
fetch("/api/models")
const endpoint = modelSource === "catalog" ? "/api/models/catalog" : "/api/models";
fetch(endpoint)
.then((res) => (res.ok ? res.json() : Promise.reject(new Error("fetch failed"))))
.then((data) => {
if (cancelled) return;
const models: ApiModel[] = Array.isArray(data?.models) ? data.models : [];
const models: ApiModel[] =
modelSource === "catalog"
? readCatalogModels(data)
: Array.isArray(data?.models)
? data.models
: [];
const filteredModels = modelFilter ? models.filter(modelFilter) : models;
const options = filteredModels.map((m) => {
const full = m.fullModel || `${m.provider}/${m.model}`;
@@ -75,7 +113,7 @@ export default function ModelSelectField({
return () => {
cancelled = true;
};
}, [modelFilter]);
}, [modelFilter, modelSource]);
if (state.status === "error" && allowCustom) {
return (

View File

@@ -264,4 +264,16 @@ test("audio transparency header is emitted only for transformed clips", () => {
]),
null
);
assert.equal(
buildModalityBridgeHeader([
{
guardrail: "audio-bridge",
meta: {
clipsProcessed: 1,
sttModel: "deepgram/nova-3\r\nx-injected: yes",
},
},
]),
"audio->text;model=deepgram/nova-3__x-injected__yes;parts=1"
);
});

View File

@@ -11,6 +11,7 @@ process.env.JWT_SECRET = process.env.JWT_SECRET || "model-alias-route-jwt";
const core = await import("../../src/lib/db/core.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const localDb = await import("../../src/lib/localDb.ts");
const route = await import("../../src/app/api/models/alias/route.ts");
const catalogRoute = await import("../../src/app/api/models/catalog/route.ts");
@@ -86,6 +87,15 @@ test("model alias route requires a dashboard session when management auth is ena
});
test("api models catalog route reuses the unified catalog diagnostics headers", async () => {
await providersDb.createProviderConnection({
provider: "deepgram",
authType: "apikey",
name: "deepgram-audio-catalog",
apiKey: "dg-test",
isActive: true,
testStatus: "active",
});
v1Catalog.__resetCatalogBuilderRunsForTest();
const response = await catalogRoute.GET(
new Request("http://localhost/api/models/catalog", {
headers: { "x-request-id": "req-model-catalog-1" },
@@ -98,6 +108,11 @@ test("api models catalog route reuses the unified catalog diagnostics headers",
assert.match(response.headers.get("X-Model-Catalog-Version") || "", /^model-metadata-v1:/);
assert.equal(typeof body.catalog, "object");
assert.equal(typeof body.catalogVersion, "string");
const nova = body.catalog.deepgram.models.find(
(model: { id?: string }) => model.id === "deepgram/nova-3"
);
assert.equal(nova.type, "audio");
assert.equal(nova.subtype, "transcription");
});
test("v1 models catalog emits diagnostics headers alongside the OpenAI-compatible list", async () => {

View File

@@ -28,20 +28,24 @@ describe("ModalityBridgeAudioTab", () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.includes("/api/models")) {
if (url.includes("/api/models/catalog")) {
return Response.json({
models: [
{
provider: "deepgram",
model: "nova-3",
type: "audio",
subtype: "transcription",
catalog: {
deepgram: {
provider: "Deepgram",
models: [
{ id: "deepgram/nova-3", type: "audio", subtype: "transcription" },
{ id: "deepgram/aura", type: "audio", subtype: "speech" },
],
},
{ provider: "deepgram", model: "aura", type: "audio", subtype: "speech" },
{ provider: "openai", model: "gpt-5.6" },
],
openai: {
provider: "OpenAI",
models: [{ id: "openai/gpt-5.6", type: "chat" }],
},
},
});
}
if (url.includes("/api/models")) return Response.json({ models: [] });
if (url.includes("/api/modality-bridge/stats")) {
return Response.json({
vision: { bridged: 0, cacheHits: 0, failures: 0, lastUsedAt: null },
@@ -99,6 +103,7 @@ describe("ModalityBridgeAudioTab", () => {
it("shows only transcription models and exposes selectable Auto", async () => {
const el = await render();
expect(fetchMock).toHaveBeenCalledWith("/api/models/catalog");
await waitFor(
() =>
Array.from(el.querySelectorAll("option")).some(