From 915aaee64befaeb77515d11fa37ecaace6708a7e Mon Sep 17 00:00:00 2001 From: lokm01 <2735155+lokm01@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:11:59 +0200 Subject: [PATCH] fix(databricks): tighten endpoint guard, pass api_base to transcription auth, drop no-op override - endpoint-name guard drops '.' so '..' and dot-traversal can no longer shape the invoke path; invalid-name test now covers databricks/.. too - _prepare_audio_transcription_request passes api_base into validate_environment (chat-handler convention), so an explicit api_base authenticates without DATABRICKS_API_BASE; regression test dispatches with per-call api_base and no env var - remove has_native_transcription_endpoint override: databricks is not in OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS, so the base-class default is what main.py already reads; cover the non-text predictions element branch --- litellm/llms/custom_httpx/llm_http_handler.py | 1 + .../audio_transcription/transformation.py | 18 +++----- ...icks_audio_transcription_transformation.py | 44 +++++++++++++++++-- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index ce9f7a2ea54..b385659d09f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1417,6 +1417,7 @@ class BaseLLMHTTPHandler: model=model, messages=[], optional_params=optional_params, + api_base=api_base, litellm_params=litellm_params, ) diff --git a/litellm/llms/databricks/audio_transcription/transformation.py b/litellm/llms/databricks/audio_transcription/transformation.py index ec431631bca..7c5e76c31e6 100644 --- a/litellm/llms/databricks/audio_transcription/transformation.py +++ b/litellm/llms/databricks/audio_transcription/transformation.py @@ -35,9 +35,10 @@ from ..common_utils import DatabricksBase, DatabricksException _OBJECT_LIST: Final = TypeAdapter(list[object]) _STRING_OBJECT_DICT: Final = TypeAdapter(dict[str, object]) -# Databricks serving endpoint names use [a-z0-9-]; the guard stays minimal -# (also permitting dots and underscores) so no valid name can ever be rejected -_ENDPOINT_NAME: Final = re.compile(r"[A-Za-z0-9._-]+") +# Databricks serving endpoint names use [a-z0-9-]; underscores are also +# permitted so no plausible name is rejected. Dots are excluded: allowing +# them would admit path-shaping inputs such as "..". +_ENDPOINT_NAME: Final = re.compile(r"[A-Za-z0-9_-]+") class DatabricksAudioTranscriptionConfig(BaseAudioTranscriptionConfig, DatabricksBase): @@ -73,15 +74,6 @@ class DatabricksAudioTranscriptionConfig(BaseAudioTranscriptionConfig, Databrick ) -> BaseLLMException: return DatabricksException(message=error_message, status_code=status_code, headers=headers) - @property - def has_native_transcription_endpoint(self) -> bool: - # Databricks is not in OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS today, so - # the main.py transport dispatch reads this as a no-op; it is set for - # parity with the xai transcription config (the precedent, where it - # IS load-bearing) and keeps this provider on the native handler if - # databricks ever joins the OpenAI-compatible audio list. - return True - def validate_environment( self, headers: dict[str, object], # mutable-ok: base class signature takes and returns dict @@ -122,7 +114,7 @@ class DatabricksAudioTranscriptionConfig(BaseAudioTranscriptionConfig, Databrick status_code=400, message=( f"Invalid Databricks endpoint name {endpoint!r}: names may only contain " - "letters, digits, dots, underscores, and hyphens." + "letters, digits, underscores, and hyphens." ), ) resolved_base: Final = self._get_api_base(api_base or os.getenv("DATABRICKS_API_BASE")).rstrip("/") diff --git a/tests/test_litellm/llms/databricks/audio_transcription/test_databricks_audio_transcription_transformation.py b/tests/test_litellm/llms/databricks/audio_transcription/test_databricks_audio_transcription_transformation.py index ed47b9a07ed..4aaa8ea7579 100644 --- a/tests/test_litellm/llms/databricks/audio_transcription/test_databricks_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/databricks/audio_transcription/test_databricks_audio_transcription_transformation.py @@ -121,12 +121,17 @@ def test_get_complete_url_from_env(monkeypatch): assert url == INVOKE_URL -def test_get_complete_url_rejects_path_shaping(): +@pytest.mark.parametrize( + "model", + ["databricks/../x", "databricks/.."], + ids=["dotdot-path", "dotdot-only"], +) +def test_get_complete_url_rejects_path_shaping(model): with pytest.raises(DatabricksException, match="Invalid Databricks endpoint name") as exc_info: CONFIG.get_complete_url( api_base=API_BASE, api_key=None, - model="databricks/../x", + model=model, optional_params={}, litellm_params={}, ) @@ -249,7 +254,11 @@ def test_transform_response_empty_transcript_is_valid(): assert response.text == "" -@pytest.mark.parametrize("payload", [{"predictions": []}, {}], ids=["empty-list", "missing-key"]) +@pytest.mark.parametrize( + "payload", + [{"predictions": []}, {}, {"predictions": [42]}], + ids=["empty-list", "missing-key", "non-text-element"], +) def test_transform_response_empty_predictions_raises(payload): raw = httpx.Response(200, json=payload, request=httpx.Request("POST", INVOKE_URL)) with pytest.raises(DatabricksException, match="no prediction text") as exc_info: @@ -336,6 +345,35 @@ def test_transcription_dispatches_to_databricks_invoke(monkeypatch): assert response.text == "transcribed text" +def test_transcription_uses_per_call_api_base_for_auth(monkeypatch): + # no DATABRICKS_API_BASE env var (the autouse fixture clears it): an + # explicit api_base= must reach validate_environment, otherwise auth + # falls into the SDK fallback even though the caller supplied a base + captured: dict = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response( + 200, + json={"predictions": ["transcribed text"]}, + request=request, + ) + + http_handler = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handler))) + response = litellm.transcription( + model="databricks/whisper-t", + file=("sample.wav", WAV_BYTES, "audio/wav"), + api_key="dapi-test", + api_base=API_BASE, + client=http_handler, + ) + + request = captured["request"] + assert str(request.url) == INVOKE_URL + assert request.headers["Authorization"] == "Bearer dapi-test" + assert response.text == "transcribed text" + + async def test_atranscription_dispatches_to_databricks_invoke(monkeypatch): monkeypatch.setenv("DATABRICKS_API_BASE", API_BASE) captured: dict = {}