feat(modality-bridge): resolve audio input capability

This commit is contained in:
diegosouzapw
2026-08-08 10:29:23 -03:00
parent 4fc04a5604
commit d7be3db040
4 changed files with 45 additions and 0 deletions

View File

@@ -50,6 +50,7 @@ export interface RegistryModel {
supportsReasoning?: boolean;
supportedThinkingEfforts?: readonly string[];
supportsVision?: boolean;
supportsAudio?: boolean;
supportsXHighEffort?: boolean;
maxOutputTokens?: number;
targetFormat?: string;

View File

@@ -121,6 +121,7 @@ export interface ResolvedModelCapabilities {
supportsThinking: boolean | null;
supportsTools: boolean | null;
supportsVision: boolean | null;
supportsAudio: boolean | null;
supportsMaxTokens: boolean;
attachment: boolean | null;
structuredOutput: boolean | null;
@@ -539,6 +540,25 @@ function resolveVisionCapability(
return null;
}
/**
* Resolve whether a chat model accepts audio input.
*
* Explicit catalog metadata wins. Synced input modalities are authoritative
* only when they contain at least one declared modality; an empty list means
* that no source knows the answer and remains `null` so Audio Bridge can act
* conservatively.
*/
export function resolveAudioCapability(
spec: Pick<ModelSpec, "supportsAudio"> | undefined,
registryModel: { supportsAudio?: boolean } | null,
modalitiesInput: readonly string[]
): boolean | null {
if (typeof registryModel?.supportsAudio === "boolean") return registryModel.supportsAudio;
if (typeof spec?.supportsAudio === "boolean") return spec.supportsAudio;
if (modalitiesInput.length === 0) return null;
return modalitiesInput.some((entry) => String(entry).toLowerCase().includes("audio"));
}
/**
* Issue #6524: an operator-set `max_output_tokens` capability override (see
* `src/lib/db/modelCapabilityOverrides.ts`) is the manual escape hatch for a
@@ -719,6 +739,7 @@ export function getResolvedModelCapabilities(
lookupKey,
customVisionOverride
);
const supportsAudio = resolveAudioCapability(spec, registryModel, modalitiesInput);
// #8250: when resolve promoted vision over a contradictory attachment=false,
// expose attachment=true so catalog / Vision Bridge / clients see one verdict.
@@ -736,6 +757,7 @@ export function getResolvedModelCapabilities(
supportsThinking,
supportsTools,
supportsVision,
supportsAudio,
supportsMaxTokens: heuristicMaxTokens(lookupKey),
attachment,
structuredOutput: synced?.structured_output ?? null,

View File

@@ -15,6 +15,7 @@ export interface ModelSpec {
supportsThinking?: boolean;
supportsTools?: boolean;
supportsVision?: boolean;
supportsAudio?: boolean;
// Model defaults to adaptive thinking and REJECTS an explicit `thinking.type:"disabled"`
// (upstream returns 400). Used to normalize the request when a combo/route substitutes
// this model after the client already chose `disabled`. See issue #3554.

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveAudioCapability } from "../../src/lib/modelCapabilities.ts";
test("explicit registry audio capability wins", () => {
assert.equal(resolveAudioCapability(undefined, { supportsAudio: true }, []), true);
});
test("explicit static audio capability is used when registry is silent", () => {
assert.equal(resolveAudioCapability({ supportsAudio: false }, null, []), false);
});
test("synced input modalities identify audio-capable and text-only models", () => {
assert.equal(resolveAudioCapability(undefined, null, ["text", "audio"]), true);
assert.equal(resolveAudioCapability(undefined, null, ["text"]), false);
});
test("missing audio capability evidence stays unknown", () => {
assert.equal(resolveAudioCapability(undefined, null, []), null);
});