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
This commit is contained in:
lokm01 2026-09-25 17:11:59 +02:00
parent 2ff1712303
commit 915aaee64b
3 changed files with 47 additions and 16 deletions

View file

@ -1417,6 +1417,7 @@ class BaseLLMHTTPHandler:
model=model,
messages=[],
optional_params=optional_params,
api_base=api_base,
litellm_params=litellm_params,
)

View file

@ -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("/")

View file

@ -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 = {}