bluewinliang commited on
Commit
963cb7e
·
verified ·
1 Parent(s): 0f40fc4

Update proxy_handler.py

Browse files
Files changed (1) hide show
  1. proxy_handler.py +142 -113
proxy_handler.py CHANGED
@@ -1,10 +1,8 @@
1
  """
2
  Proxy handler for Z.AI API requests
3
  """
4
- import json, logging, re, time, uuid, base64, hashlib, hmac, urllib.parse
5
  from typing import AsyncGenerator, Dict, Any, Tuple, List
6
- from datetime import datetime, timezone, timedelta
7
-
8
  import httpx
9
  from fastapi import HTTPException
10
  from fastapi.responses import StreamingResponse
@@ -23,67 +21,131 @@ class ProxyHandler:
23
  limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
24
  http2=True,
25
  )
26
- # 从JavaScript代码中获取的固定密钥
27
- self.primary_secret = "junjie".encode('utf-8')
28
 
29
  async def aclose(self):
30
  if not self.client.is_closed:
31
  await self.client.aclose()
32
-
33
- def _get_timestamp_millis(self) -> int:
34
- return int(time.time() * 1000)
35
-
36
  def _parse_jwt_token(self, token: str) -> Dict[str, str]:
37
- """A simple JWT payload decoder to get user ID."""
38
  try:
39
  parts = token.split('.')
40
  if len(parts) != 3:
41
- return {"userId": ""}
42
- # Add padding if necessary
43
  payload_b64 = parts[1]
 
44
  payload_b64 += '=' * (-len(payload_b64) % 4)
