mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
Merge d95a258904 into 252c71c0b2
This commit is contained in:
commit
b3ce57de48
4 changed files with 121 additions and 3 deletions
|
|
@ -936,6 +936,11 @@ def image_edit(
|
|||
client=kwargs.get("client"),
|
||||
aimage_edit=_is_async,
|
||||
)
|
||||
# Forward any caller-supplied params not covered by the optional-param
|
||||
# whitelist (e.g. image_config) for every provider that reaches the default
|
||||
# handler path, mirroring the bedrock/stability/black_forest_labs branches
|
||||
# above. Without this merge they are silently dropped before the request.
|
||||
image_edit_request_params.update(non_default_params)
|
||||
# Call the handler with _is_async flag instead of directly calling the async handler
|
||||
return base_llm_http_handler.image_edit_handler(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ else:
|
|||
LiteLLMLoggingObj = Any
|
||||
|
||||
|
||||
# OpenRouter routing/control fields that must not be forwarded from an image-edit
|
||||
# request body into the upstream /chat/completions call. Forwarding them would let
|
||||
# a caller of an allowed image-edit model redirect the request to other models or
|
||||
# providers, escaping LiteLLM's model-authorization and budget enforcement.
|
||||
OPENROUTER_ROUTING_CONTROL_PARAMS = frozenset(
|
||||
{"models", "route", "provider", "transforms"}
|
||||
)
|
||||
|
||||
|
||||
class OpenRouterImageEditConfig(BaseImageEditConfig):
|
||||
"""
|
||||
Configuration for OpenRouter image editing via chat completions.
|
||||
|
|
@ -185,10 +194,17 @@ class OpenRouterImageEditConfig(BaseImageEditConfig):
|
|||
"modalities": ["image", "text"],
|
||||
}
|
||||
|
||||
# Add mapped optional params (image_config, n, etc.)
|
||||
# Add mapped optional params (image_config, n, etc.). Skip OpenRouter
|
||||
# routing/control fields: these can arrive via forwarded non-default
|
||||
# params, and copying them into the chat body would let a caller route
|
||||
# an allowed image-edit request to other models/providers, bypassing
|
||||
# LiteLLM's model-authorization and budget checks.
|
||||
for key, value in image_edit_optional_request_params.items():
|
||||
if key not in ("model", "messages", "modalities"):
|
||||
request_body[key] = value
|
||||
if key in ("model", "messages", "modalities"):
|
||||
continue
|
||||
if key in OPENROUTER_ROUTING_CONTROL_PARAMS:
|
||||
continue
|
||||
request_body[key] = value
|
||||
|
||||
empty_files: Final = cast(RequestFiles, [])
|
||||
return request_body, empty_files
|
||||
|
|
|
|||
|
|
@ -269,6 +269,73 @@ class TestImageEditCustomPricing:
|
|||
assert use_custom_pricing_for_model(litellm_params) is False
|
||||
|
||||
|
||||
class TestImageEditDefaultPathForwardsNonDefaultParams:
|
||||
"""
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/30753
|
||||
|
||||
image_config is honored on OpenRouter image generation but was silently dropped on
|
||||
image edits: the default edit path never merged non_default_params (which still carries
|
||||
image_config) before calling the handler, unlike the bedrock/stability/black_forest_labs
|
||||
branches. The merge is now unconditional on the default handler path, so every provider
|
||||
that reaches it (openrouter, openai, azure, vertex_ai, ...) forwards those params instead
|
||||
of dropping them. OpenRouter's transform forwards extra top-level params, so once
|
||||
image_config survives the merge it reaches the provider.
|
||||
"""
|
||||
|
||||
def _run_image_edit_and_capture(self, provider: str, model: str, image_config: dict):
|
||||
from litellm.images.main import image_edit
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.images.main.get_llm_provider",
|
||||
return_value=(model, provider, None, None),
|
||||
),
|
||||
patch(
|
||||
"litellm.images.main.ProviderConfigManager.get_provider_image_edit_config",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.images.main._get_ImageEditRequestUtils",
|
||||
return_value=MagicMock(
|
||||
get_requested_image_edit_optional_param=MagicMock(return_value={}),
|
||||
get_optional_params_image_edit=MagicMock(return_value={}),
|
||||
),
|
||||
),
|
||||
patch("litellm.images.main.base_llm_http_handler") as mock_handler,
|
||||
):
|
||||
mock_handler.image_edit_handler.return_value = MagicMock()
|
||||
|
||||
image_edit(
|
||||
image=b"fake-image-data",
|
||||
prompt="add a red border",
|
||||
model=f"{provider}/{model}",
|
||||
image_config=image_config,
|
||||
)
|
||||
|
||||
return mock_handler.image_edit_handler.call_args.kwargs[
|
||||
"image_edit_optional_request_params"
|
||||
]
|
||||
|
||||
def test_openrouter_image_edit_forwards_image_config(self):
|
||||
image_config = {"aspect_ratio": "16:9", "image_size": "2K"}
|
||||
forwarded = self._run_image_edit_and_capture(
|
||||
provider="openrouter",
|
||||
model="google/gemini-3-pro-image-preview",
|
||||
image_config=image_config,
|
||||
)
|
||||
assert forwarded.get("image_config") == image_config
|
||||
|
||||
def test_default_path_forwards_image_config_for_non_openrouter_provider(self):
|
||||
# The same silent-drop affected every fallthrough provider, not just openrouter.
|
||||
image_config = {"aspect_ratio": "1:1", "image_size": "1K"}
|
||||
forwarded = self._run_image_edit_and_capture(
|
||||
provider="openai",
|
||||
model="gpt-image-1",
|
||||
image_config=image_config,
|
||||
)
|
||||
assert forwarded.get("image_config") == image_config
|
||||
|
||||
|
||||
class TestImageEditHandlerCredentialsForwarding:
|
||||
"""
|
||||
Regression tests for Vertex AI image_edit credentials bug.
|
||||
|
|
|
|||
|
|
@ -237,6 +237,36 @@ class TestOpenRouterImageEditTransformation:
|
|||
# Files should be empty (JSON mode)
|
||||
assert list(files) == []
|
||||
|
||||
def test_transform_image_edit_request_drops_openrouter_routing_controls(self):
|
||||
"""Routing/control fields must not be forwarded into the chat body.
|
||||
|
||||
They can arrive via forwarded non-default params; copying OpenRouter
|
||||
routing controls (models/route/provider/transforms) into the upstream
|
||||
request would let a caller of an allowed image-edit model redirect the
|
||||
call to other models/providers, bypassing LiteLLM's model-authorization
|
||||
and budget checks. The intended image param (image_config) must still
|
||||
pass through.
|
||||
"""
|
||||
data, _ = self.config.transform_image_edit_request(
|
||||
model=self.model,
|
||||
prompt="Edit this",
|
||||
image=self.sample_image_bytes,
|
||||
image_edit_optional_request_params={
|
||||
"image_config": {"aspect_ratio": "16:9"},
|
||||
"models": ["openai/gpt-5", "anthropic/claude"],
|
||||
"route": "fallback",
|
||||
"provider": {"order": ["openai"]},
|
||||
"transforms": ["middle-out"],
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
for routing_key in ("models", "route", "provider", "transforms"):
|
||||
assert routing_key not in data
|
||||
|
||||
assert data["image_config"] == {"aspect_ratio": "16:9"}
|
||||
|
||||
def test_transform_image_edit_request_with_bytesio(self):
|
||||
"""Test request transformation with BytesIO image input."""
|
||||
image = BytesIO(self.sample_image_bytes)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue