mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
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:
parent
2ff1712303
commit
915aaee64b
3 changed files with 47 additions and 16 deletions
|
|
@ -1417,6 +1417,7 @@ class BaseLLMHTTPHandler:
|
|||
model=model,
|
||||
messages=[],
|
||||
optional_params=optional_params,
|
||||
api_base=api_base,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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("/")
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue