Skip to content

Commit 30f80ee

Browse files
committed
feat: filter models to only image generation models
1 parent 0c6edf7 commit 30f80ee

4 files changed

Lines changed: 163 additions & 1 deletion

File tree

app/core/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ class Settings(BaseSettings):
4242

4343
# Storage Configuration
4444
STORAGE_PATH: str = "./generated_images"
45-
BASE_URL: str = "http://localhost:8000"
45+
IMAGE_BASE_URL: str = "http://localhost:8000" # URL where this API serves images
4646

4747
# Security (Optional)
4848
API_BEARER_TOKEN: str | None = None
4949

5050
# Model Registry
5151
MODEL_CACHE_TTL: int = 3600 # Cache models for 1 hour
52+
FILTER_IMAGE_MODELS: bool = True # Only return image generation models from LiteLLM
53+
DEFAULT_MODEL: str | None = None # Default model for image generation
5254

5355
# Server Configuration
5456
HOST: str = "0.0.0.0"

app/services/model_registry.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
import time
34

45
import httpx
@@ -15,6 +16,14 @@ class ModelRegistry:
1516
Caches results to avoid frequent API calls.
1617
"""
1718

19+
# Patterns to identify image generation models
20+
IMAGE_MODEL_PATTERNS = [
21+
r"dall-e",
22+
r"gpt-image",
23+
r"gemini.*image",
24+
r"imagen",
25+
]
26+
1827
# Model capabilities database (fallback for known models)
1928
KNOWN_CAPABILITIES: dict[str, ModelCapabilities] = {
2029
"dall-e-2": ModelCapabilities(
@@ -124,6 +133,11 @@ async def _load_from_litellm(self) -> list[ModelInfo]:
124133
if not model_id:
125134
continue
126135

136+
# Filter to only image generation models if enabled
137+
if settings.FILTER_IMAGE_MODELS and not self._is_image_model(model_id):
138+
logger.debug(f"Skipping non-image model: {model_id}")
139+
continue
140+
127141
# Determine provider from model ID
128142
provider = self._infer_provider(model_id)
129143

@@ -167,6 +181,13 @@ def _get_static_models(self) -> list[ModelInfo]:
167181

168182
return models
169183

184+
def _is_image_model(self, model_id: str) -> bool:
185+
"""
186+
Check if a model is an image generation model.
187+
"""
188+
model_lower = model_id.lower()
189+
return any(re.search(pattern, model_lower) for pattern in self.IMAGE_MODEL_PATTERNS)
190+
170191
def _infer_provider(self, model_id: str) -> str:
171192
"""
172193
Infer provider from model ID.

tests/test_config.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import os
2+
from unittest.mock import patch
3+
4+
5+
def test_image_base_url_from_env():
6+
"""
7+
Test IMAGE_BASE_URL is loaded from environment.
8+
"""
9+
with patch.dict(
10+
os.environ,
11+
{
12+
"IMAGE_BASE_URL": "http://image-api:8000",
13+
},
14+
clear=True,
15+
):
16+
from importlib import reload
17+
18+
import app.core.config
19+
20+
reload(app.core.config)
21+
settings = app.core.config.Settings()
22+
23+
assert settings.IMAGE_BASE_URL == "http://image-api:8000"
24+
25+
26+
def test_image_base_url_default():
27+
"""
28+
Test default IMAGE_BASE_URL when not set.
29+
"""
30+
with patch.dict(os.environ, {}, clear=True):
31+
from importlib import reload
32+
33+
import app.core.config
34+
35+
reload(app.core.config)
36+
settings = app.core.config.Settings()
37+
38+
assert settings.IMAGE_BASE_URL == "http://localhost:8000"
39+
40+
41+
def test_default_model_config():
42+
"""
43+
Test DEFAULT_MODEL configuration.
44+
"""
45+
with patch.dict(
46+
os.environ,
47+
{
48+
"DEFAULT_MODEL": "gemini/gemini-2.0-flash-exp-image-generation",
49+
},
50+
clear=True,
51+
):
52+
from importlib import reload
53+
54+
import app.core.config
55+
56+
reload(app.core.config)
57+
settings = app.core.config.Settings()
58+
59+
assert settings.DEFAULT_MODEL == "gemini/gemini-2.0-flash-exp-image-generation"
60+
61+
62+
def test_filter_image_models_default():
63+
"""
64+
Test FILTER_IMAGE_MODELS defaults to True.
65+
"""
66+
with patch.dict(os.environ, {}, clear=True):
67+
from importlib import reload
68+
69+
import app.core.config
70+
71+
reload(app.core.config)
72+
settings = app.core.config.Settings()
73+
74+
assert settings.FILTER_IMAGE_MODELS is True

tests/test_model_registry.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,68 @@ def test_infer_provider():
119119
assert registry._infer_provider("gemini-2.0-flash") == "gemini"
120120
assert registry._infer_provider("imagen-3.0") == "gemini"
121121
assert registry._infer_provider("unknown") == "unknown"
122+
123+
124+
def test_is_image_model():
125+
"""
126+
Test image model detection.
127+
"""
128+
registry = ModelRegistry()
129+
130+
# Should match image models
131+
assert registry._is_image_model("dall-e-3") is True
132+
assert registry._is_image_model("gpt-image-1") is True
133+
assert registry._is_image_model("gemini-2.0-flash-preview-image-generation") is True
134+
assert registry._is_image_model("imagen-3.0") is True
135+
136+
# Should NOT match non-image models
137+
assert registry._is_image_model("gpt-4o") is False
138+
assert registry._is_image_model("claude-3-opus") is False
139+
assert registry._is_image_model("gemini-pro") is False
140+
141+
142+
@pytest.mark.asyncio
143+
async def test_filter_image_models():
144+
"""
145+
Test that only image models are returned when filter is enabled.
146+
"""
147+
registry = ModelRegistry()
148+
149+
mock_response = {
150+
"data": [
151+
{"id": "dall-e-3"},
152+
{"id": "gpt-4o"},
153+
{"id": "claude-3-opus"},
154+
{"id": "gemini-2.0-flash-preview-image-generation"},
155+
]
156+
}
157+
158+
with patch("app.services.model_registry.settings") as mock_settings:
159+
mock_settings.LITELLM_BASE_URL = "http://litellm:4000"
160+
mock_settings.LITELLM_API_KEY = None
161+
mock_settings.litellm_available = True
162+
mock_settings.FILTER_IMAGE_MODELS = True
163+
mock_settings.MODEL_CACHE_TTL = 3600
164+
165+
# Mock HTTP response
166+
mock_resp = MagicMock()
167+
mock_resp.status_code = 200
168+
mock_resp.json.return_value = mock_response
169+
mock_resp.raise_for_status.return_value = None
170+
171+
mock_client_instance = MagicMock()
172+
mock_client_instance.get = AsyncMock(return_value=mock_resp)
173+
mock_client = MagicMock()
174+
mock_client.__aenter__ = AsyncMock(return_value=mock_client_instance)
175+
mock_client.__aexit__ = AsyncMock(return_value=None)
176+
177+
with patch("app.services.model_registry.httpx.AsyncClient", return_value=mock_client):
178+
models = await registry.load_models()
179+
180+
# Should only have image models
181+
assert len(models) == 2
182+
model_ids = [m.id for m in models]
183+
assert "dall-e-3" in model_ids
184+
assert "gemini-2.0-flash-preview-image-generation" in model_ids
185+
assert "gpt-4o" not in model_ids
186+
assert "claude-3-opus" not in model_ids

0 commit comments

Comments
 (0)