api2cursor/routes/responses.py
2026-03-09 14:18:42 +08:00

126 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""路由: /v1/responses
处理 Cursor 对 GPT/Claude-Opus 等模型发出的 Responses API 格式请求。
转换为 CC 格式后分发到对应后端,响应再转回 Responses 格式。
"""
import json
import logging
from flask import Blueprint, request, jsonify
import settings
from adapters.responses_adapter import responses_to_cc, cc_to_responses, ResponsesStreamConverter
from adapters.openai_fixer import normalize_request, fix_response, fix_stream_chunk
from adapters.openai_anthropic import cc_to_messages_request, messages_to_cc_response
from utils.http import (
build_openai_headers, build_anthropic_headers,
forward_request, sse_response,
iter_openai_sse, iter_anthropic_sse,
)
from utils.think_tag import ThinkTagExtractor
logger = logging.getLogger(__name__)
bp = Blueprint('responses', __name__)
@bp.route('/v1/responses', methods=['POST'])
def responses_endpoint():
payload = request.get_json(force=True)
model = payload.get('model', 'unknown')
is_stream = payload.get('stream', False)
mapping = settings.resolve_model(model)
backend = mapping['backend']
upstream = mapping['upstream_model']
url_base = mapping['target_url']
api_key = mapping['api_key']
logger.info(f'[Responses] {model}{upstream} 后端={backend} 流式={is_stream}')
# Responses → CC
cc_payload = responses_to_cc(payload)
cc_payload['model'] = upstream
if backend == 'openai':
return _via_openai(cc_payload, url_base, api_key, is_stream, model)
else:
return _via_anthropic(cc_payload, url_base, api_key, is_stream, model)
# ─── OpenAI 后端 ──────────────────────────────────
def _via_openai(cc_payload, url_base, api_key, is_stream, display_model):
"""通过 OpenAI 后端处理"""
cc_payload = normalize_request(cc_payload)
headers = build_openai_headers(api_key)
url = f'{url_base.rstrip("/")}/v1/chat/completions'
if not is_stream:
cc_payload['stream'] = False
resp, err = forward_request(url, headers, cc_payload)
if err:
return err
return jsonify(cc_to_responses(fix_response(resp.json()), display_model))
# 流式处理
cc_payload['stream'] = True
converter = ResponsesStreamConverter(model=display_model)
def generate():
yield from converter.start_events()
resp, err = forward_request(url, headers, cc_payload, stream=True)
if err:
yield f'event: error\ndata: {json.dumps({"error": err})}\n\n'
return
think_ext = ThinkTagExtractor()
for chunk in iter_openai_sse(resp):
if chunk is None:
yield from converter.finalize()
return
chunk = fix_stream_chunk(chunk)
for out in think_ext.process_chunk(chunk):
yield from converter.process_cc_chunk(out)
return sse_response(generate())
# ─── Anthropic 后端 ───────────────────────────────
def _via_anthropic(cc_payload, url_base, api_key, is_stream, display_model):
"""通过 Anthropic 后端处理"""
anthropic_payload = cc_to_messages_request(cc_payload)
headers = build_anthropic_headers(api_key)
url = f'{url_base.rstrip("/")}/v1/messages'
if not is_stream:
anthropic_payload['stream'] = False
resp, err = forward_request(url, headers, anthropic_payload)
if err:
return err
cc_data = messages_to_cc_response(resp.json())
return jsonify(cc_to_responses(cc_data, display_model))
# 流式处理Anthropic SSE → Responses SSE跳过 CC 中间态)
anthropic_payload['stream'] = True
converter = ResponsesStreamConverter(model=display_model)
def generate():
yield from converter.start_events()
resp, err = forward_request(url, headers, anthropic_payload, stream=True)
if err:
yield f'event: error\ndata: {json.dumps({"error": err})}\n\n'
return
for event_type, event_data in iter_anthropic_sse(resp):
yield from converter.process_anthropic_event(event_type, event_data)
yield from converter.finalize()
return sse_response(generate())