45
- payload_json = base64.urlsafe_b64decode(payload_b64).decode('utf-8')
46
- payload = json.loads(payload_json)
47
- # The JS code checks multiple keys, 'sub' is the most standard one.
48
- return {"userId": payload.get("sub", "")}
49
- except Exception as e:
50
- logger.warning(f"Failed to parse JWT token to get user ID: {e}")
51
- return {"userId": ""}
52
-
53
- def _generate_signature(self, token: str, payload_str: str, mt: str) -> Tuple[str, int]:
54
- timestamp_ms = self._get_timestamp_millis()
55
- minute_bucket = str(timestamp_ms // 60000)
56
-
57
- level1_data = f"{token}|{minute_bucket}".encode('utf-8')
58
- mac1 = hmac.new(self.primary_secret, level1_data, hashlib.sha256)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  derived_key_hex = mac1.hexdigest()
60
 
61
- level2_data = f"{payload_str}|{mt}|{timestamp_ms}".encode('utf-8')
62
- mac2 = hmac.new(derived_key_hex.encode('utf-8'), level2_data, hashlib.sha256)
 
63
  signature = mac2.hexdigest()
64
-
65
- return signature, timestamp_ms
66
 
 
 
67
  def _clean_thinking_content(self, text: str) -> str:
68
- if not text: return ""
 
 
 
 
 
 
69
  cleaned_text = text
 
 
70
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
71
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', cleaned_text, flags=re.DOTALL)
 
 
 
72
  cleaned_text = re.sub(r'<[^>]*duration="[^"]*"[^>]*>', '', cleaned_text)
73
- cleaned_text = cleaned_text.replace("</thinking>", "").replace("<Full>", "").replace("</Full>", "")
 
 
 
 
 
74
  cleaned_text = re.sub(r'</?details[^>]*>', '', cleaned_text)
 
 
75
  cleaned_text = re.sub(r'^\s*>\s*(?!>)', '', cleaned_text, flags=re.MULTILINE)
 
 
76
  cleaned_text = cleaned_text.replace("Thinking…", "")
 
 
77
  return cleaned_text.strip()
78
 
79
  def _clean_answer_content(self, text: str) -> str:
80
- if not text: return ""
 
 
 
 
 
 
81
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', text, flags=re.DOTALL)
 
82
  cleaned_text = re.sub(r'<details[^>]*>.*?</details>', '', cleaned_text, flags=re.DOTALL)
83
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
84
  return cleaned_text
85
 
86
  def _serialize_msgs(self, msgs) -> list:
 
87
  out = []
88
  for m in msgs:
89
  if hasattr(m, "dict"): out.append(m.dict())
@@ -93,91 +155,40 @@ class ProxyHandler:
93
  return out
94
 
95
  async def _prep_upstream(self, req: ChatCompletionRequest) -> Tuple[Dict[str, Any], Dict[str, str], str, str]:
96
- """Prepares the request body, headers, cookie, and URL for the upstream API."""
97
  ck = await cookie_manager.get_next_cookie()
98
  if not ck: raise HTTPException(503, "No available cookies")
99
 
100
- model = settings.UPSTREAM_MODEL if req.model == settings.MODEL_NAME else req.model
101
  chat_id = str(uuid.uuid4())
102
  request_id = str(uuid.uuid4())
103
- user_info = self._parse_jwt_token(ck)
104
- user_id = user_info.get("userId", "")
105
-
106
- body = { "stream": True, "model": model, "messages": self._serialize_msgs(req.messages), "background_tasks": {"title_generation": True, "tags_generation": True}, "chat_id": chat_id, "features": {"image_generation": False, "code_interpreter": False, "web_search": False, "auto_web_search": False, "enable_thinking": True,}, "id": request_id, "mcp_servers": ["deep-web-search"], "model_item": {"id": model, "name": "GLM-4.6", "owned_by": "openai"}, "params": {}, "tool_servers": [], "variables": {"{{USER_NAME}}": "User", "{{USER_LOCATION}}": "Unknown", "{{CURRENT_DATETIME}}": time.strftime("%Y-%m-%d %H:%M:%S"),},}
107
 
108
- # --- FIX: Timezone-aware payload generation ---
109
- # The JS code uses the local timezone. We'll simulate a common one (Asia/Shanghai, UTC+8).
110
- # This is the most likely cause of the signature mismatch.
111
- target_tz_offset_hours = 8
112
- target_tz = timezone(timedelta(hours=target_tz_offset_hours))
113
- now_local = datetime.now(target_tz)
114
- now_utc = datetime.now(timezone.utc)
115
-
116
- # JS getTimezoneOffset returns the difference in minutes from UTC, and the sign is inverted.
117
- # e.g., for UTC+8, it returns -480.
118
- timezone_offset_minutes = -target_tz_offset_hours * 60
119
 
120
- payload_data = {
121
- 'timestamp': str(self._get_timestamp_millis()),
122
- 'requestId': request_id,
123
- 'user_id': user_id,
124
- 'token': ck,
125
- 'user_agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
126
- 'current_url': f"https://chat.z.ai/c/{chat_id}",
127
- 'pathname': f"/c/{chat_id}",
128
- 'timezone': 'Asia/Shanghai', # Use a specific timezone name
129
- 'timezone_offset': str(timezone_offset_minutes),
130
- 'local_time': now_local.isoformat(timespec='milliseconds'),
131
- 'utc_time': now_utc.strftime('%a, %d %b %Y %H:%M:%S GMT'),
132
- 'version': '0.0.1',
133
- 'platform': 'web',
134
- 'language': 'zh-CN',
135
- 'languages': 'zh-CN,en',
136
- 'cookie_enabled': 'true',
137
- 'screen_width': '2560',
138
- 'screen_height': '1440',
139
- 'screen_resolution': '2560x1440',
140
- 'viewport_height': '1328',
141
- 'viewport_width': '1342',
142
- 'viewport_size': '1342x1328',
143
- 'color_depth': '24',
144
- 'pixel_ratio': '2',
145
- 'search': '',
146
- 'hash': '',
147
- 'host': 'chat.z.ai',
148
- 'hostname': 'chat.z.ai',
149
- 'protocol': 'https:',
150
- 'referrer': '',
151
- 'title': 'Chat with Z.ai - Free AI Chatbot powered by GLM-4.5',
152
- 'is_mobile': 'false',
153
- 'is_touch': 'false',
154
- 'max_touch_points': '0',
155
- 'browser_name': 'Chrome',
156
- 'os_name': 'Mac OS'
157
- }
158
-
159
- keys = sorted(payload_data.keys())
160
- sorted_payload = ",".join([f"{k},{payload_data[k]}" for k in keys])
161
- url_params = urllib.parse.urlencode(payload_data)
162
-
163
- last_message_content = ""
164
- if req.messages:
165
- last_message = req.messages[-1]
166
- if isinstance(last_message.content, str):
167
- last_message_content = last_message.content
168
 
169
- signature, sig_timestamp = self._generate_signature(ck, sorted_payload, last_message_content)
170
-
171
- final_url = f"{settings.UPSTREAM_URL}?{url_params}&signature_timestamp={sig_timestamp}"
172
 
173
- headers = { "Content-Type": "application/json", "Authorization": f"Bearer {ck}", "User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"), "Accept": "application/json, text/event-stream", "Accept-Language": "zh-CN", "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="141", "Google Chrome";v="141"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "x-fe-version": "prod-fe-1.0.79", "X-Signature": 'ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk=', "Origin": "https://chat.z.ai", "Referer": "https://chat.z.ai/",}
 
 
 
174
 
175
- return body, headers, ck, final_url
176
 
177
  async def stream_proxy_response(self, req: ChatCompletionRequest) -> AsyncGenerator[str, None]:
178
  ck = None
179
  try:
180
- body, headers, ck, url = await self._prep_upstream(req)
181
  comp_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
182
  think_open = False
183
  yielded_think_buffer = ""
@@ -190,15 +201,19 @@ class ProxyHandler:
190
  if not think_open:
191
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '<think>'}, 'finish_reason': None}]})}\n\n"
192
  think_open = True
 
