diff --git a/servers/tools-pve/litellm/compose.yaml b/servers/tools-pve/litellm/compose.yaml index a534779..becb5d6 100644 --- a/servers/tools-pve/litellm/compose.yaml +++ b/servers/tools-pve/litellm/compose.yaml @@ -48,10 +48,12 @@ services: volumes: - ./models.yaml:/etc/litellm/models.yaml:ro - ./render_config.py:/etc/litellm/render_config.py:ro + - ./personal_transcription.py:/etc/litellm/personal_transcription.py:ro + - /opt/litellm/personal-vocabulary.json:/etc/litellm/personal-vocabulary.json:ro - oauth:/tokens entrypoint: [/bin/sh, -c] command: - - 'python /etc/litellm/render_config.py && exec litellm --config /tmp/litellm.yaml --host 0.0.0.0 --port 4000 --num_workers 1' + - 'python /etc/litellm/render_config.py && exec python /etc/litellm/personal_transcription.py --config /tmp/litellm.yaml --host 0.0.0.0 --port 4000 --num_workers 1' healthcheck: test: [CMD, python, -c, 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:4000/health/liveliness", timeout=5)'] interval: 30s diff --git a/servers/tools-pve/litellm/personal_transcription.py b/servers/tools-pve/litellm/personal_transcription.py new file mode 100644 index 0000000..c340d8d --- /dev/null +++ b/servers/tools-pve/litellm/personal_transcription.py @@ -0,0 +1,49 @@ +"""Add native vocabulary only to explicitly opted-in Gemini transcription calls.""" +import json +from pathlib import Path + +VOCABULARY = Path('/etc/litellm/personal-vocabulary.json') + + +def vocabulary(path=VOCABULARY): + words = json.loads(path.read_text()) + if not isinstance(words, list) or not 1 <= len(words) <= 100: + raise ValueError('Personal vocabulary must contain 1–100 terms') + if any(not isinstance(word, str) or not word.strip() or len(word) > 100 for word in words): + raise ValueError('Invalid personal vocabulary term') + return list(dict.fromkeys(words)) + + +def install(): + from litellm.llms.gemini.audio_transcription.transformation import GeminiAudioTranscriptionConfig + original = GeminiAudioTranscriptionConfig.transform_audio_transcription_request + if getattr(original, '_personal_vocabulary_patch', False): + return + + def transform(self, model, audio_file, optional_params, litellm_params): + enabled = optional_params.get('personal_vocabulary') is True + if not enabled: + return original(self, model, audio_file, optional_params, litellm_params) + if model.removeprefix('gemini/') != 'gemini-3.5-transcribe': + raise ValueError('Personal vocabulary is restricted to Gemini 3.5 Transcribe') + if optional_params.get('timestamp_granularities') or optional_params.get('response_format') not in (None, 'json', 'text'): + raise ValueError('Personal dictation does not support timestamps or diarization') + clean = {key: value for key, value in optional_params.items() if key != 'personal_vocabulary'} + result = original(self, model, audio_file, clean, litellm_params) + config = result.data.setdefault('generation_config', {}).setdefault('transcription_config', {}) + # Fail closed if a future LiteLLM version enables conflicting features by default. + if config.get('mode'): + raise ValueError('Unexpected Gemini transcription mode') + config['custom_vocabulary'] = vocabulary() + return result + + transform._personal_vocabulary_patch = True + GeminiAudioTranscriptionConfig.transform_audio_transcription_request = transform + + +if __name__ == '__main__': + import sys + from litellm import run_server + install() + sys.argv[0] = 'litellm' + sys.exit(run_server()) diff --git a/servers/tools-pve/litellm/render_config.py b/servers/tools-pve/litellm/render_config.py index 8238f01..f20dc69 100644 --- a/servers/tools-pve/litellm/render_config.py +++ b/servers/tools-pve/litellm/render_config.py @@ -56,6 +56,8 @@ def build_config(catalog, env, exists): add(alias, 'openai/' + model, 'ZENMUX_SUBSCRIPTION_API_KEY', api_base='https://zenmux.ai/api/v1', max_retries=0) if env.get('GOOGLE_AI_STUDIO_API_KEY'): + add('gemini-transcribe-personal', 'gemini/gemini-3.5-transcribe', + 'GOOGLE_AI_STUDIO_API_KEY', personal_vocabulary=True) for alias, model in catalog['google_ai_studio'].items(): add(alias, 'gemini/' + model, 'GOOGLE_AI_STUDIO_API_KEY') if env.get('VERTEXAI_ENABLED', '').lower() == 'true' and env.get('VERTEXAI_PROJECT'):