重构代码

This commit is contained in:
h88782481 2026-03-22 08:24:19 +08:00
parent 56faf4fcf1
commit 70361242ab
9 changed files with 1195 additions and 1579 deletions

View file

@ -15,8 +15,18 @@ from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
from typing import Any, Iterator
from adapters.helpers import (
build_cc_message,
build_cc_response,
build_cc_tool_call,
build_cc_usage,
extract_text,
make_cc_chunk,
stringify_content,
)
from adapters.unified import UnifiedUsage
from utils.http import gen_id
JsonDict = dict[str, Any]
@ -85,7 +95,7 @@ def cc_to_responses(cc_resp: JsonDict, model: str = '') -> JsonDict:
'status': _response_status_from_finish_reason(finish_reason),
'model': model or cc_resp.get('model', ''),
'output': _build_responses_output(message),
'usage': _build_responses_usage(cc_resp.get('usage', {})),
'usage': UnifiedUsage.from_cc_dict(cc_resp.get('usage', {})).to_responses_dict(),
}
@ -94,31 +104,18 @@ def responses_to_cc_response(response_data: JsonDict, model: str = '') -> JsonDi
output_items = response_data.get('output', [])
content_text, reasoning_text, tool_calls = _collect_cc_parts_from_responses_output(output_items)
finish_reason = _cc_finish_reason_from_responses(response_data, tool_calls)
message = {
'role': 'assistant',
'content': content_text or None,
}
if reasoning_text:
message['reasoning_content'] = reasoning_text
if tool_calls:
message['tool_calls'] = tool_calls
usage = response_data.get('usage', {})
return {
'id': response_data.get('id', gen_id('chatcmpl-')),
'object': 'chat.completion',
'model': model or response_data.get('model', ''),
'choices': [{
'index': 0,
'message': message,
'finish_reason': finish_reason,
}],
'usage': {
'prompt_tokens': usage.get('input_tokens', 0),
'completion_tokens': usage.get('output_tokens', 0),
'total_tokens': usage.get('total_tokens', 0),
},
}
return build_cc_response(
response_id=response_data.get('id', gen_id('chatcmpl-')),
message=build_cc_message(content_text, reasoning_text, tool_calls),
finish_reason=finish_reason,
usage=build_cc_usage(
input_tokens=usage.get('input_tokens', 0),
output_tokens=usage.get('output_tokens', 0),
),
model=model or response_data.get('model', ''),
)
# ═══════════════════════════════════════════════════════════
@ -658,15 +655,7 @@ class ResponsesToCCStreamConverter:
def _make_chunk(self, delta: JsonDict, finish_reason: str | None = None) -> JsonDict:
"""构造标准 Chat Completions chunk。"""
choice: JsonDict = {'index': 0, 'delta': delta}
if finish_reason:
choice['finish_reason'] = finish_reason
return {
'id': self._id,
'object': 'chat.completion.chunk',
'model': self._model,
'choices': [choice],
}
return make_cc_chunk(self._id, delta, finish_reason, model=self._model)
# ═══════════════════════════════════════════════════════════
@ -715,7 +704,7 @@ def _append_responses_input_item(
content = message.get('content')
if role == 'system':
text = _content_to_text(content)
text = extract_text(content)
if text:
instructions.append(text)
return
@ -724,11 +713,11 @@ def _append_responses_input_item(
input_items.append({
'type': 'function_call_output',
'call_id': message.get('tool_call_id', ''),
'output': _stringify_output(content),
'output': stringify_content(content),
})
return
text = _content_to_text(content)
text = extract_text(content)
has_tool_calls = bool(message.get('tool_calls'))
if role == 'assistant' and has_tool_calls:
@ -771,7 +760,7 @@ def _convert_input_items(items: list[Any], messages: list[JsonDict]) -> None:
if role and not item_type:
msg: JsonDict = {
'role': role,
'content': _normalize_simple_content(item.get('content', '')),
'content': extract_text(item.get('content', '')),
}
if role == 'assistant' and pending_reasoning:
msg['reasoning_content'] = pending_reasoning
@ -810,7 +799,7 @@ def _append_message_item(items: list[Any], *, start: int, messages: list[JsonDic
"""将一个 message 项及其后续连续 function_call 项合并成一条消息。"""
item = items[start]
role = item.get('role', 'assistant')
content = _extract_text(item.get('content', []))
content = extract_text(item.get('content', []))
message: JsonDict = {'role': role, 'content': content or ''}
if role == 'assistant':
@ -828,7 +817,11 @@ def _append_message_item(items: list[Any], *, start: int, messages: list[JsonDic
def _append_function_call_item(item: JsonDict, messages: list[JsonDict]) -> None:
"""将独立的 Responses `function_call` 项挂接到最近的 assistant 消息上。"""
tool_call = _build_cc_tool_call(item)
tool_call = build_cc_tool_call(
call_id=item.get('call_id') or gen_id('call_'),
name=item.get('name', ''),
arguments=item.get('arguments', '{}'),
)
if messages and messages[-1]['role'] == 'assistant':
messages[-1].setdefault('tool_calls', []).append(tool_call)
@ -851,12 +844,6 @@ def _convert_function_call_output_item(item: JsonDict) -> JsonDict:
}
def _normalize_simple_content(content: Any) -> str:
"""将简单 content 载荷规范化为纯文本字符串。"""
if isinstance(content, list):
return _extract_text(content) or ''
return str(content) if content is not None else ''
def _collect_function_calls(items: list[Any], start: int) -> tuple[list[JsonDict], int]:
"""收集从指定位置开始连续出现的 `function_call` 项。"""
@ -865,24 +852,17 @@ def _collect_function_calls(items: list[Any], start: int) -> tuple[list[JsonDict
while index < len(items):
next_item = items[index]
if isinstance(next_item, dict) and next_item.get('type') == 'function_call':
tool_calls.append(_build_cc_tool_call(next_item))
tool_calls.append(build_cc_tool_call(
call_id=next_item.get('call_id') or gen_id('call_'),
name=next_item.get('name', ''),
arguments=next_item.get('arguments', '{}'),
))
index += 1
else:
break
return tool_calls, index - start
def _build_cc_tool_call(item: JsonDict) -> JsonDict:
"""将单个 Responses `function_call` 项转换为 CC `tool_call` 结构。"""
return {
'id': item.get('call_id') or gen_id('call_'),
'type': 'function',
'function': {
'name': item.get('name', ''),
'arguments': item.get('arguments', '{}'),
},
}
# ═══════════════════════════════════════════════════════════
# 非流式响应转换辅助
@ -936,14 +916,6 @@ def _make_function_call_output_item(tool_call: JsonDict) -> JsonDict:
}
def _build_responses_usage(usage: JsonDict) -> JsonDict:
"""将 Chat Completions 的 usage 字段映射为 Responses usage 结构。"""
return {
'input_tokens': usage.get('prompt_tokens', 0),
'output_tokens': usage.get('completion_tokens', 0),
'total_tokens': usage.get('total_tokens', 0),
}
def _collect_cc_parts_from_responses_output(output_items: Any) -> tuple[str, str, list[JsonDict]]:
"""从 Responses `output` 中提取文本、思考摘要和工具调用。"""
@ -959,11 +931,16 @@ def _collect_cc_parts_from_responses_output(output_items: Any) -> tuple[str, str
continue
item_type = item.get('type', '')
if item_type == 'message':
content_text += _extract_text(item.get('content', []))
content_text += extract_text(item.get('content', []))
elif item_type == 'reasoning':
reasoning_text += _extract_reasoning_text(item)
elif item_type == 'function_call':
tool_calls.append(_build_cc_tool_call_from_responses_output(item, index=len(tool_calls)))
tool_calls.append(build_cc_tool_call(
call_id=item.get('call_id') or gen_id('call_'),
name=item.get('name', ''),
arguments=item.get('arguments', '{}'),
index=len(tool_calls),
))
return content_text, reasoning_text, tool_calls
@ -980,18 +957,6 @@ def _extract_reasoning_text(item: JsonDict) -> str:
return ''.join(texts)
def _build_cc_tool_call_from_responses_output(item: JsonDict, *, index: int) -> JsonDict:
"""将 Responses `function_call` 输出项转换为 CC `tool_call`。"""
return {
'index': index,
'id': item.get('call_id') or gen_id('call_'),
'type': 'function',
'function': {
'name': item.get('name', ''),
'arguments': item.get('arguments', '{}'),
},
}
def _cc_finish_reason_from_responses(response_data: JsonDict, tool_calls: list[JsonDict]) -> str:
"""根据 Responses 完成状态推断聊天补全的 finish_reason。"""
@ -1017,57 +982,7 @@ def _map_anthropic_stop_reason(stop_reason: str) -> str:
# ═══════════════════════════════════════════════════════════
def _extract_text(content: Any) -> str:
"""从多种内容块结构中提取并拼接纯文本。"""
if isinstance(content, str):
return content
if not isinstance(content, list):
return str(content) if content else ''
texts: list[str] = []
for part in content:
if isinstance(part, str):
texts.append(part)
elif isinstance(part, dict):
part_type = part.get('type', '')
if part_type in ('output_text', 'input_text', 'text'):
texts.append(part.get('text', ''))
elif part_type == 'refusal':
texts.append(part.get('refusal', ''))
return '\n'.join(texts) if texts else ''
def _content_to_text(content: Any) -> str:
"""将任意 content 载荷转换为单个字符串。"""
if isinstance(content, str):
return content
if isinstance(content, list):
return _extract_text(content)
return str(content) if content is not None else ''
def _content_to_responses_parts(content: Any, role: str = 'user') -> list[JsonDict]:
"""将普通消息内容转换为 Responses 内容块数组。
assistant 消息使用 output_text其他角色使用 input_text
"""
if isinstance(content, list):
text = _extract_text(content)
else:
text = _content_to_text(content)
if not text:
return []
part_type = 'output_text' if role == 'assistant' else 'input_text'
return [{'type': part_type, 'text': text}]
def _stringify_output(content: Any) -> str:
"""将工具输出统一序列化为字符串,便于放入 `function_call_output`。"""
if isinstance(content, str):
return content
if content is None:
return ''
return json.dumps(content, ensure_ascii=False) if not isinstance(content, str) else content
def _build_responses_function_call_item(tool_call: JsonDict) -> JsonDict:
@ -1081,6 +996,165 @@ def _build_responses_function_call_item(tool_call: JsonDict) -> JsonDict:
}
# ═══════════════════════════════════════════════════════════
# OutboundTransformer 实现: Responses
# ═══════════════════════════════════════════════════════════
class ResponsesOutbound:
"""OpenAI Responses 后端的出站转换器。
CC 格式转换为 Responses 格式并处理响应
"""
def build_request(self, payload: JsonDict) -> JsonDict:
return cc_to_responses_request(payload)
def build_url(self, ctx) -> str:
return f'{ctx.target_url.rstrip("/")}/v1/responses'
def build_headers(self, ctx) -> dict[str, str]:
from utils.http import build_openai_headers
return build_openai_headers(ctx.api_key)
def parse_response(self, raw: JsonDict) -> JsonDict:
return responses_to_cc_response(raw)
def create_stream_processor(self) -> ResponsesStreamProcessorForCC:
return ResponsesStreamProcessorForCC()
class ResponsesStreamProcessorForCC:
"""Responses SSE → CC chunk 流式处理器。
用于 /v1/chat/completions -> /v1/responses 的桥接路径
"""
def __init__(self):
self._converter = ResponsesToCCStreamConverter()
def iter_events(self, response) -> Iterator:
from utils.http import iter_responses_sse
yield from iter_responses_sse(response)
def process_event(self, event: tuple) -> list[JsonDict]:
event_type, event_data = event
return self._converter.process_event(event_type, event_data)
def extract_usage(self, event: tuple) -> JsonDict | None:
from adapters.unified import extract_responses_usage
event_type, event_data = event
extracted = extract_responses_usage(event_data)
if extracted:
return {
'prompt_tokens': extracted.get('input_tokens', 0),
'completion_tokens': extracted.get('output_tokens', 0),
'total_tokens': extracted.get('total_tokens', 0),
}
return None
def finalize(self) -> list[JsonDict]:
return []
class ResponsesNativeOutbound:
"""Responses 后端原生透传的出站转换器。
/v1/responses /v1/responses 时直接透传不经过 CC 中间格式
"""
def build_request(self, payload: JsonDict) -> JsonDict:
return payload
def build_url(self, ctx) -> str:
return f'{ctx.target_url.rstrip("/")}/v1/responses'
def build_headers(self, ctx) -> dict[str, str]:
from utils.http import build_openai_headers
return build_openai_headers(ctx.api_key)
def parse_response(self, raw: JsonDict) -> JsonDict:
return raw
def create_stream_processor(self) -> ResponsesNativeStreamProcessor:
return ResponsesNativeStreamProcessor()
class ResponsesNativeStreamProcessor:
"""Responses 原生 SSE 透传流式处理器。
上游就是 Responses 格式只需透传事件并做轻量模型名改写
每个事件作为 SSE 字符串直接返回
"""
def iter_events(self, response) -> Iterator:
from utils.http import iter_responses_sse
yield from iter_responses_sse(response)
def process_event(self, event: tuple) -> list[JsonDict]:
event_type, event_data = event
return [{'_sse_event_type': event_type, **event_data}]
def extract_usage(self, event: tuple) -> JsonDict | None:
from adapters.unified import extract_responses_usage
_, event_data = event
return extract_responses_usage(event_data)
def finalize(self) -> list[JsonDict]:
return []
class AnthropicOutboundForResponses:
"""Anthropic 后端的出站转换器(用于 /v1/responses 路由)。
流式处理直接将 Anthropic SSE Responses SSE
跳过 CC 中间态以保留原始时序
"""
def build_request(self, payload: JsonDict) -> JsonDict:
from adapters.cc_anthropic_adapter import cc_to_messages_request
return cc_to_messages_request(payload)
def build_url(self, ctx) -> str:
return f'{ctx.target_url.rstrip("/")}/v1/messages'
def build_headers(self, ctx) -> dict[str, str]:
from utils.http import build_anthropic_headers
return build_anthropic_headers(ctx.api_key)
def parse_response(self, raw: JsonDict) -> JsonDict:
from adapters.cc_anthropic_adapter import messages_to_cc_response
return messages_to_cc_response(raw)
def create_stream_processor(self) -> AnthropicToResponsesStreamProcessor:
return AnthropicToResponsesStreamProcessor()
class AnthropicToResponsesStreamProcessor:
"""Anthropic SSE → Responses SSE 直接转换的流式处理器。
跳过 CC 中间态直接将 Anthropic 事件映射为 Responses 事件
返回的 chunk SSE 字符串
"""
def __init__(self):
self._converter = ResponsesStreamConverter()
def iter_events(self, response) -> Iterator:
from utils.http import iter_anthropic_sse
yield from iter_anthropic_sse(response)
def process_event(self, event: tuple) -> list[str]:
event_type, event_data = event
return self._converter.process_anthropic_event(event_type, event_data)
def extract_usage(self, event: tuple) -> JsonDict | None:
return None
def finalize(self) -> list[str]:
return self._converter.finalize()
def _convert_cc_tools_to_responses(tools: Any) -> list[JsonDict]:
"""将聊天补全风格的工具定义转换为 Responses `tools` 列表。"""
if not isinstance(tools, list):