193
  cleaned_full_text = self._clean_thinking_content(text)
194
- delta_to_send = cleaned_full_text[len(yielded_think_buffer):]
 
195
  if delta_to_send:
196
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': delta_to_send}, 'finish_reason': None}]})}\n\n"
197
  yielded_think_buffer = cleaned_full_text
 
198
  elif content_type == "answer":
199
  if think_open:
200
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '</think>'}, 'finish_reason': None}]})}\n\n"
201
  think_open = False
 
202
  cleaned_text = self._clean_answer_content(text)
203
  if cleaned_text:
204
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': cleaned_text}, 'finish_reason': None}]})}\n\n"
@@ -207,7 +222,6 @@ class ProxyHandler:
207
  if resp.status_code != 200:
208
  await cookie_manager.mark_cookie_failed(ck); err_body = await resp.aread()
209
  err_msg = f"Error: {resp.status_code} - {err_body.decode(errors='ignore')}"
210
- logger.error(f"Upstream error: {err_msg}")
211
  err = {"id": comp_id, "object": "chat.completion.chunk", "created": int(time.time()), "model": req.model, "choices": [{"index": 0, "delta": {"content": err_msg}, "finish_reason": "stop"}],}
212
  yield f"data: {json.dumps(err)}\n\n"; yield "data: [DONE]\n\n"; return
213
  await cookie_manager.mark_cookie_success(ck)
@@ -216,6 +230,7 @@ class ProxyHandler:
216
  for line in raw.strip().split('\n'):
217
  line = line.strip()
218
  if not line.startswith('data: '): continue
 
219
  payload_str = line[6:]
220
  if payload_str == '[DONE]':
221
  if think_open:
@@ -225,16 +240,23 @@ class ProxyHandler:
225
  return
226
  try:
227
  dat = json.loads(payload_str).get("data", {})
228
- except (json.JSONDecodeError, AttributeError): continue
 
229
 
230
  phase = dat.get("phase")
231
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
232
- if not content_chunk: continue
 
 
233
 
234
  if phase == "thinking":
235
- current_raw_thinking = content_chunk if dat.get("edit_content") is not None else current_raw_thinking + content_chunk
 
 
 
236
  async for item in yield_delta("thinking", current_raw_thinking):
237
  yield item
 
238
  elif phase == "answer":
239
  content_to_process = content_chunk
240
  if is_first_answer_chunk:
@@ -242,6 +264,7 @@ class ProxyHandler:
242
  parts = content_to_process.split('</details>', 1)
243
  content_to_process = parts[1] if len(parts) > 1 else ""
244
  is_first_answer_chunk = False
 
245
  if content_to_process:
246
  async for item in yield_delta("answer", content_to_process):
247
  yield item
@@ -251,13 +274,12 @@ class ProxyHandler:
251
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
252
  ck = None
253
  try:
254
- body, headers, ck, url = await self._prep_upstream(req)
255
  last_thinking_content = ""
256
  raw_answer_parts = []
257
  async with self.client.stream("POST", url, json=body, headers=headers) as resp:
258
  if resp.status_code != 200:
259
  await cookie_manager.mark_cookie_failed(ck); error_detail = await resp.text()
260
- logger.error(f"Upstream error: {resp.status_code} - {error_detail}")
261
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
262
  await cookie_manager.mark_cookie_success(ck)
263
 
@@ -276,11 +298,17 @@ class ProxyHandler:
276
 
277
  phase = dat.get("phase")
278
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
279
- if not content_chunk: continue
 
 
280
 
281
  if phase == "thinking":
282
- current_raw_thinking = content_chunk if dat.get("edit_content") is not None else current_raw_thinking + content_chunk
 
 
 
283
  last_thinking_content = current_raw_thinking
 
284
  elif phase == "answer":
285
  content_to_process = content_chunk
286
  if is_first_answer_chunk:
@@ -288,6 +316,7 @@ class ProxyHandler:
288
  parts = content_to_process.split('</details>', 1)
289
  content_to_process = parts[1] if len(parts) > 1 else ""
290
  is_first_answer_chunk = False
 
291
  if content_to_process:
292
  raw_answer_parts.append(content_to_process)
293
  else:
 
1
  """
2
  Proxy handler for Z.AI API requests
3
  """
4
+ import json, logging, re, time, uuid, base64, datetime, hashlib, hmac, urllib.parse
5
  from typing import AsyncGenerator, Dict, Any, Tuple, List
 
 
6
  import httpx
7
  from fastapi import HTTPException
8
  from fastapi.responses import StreamingResponse
 
21
  limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
22
  http2=True,
23
  )
 
 
24
 
25
  async def aclose(self):
26
  if not self.client.is_closed:
27
  await self.client.aclose()
28
+
 
 
 
29
  def _parse_jwt_token(self, token: str) -> Dict[str, str]:
30
+ """A simple, dependency-free JWT parser to get the user_id."""
31
  try:
32
  parts = token.split('.')
33
  if len(parts) != 3:
34
+ return {"user_id": ""}
 
35
  payload_b64 = parts[1]
36
+ # Add padding if necessary for base64 decoding
37
  payload_b64 += '=' * (-len(payload_b64) % 4)
