Skip to content

Commit e4a5da6

Browse files
committed
feat: add markdown response format for chat/LLM integrations
1 parent 576342a commit e4a5da6

15 files changed

Lines changed: 67 additions & 50 deletions

.github/workflows/release.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
name: Release
22

33
on:
4-
release:
5-
types: [published]
4+
push:
5+
tags:
6+
- 'v*'
67

78
permissions:
89
contents: read
@@ -16,7 +17,7 @@ jobs:
1617
- name: Checkout
1718
uses: actions/checkout@v4
1819
with:
19-
ref: ${{ github.event.release.tag_name }}
20+
ref: ${{ github.ref }}
2021

2122
- name: Set up Docker Buildx
2223
uses: docker/setup-buildx-action@v3

app/api/routes/generate.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import base64
22
import logging
33
from pathlib import Path
4-
from typing import List
54

65
from fastapi import APIRouter, Depends, HTTPException
76
from fastapi.responses import HTMLResponse, StreamingResponse
@@ -99,6 +98,22 @@ async def generate_image(request: ImageRequest, _: None = Depends(verify_token))
9998
},
10099
)
101100

101+
if request.response_format == "markdown":
102+
# Return ready-to-use markdown with image URL
103+
markdown = f"![Generated image]({urls[0]})"
104+
return ImageResponse(
105+
markdown=markdown,
106+
image_url=urls[0],
107+
prompt=request.prompt,
108+
model=model,
109+
provider=request.provider,
110+
metadata={
111+
"aspect_ratio": request.aspect_ratio,
112+
"quality": request.quality,
113+
"n": len(urls),
114+
},
115+
)
116+
102117
return ImageResponse(
103118
image_url=urls[0],
104119
prompt=request.prompt,

app/core/config.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import tomllib
22
from pathlib import Path
3-
from typing import Optional
43

54
from pydantic_settings import BaseSettings, SettingsConfigDict
65

@@ -34,19 +33,19 @@ class Settings(BaseSettings):
3433
)
3534

3635
# LiteLLM Configuration (Primary)
37-
LITELLM_BASE_URL: Optional[str] = None
38-
LITELLM_API_KEY: Optional[str] = None
36+
LITELLM_BASE_URL: str | None = None
37+
LITELLM_API_KEY: str | None = None
3938

4039
# Direct Provider API Keys (Fallback)
41-
OPENAI_API_KEY: Optional[str] = None
42-
GEMINI_API_KEY: Optional[str] = None
40+
OPENAI_API_KEY: str | None = None
41+
GEMINI_API_KEY: str | None = None
4342

4443
# Storage Configuration
4544
STORAGE_PATH: str = "./generated_images"
4645
BASE_URL: str = "http://localhost:8000"
4746

4847
# Security (Optional)
49-
API_BEARER_TOKEN: Optional[str] = None
48+
API_BEARER_TOKEN: str | None = None
5049

5150
# Model Registry
5251
MODEL_CACHE_TTL: int = 3600 # Cache models for 1 hour

app/core/security.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from typing import Optional
21

32
from fastapi import HTTPException, Security, status
43
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -9,7 +8,7 @@
98

109

