🔊 fix: Validate language format for OpenAI STT model (#10875)
Some checks are pending
Publish `@librechat/client` to NPM / build-and-publish (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* 🔊 fix: Validate language format for OpenAI STT model

* fix: Normalize input language model assignment in STTService

* refactor: Enhance error logging and language validation in STT and TTS services

* fix: Improve language validation in getValidatedLanguageCode function
This commit is contained in:
Danny Avila 2025-12-09 22:25:45 -05:00 committed by GitHub
parent 11923b9b96
commit 5879b3f518
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 17 deletions

View file

@ -3,8 +3,8 @@ const fs = require('fs').promises;
const FormData = require('form-data'); const FormData = require('form-data');
const { Readable } = require('stream'); const { Readable } = require('stream');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { genAzureEndpoint } = require('@librechat/api');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
const { genAzureEndpoint, logAxiosError } = require('@librechat/api');
const { extractEnvVariable, STTProviders } = require('librechat-data-provider'); const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
const { getAppConfig } = require('~/server/services/Config'); const { getAppConfig } = require('~/server/services/Config');
@ -35,6 +35,34 @@ const MIME_TO_EXTENSION_MAP = {
'audio/x-flac': 'flac', 'audio/x-flac': 'flac',
}; };
/**
* Validates and extracts ISO-639-1 language code from a locale string.
* @param {string} language - The language/locale string (e.g., "en-US", "en", "zh-CN")
* @returns {string|null} The ISO-639-1 language code (e.g., "en") or null if invalid
*/
function getValidatedLanguageCode(language) {
try {
if (!language) {
return null;
}
const normalizedLanguage = language.toLowerCase();
const isValidLocaleCode = /^[a-z]{2}(-[a-z]{2})?$/.test(normalizedLanguage);
if (isValidLocaleCode) {
return normalizedLanguage.split('-')[0];
}
logger.warn(
`[STT] Invalid language format "${language}". Expected ISO-639-1 locale code like "en-US" or "en". Skipping language parameter.`,
);
return null;
} catch (error) {
logger.error(`[STT] Error validating language code "${language}":`, error);
return null;
}
}
/** /**
* Gets the file extension from the MIME type. * Gets the file extension from the MIME type.
* @param {string} mimeType - The MIME type. * @param {string} mimeType - The MIME type.
@ -173,10 +201,9 @@ class STTService {
model: sttSchema.model, model: sttSchema.model,
}; };
if (language) { const validLanguage = getValidatedLanguageCode(language);
/** Converted locale code (e.g., "en-US") to ISO-639-1 format (e.g., "en") */ if (validLanguage) {
const isoLanguage = language.split('-')[0]; data.language = validLanguage;
data.language = isoLanguage;
} }
const headers = { const headers = {
@ -221,10 +248,9 @@ class STTService {
contentType: audioFile.mimetype, contentType: audioFile.mimetype,
}); });
if (language) { const validLanguage = getValidatedLanguageCode(language);
/** Converted locale code (e.g., "en-US") to ISO-639-1 format (e.g., "en") */ if (validLanguage) {
const isoLanguage = language.split('-')[0]; formData.append('language', validLanguage);
formData.append('language', isoLanguage);
} }
const headers = { const headers = {
@ -286,7 +312,7 @@ class STTService {
return response.data.text.trim(); return response.data.text.trim();
} catch (error) { } catch (error) {
logger.error(`STT request failed for provider ${provider}:`, error); logAxiosError({ message: `STT request failed for provider ${provider}:`, error });
throw error; throw error;
} }
} }
@ -316,7 +342,7 @@ class STTService {
const text = await this.sttRequest(provider, sttSchema, { audioBuffer, audioFile, language }); const text = await this.sttRequest(provider, sttSchema, { audioBuffer, audioFile, language });
res.json({ text }); res.json({ text });
} catch (error) { } catch (error) {
logger.error('An error occurred while processing the audio:', error); logAxiosError({ message: 'An error occurred while processing the audio:', error });
res.sendStatus(500); res.sendStatus(500);
} finally { } finally {
try { try {

View file

@ -1,7 +1,7 @@
const axios = require('axios'); const axios = require('axios');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { genAzureEndpoint } = require('@librechat/api');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
const { genAzureEndpoint, logAxiosError } = require('@librechat/api');
const { extractEnvVariable, TTSProviders } = require('librechat-data-provider'); const { extractEnvVariable, TTSProviders } = require('librechat-data-provider');
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio'); const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
const { getAppConfig } = require('~/server/services/Config'); const { getAppConfig } = require('~/server/services/Config');
@ -274,7 +274,7 @@ class TTSService {
try { try {
return await axios.post(url, data, options); return await axios.post(url, data, options);
} catch (error) { } catch (error) {
logger.error(`TTS request failed for provider ${provider}:`, error); logAxiosError({ message: `TTS request failed for provider ${provider}:`, error });
throw error; throw error;
} }
} }
@ -330,7 +330,10 @@ class TTSService {
break; break;
} }
} catch (innerError) { } catch (innerError) {
logger.error('Error processing manual update:', chunk, innerError); logAxiosError({
message: `[TTS] Error processing manual update for chunk: ${chunk?.text?.substring(0, 50)}...`,
error: innerError,
});
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).end(); return res.status(500).end();
} }
@ -342,7 +345,7 @@ class TTSService {
res.end(); res.end();
} }
} catch (error) { } catch (error) {
logger.error('Error creating the audio stream:', error); logAxiosError({ message: '[TTS] Error creating the audio stream:', error });
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).send('An error occurred'); return res.status(500).send('An error occurred');
} }
@ -412,7 +415,10 @@ class TTSService {
break; break;
} }
} catch (innerError) { } catch (innerError) {
logger.error('Error processing audio stream update:', update, innerError); logAxiosError({
message: `[TTS] Error processing audio stream update: ${update?.text?.substring(0, 50)}...`,
error: innerError,
});
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).end(); return res.status(500).end();
} }
@ -429,7 +435,7 @@ class TTSService {
res.end(); res.end();
} }
} catch (error) { } catch (error) {
logger.error('Failed to fetch audio:', error); logAxiosError({ message: '[TTS] Failed to fetch audio:', error });
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).end(); res.status(500).end();
} }