初始化提交
This commit is contained in:
commit
202731df74
28 changed files with 3140 additions and 0 deletions
126
routes/responses.py
Normal file
126
routes/responses.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""路由: /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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue