Skip to content

Commit ae1b0d4

Browse files
committed
Merge pull request #109
2 parents 475c998 + d362189 commit ae1b0d4

3 files changed

Lines changed: 221 additions & 27 deletions

File tree

src/api/routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ async def generate_content(
829829
return _build_gemini_error_response_from_handler(payload)
830830

831831
return JSONResponse(
832-
content=await _build_gemini_success_payload(payload, model)
832+
content=await _build_gemini_success_payload(payload, normalized.model)
833833
)
834834

835835
except HTTPException as exc:
@@ -862,7 +862,7 @@ async def stream_generate_content(
862862
request_base_url = _get_request_base_url(raw_request)
863863

864864
return StreamingResponse(
865-
_iterate_gemini_stream(normalized, model, request_base_url),
865+
_iterate_gemini_stream(normalized, normalized.model, request_base_url),
866866
media_type="text/event-stream",
867867
headers={
868868
"Cache-Control": "no-cache",

src/core/model_resolver.py

Lines changed: 216 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
→ resolved to "gemini-3.0-pro-image-landscape-2k"
1212
"""
1313

14+
import re
1415
from typing import Optional, Dict, Any, Tuple
1516
from ..core.logger import debug_logger
1617

@@ -96,6 +97,29 @@
9697
# 默认 aspectRatio
9798
DEFAULT_ASPECT = "landscape"
9899

100+
OPENAI_IMAGE_SIZE_RE = re.compile(r"^(?P<w>\d{2,5})\s*[xX]\s*(?P<h>\d{2,5})$")
101+
102+
# OpenAI 常见 quality → imageSize 映射
103+
# - 这里的 imageSize 是 flow2api 的“放大档位”,并不等价于 OpenAI 的像素尺寸;
104+
# 但可用作“画质/清晰度”的近似映射。
105+
OPENAI_QUALITY_MAP = {
106+
"low": None,
107+
"standard": None,
108+
"medium": "2k",
109+
"high": "4k",
110+
"hd": "4k",
111+
"ultra": "4k",
112+
}
113+
114+
# 用于把 OpenAI size(如 1024x1792)映射到最接近的 flow2api aspect 选项
115+
ASPECT_RATIO_FLOAT_MAP = {
116+
"landscape": 16 / 9,
117+
"portrait": 9 / 16,
118+
"square": 1.0,
119+
"four-three": 4 / 3,
120+
"three-four": 3 / 4,
121+
}
122+
99123

100124
# ──────────────────────────────────────────────
101125
# 视频模型简化名映射
@@ -173,47 +197,214 @@ def _extract_generation_params(request) -> Tuple[Optional[str], Optional[str]]:
173197
优先级:
174198
1. request.generationConfig.imageConfig (顶层 Gemini 参数)
175199
2. extra fields 中的 generationConfig (extra_body 透传)
200+
3. OpenAI 风格字段(size/quality)兼容:可在 generationConfig/imageConfig 或顶层 extra 中出现
176201
177202
Returns:
178203
(aspect_ratio, image_size) 归一化后的值
179204
"""
180-
aspect_ratio = None
181-
image_size = None
205+
def _normalize_str(value: Any) -> Optional[str]:
206+
if not isinstance(value, str):
207+
return None
208+
text = value.strip()
209+
return text if text else None
210+
211+
def _read_value(obj: Any, *keys: str) -> Any:
212+
if obj is None:
213+
return None
214+
if isinstance(obj, dict):
215+
for key in keys:
216+
if key in obj:
217+
return obj.get(key)
218+
return None
219+
220+
for key in keys:
221+
if hasattr(obj, key):
222+
value = getattr(obj, key, None)
223+
if value is not None:
224+
return value
225+
226+
extra = getattr(obj, "__pydantic_extra__", None) or {}
227+
for key in keys:
228+
if key in extra:
229+
return extra.get(key)
230+
return None
231+
232+
def _normalize_aspect_ratio(value: Any) -> Optional[str]:
233+
raw = _normalize_str(value)
234+
if not raw:
235+
return None
236+
237+
token = (
238+
raw.replace(":", ":")
239+
.replace("/", ":")
240+
.replace("x", ":")
241+
.replace("X", ":")
242+
.replace(" ", "")
243+
.strip()
244+
)
245+
246+
mapped = ASPECT_RATIO_MAP.get(token)
247+
if mapped:
248+
return mapped
249+
mapped = ASPECT_RATIO_MAP.get(token.lower())
250+
if mapped:
251+
return mapped
252+
mapped = ASPECT_RATIO_MAP.get(token.upper())
253+
if mapped:
254+
return mapped
255+
return token
256+
257+
def _normalize_image_size(value: Any) -> Optional[str]:
258+
raw = _normalize_str(value)
259+
if not raw:
260+
return None
261+
262+
token = raw.replace(" ", "").strip()
263+
mapped = IMAGE_SIZE_MAP.get(token)
264+
if mapped is not None:
265+
return mapped or None
266+
mapped = IMAGE_SIZE_MAP.get(token.lower())
267+
if mapped is not None:
268+
return mapped or None
269+
mapped = IMAGE_SIZE_MAP.get(token.upper())
270+
if mapped is not None:
271+
return mapped or None
272+
return token.lower()
273+
274+
def _aspect_from_openai_size(value: Any) -> Optional[str]:
275+
raw = _normalize_str(value)
276+
if not raw:
277+
return None
278+
279+
match = OPENAI_IMAGE_SIZE_RE.match(raw)
280+
if not match:
281+
return None
282+
283+
try:
284+
width = int(match.group("w"))
285+
height = int(match.group("h"))
286+
except Exception:
287+
return None
288+
289+
if width <= 0 or height <= 0:
290+
return None
291+
292+
ratio = width / height
293+
best = min(
294+
ASPECT_RATIO_FLOAT_MAP.items(),
295+
key=lambda item: abs(ratio - item[1]),
296+
)[0]
297+
return best
298+
299+
def _image_size_from_openai_quality(value: Any) -> Optional[str]:
300+
raw = _normalize_str(value)
301+
if not raw:
302+
return None
303+
304+
token = raw.strip().lower()
305+
if token in IMAGE_SIZE_MAP:
306+
return _normalize_image_size(token)
307+
308+
mapped = OPENAI_QUALITY_MAP.get(token)
309+
if mapped:
310+
return mapped
311+
return None
312+
313+
def _apply_image_config(image_config: Any, aspect_ratio: Optional[str], image_size: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
314+
# 显式 aspectRatio/imageSize
315+
if not aspect_ratio:
316+
aspect_ratio = _normalize_aspect_ratio(
317+
_read_value(image_config, "aspectRatio", "aspect_ratio", "aspect")
318+
)
319+
if not image_size:
320+
image_size = _normalize_image_size(
321+
_read_value(image_config, "imageSize", "image_size", "resolution")
322+
)
323+
324+
# OpenAI size/quality
325+
if not aspect_ratio:
326+
aspect_ratio = _aspect_from_openai_size(_read_value(image_config, "size"))
327+
if not image_size:
328+
image_size = _image_size_from_openai_quality(
329+
_read_value(image_config, "quality", "imageQuality", "image_quality")
330+
)
182331

183-
# 尝试从 generationConfig 提取
332+
return aspect_ratio, image_size
333+
334+
aspect_ratio: Optional[str] = None
335+
image_size: Optional[str] = None
336+
337+
# 1) 优先从 request.generationConfig 解析
184338
gen_config = getattr(request, "generationConfig", None)
339+
if gen_config is not None:
340+
image_config = _read_value(gen_config, "imageConfig", "image_config")
341+
if image_config is not None:
342+
aspect_ratio, image_size = _apply_image_config(
343+
image_config, aspect_ratio, image_size
344+
)
345+
346+
# 有些上游会把字段放在 generationConfig 顶层
347+
if not aspect_ratio:
348+
aspect_ratio = _normalize_aspect_ratio(
349+
_read_value(gen_config, "aspectRatio", "aspect_ratio")
350+
)
351+
if not image_size:
352+
image_size = _normalize_image_size(
353+
_read_value(gen_config, "imageSize", "image_size")
354+
)
355+
356+
if not aspect_ratio:
357+
aspect_ratio = _aspect_from_openai_size(_read_value(gen_config, "size"))
358+
if not image_size:
359+
image_size = _image_size_from_openai_quality(_read_value(gen_config, "quality"))
185360

186-
# 如果顶层没有,尝试从 extra fields (Pydantic extra="allow")
187-
if gen_config is None and hasattr(request, "__pydantic_extra__"):
361+
# 2) 顶层没有时,再尝试从 extra fields (Pydantic extra="allow") 中透传的 generationConfig
362+
if (aspect_ratio is None or image_size is None) and hasattr(request, "__pydantic_extra__"):
188363
extra = request.__pydantic_extra__ or {}
189364
gen_config_raw = extra.get("generationConfig")
190365
if not isinstance(gen_config_raw, dict):
191366
extra_body = extra.get("extra_body") or extra.get("extraBody")
192367
if isinstance(extra_body, dict):
193368
gen_config_raw = extra_body.get("generationConfig")
369+
194370
if isinstance(gen_config_raw, dict):
195-
image_config_raw = gen_config_raw.get("imageConfig", {})
196-
if isinstance(image_config_raw, dict):
197-
aspect_ratio = image_config_raw.get("aspectRatio")
198-
image_size = image_config_raw.get("imageSize")
199-
return (
200-
ASPECT_RATIO_MAP.get(aspect_ratio, aspect_ratio)
201-
if aspect_ratio
202-
else None,
203-
IMAGE_SIZE_MAP.get(image_size, image_size) if image_size else None,
371+
image_config_raw = (
372+
gen_config_raw.get("imageConfig")
373+
or gen_config_raw.get("image_config")
374+
or {}
204375
)
376+
if image_config_raw:
377+
aspect_ratio, image_size = _apply_image_config(
378+
image_config_raw, aspect_ratio, image_size
379+
)
205380

206-
if gen_config is not None:
207-
image_config = getattr(gen_config, "imageConfig", None)
208-
if image_config is not None:
209-
aspect_ratio = getattr(image_config, "aspectRatio", None)
210-
image_size = getattr(image_config, "imageSize", None)
211-
212-
# 归一化
213-
if aspect_ratio:
214-
aspect_ratio = ASPECT_RATIO_MAP.get(aspect_ratio, aspect_ratio)
215-
if image_size:
216-
image_size = IMAGE_SIZE_MAP.get(image_size, image_size)
381+
if aspect_ratio is None:
382+
aspect_ratio = _normalize_aspect_ratio(
383+
gen_config_raw.get("aspectRatio") or gen_config_raw.get("aspect_ratio")
384+
)
385+
if image_size is None:
386+
image_size = _normalize_image_size(
387+
gen_config_raw.get("imageSize") or gen_config_raw.get("image_size")
388+
)
389+
390+
if aspect_ratio is None:
391+
aspect_ratio = _aspect_from_openai_size(gen_config_raw.get("size"))
392+
if image_size is None:
393+
image_size = _image_size_from_openai_quality(gen_config_raw.get("quality"))
394+
395+
# 3) OpenAI 风格 size/quality(顶层 extra)兼容
396+
if (aspect_ratio is None or image_size is None) and hasattr(request, "__pydantic_extra__"):
397+
extra = request.__pydantic_extra__ or {}
398+
if aspect_ratio is None:
399+
aspect_ratio = _aspect_from_openai_size(extra.get("size"))
400+
if image_size is None:
401+
image_size = _image_size_from_openai_quality(extra.get("quality"))
402+
403+
# 一些上游可能直接传 aspect_ratio/image_size
404+
if aspect_ratio is None:
405+
aspect_ratio = _normalize_aspect_ratio(extra.get("aspect_ratio") or extra.get("aspectRatio"))
406+
if image_size is None:
407+
image_size = _normalize_image_size(extra.get("image_size") or extra.get("imageSize"))
217408

218409
return aspect_ratio, image_size
219410

src/core/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ class ImageConfig(BaseModel):
224224
aspectRatio: Optional[str] = None # "16:9", "9:16", "1:1", "4:3", "3:4"
225225
imageSize: Optional[str] = None # "2k", "4k"
226226

227+
# 兼容 OpenAI/NewAPI 等上游可能透传的 size/quality 或 snake_case 字段
228+
model_config = ConfigDict(extra="allow")
229+
227230

228231
class GenerationConfigParam(BaseModel):
229232
"""Gemini generationConfig parameters (for model name resolution)"""

0 commit comments

Comments
 (0)