From 8da377fc81832114a95da91df3d4fbe8ba76c217 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sat, 5 Sep 2026 15:19:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(inference):=20=E5=AA=92=E4=BD=93=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=BD=AC=E6=8D=A2=E5=8F=8C=E5=87=BD=E6=95=B0=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E6=A8=A1=E6=9D=BF=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= =?UTF-8?q?(=E5=AF=B9=E9=BD=90uapi=E9=93=81=E5=BE=8B)=E2=80=94=E2=80=94dat?= =?UTF-8?q?a=E6=A8=A1=E6=9D=BFb64media2url=E4=B8=8A=E4=BC=A0=E8=BD=AC?= =?UTF-8?q?=E5=85=AC=E7=BD=91URL/response=E6=A8=A1=E6=9D=BFdownloadfile2ur?= =?UTF-8?q?l=E7=94=9F=E6=88=90=E7=89=A9=E8=90=BD=E5=9C=B0;=E5=90=8C?= =?UTF-8?q?=E7=B1=BB=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E7=BB=9F=E4=B8=80?= =?UTF-8?q?xxx=5Ffile=E5=A5=91=E7=BA=A6;=E5=BC=82=E6=AD=A5=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E7=BB=8Fresponse=E6=A8=A1=E6=9D=BF=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=87=BAmedia=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_llm/inference.py | 74 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/pipeline_llm/inference.py b/pipeline_llm/inference.py index eba6b38..9a44a02 100644 --- a/pipeline_llm/inference.py +++ b/pipeline_llm/inference.py @@ -64,6 +64,45 @@ def _fnum(v): return 0.0 +def _media_ns_extra(): + """模板命名空间扩展:媒体文件转换函数(对齐 uapi 约定)+ request 占位。 + + 生成类模型的两条铁律(2026-09-05 用户定夺): + 上行(data 模板):上传/本地媒体用 {{b64media2url(request, xxx_file)}} + 转成本地公网 URL 再传上游(base64 落盘→/idfile 公网地址); + 下行(response 模板):上游生成物 URL 用 + {{downloadfile2url(request, output.video_url)}} 落地为本地持久 + URL 再给调用者(上游 URL 有效期短,DashScope 视频仅 24 小时)。 + request=None:内部执行器无 HTTP 请求上下文,函数走 BASEURL 环境变量 + 兜底拼公网地址(部署机须配置)。导入失败退回透传函数(不阻断调用)。 + """ + ns = {'request': None, 'json': json} + try: + from ahserver.filestorage import downloadfile2url, b64media2url + ns['downloadfile2url'] = downloadfile2url + ns['b64media2url'] = b64media2url + except Exception: + async def _d2u(request, url, **kw): + return url + async def _b2u(request, media): + return media + ns['downloadfile2url'] = _d2u + ns['b64media2url'] = _b2u + return ns + + +def _normalize_media_aliases(upstream): + """同类接口对外契约统一(2026-09-05 用户规则):上传媒体一律 xxx_file 命名。 + + 旧别名 xxx_url 归一到 xxx_file——同能力模板写法一致(如 i2v 统一 + image_file 进 → video 出),模板不用为同一类能力写多套参数名。 + """ + for media in ('image', 'video', 'audio'): + fk, uk = media + '_file', media + '_url' + if not upstream.get(fk) and upstream.get(uk): + upstream[fk] = upstream.get(uk) + + def _texts_len(messages): """messages 内容总长(估预授权用量用)。""" try: @@ -283,6 +322,7 @@ async def _build_upstream_body(ctx, payload): 'api_key': ctx.get('api_key') or '', 'org_id': ctx.get('org_id') or '', } + ns.update(_media_ns_extra()) s = await _render_tmpl(tmpl, ns) try: body = json.loads(s) @@ -406,15 +446,17 @@ async def _build_async_body(ctx, payload): upstream['model'] = ctx.get('model_id') or upstream.get('model') or '' for k, v in (ctx.get('default_params') or {}).items(): upstream.setdefault(k, v) + _normalize_media_aliases(upstream) # 上传媒体统一 xxx_file 契约 messages = upstream.pop('messages', []) model = upstream.pop('model', '') ns = { 'model': model, 'messages': messages, 'prompt': _last_prompt_text(messages), - 'params': upstream, # 业务参数(resolution/duration/image_url 等) + 'params': upstream, # 业务参数(resolution/duration/image_file 等) 'api_key': ctx.get('api_key') or '', 'org_id': ctx.get('org_id') or '', } + ns.update(_media_ns_extra()) s = await _render_tmpl(tmpl, ns) try: body = json.loads(s) @@ -545,9 +587,30 @@ async def _async_inference(ctx, payload, req_timeout): # ── 3. 组装结果 + 用量因子记账 ── out = last.get('output') or {} - content = (out.get('video_url') or out.get('image_url') or out.get('result_url') - or out.get('text') or '') usage = last.get('usage') or {} + # response 模板渲染(对齐 uapi):生成物 URL 必须经 downloadfile2url + # 落地为本地持久 URL 再给调用者(上游 URL 有效期短,视频仅 24 小时)。 + # 同类能力出参字段统一(i2v/t2v → video,t2i → image)。 + rendered = {} + resp_tmpl = (profile.get('response_template') or '').strip() + if resp_tmpl and '__note__' not in resp_tmpl: + ns = dict(out) + ns['output'] = out + ns['usage'] = usage + ns['task_id'] = task_id + 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模板渲染失败(回退原始URL,生成物未落地): %s" + % str(e)[:150]) + content = (rendered.get('video') or rendered.get('image') + or rendered.get('glb') or rendered.get('3dmodel') + or out.get('video_url') or out.get('image_url') + or out.get('result_url') or out.get('text') or '') usages = dict(usage) if isinstance(usage, dict) else {} usages['model'] = ctx.get('model_id') or '' # 计价过滤维度用请求值(与文档计价档位原文一致,如 resolution=720P); @@ -556,13 +619,16 @@ async def _async_inference(ctx, payload, req_timeout): 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) - return { + result = { "choices": [{"message": {"content": content, "role": "assistant"}, "finish_reason": "stop"}], "usage": usage, "output": out, "task_id": task_id, } + if rendered: + result["media"] = rendered # 统一出参:本地持久 URL(video/image/...) + return result # ────────────────────────── 对外主入口 ──────────────────────────