38
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
39
+ payload = json.loads(payload_bytes)
40
+ user_id = payload.get("sub", "")
41
+ return {"user_id": user_id}
42
+ except Exception:
43
+ logger.warning("Failed to parse JWT token, continuing without user_id.", exc_info=False)
44
+ return {"user_id": ""}
45
+
46
+ def _construct_payload(self, token: str, user_id: str, chat_id: str, request_id: str) -> Tuple[str, str]:
47
+ """Constructs the sorted payload string (vl) and URL parameters for signature."""
48
+ timestamp_ms = str(int(time.time() * 1000))
49
+ # Hardcoding is fine for these fingerprinting values, mimicking the JS logic
50
+ data = {
51
+ 'timestamp': timestamp_ms,
52
+ 'requestId': request_id,
53
+ 'user_id': user_id,
54
+ 'token': token,
55
+ 'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
56
+ 'current_url': f'https://chat.z.ai/c/{chat_id}',
57
+ 'pathname': f'/c/{chat_id}',
58
+ 'timezone': 'Asia/Shanghai', # Hardcoded for simplicity
59
+ 'timezone_offset': '-480', # Hardcoded for simplicity (UTC+8)
60
+ 'local_time': datetime.datetime.now().isoformat(),
61
+ 'utc_time': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
62
+ 'version': '0.0.1', 'platform': 'web', 'language': 'zh-CN', 'languages': 'zh-CN,en',
63
+ 'cookie_enabled': 'true', 'screen_width': '2560', 'screen_height': '1440',
64
+ 'screen_resolution': '2560x1440', 'viewport_height': '1328', 'viewport_width': '1342',
65
+ 'viewport_size': '1342x1328', 'color_depth': '24', 'pixel_ratio': '2',
66
+ 'search': '', 'hash': '', 'host': 'chat.z.ai', 'hostname': 'chat.z.ai',
67
+ 'protocol': 'https:', 'referrer': '', 'title': 'Chat with Z.ai - Free AI Chatbot powered by GLM-4.5',
68
+ 'is_mobile': 'false', 'is_touch': 'false', 'max_touch_points': '0',
69
+ 'browser_name': 'Chrome', 'os_name': 'Mac OS'
70
+ }
71
+
72
+ # Sort keys and create the required string formats
73
+ sorted_items = sorted(data.items())
74
+ sorted_payload_str = ','.join([f"{k},{v}" for k, v in sorted_items])
75
+ url_params_str = urllib.parse.urlencode(dict(sorted_items))
76
+
77
+ return sorted_payload_str, url_params_str
78
+
79
+ def _generate_signature(self, vl: str, mt: str, token: str) -> Dict[str, Any]:
80
+ """Generates the signature based on the provided JS logic."""
81
+ primary_secret = "junjie"
82
+ timestamp_ms = int(time.time() * 1000)
83
+
84
+ # Use 1-minute buckets as in the JS code (60 seconds * 1000 ms)
85
+ minute_bucket = timestamp_ms // 60000
86
+
87
+ # Level 1 HMAC to derive key
88
+ level1_data = f"{token}|{minute_bucket}"
89
+ mac1 = hmac.new(primary_secret.encode('utf-8'), level1_data.encode('utf-8'), hashlib.sha256)
90
  derived_key_hex = mac1.hexdigest()
91
 
92
+ # Level 2 HMAC for the final signature
93
+ level2_data = f"{vl}|{mt}|{timestamp_ms}"
94
+ mac2 = hmac.new(derived_key_hex.encode('utf-8'), level2_data.encode('utf-8'), hashlib.sha256)
95
  signature = mac2.hexdigest()
 
 
96
 
97
+ return {"signature": signature, "timestamp": timestamp_ms}
98
+
99
  def _clean_thinking_content(self, text: str) -> str:
100
+ """
101
+ Aggressively cleans raw thinking content strings based on observed patterns
102
+ from the Z.AI API.
103
+ """
104
+ if not text:
105
+ return ""
106
+
107
  cleaned_text = text
108
+
109
+ # 1. Remove specific unwanted blocks like tool calls and summaries.
110
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
111
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', cleaned_text, flags=re.DOTALL)
112
+
113
+ # 2. **FIX**: Remove tag-like metadata containing `duration` attribute.
114
+ # This handles the reported issue: `true" duration="0" ... >`
115
  cleaned_text = re.sub(r'<[^>]*duration="[^"]*"[^>]*>', '', cleaned_text)
116
+
117
+ # 3. Remove specific structural tags, but keep the content between them.
118
+ cleaned_text = cleaned_text.replace("</thinking>", "")
119
+ cleaned_text = cleaned_text.replace("<Full>", "")
120
+ cleaned_text = cleaned_text.replace("</Full>", "")
121
+ # This regex handles <details>, <details open>, and </details>
122
  cleaned_text = re.sub(r'</?details[^>]*>', '', cleaned_text)
123
+
124
+ # 4. Handle markdown blockquotes, preserving multi-level ones.
125
  cleaned_text = re.sub(r'^\s*>\s*(?!>)', '', cleaned_text, flags=re.MULTILINE)
