heiyuheiyu commited on
Commit
3fcc6a4
·
verified ·
1 Parent(s): d137394

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -311
app.py DELETED
@@ -1,311 +0,0 @@
1
- """
2
- Claude Web API Proxy - HuggingFace Spaces with ReAct
3
- """
4
- from fastapi import FastAPI, Request
5
- from fastapi.responses import StreamingResponse, JSONResponse
6
- from fastapi.middleware.cors import CORSMiddleware
7
- import httpx
8
- import json
9
- import asyncio
10
- from datetime import datetime
11
- from typing import Optional, Dict
12
- import os
13
- import uuid
14
- import re
15
- import pickle
16
- from pathlib import Path
17
- import logging
18
-
19
- # 日志配置
20
- logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
21
- logger = logging.getLogger(__name__)
22
-
23
- # 重试函数
24
- async def retry_async(fn, max_retries=3, delay=1.0):
25
- for i in range(max_retries):
26
- try:
27
- return await fn()
28
- except Exception as e:
29
- if i == max_retries - 1:
30
- raise
31
- logger.warning(f"Retry {i+1}/{max_retries}: {e}")
32
- await asyncio.sleep(delay * (i + 1))
33
-
34
- app = FastAPI(title="Claude Proxy")
35
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
36
-
37
- # 文件持久化
38
- CACHE_FILE = Path("/tmp/conversation_cache.pkl")
39
- conversation_cache: Dict[str, str] = {}
40
-
41
- def load_cache():
42
- global conversation_cache
43
- if CACHE_FILE.exists():
44
- try:
45
- with open(CACHE_FILE, 'rb') as f:
46
- conversation_cache = pickle.load(f)
47
- except:
48
- conversation_cache = {}
49
-
50
- def save_cache():
51
- try:
52
- with open(CACHE_FILE, 'wb') as f:
53
- pickle.dump(conversation_cache, f)
54
- except:
55
- pass
56
-
57
- # 智能保活
58
- last_request_time = datetime.now()
59
-
60
- async def keep_alive():
61
- while True:
62
- # 根据最后请求时间动态调整保活间隔
63
- idle_time = (datetime.now() - last_request_time).total_seconds()
64
- if idle_time < 3600: # 1小时内有请求,频繁保活
65
- await asyncio.sleep(1800) # 30分钟
66
- else: # 空闲,降低频率
67
- await asyncio.sleep(6 * 3600) # 6小时
68
-
69
- try:
70
- async with httpx.AsyncClient() as client:
71
- url = os.getenv("SPACE_URL", "")
72
- if url:
73
- await client.get(f"{url}/health")
74
- except:
75
- pass
76
-
77
- @app.on_event("startup")
78
- async def startup():
79
- load_cache()
80
- asyncio.create_task(keep_alive())
81
-
82
- @app.get("/health")
83
- async def health():
84
- return {"status": "ok", "time": datetime.now().isoformat()}
85
-
86
- @app.get("/")
87
- async def root():
88
- return {"name": "Claude Proxy", "version": "2.0-hf-react"}
89
-
90
- async def get_org_id(key: str) -> Optional[str]:
91
- try:
92
- async with httpx.AsyncClient() as client:
93
- r = await client.get('https://claude.ai/api/organizations',
94
- headers={'Cookie': f'sessionKey={key}', 'User-Agent': 'Mozilla/5.0'}, timeout=30.0)
95
- if r.status_code == 200:
96
- data = r.json()
97
- return data[0]['uuid'] if data else None
98
- except:
99
- pass
100
- return None
101
-
102
- async def create_conv(key: str, org_id: str) -> Optional[str]:
103
- try:
104
- conv_id = str(uuid.uuid4())
105
- async with httpx.AsyncClient() as client:
106
- r = await client.post(f'https://claude.ai/api/organizations/{org_id}/chat_conversations',
107
- headers={'Content-Type': 'application/json', 'Cookie': f'sessionKey={key}', 'User-Agent': 'Mozilla/5.0'},
108
- json={'uuid': conv_id, 'name': '', 'include_conversation_preferences': True}, timeout=30.0)
109
- if r.status_code == 200:
110
- return r.json().get('uuid', conv_id)
111
- except:
112
- pass
113
- return None
114
-
115
- def extract_prompt(messages):
116
- result = []
117
- for m in messages:
118
- role = m['role'].title()
119
- content = m['content']
120
-
121
- # 支持图片
122
- if isinstance(content, list):
123
- parts = []
124
- for part in content:
125
- if part.get('type') == 'text':
126
- parts.append(part['text'])
127
- elif part.get('type') == 'image_url':
128
- parts.append(f"[Image: {part['image_url']['url']}]")
129
- result.append(f"{role}: {' '.join(parts)}")
130
- else:
131
- result.append(f"{role}: {content}")
132
-
133
- return '\n\n'.join(result)
134
-
135
- def extract_attachments(messages):
136
- attachments = []
137
- for m in messages:
138
- if isinstance(m['content'], list):
139
- for part in m['content']:
140
- if part.get('type') == 'image_url':
141
- attachments.append({
142
- 'extracted_content': '',
143
- 'file_name': 'image.png',
144
- 'file_size': 0,
145
- 'file_type': 'image/png',
146
- 'url': part['image_url']['url']
147
- })
148
- return attachments
149
-
150
- def enhance_prompt(prompt: str, messages: list) -> str:
151
- has_system = any(m['role'] == 'system' for m in messages)
152
- if not has_system:
153
- return prompt
154
- return prompt + "\n\n[Environment: OpenClaw | Tools: web_search, artifacts, repl available]"
155
-
156
- def inject_react_prompt(prompt: str, tools: list) -> str:
157
- if not tools:
158
- return prompt
159
- tool_defs = '\n\n'.join([f"{t['name']}: {t.get('description', '')}" for t in tools])
160
- return prompt + f"""
161
-
162
- [Tool Calling Protocol]
163
- You have access to these tools:
164
- {tool_defs}
165
-
166
- Use this format:
167
- Thought: [reasoning]
168
- Action: [tool_name]
169
- Action Input: [JSON]
170
-
171
- After result:
172
- Observation: [result]
173
-
174
- When done:
175
- Final Answer: [response]
176
- """
177
-
178
- def parse_react_output(text: str) -> Optional[dict]:
179
- action_match = re.search(r'Action:\s*(\w+)', text)
180
- input_match = re.search(r'Action Input:\s*(\{[^}]*\})', text)
181
- if action_match and input_match:
182
- try:
183
- return {'type': 'tool_use', 'id': f'tool_{int(datetime.now().timestamp())}',
184
- 'name': action_match.group(1), 'input': json.loads(input_match.group(1))}
185
- except:
186
- return None
187
- return None
188
-
189
- @app.post("/v1/chat/completions")
190
- async def chat(request: Request):
191
- global last_request_time
192
- last_request_time = datetime.now()
193
- start_time = datetime.now()
194
-
195
- try:
196
- body = await request.json()
197
- auth = request.headers.get('Authorization', '')
198
-
199
- logger.info(f"Request received: model={body.get('model', 'claude-sonnet-4-6')}")
200
-
201
- if not auth.startswith('Bearer '):
202
- return JSONResponse({"error": {"message": "Missing auth", "type": "auth_error"}}, 401)
203
-
204
- key = auth.replace('Bearer ', '')
205
- org_id = await retry_async(lambda: get_org_id(key))
206
- if not org_id:
207
- logger.error("Invalid sessionKey")
208
- return JSONResponse({"error": {"message": "Invalid key", "type": "auth_error"}}, 401)
209
-
210
- conv_id = conversation_cache.get(key)
211
- if not conv_id:
212
- conv_id = await retry_async(lambda: create_conv(key, org_id))
213
- if conv_id:
214
- conversation_cache[key] = conv_id
215
- save_cache()
216
- if not conv_id:
217
- logger.error("Failed to create conversation")
218
- return JSONResponse({"error": {"message": "Conv error", "type": "api_error"}}, 500)
219
-
220
- messages = body.get('messages', [])
221
- tools = body.get('tools', [])
222
- prompt = extract_prompt(messages)
223
- prompt = enhance_prompt(prompt, messages)
224
- if tools:
225
- prompt = inject_react_prompt(prompt, tools)
226
-
227
- attachments = extract_attachments(messages)
228
- model = body.get('model', 'claude-sonnet-4-6')
229
- payload = {
230
- 'prompt': prompt,
231
- 'timezone': body.get('timezone', 'Asia/Shanghai'),
232
- 'locale': body.get('locale', 'zh-CN'),
233
- 'model': model,
234
- 'rendering_mode': 'messages',
235
- 'attachments': attachments,
236
- 'files': [],
237
- 'tools': [
238
- {"type": "web_search_v0", "name": "web_search"},
239
- {"type": "artifacts_v0", "name": "artifacts"},
240
- {"type": "repl_v0", "name": "repl"}
241
- ]
242
- }
243
-
244
- logger.info(f"Sending to Claude: attachments={len(attachments)}")
245
-
246
- async def generate():
247
- try:
248
- full_text = ''
249
- async with httpx.AsyncClient() as client:
250
- async with client.stream('POST',
251
- f'https://claude.ai/api/organizations/{org_id}/chat_conversations/{conv_id}/completion',
252
- headers={'Content-Type': 'application/json', 'Cookie': f'sessionKey={key}', 'User-Agent': 'Mozilla/5.0'},
253
- json=payload, timeout=120.0) as response:
254
-
255
- async for line in response.aiter_lines():
256
- if line.startswith('data: '):
257
- try:
258
- data = json.loads(line[6:])
259
- if data.get('type') == 'content_block_delta' and data.get('delta', {}).get('text'):
260
- text = data['delta']['text']
261
- full_text += text
262
-
263
- # ReAct: 检测工具调用
264
- react_result = parse_react_output(full_text)
265
- if react_result and react_result['type'] == 'tool_use':
266
- chunk = {
267
- "id": f"chatcmpl-{int(datetime.now().timestamp())}",
268
- "object": "chat.completion.chunk",
269
- "created": int(datetime.now().timestamp()),
270
- "model": model,
271
- "choices": [{
272
- "index": 0,
273
- "delta": {
274
- "tool_calls": [{
275
- "id": react_result['id'],
276
- "type": "function",
277
- "function": {
278
- "name": react_result['name'],
279
- "arguments": json.dumps(react_result['input'])
280
- }
281
- }]
282
- },
283
- "finish_reason": "tool_calls"
284
- }]
285
- }
286
- yield f"data: {json.dumps(chunk)}\n\n"
287
- yield "data: [DONE]\n\n"
288
- return
289
-
290
- # 正常文本
291
- chunk = {
292
- "id": f"chatcmpl-{int(datetime.now().timestamp())}",
293
- "object": "chat.completion.chunk",
294
- "created": int(datetime.now().timestamp()),
295
- "model": model,
296
- "choices": [{"index": 0, "delta": {"content": text}, "finish_reason": None}]
297
- }
298
- yield f"data: {json.dumps(chunk)}\n\n"
299
- except:
300
- pass
301
- yield "data: [DONE]\n\n"
302
- except Exception as e:
303
- logger.error(f"Stream error: {e}")
304
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
305
-
306
- return StreamingResponse(generate(), media_type="text/event-stream")
307
-
308
- except Exception as e:
309
- duration = (datetime.now() - start_time).total_seconds()
310
- logger.error(f"Request failed: {e}, duration={duration}s")
311
- return JSONResponse({"error": {"message": str(e), "type": "api_error"}}, 500)