api2cursor/utils/http.py
2026-03-09 14:18:42 +08:00

131 lines
4 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.

"""HTTP 工具 - 请求头构建、上游转发、SSE 流解析、响应构建"""
import json
import uuid
import logging
import requests
from flask import Response, jsonify
from config import Config
logger = logging.getLogger(__name__)
def gen_id(prefix=''):
"""生成唯一 ID"""
return f'{prefix}{uuid.uuid4().hex[:24]}'
# ─── 请求头构建 ────────────────────────────────────
def build_openai_headers(api_key):
"""构建 OpenAI 兼容请求头"""
return {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
}
def build_anthropic_headers(api_key):
"""构建 Anthropic 请求头,根据密钥前缀自动选择鉴权方式"""
headers = {
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
}
if api_key.startswith('sk-'):
headers['x-api-key'] = api_key
else:
headers['Authorization'] = f'Bearer {api_key}'
return headers
# ─── 响应构建 ──────────────────────────────────────
def sse_response(generator):
"""将生成器包装为 SSE 流式响应"""
return Response(
generator,
content_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
)
def error_json(message, error_type='proxy_error', status=502):
"""构建 JSON 错误响应"""
return jsonify({'error': {'message': str(message), 'type': error_type}}), status
# ─── 上游请求转发 ──────────────────────────────────
def forward_request(url, headers, payload, stream=False):
"""转发请求到上游 API
返回值:
成功: (response, None)
失败(流式): (None, error_body_str)
失败(非流式): (None, Flask Response)
"""
try:
resp = requests.post(
url, headers=headers, json=payload,
timeout=Config.API_TIMEOUT, stream=stream,
)
if resp.status_code != 200:
body = resp.content.decode('utf-8', errors='replace')
logger.warning(f'上游返回 {resp.status_code}: {body[:300]}')
if stream:
return None, f'上游错误 {resp.status_code}: {body}'
return None, Response(
resp.content, status=resp.status_code,
content_type=resp.headers.get('Content-Type', 'application/json'),
)
return resp, None
except requests.RequestException as e:
logger.error(f'请求上游失败: {e}')
if stream:
return None, str(e)
return None, error_json(str(e))
# ─── SSE 流解析 ───────────────────────────────────
def iter_openai_sse(response):
"""解析 OpenAI SSE 流yield chunk 字典yield None 表示 [DONE]"""
for line in response.iter_lines():
if not line:
continue
decoded = line.decode('utf-8', errors='replace')
if not decoded.startswith('data:'):
continue
data_str = decoded[5:].strip()
if data_str == '[DONE]':
yield None
return
try:
yield json.loads(data_str)
except json.JSONDecodeError:
continue
def iter_anthropic_sse(response):
"""解析 Anthropic SSE 流yield (event_type, data_dict) 元组"""
event_type = ''
for line in response.iter_lines():
if not line:
continue
decoded = line.decode('utf-8', errors='replace')
if decoded.startswith('event:'):
event_type = decoded[6:].strip()
elif decoded.startswith('data:'):
data_str = decoded[5:].strip()
if not data_str:
continue
try:
yield event_type, json.loads(data_str)
except json.JSONDecodeError:
continue