1110
async def verify_token(
12-
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
11+
credentials: HTTPAuthorizationCredentials | None = Security(security),
1312
) -> None:
1413
"""
1514
Verify Bearer token if configured.

app/schemas/requests.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Optional
1+
from typing import Literal
22

33
from pydantic import BaseModel, Field
44

@@ -24,7 +24,7 @@ class ImageRequest(BaseModel):
2424
),
2525
)
2626

27-
model: Optional[str] = Field(
27+
model: str | None = Field(
2828
default=None,
2929
description=(
3030
"Model ID for image generation. Call GET /models to see all available models. "
@@ -54,12 +54,13 @@ class ImageRequest(BaseModel):
5454
description="Number of images to generate (1-4). Some models only support n=1",
5555
)
5656

57-
response_format: Literal["url", "base64"] = Field(
57+
response_format: Literal["url", "base64", "markdown"] = Field(
5858
default="url",
5959
description=(
6060
"Response format for generated images. "
61-
"Use 'base64' for chat integrations (returns image data directly, can be displayed inline). "
62-
"Use 'url' only for web applications that can fetch external URLs."
61+
"'url': Returns image URL (for web apps). "
62+
"'base64': Returns base64 encoded data (for offline/serverless). "
63+
"'markdown': Returns ready-to-use markdown with image URL (for chat/LLM integrations)."
6364
),
6465
)
6566

app/schemas/responses.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any
22

3-
from pydantic import BaseModel, Field, HttpUrl
3+
from pydantic import BaseModel, Field
44

55

66
class ImageResponse(BaseModel):
77
"""
88
Response schema for image generation.
99
"""
1010

11-
image_url: Optional[str] = Field(
11+
image_url: str | None = Field(
1212
default=None, description="URL to access the generated image (when response_format='url')"
1313
)
1414

15-
image_base64: Optional[str] = Field(
15+
image_base64: str | None = Field(
1616
default=None, description="Base64 encoded image data (when response_format='base64')"
1717
)
1818

19-
mime_type: Optional[str] = Field(
19+
mime_type: str | None = Field(
2020
default=None, description="MIME type of the image (e.g., 'image/png')"
2121
)
2222

23+
markdown: str | None = Field(
24+
default=None,
25+
description="Ready-to-use markdown with image URL (when response_format='markdown')",
26+
)
27+
2328
prompt: str = Field(..., description="The prompt used for generation")
2429

2530
model: str = Field(..., description="The model that generated the image")
2631

2732
provider: str = Field(..., description="The provider used (litellm, openai, gemini)")
2833

29-
metadata: Dict[str, Any] = Field(
34+
metadata: dict[str, Any] = Field(
3035
default_factory=dict, description="Additional metadata about the generation"
3136
)
3237

@@ -40,7 +45,7 @@ class ModelCapabilities(BaseModel):
4045
default=False, description="Whether model supports quality parameter"
4146
)
4247

43-
supports_aspect_ratios: List[str] = Field(
48+
supports_aspect_ratios: list[str] = Field(
4449
default_factory=lambda: ["1:1"], description="Supported aspect ratios"
4550
)
4651

@@ -66,11 +71,11 @@ class ModelListResponse(BaseModel):
6671
Response schema for listing available models.
6772
"""
6873

69-
models: List[ModelInfo] = Field(..., description="List of available models")
74+
models: list[ModelInfo] = Field(..., description="List of available models")
7075

7176
cached: bool = Field(..., description="Whether results are from cache")
7277

73-
cache_expires_in: Optional[int] = Field(
78+
cache_expires_in: int | None = Field(
7479
default=None, description="Seconds until cache expiration"
7580
)
7681

app/services/gemini_service.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from typing import List
32

43
from google import genai
54
from google.genai import types
@@ -36,7 +35,7 @@ async def generate_image(
3635
aspect_ratio: str = "1:1",
3736
quality: str = "standard", # Not used for Gemini
3837
n: int = 1,
39-
) -> List[str]:
38+
) -> list[str]:
4039
"""
4140
Generate images using Google Gemini API directly.
4241
"""

app/services/litellm_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import base64
22
import logging
3-
from typing import Any, Dict, List
3+
from typing import Any
44

55
from openai import OpenAI
66

@@ -41,7 +41,7 @@ async def generate_image(
4141
aspect_ratio: str = "1:1",
4242
quality: str = "standard",
4343
n: int = 1,
44-
) -> List[str]:
44+
) -> list[str]:
4545
"""
4646
Generate images using LiteLLM proxy.
4747
@@ -62,7 +62,7 @@ async def generate_image(
6262
size = self._get_size(aspect_ratio)
6363

6464
# Build request parameters
65-
params: Dict[str, Any] = {
65+
params: dict[str, Any] = {
6666
"model": model,
6767
"prompt": prompt,
6868
"n": n,

app/services/model_registry.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
import time
3-
from typing import Dict, List, Optional
43

54
import httpx
65

@@ -17,7 +16,7 @@ class ModelRegistry:
1716
"""
1817

