"""Responses API 适配 Cursor 对 GPT/Claude-Opus 等模型使用 /v1/responses 格式。 本模块将 Responses 格式与 Chat Completions 格式互相转换: 请求: Responses → CC 响应: CC → Responses(非流式 + 流式) 流式: 支持从 CC chunks 或 Anthropic SSE 事件直接转换 """ import json import logging from utils.http import gen_id logger = logging.getLogger(__name__) # ═══════════════════════════════════════════════════════════ # 请求转换: Responses → CC # ═══════════════════════════════════════════════════════════ def responses_to_cc(payload): """将 /v1/responses 请求转换为 /v1/chat/completions 格式""" messages = [] if payload.get('instructions'): messages.append({'role': 'system', 'content': payload['instructions']}) input_data = payload.get('input', []) if isinstance(input_data, str): messages.append({'role': 'user', 'content': input_data}) elif isinstance(input_data, list): _convert_input_items(input_data, messages) result = { 'model': payload.get('model', ''), 'messages': messages, 'stream': payload.get('stream', False), } if 'tools' in payload: result['tools'] = _convert_tools(payload['tools']) for key in ('temperature', 'top_p'): if key in payload: result[key] = payload[key] if 'max_output_tokens' in payload: result['max_tokens'] = payload['max_output_tokens'] if 'tool_choice' in payload: result['tool_choice'] = payload['tool_choice'] return result # ═══════════════════════════════════════════════════════════ # 非流式响应转换: CC → Responses # ═══════════════════════════════════════════════════════════ def cc_to_responses(cc_resp, model=''): """将 CC 响应转换为 Responses 格式""" choice = (cc_resp.get('choices') or [{}])[0] msg = choice.get('message') or {} finish = choice.get('finish_reason', 'stop') output = [] if msg.get('reasoning_content'): output.append({ 'type': 'reasoning', 'id': gen_id('rs_'), 'summary': [{'type': 'summary_text', 'text': msg['reasoning_content']}], }) if msg.get('content'): output.append({ 'type': 'message', 'id': gen_id('msg_'), 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'output_text', 'text': msg['content']}], }) for tc in (msg.get('tool_calls') or []): func = tc.get('function') or {} output.append({ 'type': 'function_call', 'id': gen_id('fc_'), 'status': 'completed', 'call_id': tc.get('id', gen_id('call_')), 'name': func.get('name', ''), 'arguments': func.get('arguments', '{}'), }) usage = cc_resp.get('usage', {}) return { 'id': cc_resp.get('id', gen_id('resp_')), 'object': 'response', 'status': 'incomplete' if finish == 'length' else 'completed', 'model': model or cc_resp.get('model', ''), 'output': output, 'usage': { 'input_tokens': usage.get('prompt_tokens', 0), 'output_tokens': usage.get('completion_tokens', 0), 'total_tokens': usage.get('total_tokens', 0), }, } # ═══════════════════════════════════════════════════════════ # 流式转换器: CC chunks / Anthropic SSE → Responses SSE # ═══════════════════════════════════════════════════════════ class ResponsesStreamConverter: """有状态转换器:将 CC 流式 chunk 或 Anthropic SSE 事件转为 Responses SSE 事件""" def __init__(self, response_id=None, model=''): self.resp_id = response_id or gen_id('resp_') self.model = model # 思考内容缓冲 self._rs_buf = '' self._rs_started = False self._rs_closed = False self._rs_id = gen_id('rs_') # 文本内容缓冲 self._text_buf = '' self._text_started = False self._text_closed = False self._msg_id = gen_id('msg_') # 工具调用缓冲 {index: {name, args, call_id, fc_id}} self._tools = {} self._output_items = [] self._finished = False self._input_tokens = 0 # ─── 公开接口 ───────────────────────────────── def start_events(self): """生成流开始事件""" return [self._sse('response.created', { 'id': self.resp_id, 'object': 'response', 'status': 'in_progress', 'model': self.model, 'output': [], })] def process_cc_chunk(self, chunk): """处理 CC 格式的流式 chunk,返回 Responses SSE 事件列表""" events = [] for choice in (chunk.get('choices') or []): delta = choice.get('delta') or {} finish = choice.get('finish_reason') if delta.get('reasoning_content'): events.extend(self._on_reasoning(delta['reasoning_content'])) if delta.get('content') is not None and delta['content'] != '': events.extend(self._on_text(delta['content'])) for tc in (delta.get('tool_calls') or []): events.extend(self._on_tool_call(tc)) if finish and not self._finished: self._finished = True events.extend(self._do_finish(finish, chunk.get('usage'))) return events def process_anthropic_event(self, event_type, event_data): """直接处理 Anthropic SSE 事件(跳过 CC 中间转换,更高效)""" events = [] if event_type == 'message_start': usage = event_data.get('message', {}).get('usage', {}) self._input_tokens = usage.get('input_tokens', 0) elif event_type == 'content_block_start': block = event_data.get('content_block', {}) btype = block.get('type', '') if btype == 'thinking' and not self._rs_started: self._rs_started = True events.append(self._sse('response.output_item.added', { 'type': 'reasoning', 'id': self._rs_id, 'summary': [], })) elif btype == 'text': events.extend(self._ensure_text_started()) elif btype == 'tool_use': events.extend(self._start_tool_from_block(block)) elif event_type == 'content_block_delta': delta = event_data.get('delta', {}) dtype = delta.get('type', '') if dtype == 'thinking_delta' and delta.get('thinking'): self._rs_buf += delta['thinking'] events.append(self._sse('response.reasoning_summary_text.delta', { 'type': 'summary_text', 'delta': delta['thinking'], })) elif dtype == 'text_delta' and delta.get('text'): self._text_buf += delta['text'] events.append(self._sse('response.output_text.delta', { 'type': 'output_text', 'delta': delta['text'], })) elif dtype == 'input_json_delta' and delta.get('partial_json') and self._tools: idx = max(self._tools.keys()) self._tools[idx]['args'] += delta['partial_json'] events.append(self._sse('response.function_call_arguments.delta', { 'type': 'function_call', 'delta': delta['partial_json'], })) elif event_type == 'message_delta': delta = event_data.get('delta', {}) stop = delta.get('stop_reason', 'end_turn') usage = event_data.get('usage', {}) finish = {'tool_use': 'tool_calls', 'max_tokens': 'length'}.get(stop, 'stop') if not self._finished: self._finished = True u = { 'input_tokens': self._input_tokens, 'output_tokens': usage.get('output_tokens', 0), 'total_tokens': self._input_tokens + usage.get('output_tokens', 0), } events.extend(self._do_finish(finish, u)) return events def finalize(self): """流结束时补发未关闭的事件""" if self._finished: return [] self._finished = True return self._do_finish('stop', None) # ─── 内部事件处理 ───────────────────────────── def _on_reasoning(self, text): """处理思考内容 delta""" events = [] if not self._rs_started: self._rs_started = True events.append(self._sse('response.output_item.added', { 'type': 'reasoning', 'id': self._rs_id, 'summary': [], })) self._rs_buf += text events.append(self._sse('response.reasoning_summary_text.delta', { 'type': 'summary_text', 'delta': text, })) return events def _on_text(self, text): """处理文本内容 delta""" events = self._ensure_text_started() self._text_buf += text events.append(self._sse('response.output_text.delta', { 'type': 'output_text', 'delta': text, })) return events def _on_tool_call(self, tc): """处理工具调用 delta""" events = [] idx = tc.get('index', 0) func = tc.get('function') or {} if idx not in self._tools: if self._rs_started and not self._rs_closed: events.extend(self._close_reasoning()) if self._text_started and not self._text_closed: events.extend(self._close_text()) call_id = tc.get('id', gen_id('call_')) name = func.get('name', '') fc_id = gen_id('fc_') self._tools[idx] = {'name': name, 'args': '', 'call_id': call_id, 'fc_id': fc_id} events.append(self._sse('response.output_item.added', { 'type': 'function_call', 'id': fc_id, 'status': 'in_progress', 'call_id': call_id, 'name': name, 'arguments': '', })) if func.get('name'): self._tools[idx]['name'] = func['name'] if func.get('arguments', ''): self._tools[idx]['args'] += func['arguments'] events.append(self._sse('response.function_call_arguments.delta', { 'type': 'function_call', 'delta': func['arguments'], })) return events def _ensure_text_started(self): """确保文本输出项已开始""" events = [] if self._rs_started and not self._rs_closed: events.extend(self._close_reasoning()) if not self._text_started: self._text_started = True events.append(self._sse('response.output_item.added', { 'type': 'message', 'id': self._msg_id, 'status': 'in_progress', 'role': 'assistant', 'content': [], })) events.append(self._sse('response.content_part.added', { 'type': 'output_text', 'text': '', })) return events def _start_tool_from_block(self, block): """从 Anthropic tool_use block 开始新的工具调用""" events = [] if self._rs_started and not self._rs_closed: events.extend(self._close_reasoning()) if self._text_started and not self._text_closed: events.extend(self._close_text()) idx = len(self._tools) tool_id = block.get('id', gen_id('toolu_')) name = block.get('name', '') fc_id = gen_id('fc_') self._tools[idx] = {'name': name, 'args': '', 'call_id': tool_id, 'fc_id': fc_id} events.append(self._sse('response.output_item.added', { 'type': 'function_call', 'id': fc_id, 'status': 'in_progress', 'call_id': tool_id, 'name': name, 'arguments': '', })) return events # ─── 关闭/结束事件 ──────────────────────────── def _close_reasoning(self): if self._rs_closed: return [] self._rs_closed = True rs = { 'type': 'reasoning', 'id': self._rs_id, 'summary': [{'type': 'summary_text', 'text': self._rs_buf}], } self._output_items.append(rs) return [ self._sse('response.reasoning_summary_text.done', { 'type': 'summary_text', 'text': self._rs_buf, }), self._sse('response.output_item.done', rs), ] def _close_text(self): if self._text_closed: return [] self._text_closed = True msg = { 'type': 'message', 'id': self._msg_id, 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'output_text', 'text': self._text_buf}], } self._output_items.append(msg) return [ self._sse('response.output_text.done', {'type': 'output_text', 'text': self._text_buf}), self._sse('response.output_item.done', msg), ] def _do_finish(self, finish_reason, usage): """生成流结束的所有关闭事件""" events = [] if self._rs_started and not self._rs_closed: events.extend(self._close_reasoning()) if self._text_started and not self._text_closed: events.extend(self._close_text()) for idx in sorted(self._tools.keys()): buf = self._tools[idx] events.append(self._sse('response.function_call_arguments.done', { 'type': 'function_call', 'arguments': buf['args'], })) fc = { 'type': 'function_call', 'id': buf['fc_id'], 'status': 'completed', 'call_id': buf['call_id'], 'name': buf['name'], 'arguments': buf['args'], } events.append(self._sse('response.output_item.done', fc)) self._output_items.append(fc) usage_data = usage if isinstance(usage, dict) else {} events.append(self._sse('response.completed', { 'id': self.resp_id, 'object': 'response', 'status': 'incomplete' if finish_reason == 'length' else 'completed', 'model': self.model, 'output': self._output_items, 'usage': usage_data, })) return events def _sse(self, event_type, data): """构建 SSE 事件字符串""" return f'event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n' # ═══════════════════════════════════════════════════════════ # 内部辅助函数 # ═══════════════════════════════════════════════════════════ def _convert_input_items(items, messages): """将 Responses input 数组转换为 CC messages""" i = 0 while i < len(items): item = items[i] if isinstance(item, str): messages.append({'role': 'user', 'content': item}) i += 1 continue if not isinstance(item, dict): i += 1 continue item_type = item.get('type', '') role = item.get('role', '') # 简单角色消息(无 type 字段) if role and not item_type: content = item.get('content', '') if isinstance(content, list): content = _extract_text(content) messages.append({'role': role, 'content': content or ''}) i += 1 continue # Responses message 对象 if item_type == 'message' or (role and not item_type): role = item.get('role', 'assistant') content = _extract_text(item.get('content', [])) msg = {'role': role, 'content': content or ''} if role == 'assistant': tool_calls, consumed = _collect_function_calls(items, i + 1) if tool_calls: msg['tool_calls'] = tool_calls if not msg['content']: msg['content'] = None messages.append(msg) i += 1 + consumed continue messages.append(msg) i += 1 continue # function_call(工具调用) if item_type == 'function_call': tc = { 'id': item.get('call_id') or gen_id('call_'), 'type': 'function', 'function': { 'name': item.get('name', ''), 'arguments': item.get('arguments', '{}'), }, } if messages and messages[-1]['role'] == 'assistant': messages[-1].setdefault('tool_calls', []).append(tc) if not messages[-1].get('content'): messages[-1]['content'] = None else: messages.append({'role': 'assistant', 'content': None, 'tool_calls': [tc]}) i += 1 continue # function_call_output(工具结果) if item_type == 'function_call_output': output = item.get('output', '') if not isinstance(output, str): output = json.dumps(output, ensure_ascii=False) messages.append({ 'role': 'tool', 'tool_call_id': item.get('call_id', ''), 'content': output, }) i += 1 continue if role: messages.append({'role': role, 'content': str(item.get('content', ''))}) i += 1 def _collect_function_calls(items, start): """收集紧随 assistant message 之后的连续 function_call 项""" tool_calls = [] j = start while j < len(items): nxt = items[j] if isinstance(nxt, dict) and nxt.get('type') == 'function_call': tool_calls.append({ 'id': nxt.get('call_id') or gen_id('call_'), 'type': 'function', 'function': { 'name': nxt.get('name', ''), 'arguments': nxt.get('arguments', '{}'), }, }) j += 1 else: break return tool_calls, j - start def _extract_text(content): """从 content 中提取纯文本""" if isinstance(content, str): return content if not isinstance(content, list): return str(content) if content else '' texts = [] for part in content: if isinstance(part, str): texts.append(part) elif isinstance(part, dict): t = part.get('type', '') if t in ('output_text', 'input_text', 'text'): texts.append(part.get('text', '')) elif t == 'refusal': texts.append(part.get('refusal', '')) return '\n'.join(texts) if texts else '' def _convert_tools(tools): """将 Responses tools 转为 CC tools 格式""" result = [] for t in tools: if t.get('type') != 'function': continue if 'function' in t: result.append(t) else: result.append({ 'type': 'function', 'function': { 'name': t.get('name', ''), 'description': t.get('description', ''), 'parameters': t.get('parameters', {'type': 'object', 'properties': {}}), }, }) return result