初始化提交
This commit is contained in:
commit
202731df74
28 changed files with 3140 additions and 0 deletions
147
routes/messages.py
Normal file
147
routes/messages.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"""路由: /v1/messages
|
||||
|
||||
Anthropic Messages API 透传。当 Cursor 直接发送 Anthropic 格式请求时,
|
||||
直接转发到上游并原样返回。处理非标准的 reasoning_content 字段,
|
||||
将其注入为标准的 thinking blocks。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests as req_lib
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
import settings
|
||||
from config import Config
|
||||
from utils.http import build_anthropic_headers, forward_request, sse_response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('messages', __name__)
|
||||
|
||||
|
||||
@bp.route('/v1/messages', methods=['POST'])
|
||||
def messages_passthrough():
|
||||
payload = request.get_json(force=True)
|
||||
model = payload.get('model', 'unknown')
|
||||
is_stream = payload.get('stream', False)
|
||||
|
||||
logger.info(f'[透传] model={model} 流式={is_stream}')
|
||||
|
||||
url_base = settings.get_url()
|
||||
api_key = settings.get_key()
|
||||
headers = build_anthropic_headers(api_key)
|
||||
url = f'{url_base.rstrip("/")}/v1/messages'
|
||||
|
||||
if not is_stream:
|
||||
resp, err = forward_request(url, headers, payload)
|
||||
if err:
|
||||
return err
|
||||
data = resp.json()
|
||||
_inject_thinking(data)
|
||||
return jsonify(data)
|
||||
|
||||
# 流式透传
|
||||
def generate():
|
||||
try:
|
||||
resp = req_lib.post(
|
||||
url, headers=headers, json=payload,
|
||||
timeout=Config.API_TIMEOUT, stream=True,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
body = resp.content.decode('utf-8', errors='replace')
|
||||
logger.warning(f'上游返回 {resp.status_code}: {body[:300]}')
|
||||
yield f'data: {json.dumps({"error": {"message": body, "type": "upstream_error"}})}\n\n'
|
||||
return
|
||||
|
||||
yield from _process_stream(resp)
|
||||
except req_lib.RequestException as e:
|
||||
logger.error(f'请求上游失败: {e}')
|
||||
yield f'data: {json.dumps({"error": {"message": str(e), "type": "proxy_error"}})}\n\n'
|
||||
|
||||
return sse_response(generate())
|
||||
|
||||
|
||||
# ─── 内部辅助 ─────────────────────────────────────
|
||||
|
||||
|
||||
def _inject_thinking(data):
|
||||
"""将非标准 reasoning_content 字段注入为 Anthropic thinking block"""
|
||||
rc = data.pop('reasoning_content', None) or data.pop('reasoningContent', None)
|
||||
if not rc:
|
||||
return
|
||||
|
||||
content = data.get('content')
|
||||
if not isinstance(content, list):
|
||||
content = []
|
||||
|
||||
# 避免重复注入
|
||||
if any(isinstance(b, dict) and b.get('type') == 'thinking' for b in content):
|
||||
return
|
||||
|
||||
content.insert(0, {'type': 'thinking', 'thinking': rc})
|
||||
data['content'] = content
|
||||
logger.info(f'已注入 thinking block ({len(rc)} 字符)')
|
||||
|
||||
|
||||
def _process_stream(resp):
|
||||
"""处理 /v1/messages 流式响应,检测并注入 thinking 事件"""
|
||||
reasoning_buf = ''
|
||||
injected = False
|
||||
|
||||
for line in resp.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
decoded = line.decode('utf-8', errors='replace')
|
||||
|
||||
if not decoded.startswith('data:'):
|
||||
yield decoded + '\n\n'
|
||||
continue
|
||||
|
||||
data_str = decoded[5:].strip()
|
||||
if not data_str:
|
||||
yield decoded + '\n\n'
|
||||
continue
|
||||
|
||||
try:
|
||||
event_data = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
yield decoded + '\n\n'
|
||||
continue
|
||||
|
||||
modified = False
|
||||
|
||||
# 提取 reasoning_content
|
||||
for container_key in ('message', 'delta'):
|
||||
container = event_data.get(container_key)
|
||||
if not container:
|
||||
continue
|
||||
rc = container.pop('reasoning_content', None) or container.pop('reasoningContent', None)
|
||||
if rc:
|
||||
reasoning_buf += rc
|
||||
modified = True
|
||||
|
||||
# 在首个 text_delta 前注入 thinking blocks
|
||||
if reasoning_buf and not injected:
|
||||
if event_data.get('delta', {}).get('type') == 'text_delta':
|
||||
injected = True
|
||||
yield from _emit_thinking_blocks(reasoning_buf)
|
||||
reasoning_buf = ''
|
||||
|
||||
yield f'data: {json.dumps(event_data)}\n\n' if modified else decoded + '\n\n'
|
||||
|
||||
|
||||
def _emit_thinking_blocks(text):
|
||||
"""生成 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'
|
||||
)
|
||||
yield (
|
||||
f'event: content_block_delta\n'
|
||||
f'data: {json.dumps({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": text}})}\n\n'
|
||||
)
|
||||
yield (
|
||||
f'event: content_block_stop\n'
|
||||
f'data: {json.dumps({"type": "content_block_stop", "index": 0})}\n\n'
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue