StarrySkyWorld commited on
Commit
a27bcba
ยท
verified ยท
1 Parent(s): fbf0be3

Upload openai.py

Browse files
Files changed (1) hide show
  1. openai.py +423 -0
openai.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import struct
3
+ import gzip
4
+ import time
5
+ import uuid
6
+ import os
7
+ import re
8
+ import asyncio
9
+ import hashlib
10
+ import queue
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from typing import List, Optional, Dict, Any
13
+ from fastapi import FastAPI, Request, HTTPException, Depends
14
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
+ from fastapi.responses import StreamingResponse
16
+ from curl_cffi import requests
17
+ import uvicorn
18
+
19
+ app = FastAPI()
20
+ security = HTTPBearer()
21
+
22
+ # ้…็ฝฎ้กน๏ผŒๆ”ฏๆŒ็Žฏๅขƒๅ˜้‡่ฆ†็›–
23
+ COOKIES_PATH = os.environ.get("COOKIES_PATH", "cookies.json")
24
+ PROXY = os.environ.get("HTTP_PROXY", None) # ไธ่ฎพ็ฝฎๅˆ™ไธ่ตฐไปฃ็†
25
+
26
+ # ๅŒๆญฅ้˜ปๅกž่ฐƒ็”จ็”จ็š„็บฟ็จ‹ๆฑ 
27
+ _executor = ThreadPoolExecutor(max_workers=16)
28
+
29
+
30
+ def _load_cookies(path: str) -> dict:
31
+ try:
32
+ with open(path, 'r', encoding='utf-8') as f:
33
+ cookies_list = json.load(f)
34
+ return {c['name']: c['value'] for c in cookies_list}
35
+ except Exception as e:
36
+ print(f"Error loading cookies: {e}")
37
+ return {}
38
+
39
+
40
+ def _generate_device_id(seed: str) -> str:
41
+ h = hashlib.sha256(seed.encode()).hexdigest()
42
+ return str(int(h[:16], 16))[:19]
43
+
44
+
45
+ def _generate_session_id(seed: str) -> str:
46
+ h = hashlib.sha256(("session-" + seed).encode()).hexdigest()
47
+ return str(int(h[:16], 16))[:19]
48
+
49
+
50
+ def pack_connect_message(data: dict) -> bytes:
51
+ payload = json.dumps(data, separators=(',', ':')).encode('utf-8')
52
+ header = struct.pack('>BI', 0, len(payload))
53
+ return header + payload
54
+
55
+
56
+ def _convert_citations(text: str) -> str:
57
+ """ๅฐ† Kimi ็š„ [^N^] ๅผ•็”จๆ ผๅผ่ฝฌๆขไธบ [N]"""
58
+ return re.sub(r'\[\^(\d+)\^\]', r'[\1]', text)
59
+
60
+
61
+ def _format_references(refs: list) -> str:
62
+ """ๅฐ†ๆœ็ดขๅผ•็”จๆ ผๅผๅŒ–ไธบ markdown ่„šๆณจ"""
63
+ if not refs:
64
+ return ""
65
+ lines = ["\n\n---", "**Sources:**"]
66
+ for ref in refs:
67
+ base = ref.get("base", {})
68
+ title = base.get("title", "")
69
+ url = base.get("url", "")
70
+ ref_id = ref.get("id", "")
71
+ if title and url:
72
+ lines.append(f"[{ref_id}] [{title}]({url})")
73
+ return "\n".join(lines) + "\n"
74
+
75
+
76
+ # โ”€โ”€ ๅธง่งฃๆž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
+
78
+ def _parse_kimi_frames(buffer: bytes):
79
+ """่งฃๆž connect ๅธง๏ผŒ่ฟ”ๅ›ž (events, remaining_buffer)ใ€‚
80
+ event ็ฑปๅž‹:
81
+ - {"type": "text", "content": "..."}
82
+ - {"type": "tool_status", "name": "...", "status": "..."}
83
+ - {"type": "search_refs", "refs": [...]}
84
+ - {"type": "done"}
85
+ """
86
+ events = []
87
+ while len(buffer) >= 5:
88
+ flag, length = struct.unpack_from('>BI', buffer, 0)
89
+ if len(buffer) < 5 + length:
90
+ break
91
+ payload_bytes = buffer[5:5 + length]
92
+ buffer = buffer[5 + length:]
93
+
94
+ if flag == 2:
95
+ try:
96
+ payload_bytes = gzip.decompress(payload_bytes)
97
+ except:
98
+ pass
99
+
100
+ if flag not in (0, 2):
101
+ continue
102
+
103
+ try:
104
+ data = json.loads(payload_bytes.decode('utf-8'))
105
+ except Exception as e:
106
+ print(f"DEBUG: Error decoding frame JSON: {e}")
107
+ continue
108
+
109
+ # done ไฟกๅท
110
+ if "done" in data:
111
+ events.append({"type": "done"})
112
+ continue
113
+
114
+ # heartbeat ่ทณ่ฟ‡
115
+ if "heartbeat" in data:
116
+ continue
117
+
118
+ op = data.get("op")
119
+ if op not in ("set", "append"):
120
+ continue
121
+
122
+ # ๆ–‡ๆœฌๅ†…ๅฎน
123
+ if "block" in data and "text" in data["block"]:
124
+ content = data["block"]["text"].get("content", "")
125
+ if content:
126
+ events.append({"type": "text", "content": content})
127
+
128
+ # message.blocks ้‡Œ็š„ๆ–‡ๆœฌ โ€” ๅชๆๅ– assistant ่ง’่‰ฒ็š„๏ผŒ่ทณ่ฟ‡ user/system ๅ›žๆ˜พ
129
+ if "message" in data and "blocks" in data.get("message", {}):
130
+ msg_role = data["message"].get("role", "")
131
+ if msg_role == "assistant":
132
+ content = ""
133
+ for block in data["message"]["blocks"]:
134
+ if "text" in block:
135
+ content += block["text"].get("content", "")
136
+ if content:
137
+ events.append({"type": "text", "content": content})
138
+
139
+ # ๅทฅๅ…ท่ฐƒ็”จ็Šถๆ€
140
+ if "block" in data and "tool" in data["block"]:
141
+ tool = data["block"]["tool"]
142
+ name = tool.get("name", "")
143
+ status = tool.get("status", "")
144
+ if name and status:
145
+ events.append({"type": "tool_status", "name": name, "status": status})
146
+
147
+ # ๆœ็ดขๅผ•็”จ (usedSearchChunks ไผ˜ๅ…ˆ)
148
+ msg = data.get("message", {})
149
+ refs = msg.get("refs", {})
150
+ if "usedSearchChunks" in refs:
151
+ events.append({"type": "search_refs", "refs": refs["usedSearchChunks"]})
152
+
153
+ return events, buffer
154
+
155
+
156
+ # ็กฌ็ผ–็ ็š„ API Key๏ผŒๅŒน้…ๆ—ถไฝฟ็”จ cookies.json ่ฎค่ฏ
157
+ API_KEY = "sk-sseworld-kimi"
158
+
159
+ # โ”€โ”€ Kimi Bridge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
160
+
161
+ class KimiBridge:
162
+ def __init__(self):
163
+ self.base_url = "https://www.kimi.com"
164
+
165
+ def create_session(self, api_key: str):
166
+ if api_key == API_KEY:
167
+ cookies = _load_cookies(COOKIES_PATH)
168
+ auth_token = cookies.get("kimi-auth", "")
169
+ fingerprint_seed = "cookies-default"
170
+ else:
171
+ cookies = {}
172
+ auth_token = api_key
173
+ fingerprint_seed = api_key
174
+
175
+ device_id = _generate_device_id(fingerprint_seed)
176
+ session_id = _generate_session_id(fingerprint_seed)
177
+
178
+ headers = {
179
+ "accept": "*/*",
180
+ "accept-language": "zh-CN,zh;q=0.9",
181
+ "authorization": f"Bearer {auth_token}",
182
+ "content-type": "application/connect+json",
183
+ "connect-protocol-version": "1",
184
+ "origin": "https://www.kimi.com",
185
+ "referer": "https://www.kimi.com/",
186
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
187
+ "x-language": "zh-CN",
188
+ "x-msh-device-id": device_id,
189
+ "x-msh-platform": "web",
190
+ "x-msh-session-id": session_id,
191
+ "x-msh-version": "1.0.0",
192
+ "x-traffic-id": f"u{device_id[:20]}",
193
+ }
194
+
195
+ return requests.Session(
196
+ headers=headers,
197
+ cookies=cookies,
198
+ impersonate="chrome124",
199
+ proxy=PROXY,
200
+ )
201
+
202
+
203
+ bridge = KimiBridge()
204
+
205
+
206
+ # โ”€โ”€ OpenAI ๆ ผๅผๅŒ– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
207
+
208
+ def format_openai_stream_chunk(content: str, model: str, chat_id: str, *, role: str = None, finish_reason: str = None):
209
+ delta = {}
210
+ if role:
211
+ delta["role"] = role
212
+ if content:
213
+ delta["content"] = content
214
+ chunk = {
215
+ "id": chat_id,
216
+ "object": "chat.completion.chunk",
217
+ "created": int(time.time()),
218
+ "model": model,
219
+ "choices": [{
220
+ "index": 0,
221
+ "delta": delta,
222
+ "finish_reason": finish_reason
223
+ }]
224
+ }
225
+ return f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
226
+
227
+
228
+ # โ”€โ”€ ๅŒๆญฅ่พ…ๅŠฉๅ‡ฝๆ•ฐ (็บฟ็จ‹ๆฑ ไธญๆ‰ง่กŒ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
229
+
230
+ def _sync_kimi_request(session, url, body_bytes):
231
+ return session.post(url, data=body_bytes, stream=True, timeout=30)
232
+
233
+
234
+ def _sync_read_all(response):
235
+ """ๅŒๆญฅ่ฏปๅ–ๅฎŒๆ•ดๅ“ๅบ”๏ผŒ่ฟ”ๅ›ž (full_text, search_refs)"""
236
+ full_content = ""
237
+ search_refs = []
238
+ buffer = b""
239
+ for chunk in response.iter_content(chunk_size=None):
240
+ if not chunk:
241
+ continue
242
+ buffer += chunk
243
+ events, buffer = _parse_kimi_frames(buffer)
244
+ for ev in events:
245
+ if ev["type"] == "text":
246
+ full_content += ev["content"]
247
+ elif ev["type"] == "search_refs":
248
+ search_refs = ev["refs"]
249
+ full_content = _convert_citations(full_content)
250
+ if search_refs:
251
+ full_content += _format_references(search_refs)
252
+ return full_content
253
+
254
+
255
+ # โ”€โ”€ ่ทฏ็”ฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
256
+
257
+ @app.middleware("http")
258
+ async def log_requests(request: Request, call_next):
259
+ print(f"DEBUG: Incoming request: {request.method} {request.url}")
260
+ response = await call_next(request)
261
+ print(f"DEBUG: Response status: {response.status_code}")
262
+ return response
263
+
264
+
265
+ KIMI_MODELS = {
266
+ "kimi-k2.5": {"scenario": "SCENARIO_K2D5", "thinking": False},
267
+ "kimi-k2.5-thinking": {"scenario": "SCENARIO_K2D5", "thinking": True},
268
+ }
269
+ DEFAULT_MODEL = "kimi-k2.5"
270
+
271
+
272
+ @app.get("/v1/models")
273
+ async def list_models():
274
+ return {
275
+ "object": "list",
276
+ "data": [
277
+ {"id": mid, "object": "model", "created": 0, "owned_by": "moonshot"}
278
+ for mid in KIMI_MODELS
279
+ ]
280
+ }
281
+
282
+
283
+ @app.post("/v1/chat/completions")
284
+ async def chat_completions(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
285
+ api_key = credentials.credentials
286
+ print(f"DEBUG: chat_completions endpoint hit, key prefix: {api_key[:6]}...")
287
+ print(f"DEBUG: Request headers: {dict(request.headers)}")
288
+
289
+ session = bridge.create_session(api_key)
290
+
291
+ try:
292
+ body = await request.json()
293
+ except Exception as e:
294
+ print(f"DEBUG: Failed to parse request JSON: {e}")
295
+ raise HTTPException(status_code=400, detail="Invalid JSON body")
296
+
297
+ messages = body.get("messages", [])
298
+ model = body.get("model", "kimi-k2.5")
299
+ stream = body.get("stream", False)
300
+ model_config = KIMI_MODELS.get(model, KIMI_MODELS[DEFAULT_MODEL])
301
+
302
+ print(f"DEBUG: Received request: model={model}, thinking={model_config['thinking']}, stream={stream}, messages_count={len(messages)}")
303
+
304
+ if not messages:
305
+ raise HTTPException(status_code=400, detail="Messages are required")
306
+
307
+ # ๆž„้€  Kimi ็š„่ฏทๆฑ‚
308
+ kimi_blocks = []
309
+ for msg in messages:
310
+ role = msg.get("role", "user")
311
+ content = msg.get("content", "")
312
+ prefix = "User: " if role == "user" else "Assistant: "
313
+ kimi_blocks.append({"message_id": "", "text": {"content": f"{prefix}{content}\n"}})
314
+
315
+ kimi_payload = {
316
+ "scenario": model_config["scenario"],
317
+ "tools": [{"type": "TOOL_TYPE_SEARCH", "search": {}}],
318
+ "message": {
319
+ "role": "user",
320
+ "blocks": kimi_blocks,
321
+ "scenario": model_config["scenario"]
322
+ },
323
+ "options": {"thinking": model_config["thinking"]}
324
+ }
325
+
326
+ print(f"DEBUG: Kimi payload size: {len(json.dumps(kimi_payload))}")
327
+
328
+ url = f"{bridge.base_url}/apiv2/kimi.gateway.chat.v1.ChatService/Chat"
329
+ body_bytes = pack_connect_message(kimi_payload)
330
+
331
+ print(f"DEBUG: Forwarding to Kimi: {url}")
332
+
333
+ loop = asyncio.get_event_loop()
334
+
335
+ try:
336
+ response = await loop.run_in_executor(_executor, _sync_kimi_request, session, url, body_bytes)
337
+ print(f"DEBUG: Kimi response status: {response.status_code}")
338
+ except Exception as e:
339
+ print(f"DEBUG: Request to Kimi failed: {e}")
340
+ session.close()
341
+ raise HTTPException(status_code=500, detail=f"Failed to connect to Kimi: {str(e)}")
342
+
343
+ if response.status_code != 200:
344
+ error_text = response.text
345
+ print(f"DEBUG: Kimi error: {error_text}")
346
+ session.close()
347
+ raise HTTPException(status_code=response.status_code, detail=f"Kimi API error: {error_text}")
348
+
349
+ chat_id = str(uuid.uuid4())
350
+
351
+ if stream:
352
+ async def generate():
353
+ q = queue.Queue()
354
+ sentinel = object()
355
+ sent_role = False
356
+
357
+ def _stream_worker():
358
+ try:
359
+ buf = b""
360
+ search_refs = []
361
+ for chunk in response.iter_content(chunk_size=None):
362
+ if not chunk:
363
+ continue
364
+ buf += chunk
365
+ events, buf = _parse_kimi_frames(buf)
366
+ for ev in events:
367
+ if ev["type"] == "text":
368
+ q.put(("text", _convert_citations(ev["content"])))
369
+ elif ev["type"] == "tool_status" and ev["status"] == "STATUS_RUNNING":
370
+ q.put(("text", "\n\n> [Searching...]\n\n"))
371
+ elif ev["type"] == "search_refs":
372
+ search_refs = ev["refs"]
373
+ # ๆต็ป“ๆŸ๏ผŒ่ฟฝๅŠ ๅผ•็”จ
374
+ if search_refs:
375
+ q.put(("text", _format_references(search_refs)))
376
+ finally:
377
+ q.put(sentinel)
378
+ session.close()
379
+
380
+ loop.run_in_executor(_executor, _stream_worker)
381
+
382
+ while True:
383
+ try:
384
+ item = await loop.run_in_executor(None, q.get, True, 0.5)
385
+ except:
386
+ continue
387
+ if item is sentinel:
388
+ break
389
+ _, content = item
390
+ if not sent_role:
391
+ yield format_openai_stream_chunk(content, model, chat_id, role="assistant")
392
+ sent_role = True
393
+ else:
394
+ yield format_openai_stream_chunk(content, model, chat_id)
395
+
396
+ # finish_reason: stop
397
+ yield format_openai_stream_chunk("", model, chat_id, finish_reason="stop")
398
+ yield "data: [DONE]\n\n"
399
+
400
+ return StreamingResponse(generate(), media_type="text/event-stream")
401
+ else:
402
+ try:
403
+ full_content = await loop.run_in_executor(_executor, _sync_read_all, response)
404
+ finally:
405
+ session.close()
406
+
407
+ return {
408
+ "id": chat_id,
409
+ "object": "chat.completion",
410
+ "created": int(time.time()),
411
+ "model": model,
412
+ "choices": [{
413
+ "index": 0,
414
+ "message": {"role": "assistant", "content": full_content},
415
+ "finish_reason": "stop"
416
+ }],
417
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
418
+ }
419
+
420
+
421
+ if __name__ == "__main__":
422
+ import uvicorn
423
+ uvicorn.run("openai:app", host="127.0.0.1", port=8001, reload=False, log_level="debug")