fix(llm): strip internal params on embedding path and bedrock invoke delegates

The request-body filter was wired into the chat path but not into
llm_http_handler.embedding(), so providers that splat optional_params into the
embedding body (e.g. VoyageAI builds {"input": ..., "model": ..., **optional_params})
still leaked internal knobs and could 400 on a strict schema. Apply
strip_internal_params_from_request_body there too.

In AmazonInvokeConfig.transform_request the inference_params branches were
stripped but the anthropic, nova, twelvelabs and openai delegate branches
forwarded the raw optional_params to their sub-transforms. Build one sanitized
copy up front and use it for both the inference_params branches and the
delegates; this also drops the now-redundant stream_chunk_size pop and the
caller-dict mutation.

Adds regression coverage for the embedding path and the four invoke delegate
providers.
This commit is contained in:
mateo-berri 2026-06-19 04:03:13 +00:00
parent 237dcc001c
commit cb2badfebd
No known key found for this signature in database
4 changed files with 103 additions and 11 deletions

View file

@ -152,8 +152,10 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
headers: dict,
) -> dict:
## SETUP ##
stream = optional_params.pop("stream", None)
optional_params.pop("stream_chunk_size", None)
sanitized_params = strip_internal_params_from_request_body(
copy.deepcopy(optional_params)
)
stream = sanitized_params.pop("stream", None)
custom_prompt_dict: dict = litellm_params.pop("custom_prompt_dict", None) or {}
hf_model_name = litellm_params.get("hf_model_name", None)
@ -165,12 +167,9 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
provider=provider,
custom_prompt_dict=custom_prompt_dict,
)
inference_params = strip_internal_params_from_request_body(
copy.deepcopy(optional_params)
)
inference_params = {
k: v
for k, v in inference_params.items()
for k, v in sanitized_params.items()
if k not in self.aws_authentication_params
}
request_data: dict = {}
@ -197,7 +196,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
litellm.AmazonAnthropicClaudeConfig().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
optional_params=sanitized_params,
litellm_params=litellm_params,
headers=headers,
)
@ -208,7 +207,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
return litellm.AmazonInvokeNovaConfig().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
optional_params=sanitized_params,
litellm_params=litellm_params,
headers=headers,
)
@ -239,7 +238,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
return litellm.AmazonTwelveLabsPegasusConfig().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
optional_params=sanitized_params,
litellm_params=litellm_params,
headers=headers,
)
@ -248,7 +247,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
return litellm.AmazonBedrockOpenAIConfig().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
optional_params=sanitized_params,
litellm_params=litellm_params,
headers=headers,
)

View file

@ -895,7 +895,7 @@ class BaseLLMHTTPHandler:
data = provider_config.transform_embedding_request(
model=model,
input=input,
optional_params=optional_params,
optional_params=strip_internal_params_from_request_body(optional_params),
headers=headers,
)

View file

@ -71,3 +71,35 @@ def test_invoke_request_does_not_leak_internal_params(model):
for param in LiteLLMInternalParam:
assert param.value not in serialized, f"{param.value} leaked into {model} body"
assert "max_tokens" in serialized and "temperature" in serialized
@pytest.mark.parametrize(
"model",
[
"anthropic.claude-3-sonnet-20240229-v1:0",
"amazon.nova-micro-v1:0",
"twelvelabs.pegasus-1-2-v1:0",
"openai.gpt-oss-20b-1:0",
],
)
def test_invoke_delegate_paths_do_not_leak_internal_params(model):
"""The anthropic, nova, twelvelabs and openai invoke providers delegate to a
sub-transform instead of building the body from inference_params. Those
delegates splat optional_params into their own request body, so the internal
knobs must be stripped before the hand-off or they leak just like the
inference_params splat did (#30371)."""
seeded = {param.value: "internal" for param in LiteLLMInternalParam}
seeded.update({"temperature": 0.5})
request_body = AmazonInvokeConfig().transform_request(
model=model,
messages=[{"role": "user", "content": "hi"}],
optional_params=seeded,
litellm_params={},
headers={},
)
serialized = json.dumps(request_body)
for param in LiteLLMInternalParam:
assert param.value not in serialized, f"{param.value} leaked into {model} body"
assert "temperature" in serialized

View file

@ -18,6 +18,67 @@ from litellm.llms.custom_httpx.llm_http_handler import (
from litellm.types.router import GenericLiteLLMParams
def test_embedding_strips_internal_params_from_request_body():
"""Regression: the embedding path must strip LiteLLM-internal optional_params
before the request body is built. Several embedding transforms (e.g. VoyageAI)
splat optional_params into the wire body, so a leaked internal knob such as
cache_control_injection_points would 400 on a strict-schema provider -- the
same failure the chat path already prevents on line 450."""
from litellm.llms.voyage.embedding.transformation import VoyageEmbeddingConfig
from litellm.types.internal_params import LiteLLMInternalParam
from litellm.types.utils import EmbeddingResponse
from litellm.utils import ProviderConfigManager
handler = BaseLLMHTTPHandler()
captured: dict = {}
def _capture_post(*args, **kwargs):
captured["data"] = kwargs["data"]
return httpx.Response(
200,
json={
"model": "voyage-3",
"object": "list",
"data": [{"embedding": [0.1], "index": 0, "object": "embedding"}],
"usage": {"total_tokens": 1},
},
request=httpx.Request("POST", "https://api.voyageai.com/v1/embeddings"),
)
mock_client = Mock(spec=HTTPHandler)
mock_client.post = Mock(side_effect=_capture_post)
seeded = {param.value: "internal" for param in LiteLLMInternalParam}
seeded["output_dimension"] = 256
with patch.object(
ProviderConfigManager,
"get_provider_embedding_config",
return_value=VoyageEmbeddingConfig(),
):
handler.embedding(
model="voyage-3",
input=["hello world"],
timeout=10.0,
custom_llm_provider="voyage",
logging_obj=Mock(),
api_base=None,
optional_params=seeded,
litellm_params={},
model_response=EmbeddingResponse(),
api_key="test-key",
client=mock_client,
)
body = captured["data"]
for param in LiteLLMInternalParam:
assert (
param.value not in body
), f"{param.value} leaked into voyage embedding body"
assert "output_dimension" in body
def test_prepare_fake_stream_request():
# Initialize the BaseLLMHTTPHandler
handler = BaseLLMHTTPHandler()