补全注释

This commit is contained in:
h88782481 2026-03-10 08:43:07 +08:00
parent 96fbc4da80
commit f193c48ce1
13 changed files with 115 additions and 14 deletions

View file

@ -116,6 +116,7 @@ class AnthropicStreamConverter:
"""
def __init__(self, request_id: str | None = None):
"""初始化流式转换所需的请求标识、工具索引和 usage 状态。"""
self._id = request_id or gen_id('chatcmpl-')
self._tool_index = -1
self._input_tokens = 0
@ -509,7 +510,7 @@ def _convert_tools(tools: Any) -> list[JsonDict]:
def _convert_tool_definition(tool: Any) -> JsonDict | None:
"""转换单个工具定义"""
"""将 OpenAI 或 Anthropic 风格的工具定义统一转换为 Anthropic `tools` 项"""
if not isinstance(tool, dict):
return None

View file

@ -128,7 +128,11 @@ def responses_to_cc_response(response_data: JsonDict, model: str = '') -> JsonDi
@dataclass
class _ToolBuffer:
"""缓存单个工具调用的流式状态。"""
"""缓存单个工具调用的流式状态。
Responses 风格的 function_call 会把名称参数增量和完成时机拆散在多个事件里
转换器需要用这个缓冲结构暂存工具标识与累计参数便于后续按顺序补齐事件
"""
name: str
args: str
@ -155,6 +159,7 @@ class ResponsesStreamConverter:
"""
def __init__(self, response_id: str | None = None, model: str = ''):
"""初始化 Responses 流式状态机所需的各类缓冲区与标识。"""
self.resp_id = response_id or gen_id('resp_')
self.model = model
@ -366,6 +371,7 @@ class ResponsesStreamConverter:
return events
def _on_tool_call(self, tool_call: JsonDict) -> list[str]:
"""处理来自 CC chunk 的工具调用增量,并映射成 Responses function_call 事件。"""
events: list[str] = []
index = tool_call.get('index', 0)
function_data = tool_call.get('function') or {}
@ -382,6 +388,7 @@ class ResponsesStreamConverter:
return events
def _ensure_text_started(self) -> list[str]:
"""确保 assistant 文本输出项已开启,并在必要时先关闭 reasoning 项。"""
events: list[str] = []
if self._rs_started and not self._rs_closed:
events.extend(self._close_reasoning())
@ -401,6 +408,7 @@ class ResponsesStreamConverter:
return events
def _start_tool_from_block(self, block: JsonDict) -> list[str]:
"""根据 Anthropic `tool_use` block 创建对应的 function_call 输出项。"""
return self._start_tool(
index=len(self._tools),
call_id=block.get('id', gen_id('toolu_')),
@ -408,6 +416,7 @@ class ResponsesStreamConverter:
)
def _start_tool(self, *, index: int, call_id: str, name: str) -> list[str]:
"""开启一个新的 Responses function_call 输出项,并关闭前置输出段。"""
events: list[str] = []
if self._rs_started and not self._rs_closed:
events.extend(self._close_reasoning())
@ -432,6 +441,7 @@ class ResponsesStreamConverter:
return events
def _append_tool_arguments(self, index: int, arguments_delta: str) -> list[str]:
"""向指定 function_call 缓冲区追加参数增量,并发出对应 SSE 事件。"""
buffer = self._tools[index]
buffer.args += arguments_delta
return [self._sse('response.function_call_arguments.delta', {
@ -440,6 +450,7 @@ class ResponsesStreamConverter:
})]
def _close_reasoning(self) -> list[str]:
"""关闭 reasoning 输出项,并补发 summary 完成事件。"""
if self._rs_closed:
return []
self._rs_closed = True
@ -459,6 +470,7 @@ class ResponsesStreamConverter:
]
def _close_text(self) -> list[str]:
"""关闭 assistant 文本输出项,并补发文本完成事件。"""
if self._text_closed:
return []
self._text_closed = True
@ -525,6 +537,7 @@ class ResponsesStreamConverter:
return events
def _sse(self, event_type: str, data: JsonDict) -> str:
"""将事件类型与负载编码为标准 Responses SSE 字符串。"""
return f'event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n'
def _rewrite_top_level_model(self, payload: JsonDict) -> JsonDict:
@ -554,6 +567,7 @@ class ResponsesToCCStreamConverter:
"""
def __init__(self, request_id: str | None = None, model: str = ''):
"""初始化 Responses → Chat Completions 流式转换所需的状态。"""
self._id = request_id or gen_id('chatcmpl-')
self._model = model
self._tool_index = 0
@ -652,6 +666,7 @@ class ResponsesToCCStreamConverter:
def _copy_request_options(payload: JsonDict, result: JsonDict) -> None:
"""将 Responses 请求中的通用选项复制到 CC 请求体。"""
if 'tools' in payload:
result['tools'] = _convert_tools(payload['tools'])
for key in ('temperature', 'top_p'):
@ -664,6 +679,7 @@ def _copy_request_options(payload: JsonDict, result: JsonDict) -> None:
def _copy_responses_request_options(payload: JsonDict, result: JsonDict) -> None:
"""将聊天补全请求中的通用选项复制到原生 Responses 请求体。"""
if 'tools' in payload:
result['tools'] = _convert_cc_tools_to_responses(payload['tools'])
for key in ('temperature', 'top_p', 'tool_choice'):
@ -678,6 +694,7 @@ def _append_responses_input_item(
instructions: list[str],
input_items: list[JsonDict],
) -> None:
"""将单条 Chat Completions 消息追加为 Responses `input` 项。"""
if not isinstance(message, dict):
return
@ -711,6 +728,7 @@ def _append_responses_input_item(
def _convert_input_items(items: list[Any], messages: list[JsonDict]) -> None:
"""将 Responses `input` 数组重建为 Chat Completions `messages` 列表。"""
index = 0
while index < len(items):
item = items[index]
@ -756,6 +774,7 @@ def _convert_input_items(items: list[Any], messages: list[JsonDict]) -> None:
def _append_message_item(items: list[Any], *, start: int, messages: list[JsonDict]) -> int:
"""将一个 message 项及其后续连续 function_call 项合并成一条消息。"""
item = items[start]
role = item.get('role', 'assistant')
content = _extract_text(item.get('content', []))
@ -775,6 +794,7 @@ 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)
if messages and messages[-1]['role'] == 'assistant':
@ -787,6 +807,7 @@ def _append_function_call_item(item: JsonDict, messages: list[JsonDict]) -> None
def _convert_function_call_output_item(item: JsonDict) -> JsonDict:
"""将 Responses 的 `function_call_output` 项转换为 OpenAI `tool` 消息。"""
output = item.get('output', '')
if not isinstance(output, str):
output = json.dumps(output, ensure_ascii=False)
@ -798,12 +819,14 @@ 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` 项。"""
tool_calls: list[JsonDict] = []
index = start
while index < len(items):
@ -817,6 +840,7 @@ def _collect_function_calls(items: list[Any], start: int) -> tuple[list[JsonDict
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',
@ -833,6 +857,7 @@ def _build_cc_tool_call(item: JsonDict) -> JsonDict:
def _build_responses_output(message: JsonDict) -> list[JsonDict]:
"""将单条聊天补全消息展开为 Responses `output` 数组。"""
output: list[JsonDict] = []
if message.get('reasoning_content'):
@ -846,6 +871,7 @@ def _build_responses_output(message: JsonDict) -> list[JsonDict]:
def _make_reasoning_output_item(text: str) -> JsonDict:
"""构造单个 Responses reasoning 输出项。"""
return {
'type': 'reasoning',
'id': gen_id('rs_'),
@ -854,6 +880,7 @@ def _make_reasoning_output_item(text: str) -> JsonDict:
def _make_message_output_item(text: str) -> JsonDict:
"""构造单个 Responses message 输出项。"""
return {
'type': 'message',
'id': gen_id('msg_'),
@ -864,6 +891,7 @@ def _make_message_output_item(text: str) -> JsonDict:
def _make_function_call_output_item(tool_call: JsonDict) -> JsonDict:
"""构造单个 Responses function_call 输出项。"""
function_data = tool_call.get('function') or {}
return {
'type': 'function_call',
@ -876,6 +904,7 @@ 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),
@ -884,6 +913,7 @@ def _build_responses_usage(usage: JsonDict) -> JsonDict:
def _collect_cc_parts_from_responses_output(output_items: Any) -> tuple[str, str, list[JsonDict]]:
"""从 Responses `output` 中提取文本、思考摘要和工具调用。"""
content_text = ''
reasoning_text = ''
tool_calls: list[JsonDict] = []
@ -906,6 +936,7 @@ def _collect_cc_parts_from_responses_output(output_items: Any) -> tuple[str, str
def _extract_reasoning_text(item: JsonDict) -> str:
"""从 Responses reasoning 项中拼接出完整的摘要文本。"""
summary = item.get('summary', [])
if not isinstance(summary, list):
return ''
@ -917,6 +948,7 @@ def _extract_reasoning_text(item: JsonDict) -> str:
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_'),
@ -929,6 +961,7 @@ def _build_cc_tool_call_from_responses_output(item: JsonDict, *, index: int) ->
def _cc_finish_reason_from_responses(response_data: JsonDict, tool_calls: list[JsonDict]) -> str:
"""根据 Responses 完成状态推断聊天补全的 finish_reason。"""
if tool_calls:
return 'tool_calls'
if response_data.get('status') == 'incomplete':
@ -937,10 +970,12 @@ def _cc_finish_reason_from_responses(response_data: JsonDict, tool_calls: list[J
def _response_status_from_finish_reason(finish_reason: str) -> str:
"""将聊天补全 finish_reason 映射为 Responses 顶层状态。"""
return 'incomplete' if finish_reason == 'length' else 'completed'
def _map_anthropic_stop_reason(stop_reason: str) -> str:
"""将 Anthropic 的 stop_reason 映射为聊天补全风格的结束原因。"""
return {'tool_use': 'tool_calls', 'max_tokens': 'length'}.get(stop_reason, 'stop')
@ -950,6 +985,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):
@ -969,6 +1005,7 @@ def _extract_text(content: Any) -> str:
def _content_to_text(content: Any) -> str:
"""将任意 content 载荷转换为单个字符串。"""
if isinstance(content, str):
return content
if isinstance(content, list):
@ -977,6 +1014,7 @@ def _content_to_text(content: Any) -> str:
def _content_to_responses_parts(content: Any) -> list[JsonDict]:
"""将普通消息内容转换为 Responses `input_text` 数组。"""
if isinstance(content, list):
text = _extract_text(content)
else:
@ -985,6 +1023,7 @@ def _content_to_responses_parts(content: Any) -> list[JsonDict]:
def _stringify_output(content: Any) -> str:
"""将工具输出统一序列化为字符串,便于放入 `function_call_output`。"""
if isinstance(content, str):
return content
if content is None:
@ -993,6 +1032,7 @@ def _stringify_output(content: Any) -> str:
def _build_responses_function_call_item(tool_call: JsonDict) -> JsonDict:
"""将 CC `tool_call` 结构转换为 Responses `function_call` 输入项。"""
function_data = tool_call.get('function') or {}
return {
'type': 'function_call',
@ -1003,6 +1043,7 @@ def _build_responses_function_call_item(tool_call: JsonDict) -> JsonDict:
def _convert_cc_tools_to_responses(tools: Any) -> list[JsonDict]:
"""将聊天补全风格的工具定义转换为 Responses `tools` 列表。"""
if not isinstance(tools, list):
return []
@ -1024,6 +1065,7 @@ def _convert_cc_tools_to_responses(tools: Any) -> list[JsonDict]:
def _convert_tools(tools: Any) -> list[JsonDict]:
"""规范化 Responses 请求中的工具定义列表。"""
if not isinstance(tools, list):
return []
@ -1036,6 +1078,7 @@ def _convert_tools(tools: Any) -> list[JsonDict]:
def _convert_tool_definition(tool: Any) -> JsonDict | None:
"""将扁平工具定义补成标准 Chat Completions `function` 工具格式。"""
if not isinstance(tool, dict):
return None
if tool.get('type') != 'function':

14
app.py
View file

@ -19,6 +19,11 @@ logger = logging.getLogger(__name__)
def create_app():
"""创建并配置 Flask 应用实例。
这里统一完成跨路由共享的初始化逻辑包括配置加载跨域错误处理
访问鉴权健康检查以及蓝图注册
"""
app = Flask(__name__)
CORS(app)
settings.load()
@ -27,20 +32,28 @@ def create_app():
@app.errorhandler(404)
def not_found(e):
"""将未匹配到的路径统一转换为 JSON 404 响应。"""
return jsonify({'error': {'message': '未找到', 'type': 'not_found'}}), 404
@app.errorhandler(405)
def method_not_allowed(e):
"""将不支持的请求方法统一转换为 JSON 405 响应。"""
return jsonify({'error': {'message': '方法不允许', 'type': 'method_not_allowed'}}), 405
@app.errorhandler(500)
def internal_error(e):
"""将未捕获的服务端异常统一包装为 JSON 500 响应。"""
return jsonify({'error': {'message': '服务器内部错误', 'type': 'server_error'}}), 500
# ─── 全局鉴权中间件 ──────────────────────────
@app.before_request
def check_access():
"""在进入业务路由前校验访问密钥。
当配置了 `ACCESS_API_KEY` 除健康检查和管理面板相关路径外
所有请求都必须携带正确的 Bearer Token `x-api-key`
"""
if not Config.ACCESS_API_KEY:
return
@ -61,6 +74,7 @@ def create_app():
@app.route('/health', methods=['GET'])
def health():
"""返回服务健康状态和当前生效的上游地址。"""
return jsonify({'status': 'ok', 'target': settings.get_url()})
# ─── 注册路由蓝图 ────────────────────────────

View file

@ -4,6 +4,12 @@ import os
class Config:
"""集中声明服务运行依赖的环境变量配置。
这个类不承担运行时逻辑只作为模块级配置容器统一暴露上游地址
鉴权密钥端口超时和调试开关供应用启动路由鉴权和请求转发层共享
"""
# 上游 API 地址
PROXY_TARGET_URL = os.getenv('PROXY_TARGET_URL', 'https://api.anthropic.com')
# 上游 API 密钥

View file

@ -27,11 +27,13 @@ bp = Blueprint('admin', __name__)
@bp.route('/admin')
@bp.route('/admin/')
def admin_page():
"""返回管理面板首页 HTML 页面,供浏览器进入配置界面。"""
return send_from_directory(_STATIC_DIR, 'admin.html')
@bp.route('/static/<path:filename>')
def static_files(filename):
"""提供管理面板所需的静态资源文件。"""
return send_from_directory(_STATIC_DIR, filename)
@ -40,6 +42,7 @@ def static_files(filename):
@bp.route('/v1/models', methods=['GET'])
def list_models():
"""返回当前配置的模型列表,供 Cursor 拉取可用模型。"""
mappings = settings.get().get('model_mappings', {})
models = [{
'id': name,
@ -61,6 +64,7 @@ def list_models():
@bp.route('/api/admin/login', methods=['POST'])
def admin_login():
"""校验管理面板登录密钥,并返回是否允许进入后台。"""
data = request.get_json(force=True)
if not Config.ACCESS_API_KEY:
return jsonify({'ok': True, 'message': '未配置鉴权'})
@ -74,6 +78,7 @@ def admin_login():
@bp.route('/api/admin/settings', methods=['GET'])
def get_settings():
"""读取当前生效的全局代理配置。"""
err = _check_auth()
if err:
return err
@ -88,6 +93,7 @@ def get_settings():
@bp.route('/api/admin/settings', methods=['PUT'])
def update_settings():
"""更新全局上游地址与密钥配置。"""
err = _check_auth()
if err:
return err
@ -104,6 +110,7 @@ def update_settings():
@bp.route('/api/admin/mappings', methods=['GET'])
def list_mappings():
"""列出所有模型映射配置,供管理面板读取和展示。"""
err = _check_auth()
if err:
return err
@ -112,6 +119,7 @@ def list_mappings():
@bp.route('/api/admin/mappings', methods=['POST'])
def add_mapping():
"""新增一条模型映射,并写入持久化配置。"""
err = _check_auth()
if err:
return err
@ -133,6 +141,7 @@ def add_mapping():
@bp.route('/api/admin/mappings/<path:name>', methods=['PUT'])
def update_mapping(name):
"""更新指定名称的模型映射,必要时支持重命名。"""
err = _check_auth()
if err:
return err
@ -158,6 +167,7 @@ def update_mapping(name):
@bp.route('/api/admin/mappings/<path:name>', methods=['DELETE'])
def delete_mapping(name):
"""删除指定名称的模型映射,并在存在时同步保存配置。"""
err = _check_auth()
if err:
return err
@ -185,7 +195,10 @@ def _check_auth():
def _save_and_respond(data, log_msg):
"""保存配置并返回响应"""
"""保存配置并返回统一成功响应。
当写盘失败时这里也负责把异常转成结构化的 JSON 错误返回
"""
try:
settings.save(data)
except OSError as e:

View file

@ -149,6 +149,7 @@ def _handle_openai_stream(
payload['stream'] = True
def generate():
"""消费上游 OpenAI SSE并逐段产出给 Cursor 的聊天补全流。"""
resp, err = forward_request(url, headers, payload, stream=True)
if err:
yield chat_error_chunk(str(err))
@ -235,6 +236,7 @@ def _handle_responses_stream(
converter = ResponsesToCCStreamConverter(model=ctx.client_model)
def generate():
"""消费上游 Responses 事件,并实时转换成聊天补全 chunk。"""
resp, err = forward_request(url, headers, payload, stream=True)
if err:
yield chat_error_chunk(str(err))
@ -314,6 +316,7 @@ def _handle_anthropic_stream(
converter = AnthropicStreamConverter()
def generate():
"""消费上游 Anthropic 事件流,并逐步映射为聊天补全 SSE。"""
resp, err = forward_request(url, headers, payload, stream=True)
if err:
yield chat_error_chunk(str(err))

View file

@ -19,7 +19,11 @@ logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class RouteContext:
"""数据面路由使用的标准请求上下文。"""
"""数据面路由使用的标准请求上下文。
路由层会先根据客户端模型名解析出统一上下文后续处理函数只需要关心
上游模型后端类型目标地址鉴权信息和流式标记而不必重复访问配置层
"""
client_model: str
upstream_model: str

View file

@ -22,6 +22,7 @@ bp = Blueprint('messages', __name__)
@bp.route('/v1/messages', methods=['POST'])
def messages_passthrough():
"""透传 Anthropic Messages 请求,并在必要时补齐 thinking 兼容层。"""
payload = request.get_json(force=True)
model = payload.get('model', 'unknown')
is_stream = payload.get('stream', False)
@ -43,6 +44,7 @@ def messages_passthrough():
# 流式透传
def generate():
"""建立上游流式连接并逐段回传处理后的 SSE 数据。"""
try:
resp = req_lib.post(
url, headers=headers, json=payload,
@ -132,7 +134,7 @@ def _process_stream(resp):
def _emit_thinking_blocks(text):
"""生成 thinking block 的 SSE 事件序列"""
"""生成一组等价的 Anthropic thinking block SSE 事件。"""
yield (
f'event: content_block_start\n'
f'data: {json.dumps({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}})}\n\n'

View file

@ -126,6 +126,7 @@ def _handle_openai_stream(
converter = ResponsesStreamConverter(model=ctx.client_model)
def generate():
"""消费 OpenAI 聊天补全流,并实时改写为 Responses SSE。"""
yield from converter.start_events()
resp, err = forward_request(url, headers, cc_payload, stream=True)
@ -205,6 +206,7 @@ def _handle_responses_stream(
converter = ResponsesStreamConverter(model=ctx.client_model)
def generate():
"""透传上游原生 Responses 流,并做轻量模型名改写。"""
resp, err = forward_request(url, headers, payload, stream=True)
if err:
yield responses_error_event(str(err))
@ -275,6 +277,7 @@ def _handle_anthropic_stream(
converter = ResponsesStreamConverter(model=ctx.client_model)
def generate():
"""消费 Anthropic SSE并直接映射为 Responses 事件序列。"""
yield from converter.start_events()
resp, err = forward_request(url, headers, anthropic_payload, stream=True)

View file

@ -27,7 +27,10 @@ _DEFAULTS = {
def load():
"""从文件加载配置"""
"""从持久化文件读取配置并刷新内存缓存。
如果配置文件不存在或内容损坏会回退到默认值保证服务仍然可以正常启动
"""
global _cache
with _lock:
if os.path.exists(SETTINGS_FILE):
@ -42,7 +45,10 @@ def load():
def save(data):
"""保存配置到文件"""
"""将当前配置写回到持久化文件并同步缓存。
保存前会确保数据目录存在并始终以默认配置为基底合并缺失字段
"""
global _cache
with _lock:
os.makedirs(DATA_DIR, exist_ok=True)
@ -52,24 +58,24 @@ def save(data):
def get():
"""获取当前配置(优先使用缓存)"""
"""获取当前配置快照,优先返回内存缓存中的结果。"""
if _cache is None:
return load()
return dict(_cache)
def get_url():
"""获取生效的上游 URL配置文件优先环境变量兜底"""
"""获取当前生效的上游 URL优先使用持久化配置。"""
return get().get('proxy_target_url') or Config.PROXY_TARGET_URL
def get_key():
"""获取生效的 API 密钥:配置文件优先,环境变量兜底"""
"""获取当前生效的 API 密钥,优先使用持久化配置。"""
return get().get('proxy_api_key') or Config.PROXY_API_KEY
def resolve_model(model_name):
"""解析模型映射,返回 {upstream_model, backend, target_url, api_key}"""
"""解析模型映射并返回完整的上游路由信息。"""
settings = get()
mappings = settings.get('model_mappings', {})
base_url, base_key = get_url(), get_key()
@ -95,6 +101,10 @@ def resolve_model(model_name):
def _auto_detect(name):
"""根据模型名自动判断后端类型"""
"""根据模型名关键字推断默认后端协议类型。
当前规则较为保守命中 `claude` `anthropic` Anthropic
其余模型默认视为 OpenAI 兼容后端
"""
lower = (name or '').lower()
return 'anthropic' if ('claude' in lower or 'anthropic' in lower) else 'openai'

View file

@ -19,6 +19,7 @@ from app import create_app
def main():
"""加载应用并以 Waitress 方式启动代理服务。"""
app = create_app()
print(f'代理服务启动于 0.0.0.0:{Config.PROXY_PORT}')
print(f'上游地址: {Config.PROXY_TARGET_URL}')

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def gen_id(prefix: str = '') -> str:
"""生成唯一 ID"""
"""生成带可选前缀的短随机 ID用于请求和工具调用标识。"""
return f'{prefix}{uuid.uuid4().hex[:24]}'
@ -57,7 +57,7 @@ def sse_response(generator):
def error_json(message, error_type='proxy_error', status=502):
"""建 JSON 错误响应"""
"""造统一的 JSON 错误响应,供非流式链路直接返回。"""
return jsonify({'error': {'message': str(message), 'type': error_type}}), status

View file

@ -33,6 +33,7 @@ class ThinkTagExtractor:
"""
def __init__(self):
"""初始化跨 chunk 的 thinking 状态跟踪。"""
self._in_thinking = False
def process_chunk(self, chunk):