Skip to content

Commit 3cec659

Browse files
authored
🤖 fix: respect direct OpenAI voice routing (#3090)
## Summary Voice transcription now follows the same routing preference as OpenAI models, so putting Direct ahead of Mux Gateway in Settings keeps voice input on the direct OpenAI transcription endpoint. ## Background `VoiceService` hardcoded a gateway-first choice whenever both a Mux Gateway token and an OpenAI API key were present. That ignored Provider settings, where users can prefer direct routing for OpenAI, and made voice input unexpectedly go through Mux Gateway. ## Implementation - route `openai:whisper-1` through the shared routing resolver using the saved `routePriority` / `routeOverrides` - keep voice transcription scoped to the backends it actually supports today: direct OpenAI and Mux Gateway - add a regression test covering the direct-before-gateway provider ordering ## Validation - `bun test src/node/services/voiceService.test.ts` - `make typecheck` - `make static-check` ## Risks Low and localized to voice transcription backend selection. The change only affects whether voice requests use direct OpenAI or Mux Gateway when both are configured. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$unknown`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=unknown -->
1 parent b70d95e commit 3cec659

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

src/node/services/voiceService.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,41 @@ describe("VoiceService.transcribe", () => {
192192
});
193193
});
194194

195+
it("respects direct-before-gateway route priority when both are configured", async () => {
196+
await withTempConfig(async (config, service) => {
197+
config.saveProvidersConfig({
198+
"mux-gateway": {
199+
couponCode: "gateway-token",
200+
},
201+
openai: {
202+
apiKey: "sk-test",
203+
},
204+
});
205+
await config.editConfig((cfg) => {
206+
cfg.routePriority = ["direct", "mux-gateway"];
207+
return cfg;
208+
});
209+
210+
const fetchSpy = spyOn(globalThis, "fetch");
211+
fetchSpy.mockResolvedValue(new Response("transcribed text"));
212+
213+
try {
214+
const result = await service.transcribe("Zm9v");
215+
216+
expect(result).toEqual({ success: true, data: "transcribed text" });
217+
expect(fetchSpy).toHaveBeenCalledTimes(1);
218+
219+
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit | undefined];
220+
expect(url).toBe("https://api.openai.com/v1/audio/transcriptions");
221+
expect(init?.headers).toEqual({
222+
Authorization: "Bearer sk-test",
223+
});
224+
} finally {
225+
fetchSpy.mockRestore();
226+
}
227+
});
228+
});
229+
195230
it("falls back to OpenAI when gateway is disabled", async () => {
196231
await withTempConfig(async (config, service) => {
197232
config.saveProvidersConfig({

src/node/services/voiceService.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolveRoute } from "@/common/routing";
12
import { MUX_GATEWAY_ORIGIN } from "@/common/constants/muxGatewayOAuth";
23
import type { ExternalSecretResolver } from "@/common/types/secrets";
34
import type { Result } from "@/common/types/result";
@@ -11,6 +12,8 @@ import { log } from "./log";
1112

1213
const OPENAI_TRANSCRIPTION_URL = "https://api.openai.com/v1/audio/transcriptions";
1314
const MUX_GATEWAY_TRANSCRIPTION_PATH = "/api/v1/openai/v1/audio/transcriptions";
15+
const OPENAI_TRANSCRIPTION_MODEL = "openai:whisper-1";
16+
const DEFAULT_TRANSCRIPTION_ROUTE_PRIORITY = ["mux-gateway", "direct"];
1417

1518
interface OpenAITranscriptionConfig {
1619
apiKey?: string;
@@ -63,12 +66,18 @@ export class VoiceService {
6366
!isProviderDisabledInConfig(openaiConfig ?? {}) &&
6467
!!openaiApiKey &&
6568
(this.policyService?.isProviderAllowed("openai") ?? true);
69+
const transcriptionRoute = this.resolveTranscriptionRoute({
70+
routePriority: mainConfig.routePriority,
71+
routeOverrides: mainConfig.routeOverrides,
72+
gatewayAvailable,
73+
openaiAvailable,
74+
});
6675

67-
if (gatewayAvailable) {
76+
if (transcriptionRoute === "mux-gateway" && gatewayToken) {
6877
return await this.transcribeWithGateway(audioBase64, gatewayToken, gatewayConfig);
6978
}
7079

71-
if (openaiAvailable) {
80+
if (transcriptionRoute === "openai" && openaiApiKey) {
7281
return await this.transcribeWithOpenAI(audioBase64, openaiApiKey, openaiConfig);
7382
}
7483

@@ -91,6 +100,42 @@ export class VoiceService {
91100
}
92101
}
93102

103+
private resolveTranscriptionRoute(options: {
104+
routePriority?: string[];
105+
routeOverrides?: Record<string, string>;
106+
gatewayAvailable: boolean;
107+
openaiAvailable: boolean;
108+
}): "mux-gateway" | "openai" | null {
109+
// User rationale: when Settings routes OpenAI directly, voice transcription should use
110+
// the same direct path instead of silently detouring through Mux Gateway.
111+
const route = resolveRoute(
112+
OPENAI_TRANSCRIPTION_MODEL,
113+
options.routePriority ?? DEFAULT_TRANSCRIPTION_ROUTE_PRIORITY,
114+
options.routeOverrides ?? {},
115+
(provider) => {
116+
if (provider === "mux-gateway") {
117+
return options.gatewayAvailable;
118+
}
119+
120+
if (provider === "openai") {
121+
return options.openaiAvailable;
122+
}
123+
124+
return false;
125+
}
126+
);
127+
128+
if (route.routeProvider === "mux-gateway" && options.gatewayAvailable) {
129+
return "mux-gateway";
130+
}
131+
132+
if (route.routeProvider === "openai" && options.openaiAvailable) {
133+
return "openai";
134+
}
135+
136+
return null;
137+
}
138+
94139
private async transcribeWithGateway(
95140
audioBase64: string,
96141
couponCode: string,

0 commit comments

Comments
 (0)