diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index 31f8e6d..d806140 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -1338,6 +1338,33 @@ def _media_array_expr(elements): return ''.join(segs), params +def _dashscope_envelope(example): + """DashScope SDK kwargs 形态 → 原生 HTTP 信封归一(2026-09-08 实测根因)。 + + 文档常只给 Python SDK 示例(MultiModalConversation.call(messages=..., + size=...)),提取 LLM 照抄成平铺 {model, messages, size, watermark...}; + 但原生 HTTP 端点要求 {"model", "input": {"messages": [...]}, + "parameters": {...}}——平铺体提交必 400 InvalidParameter + (qwen-image-plus 实测「messages parameter invalid」)。 + 代码级确定性归一(不靠提取 LLM 自觉):顶层有 messages 且无 input 信封 + 时,messages 进 input,除 model/input/parameters 外的业务键全进 parameters。 + """ + if not isinstance(example, dict) or not example: + return example + if 'input' in example or 'parameters' in example: + return example # 已是信封形态(curl 示例),不动 + msgs = example.get('messages') + if not isinstance(msgs, list): + return example # 非 SDK kwargs 形态,不动 + out = {'model': example.get('model', '')} + out['input'] = {'messages': msgs} + params = {k: v for k, v in example.items() + if k not in ('model', 'messages')} + if params: + out['parameters'] = params + return out + + def _tpl_from_example(example, capability): """文档请求示例 → Jinja2 请求体模板(结构逐字保留,值换成运行时变量)。 @@ -1493,6 +1520,25 @@ def _resp_tpl_for(capability, spec): if not field and hits: field = hits[0] note = '' + proto = str(spec.get('protocol') or '').strip() + if proto == 'dashscope_sync': + # DashScope 同步生成实测响应形态(2026-09-08 qwen-image-plus 真图验证): + # output.choices[0].message.content[0].{image|video|audio} + # 同步路径渲染命名空间无 task_id/status(StrictUndefined 会崩), + # 模板只引用 output/usage/request 三个必有变量。 + # 优先级:文档响应示例给出的 output.* 产物路径 > 实测兜底形态。 + if field and field.startswith('output.'): + path_expr = field + note = '' # 文档确证,无需兜底声明 + else: + seg_key = {'image': 'image', 'video': 'video', 'audio': 'audio'}.get(outkey, outkey) + path_expr = 'output.choices[0].message.content[0].%s' % seg_key + if not field: + note = ('产物路径按 DashScope 同步生成实测形态生成(%s),' + '文档未提供响应示例,如上游形态不同需修正' % path_expr) + resp = ('{"%s": "{{ downloadfile2url(request, %s) }}", ' + '"usage": {{ json.dumps(usage) }}}' % (outkey, path_expr)) + return resp, note if not field: field = _MEDIA_OUT_FIELD.get(outkey, 'result_url') note = ('产物字段名「%s」按供应商惯例兜底(文档未提供响应示例或示例中无 URL 字段),' @@ -1563,6 +1609,10 @@ def _gen_templates(protocol: str, capability: str, spec: dict) -> dict: example = _example_to_nested(spec.get("request_fields")) notes.append("文档未提供完整请求示例,模板按 request_fields 字段路径生成," "业务参数无默认值(调用时必须显式传入),建议人工核对") + # DashScope SDK kwargs → 原生 HTTP 信封归一(2026-09-08 qwen-image-plus 实测: + # 平铺体提交 400 InvalidParameter,必须 input.messages + parameters 信封) + if protocol in ('dashscope_sync', 'dashscope_async'): + example = _dashscope_envelope(example) req, media_params, biz_params, _used = _tpl_from_example(example, capability) if capability in _MEDIA_CAPS: resp, rnote = _resp_tpl_for(capability, spec)