126
+
127
+ # 5. Remove other known text artifacts.
128
  cleaned_text = cleaned_text.replace("Thinking…", "")
129
+
130
+ # 6. Final strip to clean up residual whitespace.
131
  return cleaned_text.strip()
132
 
133
  def _clean_answer_content(self, text: str) -> str:
134
+ """
135
+ Cleans unwanted tags from answer content.
136
+ Does NOT strip whitespace to preserve markdown in streams.
137
+ """
138
+ if not text:
139
+ return ""
140
+ # Remove tool call blocks
141
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', text, flags=re.DOTALL)
142
+ # Remove any residual details/summary blocks that might leak into the answer
143
  cleaned_text = re.sub(r'<details[^>]*>.*?</details>', '', cleaned_text, flags=re.DOTALL)
144
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
145
  return cleaned_text
146
 
147
  def _serialize_msgs(self, msgs) -> list:
148
+ """Converts message objects to a list of dictionaries."""
149
  out = []
150
  for m in msgs:
151
  if hasattr(m, "dict"): out.append(m.dict())
 
155
  return out
156
 
157
  async def _prep_upstream(self, req: ChatCompletionRequest) -> Tuple[Dict[str, Any], Dict[str, str], str, str]:
158
+ """Prepares the request body, headers, URL, and cookie for the upstream API."""
159
  ck = await cookie_manager.get_next_cookie()
160
  if not ck: raise HTTPException(503, "No available cookies")
161
 
162
+ # 1. Extract necessary info for signature
163
  chat_id = str(uuid.uuid4())
164
  request_id = str(uuid.uuid4())
165
+ user_id = self._parse_jwt_token(ck).get("user_id", "")
 
 
 
166
 
167
+ last_message = req.messages[-1] if req.messages else None
168
+ # 'mt' is the content of the last message
169
+ mt = last_message.content if last_message and isinstance(last_message.content, str) else ""
 
 
 
 
 
 
 
 
170
 
171
+ # 2. Generate signature components
172
+ # 'vl' is the sorted payload string
173
+ vl, url_params = self._construct_payload(ck, user_id, chat_id, request_id)
174
+ sig_data = self._generate_signature(vl, mt, ck)
175
+ signature = sig_data["signature"]
176
+ timestamp = sig_data["timestamp"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # 3. Construct the final dynamic URL
179
+ final_url = f"{settings.UPSTREAM_URL}?{url_params}&signature_timestamp={timestamp}"
 
180
 
181
+ # 4. Prepare body and headers
182
+ model = settings.UPSTREAM_MODEL if req.model == settings.MODEL_NAME else req.model
183
+ body = { "stream": True, "model": model, "messages": self._serialize_msgs(req.messages), "background_tasks": {"title_generation": True, "tags_generation": True}, "chat_id": chat_id, "features": {"image_generation": False, "code_interpreter": False, "web_search": False, "auto_web_search": False, "enable_thinking": True,}, "id": request_id, "mcp_servers": ["deep-web-search"], "model_item": {"id": model, "name": "GLM-4.6", "owned_by": "openai"}, "params": {}, "tool_servers": [], "variables": {"{{USER_NAME}}": "User", "{{USER_LOCATION}}": "Unknown", "{{CURRENT_DATETIME}}": time.strftime("%Y-%m-%d %H:%M:%S"),},}
184
+ headers = { "Content-Type": "application/json", "Authorization": f"Bearer {ck}", "User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"), "Accept": "application/json, text/event-stream", "Accept-Language": "zh-CN", "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="141", "Google Chrome";v="141"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "x-fe-version": "prod-fe-1.0.79", "X-Signature": signature, "Origin": "https://chat.z.ai", "Referer": "https://chat.z.ai/",}
185
 
186
+ return body, headers, final_url, ck
187
 
188
  async def stream_proxy_response(self, req: ChatCompletionRequest) -> AsyncGenerator[str, None]:
189
  ck = None
190
  try:
191
+ body, headers, url, ck = await self._prep_upstream(req)
192
  comp_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
193
  think_open = False
194
  yielded_think_buffer = ""
 
201
  if not think_open:
202
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '<think>'}, 'finish_reason': None}]})}\n\n"
203
  think_open = True
