From bb7053f16c08e5df456692b70a78e9f25792aca6 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 29 Mar 2026 15:37:59 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20respect=20direct=20OpenAI?= =?UTF-8?q?=20voice=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Respect provider route priority when choosing the voice transcription backend. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$unknown`_ --- src/node/services/voiceService.test.ts | 35 ++++++++++++++++++ src/node/services/voiceService.ts | 49 ++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/node/services/voiceService.test.ts b/src/node/services/voiceService.test.ts index 2dc07dd497..9d55753686 100644 --- a/src/node/services/voiceService.test.ts +++ b/src/node/services/voiceService.test.ts @@ -192,6 +192,41 @@ describe("VoiceService.transcribe", () => { }); }); + it("respects direct-before-gateway route priority when both are configured", async () => { + await withTempConfig(async (config, service) => { + config.saveProvidersConfig({ + "mux-gateway": { + couponCode: "gateway-token", + }, + openai: { + apiKey: "sk-test", + }, + }); + await config.editConfig((cfg) => { + cfg.routePriority = ["direct", "mux-gateway"]; + return cfg; + }); + + const fetchSpy = spyOn(globalThis, "fetch"); + fetchSpy.mockResolvedValue(new Response("transcribed text")); + + try { + const result = await service.transcribe("Zm9v"); + + expect(result).toEqual({ success: true, data: "transcribed text" }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit | undefined]; + expect(url).toBe("https://api.openai.com/v1/audio/transcriptions"); + expect(init?.headers).toEqual({ + Authorization: "Bearer sk-test", + }); + } finally { + fetchSpy.mockRestore(); + } + }); + }); + it("falls back to OpenAI when gateway is disabled", async () => { await withTempConfig(async (config, service) => { config.saveProvidersConfig({ diff --git a/src/node/services/voiceService.ts b/src/node/services/voiceService.ts index b7c00035c4..c041ce9dbf 100644 --- a/src/node/services/voiceService.ts +++ b/src/node/services/voiceService.ts @@ -1,3 +1,4 @@ +import { resolveRoute } from "@/common/routing"; import { MUX_GATEWAY_ORIGIN } from "@/common/constants/muxGatewayOAuth"; import type { ExternalSecretResolver } from "@/common/types/secrets"; import type { Result } from "@/common/types/result"; @@ -11,6 +12,8 @@ import { log } from "./log"; const OPENAI_TRANSCRIPTION_URL = "https://api.openai.com/v1/audio/transcriptions"; const MUX_GATEWAY_TRANSCRIPTION_PATH = "/api/v1/openai/v1/audio/transcriptions"; +const OPENAI_TRANSCRIPTION_MODEL = "openai:whisper-1"; +const DEFAULT_TRANSCRIPTION_ROUTE_PRIORITY = ["mux-gateway", "direct"]; interface OpenAITranscriptionConfig { apiKey?: string; @@ -63,12 +66,18 @@ export class VoiceService { !isProviderDisabledInConfig(openaiConfig ?? {}) && !!openaiApiKey && (this.policyService?.isProviderAllowed("openai") ?? true); + const transcriptionRoute = this.resolveTranscriptionRoute({ + routePriority: mainConfig.routePriority, + routeOverrides: mainConfig.routeOverrides, + gatewayAvailable, + openaiAvailable, + }); - if (gatewayAvailable) { + if (transcriptionRoute === "mux-gateway" && gatewayToken) { return await this.transcribeWithGateway(audioBase64, gatewayToken, gatewayConfig); } - if (openaiAvailable) { + if (transcriptionRoute === "openai" && openaiApiKey) { return await this.transcribeWithOpenAI(audioBase64, openaiApiKey, openaiConfig); } @@ -91,6 +100,42 @@ export class VoiceService { } } + private resolveTranscriptionRoute(options: { + routePriority?: string[]; + routeOverrides?: Record; + gatewayAvailable: boolean; + openaiAvailable: boolean; + }): "mux-gateway" | "openai" | null { + // User rationale: when Settings routes OpenAI directly, voice transcription should use + // the same direct path instead of silently detouring through Mux Gateway. + const route = resolveRoute( + OPENAI_TRANSCRIPTION_MODEL, + options.routePriority ?? DEFAULT_TRANSCRIPTION_ROUTE_PRIORITY, + options.routeOverrides ?? {}, + (provider) => { + if (provider === "mux-gateway") { + return options.gatewayAvailable; + } + + if (provider === "openai") { + return options.openaiAvailable; + } + + return false; + } + ); + + if (route.routeProvider === "mux-gateway" && options.gatewayAvailable) { + return "mux-gateway"; + } + + if (route.routeProvider === "openai" && options.openaiAvailable) { + return "openai"; + } + + return null; + } + private async transcribeWithGateway( audioBase64: string, couponCode: string,