diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 3e0c4a5a3c..fea1fb05c5 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -372,7 +372,9 @@ export function translateRequest( preserveReasoningContent: isReasoner, // Per-provider/model preserveVideoUrl flag from compat overrides. // Falls back to true for moonshot/kimi when unset (legacy behavior). - preserveVideoUrl: getModelPreserveVideoUrl(normalizedProvider, options?.model ?? "") ?? (normalizedProvider === "moonshot" || normalizedProvider === "kimi"), + preserveVideoUrl: + getModelPreserveVideoUrl(normalizedProvider, normalizedModel) ?? + (normalizedProvider === "moonshot" || normalizedProvider === "kimi"), }); } diff --git a/src/lib/db/models/compat.ts b/src/lib/db/models/compat.ts index 54b05ba3a6..2e0a645724 100644 --- a/src/lib/db/models/compat.ts +++ b/src/lib/db/models/compat.ts @@ -17,6 +17,7 @@ export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey }; export type ModelCompatPerProtocol = { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean; + preserveVideoUrl?: boolean; /** Merged into upstream HTTP requests for this model (after default auth headers). */ upstreamHeaders?: Record; }; @@ -76,6 +77,7 @@ export function deepMergeCompatByProtocol( const hasDelta = Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") || Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole") || + Object.prototype.hasOwnProperty.call(deltas, "preserveVideoUrl") || Object.prototype.hasOwnProperty.call(deltas, "upstreamHeaders"); if (!hasDelta) continue; const cur: ModelCompatPerProtocol = { ...(out[key] || {}) }; @@ -85,6 +87,9 @@ export function deepMergeCompatByProtocol( if ("preserveOpenAIDeveloperRole" in deltas) { cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole); } + if ("preserveVideoUrl" in deltas) { + cur.preserveVideoUrl = Boolean(deltas.preserveVideoUrl); + } if ("upstreamHeaders" in deltas) { const uh = deltas.upstreamHeaders; if (uh === undefined) { @@ -105,6 +110,7 @@ export type ModelCompatOverride = { id: string; normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean; + preserveVideoUrl?: boolean; compatByProtocol?: CompatByProtocolMap; upstreamHeaders?: Record; isHidden?: boolean; @@ -159,6 +165,7 @@ export function getModelCompatOverrides(providerId: string): ModelCompatOverride export type ModelCompatPatch = { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean | null; + preserveVideoUrl?: boolean | null; compatByProtocol?: CompatByProtocolMap; /** Replace top-level extra headers for override-only rows; omit to leave unchanged. */ upstreamHeaders?: Record | null; @@ -195,6 +202,13 @@ export function mergeModelCompatOverride( next.preserveOpenAIDeveloperRole = Boolean(patch.preserveOpenAIDeveloperRole); } } + if ("preserveVideoUrl" in patch) { + if (patch.preserveVideoUrl === null) { + delete next.preserveVideoUrl; + } else { + next.preserveVideoUrl = Boolean(patch.preserveVideoUrl); + } + } if (patch.compatByProtocol && Object.keys(patch.compatByProtocol).length > 0) { const merged = deepMergeCompatByProtocol(next.compatByProtocol, patch.compatByProtocol); if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged; @@ -211,6 +225,7 @@ export function mergeModelCompatOverride( } const filtered = list.filter((e) => e.id !== modelId); const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole"); + const hasVideoUrlFlag = Object.prototype.hasOwnProperty.call(next, "preserveVideoUrl"); const hasTopUpstream = next.upstreamHeaders && Object.keys(next.upstreamHeaders).length > 0; if ("isHidden" in patch) { if (patch.isHidden === null) { @@ -231,6 +246,7 @@ export function mergeModelCompatOverride( if ( next.normalizeToolCallId || hasPreserveFlag || + hasVideoUrlFlag || hasHiddenFlag || hasDeletedFlag || compatByProtocolHasEntries(next.compatByProtocol) || diff --git a/tests/unit/preserve-video-url-compat.test.ts b/tests/unit/preserve-video-url-compat.test.ts index 32ba05b2a1..9a6b3f4820 100644 --- a/tests/unit/preserve-video-url-compat.test.ts +++ b/tests/unit/preserve-video-url-compat.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "node:test"; -import { ok, equal } from "node:assert/strict"; +import { deepEqual, equal, ok } from "node:assert/strict"; describe("getModelPreserveVideoUrl", () => { it("exports getModelPreserveVideoUrl as a function", async () => { @@ -8,8 +8,7 @@ describe("getModelPreserveVideoUrl", () => { }); it("fallback preserves moonshot and kimi legacy behavior", () => { - const fallback = (provider: string) => - provider === "moonshot" || provider === "kimi"; + const fallback = (provider: string) => provider === "moonshot" || provider === "kimi"; ok(fallback("moonshot")); ok(fallback("kimi")); equal(fallback("dashscope"), false); @@ -27,19 +26,67 @@ describe("getModelPreserveVideoUrl", () => { }); it("mergeModelCompatOverride accepts preserveVideoUrl", async () => { - const { mergeModelCompatOverride, removeModelCompatOverride } = await import("@/lib/db/models/compat"); + const { mergeModelCompatOverride, removeModelCompatOverride } = + await import("@/lib/db/models/compat"); + const { getModelPreserveVideoUrl } = await import("@/lib/db/models/modelPreserveVideoUrl"); const PROVIDER = "test_provider_9248v3"; const MODEL = "test_model_qwen_vl"; - mergeModelCompatOverride(PROVIDER, MODEL, { preserveVideoUrl: true }); - removeModelCompatOverride(PROVIDER, MODEL); - ok(true, "should accept preserveVideoUrl in ModelCompatPatch"); + try { + mergeModelCompatOverride(PROVIDER, MODEL, { preserveVideoUrl: true }); + equal(getModelPreserveVideoUrl(PROVIDER, MODEL), true); + } finally { + removeModelCompatOverride(PROVIDER, MODEL); + } + }); + + it("translateRequest resolves preserveVideoUrl with the routed model", async () => { + const { mergeModelCompatOverride, removeModelCompatOverride } = + await import("@/lib/db/models/compat"); + const { translateRequest } = await import("@omniroute/open-sse/translator/index.ts"); + const provider = "test-provider-video-override"; + const model = "test-model-video-override"; + + mergeModelCompatOverride(provider, model, { preserveVideoUrl: true }); + try { + const translated = translateRequest( + "openai", + "openai", + model, + { + messages: [ + { + role: "user", + content: [ + { + type: "video_url", + video_url: { url: "https://cdn.example.com/input.mp4" }, + }, + ], + }, + ], + }, + false, + null, + provider + ) as { messages: Array<{ content: Array<{ type: string }> }> }; + + deepEqual( + translated.messages[0].content.map((part) => part.type), + ["video_url"] + ); + } finally { + removeModelCompatOverride(provider, model); + } }); it("deepMergeCompatByProtocol accepts preserveVideoUrl under openai protocol", async () => { const { deepMergeCompatByProtocol } = await import("@/lib/db/models/compat"); - const result = deepMergeCompatByProtocol({}, { - openai: { preserveVideoUrl: true }, - }); + const result = deepMergeCompatByProtocol( + {}, + { + openai: { preserveVideoUrl: true }, + } + ); // Valid protocol keys are 'openai', 'openai-responses', 'claude' equal(result.openai?.preserveVideoUrl, true); });