204
+
205
  cleaned_full_text = self._clean_thinking_content(text)
206
+ delta_to_send = cleaned_full_text[len(yielded_think_buffer):] if cleaned_full_text.startswith(yielded_think_buffer) else cleaned_full_text
207
+
208
  if delta_to_send:
209
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': delta_to_send}, 'finish_reason': None}]})}\n\n"
210
  yielded_think_buffer = cleaned_full_text
211
+
212
  elif content_type == "answer":
213
  if think_open:
214
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '</think>'}, 'finish_reason': None}]})}\n\n"
215
  think_open = False
216
+
217
  cleaned_text = self._clean_answer_content(text)
218
  if cleaned_text:
219
  yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': cleaned_text}, 'finish_reason': None}]})}\n\n"
 
222
  if resp.status_code != 200:
223
  await cookie_manager.mark_cookie_failed(ck); err_body = await resp.aread()
224
  err_msg = f"Error: {resp.status_code} - {err_body.decode(errors='ignore')}"
 
225
  err = {"id": comp_id, "object": "chat.completion.chunk", "created": int(time.time()), "model": req.model, "choices": [{"index": 0, "delta": {"content": err_msg}, "finish_reason": "stop"}],}
226
  yield f"data: {json.dumps(err)}\n\n"; yield "data: [DONE]\n\n"; return
227
  await cookie_manager.mark_cookie_success(ck)
 
230
  for line in raw.strip().split('\n'):
231
  line = line.strip()
232
  if not line.startswith('data: '): continue
233
+
234
  payload_str = line[6:]
235
  if payload_str == '[DONE]':
236
  if think_open:
 
240
  return
241
  try:
242
  dat = json.loads(payload_str).get("data", {})
243
+ except (json.JSONDecodeError, AttributeError):
244
+ continue
245
 
246
  phase = dat.get("phase")
247
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
248
+
249
+ if not content_chunk:
250
+ continue
251
 
252
  if phase == "thinking":
253
+ if dat.get("edit_content") is not None:
254
+ current_raw_thinking = content_chunk
255
+ else:
256
+ current_raw_thinking += content_chunk
257
  async for item in yield_delta("thinking", current_raw_thinking):
258
  yield item
259
+
260
  elif phase == "answer":
261
  content_to_process = content_chunk
262
  if is_first_answer_chunk:
 
264
  parts = content_to_process.split('</details>', 1)
265
  content_to_process = parts[1] if len(parts) > 1 else ""
266
  is_first_answer_chunk = False
267
+
268
  if content_to_process:
269
  async for item in yield_delta("answer", content_to_process):
270
  yield item
 
274
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
275
  ck = None
276
  try:
277
+ body, headers, url, ck = await self._prep_upstream(req)
278
  last_thinking_content = ""
279
  raw_answer_parts = []
280
  async with self.client.stream("POST", url, json=body, headers=headers) as resp:
281
  if resp.status_code != 200:
282
  await cookie_manager.mark_cookie_failed(ck); error_detail = await resp.text()
 
283
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
284
  await cookie_manager.mark_cookie_success(ck)
285
 
 
298
 
299
  phase = dat.get("phase")
300
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
301
+
302
+ if not content_chunk:
303
+ continue
304
 
305
  if phase == "thinking":
306
+ if dat.get("edit_content") is not None:
307
+ current_raw_thinking = content_chunk
308
+ else:
309
+ current_raw_thinking += content_chunk
310
  last_thinking_content = current_raw_thinking
311
+
312
  elif phase == "answer":
313
  content_to_process = content_chunk
314
  if is_first_answer_chunk:
 
316
  parts = content_to_process.split('</details>', 1)
317
  content_to_process = parts[1] if len(parts) > 1 else ""
318
  is_first_answer_chunk = False
319
+
320
  if content_to_process:
321
  raw_answer_parts.append(content_to_process)
322
  else: