fix: 文生图连接测试 size 1024x1024 + 媒体端点智能推导 + 图像/视频模型 failover 路由(默认模型第一顺位)
This commit is contained in:
1 parent
7674729804
commit
0b00d6da45
2 files changed
+1304
-1229
No files matched your search
@@ -839,6 +839,86 @@ async def _agnes_post_json(api_url: str, headers: dict, payload: dict, timeout)
|
||||
|
||||
return None, last_err
|
||||
|
||||
|
||||
def _media_api_url(ai_model, provider, media_kind: str) -> str:
|
||||
"""推导图像/视频生成地址,避免把 chat/completions 端点误用作媒体端点。
|
||||
|
||||
- 若模型 endpoint 已是对应的媒体端点(含 /images/generations 或 /videos),直接使用;
|
||||
- 否则从 chat endpoint 反推 base,再拼 /v1/images|videos/generations;
|
||||
- 若 endpoint 为空,则回退供应商 api_base_url + 媒体路径。
|
||||
"""
|
||||
suffix = "/v1/images/generations" if media_kind == "image" else "/v1/videos"
|
||||
chat_suffixes = ["/v1/chat/completions", "/chat/completions"]
|
||||
ep = (ai_model.endpoint or "").strip()
|
||||
if ep:
|
||||
if suffix in ep:
|
||||
return ep
|
||||
base = ep
|
||||
for cs in chat_suffixes:
|
||||
if base.endswith(cs):
|
||||
base = base[: -len(cs)]
|
||||
break
|
||||
else:
|
||||
if "/v1" in base:
|
||||
base = base.rsplit("/v1", 1)[0]
|
||||
return f"{base.rstrip('/')}{suffix}"
|
||||
if provider and provider.api_base_url:
|
||||
return f"{provider.api_base_url.rstrip('/')}{suffix}"
|
||||
return ""
|
||||
|
||||
def _build_media_route(db, capability: str, preferred_id: str = None):
|
||||
"""构造媒体生成模型路由顺序:
|
||||
1) 按能力默认模型(后台"能力默认模型"或全局默认) 作为第一顺位
|
||||
2) 调用方显式指定的模型(若不同于默认) 次位
|
||||
3) 其余启用且具备该能力的模型,按 is_default 降序、created_at 升序 排列
|
||||
去重后返回 AIModel 列表。
|
||||
"""
|
||||
from ..models import AIModel as _AM
|
||||
|
||||
selected = []
|
||||
seen = set()
|
||||
|
||||
def _add(m):
|
||||
if m and m.id not in seen:
|
||||
selected.append(m)
|
||||
seen.add(m.id)
|
||||
|
||||
default_model = resolve_auto_model(capability, db)
|
||||
if default_model:
|
||||
_add(default_model)
|
||||
|
||||
if preferred_id:
|
||||
pref = db.query(_AM).filter(_AM.id == preferred_id, _AM.is_enabled == True).first()
|
||||
_add(pref)
|
||||
|
||||
others = db.query(_AM).filter(
|
||||
_AM.is_enabled == True,
|
||||
_AM.capabilities.op("JSON_CONTAINS")(_AM.capabilities, f'"{capability}"'),
|
||||
).order_by(_AM.is_default.desc(), _AM.created_at.asc()).all()
|
||||
for m in others:
|
||||
_add(m)
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
async def _run_media_route(db, route, prompt, user_id, media_kind):
|
||||
"""依次尝试路由中的模型生成媒体;首个成功(非 ⚠️ 开头)即返回,否则尝试下一个。"""
|
||||
last_err = None
|
||||
for idx, m in enumerate(route, 1):
|
||||
logger.info(f"[_run_media_route] ({idx}/{len(route)}) 尝试模型 {m.name} ({m.id})")
|
||||
try:
|
||||
res = await _generate_media(db, m, prompt, user_id)
|
||||
except Exception as e:
|
||||
last_err = f"模型 {m.name} 异常: {str(e)[:160]}"
|
||||
logger.warning(f"[_run_media_route] 模型 {m.name} 异常: {e}")
|
||||
continue
|
||||
if res and not res.startswith("⚠️"):
|
||||
return res
|
||||
last_err = res
|
||||
logger.warning(f"[_run_media_route] 模型 {m.name} 失败: {str(res)[:160]}")
|
||||
return last_err or "⚠️ 所有模型均生成失败"
|
||||
|
||||
|
||||
async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str:
|
||||
|
||||
|
||||
@@ -880,6 +960,9 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
timeout = ai_model.timeout or 120
|
||||
# 单次媒体调用超时封顶:避免单个供应商长时间挂起拖垮整条生成链路;
|
||||
# 多模型 failover 由 _generate_media_task 外层循环保证
|
||||
call_timeout = min(timeout, 300 if "video" in caps else 180)
|
||||
|
||||
|
||||
|
||||
@@ -895,15 +978,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
api_url = ai_model.endpoint
|
||||
|
||||
|
||||
|
||||
if not api_url:
|
||||
|
||||
|
||||
|
||||
api_url = f"{provider.api_base_url.rstrip('/')}/v1/images/generations" if provider else ""
|
||||
api_url = _media_api_url(ai_model, provider, "image")
|
||||
|
||||
|
||||
|
||||
@@ -951,7 +1026,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
"size": "1024x768",
|
||||
"size": "1024x1024",
|
||||
|
||||
|
||||
|
||||
@@ -960,7 +1035,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
async with span("media.agnes_call"):
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, call_timeout)
|
||||
|
||||
|
||||
|
||||
@@ -992,7 +1067,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
resp2, _ = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
resp2, _ = await _agnes_post_json(api_url, headers, payload, call_timeout)
|
||||
|
||||
|
||||
|
||||
@@ -1092,15 +1167,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
api_url = ai_model.endpoint
|
||||
|
||||
|
||||
|
||||
if not api_url:
|
||||
|
||||
|
||||
|
||||
api_url = f"{provider.api_base_url.rstrip('/')}/v1/videos" if provider else ""
|
||||
api_url = _media_api_url(ai_model, provider, "video")
|
||||
|
||||
|
||||
|
||||
@@ -1169,7 +1236,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
async with span("media.agnes_call"):
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, call_timeout)
|
||||
|
||||
|
||||
|
||||
@@ -1197,7 +1264,7 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
resp2, _ = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
resp2, _ = await _agnes_post_json(api_url, headers, payload, call_timeout)
|
||||
|
||||
|
||||
|
||||
@@ -1430,22 +1497,27 @@ async def _generate_media_task(ai_msg_id: str, ai_model_id: str, prompt: str, us
|
||||
logger.info(f"[_generate_media_task] 启动 media 生成 msg={ai_msg_id} model={ai_model_id} prompt={prompt[:60]!r}")
|
||||
caps = [c.lower() for c in (ai_model.capabilities or [])]
|
||||
is_video = "video" in caps
|
||||
timeout_seconds = 300 if is_video else 180
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
_generate_media(db, ai_model, prompt, user_id),
|
||||
timeout=timeout_seconds
|
||||
)
|
||||
media_kind = "video" if is_video else "image"
|
||||
|
||||
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"[_generate_media_task] media 生成超时 msg={ai_msg_id} is_video={is_video} timeout={timeout_seconds}s")
|
||||
# 构造模型路由:默认模型第一顺位,之后按路由策略逐一尝试
|
||||
route = _build_media_route(db, media_kind, ai_model_id)
|
||||
if not route:
|
||||
result = f"⚠️ 未找到可用的{'视频' if is_video else '图像'}模型,请在后台「大模型库」中启用具备该能力的模型。"
|
||||
else:
|
||||
_media_name = "视频" if is_video else "图像"
|
||||
result = f"⚠️ 媒体生成超时:{_media_name}生成耗时超过 {timeout_seconds} 秒,请稍后重试。"
|
||||
except Exception as e:
|
||||
logger.warning(f"[_generate_media_task] media 生成异常 msg={ai_msg_id}: {e}", exc_info=True)
|
||||
result = f"⚠️ 媒体生成失败:{str(e)[:200]}"
|
||||
per_call = 300 if is_video else 180
|
||||
route_timeout = min(per_call * len(route) + 30, 900 if is_video else 600)
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
_run_media_route(db, route, prompt, user_id, media_kind),
|
||||
timeout=route_timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"[_generate_media_task] media 路由全部超时 msg={ai_msg_id} is_video={is_video} timeout={route_timeout}s")
|
||||
result = f"⚠️ {_media_name}生成超时:已依次尝试 {len(route)} 个模型仍失败,请稍后重试。"
|
||||
except Exception as e:
|
||||
logger.warning(f"[_generate_media_task] media 路由异常 msg={ai_msg_id}: {e}", exc_info=True)
|
||||
result = f"⚠️ {_media_name}生成失败:{str(e)[:200]}"
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user