feat(inference): 同步生成执行路径——交互形态第三种(2026-09-08用户定夺:stream/async之外,客户提交后模型即时给出产物)

- _sync_generation_inference:一次POST即时拿产物,复用异步路径的response模板渲染
  +downloadfile2url落地+usages记账;模板失效按DashScope实测形态搜索产物
  (output.choices[0].message.content[0].{image|video|audio})
- _post_upstream抽公共(require_choices参数:chat语义/生成语义);上游业务错误如实抛
- chat_inference同步分流三分支:CHAT_CAPS→chat;生成类(t2i/i2i/t2v/i2v/r2v/tts/asr)
  →同步生成;其余(embedding/rerank)如实报错禁静默
- 协议字典加dashscope_sync种子(同步生成,不带X-DashScope-Async头)
- 根治qwen-image-plus:文档MultiModalConversation.call(stream=False)被误配async
  +异步头→密钥不支持异步403;sync路径原先只认chat,t2i无执行路径
This commit is contained in:
yumoqing 2026-09-08 13:33:26 +08:00
parent 5a1a567720
commit eb6ca2f4bb
2 changed files with 126 additions and 10 deletions

View File

@ -2,6 +2,7 @@
"appcodes": [
{"parentid": "llm_protocol", "parentname": "供应商协议", "items": [
{"k": "openai_compat", "v": "OpenAI兼容"},
{"k": "dashscope_sync", "v": "DashScope同步生成(即时返回)"},
{"k": "dashscope_async", "v": "DashScope异步任务"},
{"k": "custom", "v": "自定义"}
]},

View File

@ -381,9 +381,11 @@ async def _build_upstream_body(ctx, payload):
return body
async def _call_upstream_chat(ctx, payload):
"""POST 上游path/headers/body 全模板组包),带瞬时错误重试。
async def _post_upstream(ctx, payload, require_choices=True):
"""通用 POST 上游path/headers/body 全模板组包),带瞬时错误重试。
require_choices=Truechat 语义顶层必须有 choices
False生成类语义任意 JSON dict 即有效产物由 response 模板/搜索提取
返回上游响应 dict失败抛 ValueError消息真实可行动
"""
import aiohttp
@ -424,8 +426,9 @@ async def _call_upstream_chat(ctx, payload):
continue
raise err
data = await resp.json(content_type=None)
if not isinstance(data, dict) or 'choices' not in data:
err = ValueError('上游响应缺 choices: %s' % (
if not isinstance(data, dict) or (require_choices and 'choices' not in data):
err = ValueError('上游响应%s: %s' % (
'缺 choices' if require_choices else '非 JSON 对象',
json.dumps(data, ensure_ascii=False, default=str)[:300]))
if attempt < _MAX_ATTEMPTS - 1:
last = err
@ -448,6 +451,111 @@ async def _call_upstream_chat(ctx, payload):
raise last if last else ValueError('上游调用失败')
async def _call_upstream_chat(ctx, payload):
"""POST 上游 chat/completionsOpenAI 兼容,顶层必须有 choices"""
return await _post_upstream(ctx, payload, require_choices=True)
def _find_artifact_url(out):
"""从上游 output 搜索生成产物 URLresponse 模板缺失/渲染失败时的兜底)。
DashScope 同步生成实测形态2026-09-08
output.choices[0].message.content[0].{image|video|audio}
旧形态output.results[0].url诚实搜索找不到返回空串
"""
try:
for ch in (out.get('choices') or []):
content = ((ch.get('message') or {}).get('content'))
segs = (content if isinstance(content, list)
else ([{'text': content}] if isinstance(content, str) else []))
for seg in segs:
if isinstance(seg, dict):
for k in ('image', 'video', 'audio'):
v = seg.get(k)
if isinstance(v, str) and v.strip():
return v
for r in (out.get('results') or []):
if isinstance(r, dict):
for k in ('url', 'image_url', 'video_url'):
if r.get(k):
return str(r[k])
except Exception:
pass
return ''
async def _sync_generation_inference(ctx, payload):
"""同步生成模型执行路径2026-09-08 用户定夺补全的第三种交互形态)。
交互形态三种流式chat stream前端 NDJSON 交付层/ 异步任务提交
轮询_async_inference/ **同步生成客户提交后模型即时给出产物**
此前 sync 路径只认 OpenAI chatchoices+CHAT_CAPS 门禁DashScope
MultiModalConversation 类同步生成模型qwen-image-plus 实测 403/无路径
没有执行路径本路径 = 一次 POST 拿结果复用异步路径的 response 模板
渲染 + downloadfile2url 落地 + usages 记账模板失效时按形态搜索产物
"""
from .gateway import govern_settle
profile = ctx.get('profile') or {}
try:
data = await _post_upstream(ctx, payload, require_choices=False)
except Exception as e:
note = str(e)[:200]
await govern_settle(ctx, False, 0, 0,
'429 upstream: ' + note if ' 429' in note else note)
raise GovernError(str(e))
# 上游业务错误DashScope顶层 code/message无 output——如实报错不粉饰
if data.get('code') and not (data.get('output') or data.get('choices')):
msg = '上游返回业务错误: %s %srequest_id=%s' % (
data.get('code'), str(data.get('message') or '')[:200],
data.get('request_id') or '')
await govern_settle(ctx, False, 0, 0, msg[:200])
raise GovernError(msg)
out = data.get('output') or {}
usage = data.get('usage') or {}
# response 模板渲染(同异步路径):产物 URL 经 downloadfile2url 落地
rendered = {}
resp_tmpl = (profile.get('response_template') or '').strip()
if resp_tmpl and '__note__' not in resp_tmpl:
ns = dict(data)
ns['output'] = out
ns['usage'] = usage
ns['request_id'] = data.get('request_id') or ''
ns.update(_media_ns_extra())
try:
s = await _render_tmpl(resp_tmpl, ns)
parsed = json.loads(s)
if isinstance(parsed, dict):
rendered = parsed
except Exception as e:
print("[inference] 同步生成 response模板渲染失败回退产物搜索: %s"
% str(e)[:150])
content = (rendered.get('video') or rendered.get('image')
or rendered.get('audio') or rendered.get('glb')
or rendered.get('3dmodel') or _find_artifact_url(out) or '')
if not content:
msg = ('同步生成调用成功但未在响应中找到产物(模板渲染与形态搜索均未命中)。'
'上游响应:%s' % json.dumps(data, ensure_ascii=False, default=str)[:300])
await govern_settle(ctx, False, 0, 0, msg[:200])
raise GovernError(msg)
usages = dict(usage) if isinstance(usage, dict) else {}
usages['model'] = ctx.get('model_id') or ''
for k in ('resolution', 'size', 'duration'):
if payload.get(k) not in (None, ''):
usages.setdefault(k, payload.get(k))
await govern_settle(ctx, True, 0, 0, '' if usage else 'no-usage', usages)
result = {
"choices": [{"message": {"content": content, "role": "assistant"},
"finish_reason": "stop"}],
"usage": usage,
"output": out,
}
if rendered:
result["media"] = rendered # 统一出参:本地持久 URLimage/video/...
return result
# ────────────────────────── 异步执行器(提交→轮询→取结果) ──────────────────────────
# 异步模型sync_mode='async',如 DashScope 视频/图像生成):
# 1. 提交POST 提交模板(模型挂的 profile→ 返回 output.task_id
@ -721,16 +829,23 @@ async def chat_inference(org_id, user_id, payload, model_name='', task_ref=''):
model_row = ctx.get('model_row') or {}
if (model_row.get('sync_mode') or 'sync') == 'async':
return await _async_inference(ctx, payload, req_timeout)
# 同步模型守能力边界聊天入口只认文本输出的对话能力t2t/i2t/m2t——
# embedding/rerank/图视频生成等另有专属入口list_models 按能力分栏选择)。
# 同步模型按能力三分支2026-09-08 用户定夺:交互形态第三种——同步生成):
# ① 对话能力t2t/i2t/m2t→ OpenAI chat 语义choices 结构)
# ② 生成类能力t2i/tts 等)→ 一次 POST 即时拿产物DashScope
# MultiModalConversation 类),走 response 模板/形态搜索提取 + 落地记账
# ③ 其余embedding/rerank 等)→ 无执行路径,如实报错(禁静默)
from .selection import CHAT_CAPS
cap = (model_row.get('capability') or 't2t').strip().lower()
_GEN_OK = ('t2i', 'i2i', 't2v', 'i2v', 'r2v', 'tts', 'asr')
if cap in _GEN_OK:
return await _sync_generation_inference(ctx, payload)
if cap not in CHAT_CAPS:
from .gateway import govern_settle
await govern_settle(ctx, False, 0, 0,
'capability mismatch: %s not in chat caps' % cap)
raise GovernError('模型「%s」能力为 %s,聊天入口仅支持 %s——请按能力分类选择模型' % (
model_row.get('name', ''), cap, '/'.join(CHAT_CAPS)))
await govern_settle(ctx, False, 0, 0, 'capability %s 无执行路径' % cap)
raise GovernError('模型「%s」能力为 %schat 入口支持对话(%s)与生成(%s)——'
'%s 请走其专属入口' % (
model_row.get('name', ''), cap, '/'.join(CHAT_CAPS),
'/'.join(_GEN_OK), cap))
try:
data = await _call_upstream_chat(ctx, payload)
except Exception as e: