mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
feat(hosted_vllm): add image edit support
Register a HostedVLLMImageEditConfig so hosted_vllm/<model> deployments route POST /v1/images/edits to the vLLM-Omni OpenAI-compatible endpoint instead of failing with 'image edit is not supported for hosted_vllm' before any request is sent
This commit is contained in:
parent
6a425a5cc5
commit
88d2d77553
4 changed files with 169 additions and 0 deletions
9
litellm/llms/hosted_vllm/image_edit/__init__.py
Normal file
9
litellm/llms/hosted_vllm/image_edit/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
|
||||
|
||||
from .transformation import HostedVLLMImageEditConfig
|
||||
|
||||
__all__ = ("HostedVLLMImageEditConfig",)
|
||||
|
||||
|
||||
def get_hosted_vllm_image_edit_config(model: str) -> BaseImageEditConfig:
|
||||
return HostedVLLMImageEditConfig()
|
||||
42
litellm/llms/hosted_vllm/image_edit/transformation.py
Normal file
42
litellm/llms/hosted_vllm/image_edit/transformation.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Image edits for Hosted VLLM (vLLM-Omni OpenAI-compatible /v1/images/edits)."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
||||
class HostedVLLMImageEditConfig(OpenAIImageEditConfig):
|
||||
"""
|
||||
vLLM-Omni images edits API follows the OpenAI multipart contract.
|
||||
|
||||
https://docs.vllm.ai/projects/vllm-omni/en/latest/serving/images_api/
|
||||
"""
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict, # mutable-ok: BaseImageEditConfig contract
|
||||
model: str,
|
||||
api_key: str | None = None,
|
||||
litellm_params: dict | None = None, # mutable-ok: BaseImageEditConfig contract
|
||||
api_base: str | None = None,
|
||||
) -> dict: # mutable-ok: BaseImageEditConfig contract
|
||||
resolved_key: Final = api_key or get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key"
|
||||
return {**headers, "Authorization": f"Bearer {resolved_key}"} # mutable-ok: httpx headers are a dict
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
model: str,
|
||||
api_base: str | None,
|
||||
litellm_params: dict, # mutable-ok: BaseImageEditConfig contract
|
||||
) -> str:
|
||||
resolved_api_base: Final = api_base or get_secret_str("HOSTED_VLLM_API_BASE")
|
||||
if resolved_api_base is None:
|
||||
raise ValueError(
|
||||
"api_base not set for Hosted VLLM images edits API. "
|
||||
"Set via api_base parameter or HOSTED_VLLM_API_BASE environment variable"
|
||||
)
|
||||
trimmed: Final = resolved_api_base.rstrip("/")
|
||||
if trimmed.endswith("/v1"):
|
||||
return f"{trimmed}/images/edits"
|
||||
return f"{trimmed}/v1/images/edits"
|
||||
|
|
@ -9239,6 +9239,10 @@ class ProviderConfigManager:
|
|||
from litellm.llms.openai.image_edit import get_openai_image_edit_config
|
||||
|
||||
return get_openai_image_edit_config(model=model)
|
||||
if LlmProviders.HOSTED_VLLM == provider:
|
||||
from litellm.llms.hosted_vllm.image_edit import get_hosted_vllm_image_edit_config
|
||||
|
||||
return get_hosted_vllm_image_edit_config(model=model)
|
||||
elif LlmProviders.AZURE == provider:
|
||||
from litellm.llms.azure.image_edit.transformation import (
|
||||
AzureImageEditConfig,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
"""Tests for hosted_vllm image edits (vLLM-Omni /v1/images/edits)."""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||||
from litellm.llms.hosted_vllm.image_edit import get_hosted_vllm_image_edit_config
|
||||
from litellm.llms.hosted_vllm.image_edit.transformation import HostedVLLMImageEditConfig
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
PNG_BYTES = b"\x89PNG\r\n\x1a\nfakepng"
|
||||
MODEL = "Qwen/Qwen-Image-Edit-2511"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_hosted_vllm_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
|
||||
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
|
||||
|
||||
|
||||
def test_provider_config_registration():
|
||||
config = ProviderConfigManager.get_provider_image_edit_config(
|
||||
model=f"hosted_vllm/{MODEL}",
|
||||
provider=LlmProviders.HOSTED_VLLM,
|
||||
)
|
||||
|
||||
assert isinstance(config, HostedVLLMImageEditConfig)
|
||||
assert isinstance(get_hosted_vllm_image_edit_config(MODEL), HostedVLLMImageEditConfig)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"api_base",
|
||||
["http://localhost:8091", "http://localhost:8091/", "http://localhost:8091/v1", "http://localhost:8091/v1/"],
|
||||
)
|
||||
def test_get_complete_url_appends_images_edits(api_base: str):
|
||||
config = HostedVLLMImageEditConfig()
|
||||
|
||||
assert (
|
||||
config.get_complete_url(model=MODEL, api_base=api_base, litellm_params={})
|
||||
== "http://localhost:8091/v1/images/edits"
|
||||
)
|
||||
|
||||
|
||||
def test_get_complete_url_falls_back_to_env(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("HOSTED_VLLM_API_BASE", "http://vllm-omni:8000/v1")
|
||||
config = HostedVLLMImageEditConfig()
|
||||
|
||||
assert (
|
||||
config.get_complete_url(model=MODEL, api_base=None, litellm_params={})
|
||||
== "http://vllm-omni:8000/v1/images/edits"
|
||||
)
|
||||
|
||||
|
||||
def test_get_complete_url_requires_api_base():
|
||||
config = HostedVLLMImageEditConfig()
|
||||
|
||||
with pytest.raises(ValueError, match="api_base not set"):
|
||||
config.get_complete_url(model=MODEL, api_base=None, litellm_params={})
|
||||
|
||||
|
||||
def test_validate_environment_defaults_to_fake_api_key():
|
||||
headers = HostedVLLMImageEditConfig().validate_environment(headers={}, model=MODEL)
|
||||
|
||||
assert headers == {"Authorization": "Bearer fake-api-key"}
|
||||
|
||||
|
||||
def test_validate_environment_uses_provided_api_key_and_keeps_headers():
|
||||
headers = HostedVLLMImageEditConfig().validate_environment(
|
||||
headers={"X-Test": "1"},
|
||||
model=MODEL,
|
||||
api_key="my-custom-key",
|
||||
)
|
||||
|
||||
assert headers == {"X-Test": "1", "Authorization": "Bearer my-custom-key"}
|
||||
|
||||
|
||||
def test_validate_environment_falls_back_to_env_api_key(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "env-key")
|
||||
|
||||
headers = HostedVLLMImageEditConfig().validate_environment(headers={}, model=MODEL)
|
||||
|
||||
assert headers["Authorization"] == "Bearer env-key"
|
||||
|
||||
|
||||
def test_image_edit_posts_multipart_to_vllm_omni():
|
||||
captured: list[httpx.Request] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
captured.append(request)
|
||||
return httpx.Response(200, json={"created": 1712697600, "data": [{"b64_json": "aW1n"}]})
|
||||
|
||||
response = litellm.image_edit(
|
||||
model=f"hosted_vllm/{MODEL}",
|
||||
image=PNG_BYTES,
|
||||
prompt="add a hat",
|
||||
api_base="http://localhost:8091",
|
||||
api_key="test-key",
|
||||
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handler))),
|
||||
seed=42,
|
||||
)
|
||||
|
||||
assert response.data
|
||||
assert len(captured) == 1
|
||||
request = captured[0]
|
||||
assert str(request.url) == "http://localhost:8091/v1/images/edits"
|
||||
assert request.headers["authorization"] == "Bearer test-key"
|
||||
assert request.headers["content-type"].startswith("multipart/form-data")
|
||||
assert b'name="image[]"' in request.content
|
||||
assert PNG_BYTES in request.content
|
||||
assert f'name="model"\r\n\r\n{MODEL}'.encode() in request.content
|
||||
assert b'name="prompt"\r\n\r\nadd a hat' in request.content
|
||||
assert b'name="seed"\r\n\r\n42' in request.content
|
||||
Loading…
Add table
Reference in a new issue