fix: 补充 OpenAI CLI normalizer 图片格式双向转换

- 将 _image_url_to_block 从 OpenAINormalizer 提升到基类 FormatNormalizer
- OpenAI CLI normalizer 新增 ImageBlock 的解析和输出支持
- 修复 Chat -> CLI 跨格式路由时图片内容丢失

Co-Authored-By: LewisPen <LewisPen@nyadoo.com>
This commit is contained in:
fawney19
2026-02-13 13:59:57 +08:00
parent d690dcadc7
commit 599a62d722
3 changed files with 48 additions and 11 deletions

View File

@@ -8,7 +8,13 @@
from abc import ABC, abstractmethod
from typing import Any
from .internal import FormatCapabilities, InternalError, InternalRequest, InternalResponse
from .internal import (
FormatCapabilities,
ImageBlock,
InternalError,
InternalRequest,
InternalResponse,
)
from .internal_video import InternalVideoPollResult, InternalVideoRequest, InternalVideoTask
from .stream_events import InternalStreamEvent
from .stream_state import StreamState
@@ -98,6 +104,17 @@ class FormatNormalizer(ABC):
"""将内部错误表示转换为格式特定错误"""
raise NotImplementedError
# ============ 图片工具方法 ============
@staticmethod
def _image_url_to_block(url: str) -> ImageBlock:
"""将 image_url 字符串转换为 ImageBlock支持 data URL 和外部 URL"""
if url.startswith("data:") and ";base64," in url:
header, _, data = url.partition(",")
media_type = header.split(";")[0].split(":", 1)[-1]
return ImageBlock(data=data, media_type=media_type)
return ImageBlock(url=url)
# ============ 视频转换(可选) ============
def video_request_to_internal(self, request: dict[str, Any]) -> InternalVideoRequest:

View File

@@ -1476,13 +1476,6 @@ class OpenAINormalizer(FormatNormalizer):
},
}
def _image_url_to_block(self, url: str) -> ImageBlock:
if url.startswith("data:") and ";base64," in url:
header, _, data = url.partition(",")
media_type = header.split(";")[0].split(":", 1)[-1]
return ImageBlock(data=data, media_type=media_type)
return ImageBlock(url=url)
def _role_from_openai(self, role: str) -> Role:
if role == "user":
return Role.USER

View File

@@ -24,6 +24,7 @@ from src.core.api_format.conversion.internal import (
ContentType,
ErrorType,
FormatCapabilities,
ImageBlock,
InstructionSegment,
InternalError,
InternalMessage,
@@ -1111,6 +1112,15 @@ class OpenAICliNormalizer(FormatNormalizer):
if text:
blocks.append(TextBlock(text=text))
continue
if ptype in ("input_image", "output_image"):
image_url = part.get("image_url") or part.get("url") or ""
if isinstance(image_url, str) and image_url:
img = self._image_url_to_block(image_url)
img.extra.update(self._extract_extra(part, {"type", "image_url", "url"}))
blocks.append(img)
else:
blocks.append(UnknownBlock(raw_type=ptype, payload=part))
continue
blocks.append(UnknownBlock(raw_type=ptype or "unknown", payload=part))
return blocks
@@ -1180,7 +1190,7 @@ class OpenAICliNormalizer(FormatNormalizer):
if system_to_developer and role == "system":
role = "developer"
content_items: list[dict[str, Any]] = []
has_text = False
has_content = False
for block in msg.content:
if isinstance(block, (ToolUseBlock, ToolResultBlock)):
@@ -1189,13 +1199,30 @@ class OpenAICliNormalizer(FormatNormalizer):
continue # 已在上面处理
if isinstance(block, UnknownBlock):
continue # 跳过其他未知块
if isinstance(block, ImageBlock):
if block.data and block.media_type:
image_url = f"data:{block.media_type};base64,{block.data}"
elif block.url:
image_url = block.url
else:
continue
image_type = "output_image" if role == "assistant" else "input_image"
item: dict[str, Any] = {
"type": image_type,
"image_url": image_url,
}
if block.extra.get("detail"):
item["detail"] = block.extra["detail"]
content_items.append(item)
has_content = True
continue
if isinstance(block, TextBlock) and block.text:
# assistant 角色使用 output_text其他角色使用 input_text
text_type = "output_text" if role == "assistant" else "input_text"
content_items.append({"type": text_type, "text": block.text})
has_text = True
has_content = True
if has_text:
if has_content:
out.append({"type": "message", "role": role, "content": content_items})
return out