1918
# Model capabilities database (fallback for known models)
20-
KNOWN_CAPABILITIES: Dict[str, ModelCapabilities] = {
19+
KNOWN_CAPABILITIES: dict[str, ModelCapabilities] = {
2120
"dall-e-2": ModelCapabilities(
2221
supports_quality=False, supports_aspect_ratios=["1:1"], max_images=4
2322
),
@@ -40,8 +39,8 @@ class ModelRegistry:
4039
}
4140

4241
def __init__(self):
43-
self._models: List[ModelInfo] = []
44-
self._cache_timestamp: Optional[float] = None
42+
self._models: list[ModelInfo] = []
43+
self._cache_timestamp: float | None = None
4544

4645
@property
4746
def cache_valid(self) -> bool:
@@ -52,14 +51,14 @@ def cache_valid(self) -> bool:
5251
return age < settings.MODEL_CACHE_TTL
5352

5453
@property
55-
def cache_age(self) -> Optional[int]:
54+
def cache_age(self) -> int | None:
5655
"""Get cache age in seconds."""
5756
if not self._cache_timestamp:
5857
return None
5958
return int(time.time() - self._cache_timestamp)
6059

6160
@property
62-
def cache_expires_in(self) -> Optional[int]:
61+
def cache_expires_in(self) -> int | None:
6362
"""Get seconds until cache expiration."""
6463
if not self._cache_timestamp:
6564
return None
@@ -69,7 +68,7 @@ def cache_expires_in(self) -> Optional[int]:
6968
remaining = settings.MODEL_CACHE_TTL - age
7069
return max(0, remaining)
7170

72-
async def load_models(self, force: bool = False) -> List[ModelInfo]:
71+
async def load_models(self, force: bool = False) -> list[ModelInfo]:
7372
"""
7473
Load available models from LiteLLM or fallback to static list.
7574
@@ -104,7 +103,7 @@ async def load_models(self, force: bool = False) -> List[ModelInfo]:
104103

105104
return self._models
106105

107-
async def _load_from_litellm(self) -> List[ModelInfo]:
106+
async def _load_from_litellm(self) -> list[ModelInfo]:
108107
"""
109108
Load models from LiteLLM /v1/models endpoint.
110109
"""
@@ -135,7 +134,7 @@ async def _load_from_litellm(self) -> List[ModelInfo]:
135134

136135
return models
137136

138-
def _get_static_models(self) -> List[ModelInfo]:
137+
def _get_static_models(self) -> list[ModelInfo]:
139138
"""
140139
Return static list of known models based on available API keys.
141140
"""
@@ -201,13 +200,13 @@ def _get_capabilities(self, model_id: str) -> ModelCapabilities:
201200
max_images=4,
202201
)
203202

204-
def get_models(self) -> List[ModelInfo]:
203+
def get_models(self) -> list[ModelInfo]:
205204
"""
206205
Get cached models (non-async).
207206
"""
208207
return self._models
209208

210-
def get_model(self, model_id: str) -> Optional[ModelInfo]:
209+
def get_model(self, model_id: str) -> ModelInfo | None:
211210
"""
212211
Get specific model info.
213212
"""

app/services/openai_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import base64
22
import logging
3-
from typing import Any, Dict, List
3+
from typing import Any
44

55
from openai import OpenAI
66

@@ -53,7 +53,7 @@ async def generate_image(
5353
aspect_ratio: str = "1:1",
5454
quality: str = "standard",
5555
n: int = 1,
56-
) -> List[str]:
56+
) -> list[str]:
5757
"""
5858
Generate images using OpenAI API directly.
5959
"""
@@ -66,7 +66,7 @@ async def generate_image(
6666
model_info = model_registry.get_model(model)
6767

6868
# Build request parameters
69-
params: Dict[str, Any] = {
69+
params: dict[str, Any] = {
7070
"model": model,
7171
"prompt": prompt,
7272
"n": n,

0 commit comments

Comments
 (0)