This commit is contained in:
Ewertonslv 2026-09-22 14:35:20 +05:00 • committed by GitHub
commit b3ce57de48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 121 additions and 3 deletions

View file

@ -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,

View file

@ -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

View file

@ -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.

View file

@ -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)