Delete replicate.py
Browse files- replicate.py +0 -199
replicate.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import uuid
|
| 3 |
-
import asyncio
|
| 4 |
-
from typing import Dict, Optional, Tuple, List, AsyncGenerator, Any
|
| 5 |
-
import httpx
|
| 6 |
-
|
| 7 |
-
from utils import get_proxies, load_module, create_proxy_mounts
|
| 8 |
-
from config import AMAZONQ_API_URL, DEFAULT_HEADERS
|
| 9 |
-
|
| 10 |
-
try:
|
| 11 |
-
_parser = load_module("v2_claude_parser", "claude_parser.py")
|
| 12 |
-
EventStreamParser = _parser.EventStreamParser
|
| 13 |
-
extract_event_info = _parser.extract_event_info
|
| 14 |
-
except Exception as e:
|
| 15 |
-
print(f"Warning: Failed to load claude_parser: {e}")
|
| 16 |
-
EventStreamParser = None
|
| 17 |
-
extract_event_info = None
|
| 18 |
-
|
| 19 |
-
class StreamTracker:
|
| 20 |
-
def __init__(self):
|
| 21 |
-
self.has_content = False
|
| 22 |
-
|
| 23 |
-
async def track(self, gen: AsyncGenerator[str, None]) -> AsyncGenerator[str, None]:
|
| 24 |
-
async for item in gen:
|
| 25 |
-
if item:
|
| 26 |
-
self.has_content = True
|
| 27 |
-
yield item
|
| 28 |
-
|
| 29 |
-
def load_template() -> Tuple[str, Dict[str, str]]:
|
| 30 |
-
"""
|
| 31 |
-
加载 Amazon Q API 请求模板
|
| 32 |
-
|
| 33 |
-
Returns:
|
| 34 |
-
(url, headers): API 端点 URL 和默认请求头
|
| 35 |
-
"""
|
| 36 |
-
return AMAZONQ_API_URL, DEFAULT_HEADERS.copy()
|
| 37 |
-
|
| 38 |
-
def _merge_headers(as_log: Dict[str, str], bearer_token: str) -> Dict[str, str]:
|
| 39 |
-
headers = dict(as_log)
|
| 40 |
-
for k in list(headers.keys()):
|
| 41 |
-
kl = k.lower()
|
| 42 |
-
if kl in ("content-length","host","connection","transfer-encoding"):
|
| 43 |
-
headers.pop(k, None)
|
| 44 |
-
def set_header(name: str, value: str):
|
| 45 |
-
for key in list(headers.keys()):
|
| 46 |
-
if key.lower() == name.lower():
|
| 47 |
-
del headers[key]
|
| 48 |
-
headers[name] = value
|
| 49 |
-
set_header("Authorization", f"Bearer {bearer_token}")
|
| 50 |
-
set_header("amz-sdk-invocation-id", str(uuid.uuid4()))
|
| 51 |
-
return headers
|
| 52 |
-
|
| 53 |
-
async def send_chat_request(
|
| 54 |
-
access_token: str,
|
| 55 |
-
messages: List[Dict[str, Any]],
|
| 56 |
-
model: Optional[str] = None,
|
| 57 |
-
stream: bool = False,
|
| 58 |
-
timeout: Tuple[int,int] = (30,300),
|
| 59 |
-
client: Optional[httpx.AsyncClient] = None,
|
| 60 |
-
raw_payload: Dict[str, Any] = None
|
| 61 |
-
) -> Tuple[Optional[str], Optional[AsyncGenerator[str, None]], StreamTracker, Optional[AsyncGenerator[Any, None]]]:
|
| 62 |
-
"""
|
| 63 |
-
发送聊天请求到 Amazon Q API
|
| 64 |
-
|
| 65 |
-
Args:
|
| 66 |
-
access_token: Amazon Q access token
|
| 67 |
-
messages: 消息列表(已废弃,使用 raw_payload)
|
| 68 |
-
model: 模型名称(已废弃,使用 raw_payload)
|
| 69 |
-
stream: 是否流式响应
|
| 70 |
-
timeout: 超时配置
|
| 71 |
-
client: HTTP 客户端
|
| 72 |
-
raw_payload: Claude API 转换后的请求体(必需)
|
| 73 |
-
"""
|
| 74 |
-
if raw_payload is None:
|
| 75 |
-
raise ValueError("raw_payload is required")
|
| 76 |
-
|
| 77 |
-
url, headers_from_log = load_template()
|
| 78 |
-
headers_from_log["amz-sdk-invocation-id"] = str(uuid.uuid4())
|
| 79 |
-
|
| 80 |
-
# Use raw payload (for Claude API)
|
| 81 |
-
body_json = raw_payload
|
| 82 |
-
# Ensure conversationId is set if missing
|
| 83 |
-
if "conversationState" in body_json and "conversationId" not in body_json["conversationState"]:
|
| 84 |
-
body_json["conversationState"]["conversationId"] = str(uuid.uuid4())
|
| 85 |
-
|
| 86 |
-
payload_str = json.dumps(body_json, ensure_ascii=False)
|
| 87 |
-
headers = _merge_headers(headers_from_log, access_token)
|
| 88 |
-
|
| 89 |
-
local_client = False
|
| 90 |
-
if client is None:
|
| 91 |
-
local_client = True
|
| 92 |
-
mounts = create_proxy_mounts()
|
| 93 |
-
# 增加连接超时时间,避免 TLS 握手超时
|
| 94 |
-
timeout_config = httpx.Timeout(connect=60.0, read=timeout[1], write=timeout[0], pool=10.0)
|
| 95 |
-
# 只在有代理时才传递 mounts 参数
|
| 96 |
-
if mounts:
|
| 97 |
-
client = httpx.AsyncClient(mounts=mounts, timeout=timeout_config)
|
| 98 |
-
else:
|
| 99 |
-
client = httpx.AsyncClient(timeout=timeout_config)
|
| 100 |
-
|
| 101 |
-
# Use manual request sending to control stream lifetime
|
| 102 |
-
req = client.build_request("POST", url, headers=headers, content=payload_str)
|
| 103 |
-
|
| 104 |
-
resp = None
|
| 105 |
-
try:
|
| 106 |
-
resp = await client.send(req, stream=True)
|
| 107 |
-
|
| 108 |
-
if resp.status_code >= 400:
|
| 109 |
-
try:
|
| 110 |
-
await resp.read()
|
| 111 |
-
err = resp.text
|
| 112 |
-
except Exception:
|
| 113 |
-
err = f"HTTP {resp.status_code}"
|
| 114 |
-
await resp.aclose()
|
| 115 |
-
if local_client:
|
| 116 |
-
await client.aclose()
|
| 117 |
-
raise httpx.HTTPError(f"Upstream error {resp.status_code}: {err}")
|
| 118 |
-
|
| 119 |
-
tracker = StreamTracker()
|
| 120 |
-
|
| 121 |
-
# Track if the response has been consumed to avoid double-close
|
| 122 |
-
response_consumed = False
|
| 123 |
-
|
| 124 |
-
async def _iter_events() -> AsyncGenerator[Any, None]:
|
| 125 |
-
nonlocal response_consumed
|
| 126 |
-
try:
|
| 127 |
-
# Use EventStreamParser from claude_parser.py
|
| 128 |
-
async def byte_gen():
|
| 129 |
-
async for chunk in resp.aiter_bytes():
|
| 130 |
-
if chunk:
|
| 131 |
-
yield chunk
|
| 132 |
-
|
| 133 |
-
async for message in EventStreamParser.parse_stream(byte_gen()):
|
| 134 |
-
event_info = extract_event_info(message)
|
| 135 |
-
if event_info:
|
| 136 |
-
event_type = event_info.get('event_type')
|
| 137 |
-
payload = event_info.get('payload')
|
| 138 |
-
if event_type and payload:
|
| 139 |
-
yield (event_type, payload)
|
| 140 |
-
except Exception:
|
| 141 |
-
if not tracker.has_content:
|
| 142 |
-
raise
|
| 143 |
-
finally:
|
| 144 |
-
response_consumed = True
|
| 145 |
-
await resp.aclose()
|
| 146 |
-
if local_client:
|
| 147 |
-
await client.aclose()
|
| 148 |
-
|
| 149 |
-
if stream:
|
| 150 |
-
# Wrap generator to ensure cleanup on early termination
|
| 151 |
-
async def _safe_iter_events():
|
| 152 |
-
try:
|
| 153 |
-
# 托底方案: 300秒强制超时
|
| 154 |
-
async with asyncio.timeout(300):
|
| 155 |
-
async for item in _iter_events():
|
| 156 |
-
yield item
|
| 157 |
-
except asyncio.TimeoutError:
|
| 158 |
-
# 超时强制关闭
|
| 159 |
-
if resp and not resp.is_closed:
|
| 160 |
-
await resp.aclose()
|
| 161 |
-
if local_client and client:
|
| 162 |
-
await client.aclose()
|
| 163 |
-
raise
|
| 164 |
-
except GeneratorExit:
|
| 165 |
-
# Generator was closed without being fully consumed
|
| 166 |
-
# Ensure cleanup happens even if finally block wasn't reached
|
| 167 |
-
if resp and not resp.is_closed:
|
| 168 |
-
await resp.aclose()
|
| 169 |
-
if local_client and client:
|
| 170 |
-
await client.aclose()
|
| 171 |
-
raise
|
| 172 |
-
except Exception:
|
| 173 |
-
# Any exception should also trigger cleanup
|
| 174 |
-
if resp and not resp.is_closed:
|
| 175 |
-
await resp.aclose()
|
| 176 |
-
if local_client and client:
|
| 177 |
-
await client.aclose()
|
| 178 |
-
raise
|
| 179 |
-
return None, None, tracker, _safe_iter_events()
|
| 180 |
-
else:
|
| 181 |
-
# Non-streaming: consume all events
|
| 182 |
-
try:
|
| 183 |
-
async for _ in _iter_events():
|
| 184 |
-
pass
|
| 185 |
-
finally:
|
| 186 |
-
# Ensure response is closed even if iteration is incomplete
|
| 187 |
-
if not response_consumed and resp:
|
| 188 |
-
await resp.aclose()
|
| 189 |
-
if local_client:
|
| 190 |
-
await client.aclose()
|
| 191 |
-
return None, None, tracker, None
|
| 192 |
-
|
| 193 |
-
except Exception:
|
| 194 |
-
# Critical: close response on any exception before generators are created
|
| 195 |
-
if resp and not resp.is_closed:
|
| 196 |
-
await resp.aclose()
|
| 197 |
-
if local_client and client:
|
| 198 |
-
await client.aclose()
|
| 199 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|