From 01dfe9e5efa15da8d70160ba2db75c3122e17f4e Mon Sep 17 00:00:00 2001 From: SecurityQQ Date: Mon, 31 Aug 2026 23:35:44 -0700 Subject: [PATCH] fix(react): audio:"native" sends the unified audio field, not fal.generate_audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sugar blindly injected provider_options.fal.generate_audio into every varg gateway model, relying on the varg API silently stripping the flag on models whose fal schema doesn't declare it. The API now rejects unknown provider_options keys with a 422 (silent stripping was losing real fields), so the blind injection started failing renders on ~26 fal video models. New contract: for varg models the sugar sets providerOptions.varg.audio = true — the UNIFIED native-audio field. VargVideoModel lifts it out of the varg namespace onto the request body top level; the API maps it per model via mapping_rules (rename audio -> generate_audio, migration 0147) and models without native-audio support silently ignore it (same best-effort contract as the unified resolution field). Direct fal/magnific providers keep their own generate_audio handling — they bypass the varg API. --- src/ai-sdk/providers/varg.ts | 20 +++++++++++++++++--- src/react/elements.ts | 21 ++++++++++++--------- src/react/tests/audio-element.test.ts | 12 ++++++++---- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/ai-sdk/providers/varg.ts b/src/ai-sdk/providers/varg.ts index 6070c67b..d43d97b5 100644 --- a/src/ai-sdk/providers/varg.ts +++ b/src/ai-sdk/providers/varg.ts @@ -382,9 +382,23 @@ class VargVideoModel implements VideoModelV3 { } if (options.providerOptions?.varg) { - params.provider_options = checkVargProviderOptions( - options.providerOptions.varg as Record, - ); + const vargOpts = { + ...(options.providerOptions.varg as Record), + }; + // Unified varg-API fields riding the `varg` namespace are lifted to the + // request body top level — they are NOT provider_options. `audio` is the + // unified native-audio request (audio: "native" sugar): the API maps it + // per model via mapping_rules (rename audio → generate_audio) and models + // without the rule silently ignore it. Sending it inside + // provider_options.fal instead used to rely on the API silently + // stripping unknown keys, which now 422s. + if (vargOpts.audio !== undefined) { + params.audio = vargOpts.audio; + delete vargOpts.audio; + } + if (Object.keys(vargOpts).length > 0) { + params.provider_options = checkVargProviderOptions(vargOpts); + } } const result = await executeJob(this.baseUrl, this.apiKey, "video", params); diff --git a/src/react/elements.ts b/src/react/elements.ts index c5bbce77..ec3bde52 100644 --- a/src/react/elements.ts +++ b/src/react/elements.ts @@ -132,10 +132,15 @@ function attachAudioGetter< * IGNORED_PROPS_BY_TYPE) — the expanded providerOptions carry the * semantic difference, so pre-existing configurations keep their keys. * - * Provider mapping (verified against the varg API deep-merge behavior): + * Provider mapping: * - direct fal models → `providerOptions.fal.generate_audio` - * - varg gateway models → `providerOptions.varg.fal.generate_audio` - * (the API forwards `provider_options.fal.*` to the fal payload) + * - varg gateway models → `providerOptions.varg.audio` (the UNIFIED field — + * lifted to the request body top level by VargVideoModel and mapped per + * model by the API's mapping_rules; models without native-audio support + * silently ignore it). Do NOT inject provider_options.fal.generate_audio + * here: the API rejects unknown provider_options keys with a 422, so the + * old blind injection broke every fal video model whose schema lacks + * generate_audio. */ function expandNativeAudio(props: VideoProps): VideoProps { if (props.audio !== "native") return props; @@ -155,12 +160,10 @@ function expandNativeAudio(props: VideoProps): VideoProps { string, unknown >; - const falOpts = { ...((vargOpts.fal as object) ?? {}) } as Record< - string, - unknown - >; - if (falOpts.generate_audio === undefined) falOpts.generate_audio = true; - vargOpts.fal = falOpts; + // Unified native-audio request — best-effort by design: the varg API + // maps it to the provider's own flag on models that support it and + // silently drops it elsewhere (mapping_rules, migration 0147). + if (vargOpts.audio === undefined) vargOpts.audio = true; providerOptions.varg = vargOpts as Record; } else { // Default to the fal namespace (fal is the only provider with a diff --git a/src/react/tests/audio-element.test.ts b/src/react/tests/audio-element.test.ts index ea8bd888..f401819b 100644 --- a/src/react/tests/audio-element.test.ts +++ b/src/react/tests/audio-element.test.ts @@ -200,7 +200,12 @@ describe("audio: native sugar", () => { expect(po.fal?.generate_audio).toBe(true); }); - test("expands into varg.fal namespace for varg gateway models", () => { + test("expands to the unified varg.audio field for varg gateway models", () => { + // NOT provider_options.fal.generate_audio: the varg API rejects unknown + // provider_options keys with a 422 on models whose fal schema lacks the + // flag. The unified `audio` field is mapped per model by the API's + // mapping_rules (rename audio → generate_audio) and silently ignored by + // models without native-audio support. const vid = Video({ prompt: "sunset", audio: "native", @@ -213,9 +218,8 @@ describe("audio: native sugar", () => { string, Record >; - expect((po.varg?.fal as Record)?.generate_audio).toBe( - true, - ); + expect(po.varg?.audio).toBe(true); + expect(po.varg?.fal).toBeUndefined(); expect(vid.props.keepAudio).toBe(true); });