mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(watsonx): read pass-through credentials from use_in_pass_through deployments (#49462)
* fix(watsonx): read passthrough credentials from use_in_pass_through deployments
/watsonx/{endpoint} always called the provider config with api_key=None and api_base=None, so it only ever
worked from WATSONX_API_KEY / WATSONX_API_BASE. It now asks PassthroughEndpointRouter for the proxy-wide
watsonx deployment flagged use_in_pass_through, like the other passthrough routes, and keeps the env
fallback when none is flagged.
Co-authored-by: T K Chandra Hasan <t.k.chandra.hasan@ibm.com>
* fix(watsonx): keep env key precedence when no deployment is flagged
Resolve only use_in_pass_through deployments in the route. get_proxy_wide_credentials also falls back to
WATSONX_API_KEY, which put it ahead of WX_API_KEY in generate_iam_token
---------
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: T K Chandra Hasan <t.k.chandra.hasan@ibm.com>
This commit is contained in:
parent
ee39de7107
commit
948445c828
4 changed files with 217 additions and 4 deletions
|
|
@ -4474,10 +4474,16 @@ async def watsonx_proxy_route(
|
||||||
if provider_config is None:
|
if provider_config is None:
|
||||||
raise HTTPException(status_code=404, detail="Watsonx passthrough config not found")
|
raise HTTPException(status_code=404, detail="Watsonx passthrough config not found")
|
||||||
|
|
||||||
|
watsonx_credentials: Final = passthrough_endpoint_router.get_proxy_wide_deployment_credentials(
|
||||||
|
custom_llm_provider="watsonx"
|
||||||
|
)
|
||||||
|
api_key: Final = watsonx_credentials.api_key if watsonx_credentials is not None else None
|
||||||
|
api_base: Final = watsonx_credentials.api_base if watsonx_credentials is not None else None
|
||||||
|
|
||||||
# Get complete URL with version parameter
|
# Get complete URL with version parameter
|
||||||
complete_url, _ = provider_config.get_complete_url(
|
complete_url, _ = provider_config.get_complete_url(
|
||||||
api_base=None,
|
api_base=api_base,
|
||||||
api_key=None,
|
api_key=api_key,
|
||||||
model="",
|
model="",
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
request_query_params=None,
|
request_query_params=None,
|
||||||
|
|
@ -4491,8 +4497,8 @@ async def watsonx_proxy_route(
|
||||||
messages=[],
|
messages=[],
|
||||||
optional_params={},
|
optional_params={},
|
||||||
litellm_params={},
|
litellm_params={},
|
||||||
api_key=None,
|
api_key=api_key,
|
||||||
api_base=None,
|
api_base=api_base,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for streaming
|
# Check for streaming
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,19 @@ class PassthroughEndpointRouter:
|
||||||
api_base=deployment_credentials.api_base if deployment_credentials is not None else None,
|
api_base=deployment_credentials.api_base if deployment_credentials is not None else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_proxy_wide_deployment_credentials(self, custom_llm_provider: str) -> _PassThroughCredentials | None:
|
||||||
|
deployment_credentials: Final = self._get_deployment_credentials(
|
||||||
|
custom_llm_provider=custom_llm_provider,
|
||||||
|
region_name=None,
|
||||||
|
include_team_deployments=False,
|
||||||
|
)
|
||||||
|
if deployment_credentials is None:
|
||||||
|
return None
|
||||||
|
api_key: Final = _resolve_oidc_reference(deployment_credentials.api_key)
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
return _PassThroughCredentials(api_key=api_key, api_base=deployment_credentials.api_base)
|
||||||
|
|
||||||
def has_credentials(self, custom_llm_provider: str) -> bool:
|
def has_credentials(self, custom_llm_provider: str) -> bool:
|
||||||
if (
|
if (
|
||||||
self._get_deployment_credentials(
|
self._get_deployment_credentials(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from integration._support.client import Gateway, JsonValue, object_value
|
||||||
|
from integration._support.process import owned_proxy
|
||||||
|
from integration._support.wire import Reply, Request, wire_server
|
||||||
|
|
||||||
|
_DEPLOYMENT_KEY: Final = "synthetic-watsonx-deployment-key"
|
||||||
|
_IAM_TOKEN: Final = "synthetic-watsonx-iam-token"
|
||||||
|
_WATSONX_ENV: Final = (
|
||||||
|
"WATSONX_API_KEY",
|
||||||
|
"WATSONX_APIKEY",
|
||||||
|
"WX_API_KEY",
|
||||||
|
"WATSONX_API_BASE",
|
||||||
|
"WATSONX_URL",
|
||||||
|
"WX_URL",
|
||||||
|
"WML_URL",
|
||||||
|
"WATSONX_TOKEN",
|
||||||
|
"WATSONX_ZENAPIKEY",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _config(directory: Path, api_base: str) -> Path:
|
||||||
|
stock: Final = object_value(yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()))
|
||||||
|
config: Final[dict[str, JsonValue]] = {
|
||||||
|
**stock,
|
||||||
|
"model_list": [
|
||||||
|
{
|
||||||
|
"model_name": "granite",
|
||||||
|
"litellm_params": {
|
||||||
|
"model": "watsonx/ibm/granite-3-8b-instruct",
|
||||||
|
"api_key": _DEPLOYMENT_KEY,
|
||||||
|
"api_base": api_base,
|
||||||
|
"project_id": "synthetic-project",
|
||||||
|
"use_in_pass_through": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
path: Final = directory / "watsonx-passthrough.yaml"
|
||||||
|
path.write_text(yaml.safe_dump(config))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_watsonx_passthrough_uses_the_flagged_deployment_key_and_base(gateway: Gateway, tmp_path: Path) -> None:
|
||||||
|
marker: Final = "watsonx-" + uuid.uuid4().hex
|
||||||
|
|
||||||
|
def respond(request: Request) -> Reply:
|
||||||
|
if request.method == "POST" and request.target == "/identity/token":
|
||||||
|
assert f"apikey={_DEPLOYMENT_KEY}".encode() in request.body, request.body
|
||||||
|
return Reply(body=json.dumps({"access_token": _IAM_TOKEN, "expires_in": 3600}).encode())
|
||||||
|
if request.method == "POST" and request.target.startswith("/ml/v1/text/generation?"):
|
||||||
|
assert request.headers["authorization"] == f"Bearer {_IAM_TOKEN}", request.headers
|
||||||
|
assert marker.encode() in request.body, request.body[:400]
|
||||||
|
return Reply(
|
||||||
|
body=json.dumps(
|
||||||
|
{"model_id": "ibm/granite-3-8b-instruct", "results": [{"generated_text": marker}]}
|
||||||
|
).encode()
|
||||||
|
)
|
||||||
|
return Reply(status=404)
|
||||||
|
|
||||||
|
with wire_server(respond) as wire:
|
||||||
|
with owned_proxy(
|
||||||
|
gateway,
|
||||||
|
tmp_path,
|
||||||
|
{"WATSONX_IAM_URL": f"{wire.url}/identity/token"},
|
||||||
|
config=_config(tmp_path, wire.url),
|
||||||
|
remove_environment=_WATSONX_ENV,
|
||||||
|
) as candidate:
|
||||||
|
response: Final = candidate.request(
|
||||||
|
"POST",
|
||||||
|
"/watsonx/ml/v1/text/generation",
|
||||||
|
{"model_id": "ibm/granite-3-8b-instruct", "input": marker, "project_id": "synthetic-project"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json()["results"][0]["generated_text"] == marker
|
||||||
|
forwarded: Final = tuple((request.method, request.target.split("?")[0]) for request in wire.drain())
|
||||||
|
assert forwarded == (("POST", "/identity/token"), ("POST", "/ml/v1/text/generation")), forwarded
|
||||||
|
|
@ -437,3 +437,116 @@ class TestWatsonxProxyRoute:
|
||||||
call_args["target"]
|
call_args["target"]
|
||||||
== f"https://us-south.ml.cloud.ibm.com/{endpoint_path}"
|
== f"https://us-south.ml.cloud.ibm.com/{endpoint_path}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_WATSONX_ENV = (
|
||||||
|
"WATSONX_API_KEY",
|
||||||
|
"WATSONX_APIKEY",
|
||||||
|
"WX_API_KEY",
|
||||||
|
"WATSONX_API_BASE",
|
||||||
|
"WATSONX_URL",
|
||||||
|
"WX_URL",
|
||||||
|
"WML_URL",
|
||||||
|
"WATSONX_TOKEN",
|
||||||
|
"WATSONX_ZENAPIKEY",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _watsonx_deployment(api_key: str, api_base: str, use_in_pass_through: bool) -> dict:
|
||||||
|
return {
|
||||||
|
"model_name": "granite",
|
||||||
|
"litellm_params": {
|
||||||
|
"model": "watsonx/ibm/granite-3-8b-instruct",
|
||||||
|
"api_key": api_key,
|
||||||
|
"api_base": api_base,
|
||||||
|
"project_id": "proj",
|
||||||
|
"use_in_pass_through": use_in_pass_through,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _post_request(body: dict) -> MagicMock:
|
||||||
|
request = MagicMock(spec=Request)
|
||||||
|
request.method = "POST"
|
||||||
|
request.query_params = {}
|
||||||
|
request.headers = {"content-type": "application/json"}
|
||||||
|
request.json = AsyncMock(return_value=body)
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
def _iam_response(token: str) -> MagicMock:
|
||||||
|
response = MagicMock()
|
||||||
|
response.json.return_value = {"access_token": token, "expires_in": 3600}
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def _route_target_and_iam_apikey(
|
||||||
|
monkeypatch, llm_router: litellm.Router, env: dict[str, str]
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
from litellm.llms.watsonx.common_utils import iam_token_cache
|
||||||
|
from litellm.proxy.pass_through_endpoints.passthrough_endpoint_router import PassthroughEndpointRouter
|
||||||
|
|
||||||
|
for name in _WATSONX_ENV:
|
||||||
|
monkeypatch.delenv(name, raising=False)
|
||||||
|
for name, value in env.items():
|
||||||
|
monkeypatch.setenv(name, value)
|
||||||
|
monkeypatch.setattr(litellm, "credential_list", [])
|
||||||
|
iam_token_cache.flush_cache()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router",
|
||||||
|
PassthroughEndpointRouter(llm_router_getter=lambda: llm_router),
|
||||||
|
)
|
||||||
|
endpoint_func = AsyncMock(return_value={"results": []})
|
||||||
|
with (
|
||||||
|
patch.object(litellm.module_level_client, "post", return_value=_iam_response("iam-token-1")) as iam_post,
|
||||||
|
patch(
|
||||||
|
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route",
|
||||||
|
return_value=endpoint_func,
|
||||||
|
) as create_route,
|
||||||
|
):
|
||||||
|
await watsonx_proxy_route(
|
||||||
|
endpoint="ml/v1/text/generation",
|
||||||
|
request=_post_request({"input": "hi", "project_id": "proj"}),
|
||||||
|
fastapi_response=MagicMock(spec=Response),
|
||||||
|
user_api_key_dict=MagicMock(spec=UserAPIKeyAuth),
|
||||||
|
)
|
||||||
|
route_kwargs = create_route.call_args.kwargs
|
||||||
|
return (
|
||||||
|
route_kwargs["target"],
|
||||||
|
route_kwargs["custom_headers"]["Authorization"],
|
||||||
|
iam_post.call_args.kwargs["data"]["apikey"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_watsonx_route_uses_the_use_in_pass_through_deployment_key_and_base(monkeypatch):
|
||||||
|
llm_router = litellm.Router(
|
||||||
|
model_list=[_watsonx_deployment("wx-deployment-key", "https://eu-de.ml.cloud.ibm.com", use_in_pass_through=True)]
|
||||||
|
)
|
||||||
|
|
||||||
|
target, authorization, iam_apikey = await _route_target_and_iam_apikey(monkeypatch, llm_router, env={})
|
||||||
|
|
||||||
|
assert target == "https://eu-de.ml.cloud.ibm.com/ml/v1/text/generation"
|
||||||
|
assert authorization == "Bearer iam-token-1"
|
||||||
|
assert iam_apikey == "wx-deployment-key"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_watsonx_route_falls_back_to_env_when_no_deployment_is_flagged(monkeypatch):
|
||||||
|
llm_router = litellm.Router(
|
||||||
|
model_list=[_watsonx_deployment("wx-deployment-key", "https://eu-de.ml.cloud.ibm.com", use_in_pass_through=False)]
|
||||||
|
)
|
||||||
|
|
||||||
|
target, authorization, iam_apikey = await _route_target_and_iam_apikey(
|
||||||
|
monkeypatch,
|
||||||
|
llm_router,
|
||||||
|
env={
|
||||||
|
"WX_API_KEY": "wx-env-key",
|
||||||
|
"WATSONX_API_KEY": "watsonx-env-key",
|
||||||
|
"WATSONX_API_BASE": "https://us-south.ml.cloud.ibm.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert target == "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation"
|
||||||
|
assert authorization == "Bearer iam-token-1"
|
||||||
|
assert iam_apikey == "wx-env-key"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue