Skip to content

Commit 4623351

Browse files
committed
restore fallback language mapper table
1 parent 3c5d103 commit 4623351

4 files changed

Lines changed: 55 additions & 10 deletions

File tree

‎backend/app/models/llm/constants.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,26 @@ def normalize_bcp47_language(value: str) -> str:
199199
DEFAULT_ELEVENLABS_STT_MODEL = "scribe_v2"
200200
DEFAULT_ELEVENLABS_TTS_MODEL = "eleven_v3"
201201

202+
# Kaapi BCP-47 tag -> Gemini TTS language. Full locales, not bare ISO codes:
203+
# en-IN keeps the Indian accent where "en" does not. Unlisted tags fall back
204+
# to auto-detect.
205+
BCP47_LOCALE_TO_GEMINI_LANG: dict[str, str] = {
206+
"en-IN": "en-IN",
207+
"hi-IN": "hi-IN",
208+
"bn-IN": "bn-IN",
209+
"ta-IN": "ta-IN",
210+
"te-IN": "te-IN",
211+
"mr-IN": "mr-IN",
212+
"gu-IN": "gu-IN",
213+
"kn-IN": "kn-IN",
214+
"ml-IN": "ml-IN",
215+
"pa-IN": "pa-IN",
216+
# Kaapi uses the non-standard "od-IN" for Odia; the real tag is "or-IN".
217+
"od-IN": "or-IN",
218+
# "as-IN": not supported by Gemini
219+
"sd-IN": "sd-IN",
220+
}
221+
202222
# BCP-47 language tag → ElevenLabs ISO 639-1 code (Indic + English)
203223
BCP47_TO_ELEVENLABS_LANG: dict[str, str] = {
204224
"en-IN": "en",

‎backend/app/services/llm/mappers.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from app.models.llm import KaapiCompletionConfig, NativeCompletionConfig
99
from app.models.llm.request import STTLLMParams, TextLLMParams, TTSLLMParams
1010
from app.models.llm.constants import (
11+
BCP47_LOCALE_TO_GEMINI_LANG,
1112
BCP47_TO_ELEVENLABS_LANG,
1213
DEFAULT_ELEVENLABS_STT_MODEL,
1314
DEFAULT_ELEVENLABS_TTS_MODEL,
@@ -268,7 +269,7 @@ def map_kaapi_to_google_params(
268269
- instructions → instructions (for STT prompts, if available)
269270
- temperature / top_p → not sent (deprecated by Gemini)
270271
- max_output_tokens → same name (text only)
271-
- language → full BCP-47 locale passed through (TTS only)
272+
- language → full Gemini locale via BCP47_LOCALE_TO_GEMINI_LANG; unsupported → auto-detect (TTS only)
272273
- thinking_level → thinking_config.thinking_level (text only)
273274
- output_schema → output_schema, converted to Gemini's shape (text only)
274275
- knowledge_base_ids → FileSearch tool store names (text only)
@@ -342,10 +343,16 @@ def map_kaapi_to_google_params(
342343
response_format = params.get("response_format") or "wav"
343344
google_params["response_format"] = response_format
344345

345-
# Full BCP-47 locale, not the bare ISO code: collapsing en-IN to en loses the accent.
346+
# Language: Only set if explicitly provided (None/missing = auto-detect)
346347
language = params.get("language")
347348
if language:
348-
google_params["language"] = language
349+
google_lang = BCP47_LOCALE_TO_GEMINI_LANG.get(language)
350+
if not google_lang:
351+
warnings.append(
352+
f"Unsupported language '{language}' for Gemini TTS, using auto-detect"
353+
)
354+
else:
355+
google_params["language"] = google_lang
349356

350357
elif completion_type == CompletionType.STT:
351358
# STT mode - instructions, input_language, output_language, response_format

‎backend/app/tests/services/llm/test_mappers.py‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,11 +408,12 @@ def test_tts_language_missing_uses_auto_detect(self):
408408
assert "language" not in result # Not set = auto-detect
409409
assert warnings == []
410410

411-
def test_tts_non_indic_language_passes_through(self):
411+
@pytest.mark.parametrize("language", ["fr-FR", "as-IN"])
412+
def test_tts_unsupported_language_falls_back_to_auto_detect(self, language):
412413
kaapi_params = TTSLLMParams(
413414
model="gemini-2.5-pro",
414415
voice="Kore",
415-
language="fr-FR",
416+
language=language,
416417
)
417418

418419
result, warnings = map_kaapi_to_google_params(
@@ -422,7 +423,22 @@ def test_tts_non_indic_language_passes_through(self):
422423
assert result["model"] == "gemini-2.5-pro"
423424
assert result["voice"] == "Kore"
424425
assert result["response_format"] == "wav"
425-
assert result["language"] == "fr-FR"
426+
assert "language" not in result
427+
assert warnings == [
428+
f"Unsupported language '{language}' for Gemini TTS, using auto-detect"
429+
]
430+
431+
def test_tts_odia_maps_to_standard_locale(self):
432+
# Kaapi's canonical Odia tag is the non-standard od-IN.
433+
kaapi_params = TTSLLMParams(
434+
model="gemini-2.5-pro", voice="Kore", language="odia"
435+
)
436+
437+
result, warnings = map_kaapi_to_google_params(
438+
kaapi_params.model_dump(exclude_none=True), completion_type="tts"
439+
)
440+
441+
assert result["language"] == "or-IN"
426442
assert warnings == []
427443

428444
def test_tts_without_language_leaves_it_unset(self):
@@ -1198,7 +1214,7 @@ def test_text_completion_maps_via_google_mapper(self, db: Session):
11981214
assert native_config.provider == "google-native"
11991215
assert native_config.params["model"] == "gemini-2.5-pro"
12001216

1201-
def test_unlisted_language_passes_through_without_warning(self, db: Session):
1217+
def test_unlisted_language_falls_back_to_auto_detect(self, db: Session):
12021218
kaapi_config = build_kaapi_completion_config(
12031219
provider="google",
12041220
type="tts",
@@ -1213,8 +1229,10 @@ def test_unlisted_language_passes_through_without_warning(self, db: Session):
12131229
)
12141230

12151231
assert native_config.provider == "google-native"
1216-
assert native_config.params["language"] == "xx-YY"
1217-
assert warnings == []
1232+
assert "language" not in native_config.params
1233+
assert warnings == [
1234+
"Unsupported language 'xx-YY' for Gemini TTS, using auto-detect"
1235+
]
12181236

12191237

12201238
class TestTransformGoogleGCPRouting:

‎docs/wiki/modules/llm-call.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ All paths relative to `backend/app/`.
3434

3535
## Services / CRUD
3636
- `services/llm/` — `mappers.py` (Kaapi params → provider API), `providers/`, `chain/`, `guardrails.py`, `jobs.py`
37-
- `mappers.py` is the **only** param mapper in the codebase; the assessment fork was merged back into it. Callers: `crud/evaluations/{batch,fast,judge}.py`, `crud/assessment/batch.py`, `services/assessment/api/batch.py`, `services/assessment/prefilter/request_builder.py`. Structured output is keyed `output_schema` for every provider (OpenAI `text.format`, Anthropic `output_config.format`, Gemini `output_schema`), and Anthropic reads `effort` into `output_config.effort`. Every param that only assessment set (`top_p`, `max_output_tokens`, `thinking_level`, `thinking`, `output_schema`) defaults to `None` on `TextLLMParams` and is dropped by `ParamSerialization._dump_compact`, so a config that does not set it maps exactly as before. Anthropic never receives `temperature`/`top_p`: the Messages API returns 400 for a non-default sampling value on every Claude model Kaapi serves, so the mapper drops both, warning only when the value was not Anthropic's own default of 1.0. Gemini likewise never receives `temperature`/`top_p` for text or STT: newer models return 400 for them and they have been no-ops since 3.6 Flash, so the mapper does not send them. Gemini TTS `language` is passed through as the full BCP-47 locale (`en-IN`, not `en`) so the accent survives. `normalize_llm_text` no longer lives here; it moved to `services/assessment/validators.py`, the only place that calls it.
37+
- `mappers.py` is the **only** param mapper in the codebase; the assessment fork was merged back into it. Callers: `crud/evaluations/{batch,fast,judge}.py`, `crud/assessment/batch.py`, `services/assessment/api/batch.py`, `services/assessment/prefilter/request_builder.py`. Structured output is keyed `output_schema` for every provider (OpenAI `text.format`, Anthropic `output_config.format`, Gemini `output_schema`), and Anthropic reads `effort` into `output_config.effort`. Every param that only assessment set (`top_p`, `max_output_tokens`, `thinking_level`, `thinking`, `output_schema`) defaults to `None` on `TextLLMParams` and is dropped by `ParamSerialization._dump_compact`, so a config that does not set it maps exactly as before. Anthropic never receives `temperature`/`top_p`: the Messages API returns 400 for a non-default sampling value on every Claude model Kaapi serves, so the mapper drops both, warning only when the value was not Anthropic's own default of 1.0. Gemini likewise never receives `temperature`/`top_p` for text or STT: newer models return 400 for them and they have been no-ops since 3.6 Flash, so the mapper does not send them. Gemini TTS `language` maps through `BCP47_LOCALE_TO_GEMINI_LANG` to a full locale (`en-IN`, not `en`, so the accent survives; Kaapi's `od-IN` → `or-IN`); an unlisted tag (e.g. `as-IN`) is dropped with a warning and Gemini auto-detects. `normalize_llm_text` no longer lives here; it moved to `services/assessment/validators.py`, the only place that calls it.
3838
- `services/guardrails/` — validator execution
3939
- `crud/llm.py`, `crud/llm_chain.py`, `crud/config/` — persistence
4040

0 commit comments

Comments
 (0)