From 01a5a4b5d244c16371d2532172b90c62fd09bc35 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Thu, 18 Jun 2026 19:40:05 +0300 Subject: [PATCH] refactor(ai): explicit STT request format instead of OpenRouter host-sniffing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the implicit `hostname endsWith openrouter.ai` detection with an explicit, admin-chosen provider field `sttApiStyle` ('multipart' = OpenAI- compatible multipart /audio/transcriptions; 'json' = OpenRouter-style JSON + base64 input_audio). The transcription path now branches on the stored field, not on the URL — nothing hidden from the admin. - ai.types: add SttApiStyle + STT_API_STYLES; field on AiProviderSettings and MaskedAiSettings (resolved via ResolvedAiConfig). - update-ai-settings.dto: validate sttApiStyle with @IsIn(STT_API_STYLES). - ai-settings.service: plumb sttApiStyle through resolve()/getMasked() and the non-secret update whitelist; workspace.repo: add it to the ALLOWED array so it persists. - ai.service: drop isOpenRouter(); transcribe() branches on cfg.sttApiStyle; rename helper to transcribeJsonBase64 with provider-neutral error text and a BadRequestException (400) when the base URL is missing for the JSON style. - client: SttApiStyle type on IAiSettings/IAiSettingsUpdate; "Request format" Select on the Voice/STT settings card; i18n. --- .../public/locales/en-US/translation.json | 6 +- .../components/ai-provider-settings.tsx | 26 +++++++- .../workspace/services/ai-settings-service.ts | 8 +++ .../repos/workspace/workspace.repo.ts | 2 +- .../integrations/ai/ai-settings.service.ts | 7 +++ apps/server/src/integrations/ai/ai.service.ts | 60 ++++++++----------- apps/server/src/integrations/ai/ai.types.ts | 8 +++ .../ai/dto/update-ai-settings.dto.ts | 6 +- 8 files changed, 85 insertions(+), 38 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 8cfd742c..87e8af42 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1191,5 +1191,9 @@ "Transcription failed": "Transcription failed", "Voice dictation is not configured": "Voice dictation is not configured", "Microphone is unavailable or already in use": "Microphone is unavailable or already in use", - "Audio recording is not available in this browser/context": "Audio recording is not available in this browser/context" + "Audio recording is not available in this browser/context": "Audio recording is not available in this browser/context", + "Request format": "Request format", + "How transcription requests are sent to the endpoint": "How transcription requests are sent to the endpoint", + "OpenAI-compatible (multipart/form-data)": "OpenAI-compatible (multipart/form-data)", + "OpenRouter (JSON, base64 audio)": "OpenRouter (JSON, base64 audio)" } diff --git a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx index b908fc03..78727bda 100644 --- a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx +++ b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx @@ -9,6 +9,7 @@ import { Modal, Paper, PasswordInput, + Select, Stack, Switch, Text, @@ -32,7 +33,10 @@ import { useTestAiConnectionMutation, useUpdateAiSettingsMutation, } from "@/features/workspace/queries/ai-settings-query.ts"; -import { IAiSettingsUpdate } from "@/features/workspace/services/ai-settings-service.ts"; +import { + IAiSettingsUpdate, + SttApiStyle, +} from "@/features/workspace/services/ai-settings-service.ts"; import AiMcpServers from "./ai-mcp-servers.tsx"; // No driver field: every endpoint is OpenAI-compatible, so the form carries only @@ -50,6 +54,7 @@ const formSchema = z.object({ // STT-specific fields. Empty base URL / key fall back to the chat ones. sttModel: z.string(), sttBaseUrl: z.string(), + sttApiStyle: z.enum(["multipart", "json"]), sttApiKey: z.string(), }); @@ -139,6 +144,7 @@ export default function AiProviderSettings() { embeddingApiKey: "", sttModel: "", sttBaseUrl: "", + sttApiStyle: "multipart" as SttApiStyle, sttApiKey: "", }, }); @@ -157,6 +163,7 @@ export default function AiProviderSettings() { embeddingApiKey: "", sttModel: settings.sttModel ?? "", sttBaseUrl: settings.sttBaseUrl ?? "", + sttApiStyle: settings.sttApiStyle ?? "multipart", sttApiKey: "", }); form.resetDirty(); @@ -184,6 +191,7 @@ export default function AiProviderSettings() { // server-side. sttModel: values.sttModel, sttBaseUrl: values.sttBaseUrl, + sttApiStyle: values.sttApiStyle, }; // Key semantics (never send the stored key back): @@ -671,6 +679,22 @@ export default function AiProviderSettings() { +