bluewinliang commited on
Commit
fabff21
·
verified ·
1 Parent(s): e094ace

Update proxy_handler.py

Browse files
Files changed (1) hide show
  1. proxy_handler.py +50 -20
proxy_handler.py CHANGED
@@ -58,14 +58,26 @@ class ProxyHandler:
58
  Returns:
59
  A dictionary with 'signature' and 'timestamp'.
60
  """
 
61
  timestamp_ms = self._get_timestamp_millis()
 
 
62
  b64_encoded_t = base64.b64encode(t_payload.encode("utf-8")).decode("utf-8")
 
 
63
  message_string = f"{e_payload}|{b64_encoded_t}|{timestamp_ms}"
 
 
64
  n = timestamp_ms // (5 * 60 * 1000)
 
 
65
  msg1 = str(n).encode("utf-8")
66
  intermediate_key = hmac.new(self.primary_secret, msg1, hashlib.sha256).hexdigest()
 
 
67
  msg2 = message_string.encode("utf-8")
68
  final_signature = hmac.new(intermediate_key.encode("utf-8"), msg2, hashlib.sha256).hexdigest()
 
69
  return {"signature": final_signature, "timestamp": timestamp_ms}
70
 
71
  def _clean_thinking_content(self, text: str) -> str:
@@ -79,15 +91,13 @@ class ProxyHandler:
79
 
80
  def _clean_answer_content(self, text: str) -> str:
81
  if not text: return ""
82
- # This function cleans tags that might appear in any subsequent chunk, not just the first one.
83
- cleaned_text = re.sub(r'<details[^>]*>.*?</details>', '', text, flags=re.DOTALL)
84
- cleaned_text = re.sub(r'<glm_block.*?</glm_block>|<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
85
- cleaned_text = re.sub(r'\s*duration="\d+"[^>]*>', '', cleaned_text)
86
  return cleaned_text
87
 
88
  def _serialize_msgs(self, msgs) -> list:
89
  out = []
90
  for m in msgs:
 
91
  if hasattr(m, "dict"): out.append(m.dict())
92
  elif hasattr(m, "model_dump"): out.append(m.model_dump())
93
  elif isinstance(m, dict): out.append(m)
@@ -95,26 +105,39 @@ class ProxyHandler:
95
  return out
96
 
97
  async def _prep_upstream(self, req: ChatCompletionRequest) -> Tuple[Dict[str, Any], Dict[str, str], str, str]:
 
98
  ck = await cookie_manager.get_next_cookie()
99
  if not ck: raise HTTPException(503, "No available cookies")
100
 
101
  model = settings.UPSTREAM_MODEL if req.model == settings.MODEL_NAME else req.model
 
 
102
 
 
 
 
 
 
103
  payload_user_id = str(uuid.uuid4())
104
  payload_request_id = str(uuid.uuid4())
105
  payload_timestamp = str(self._get_timestamp_millis())
106
 
 
107
  e_payload = f"requestId,{payload_request_id},timestamp,{payload_timestamp},user_id,{payload_user_id}"
 
 
108
  t_payload = ""
109
  if req.messages:
110
  last_message = req.messages[-1]
111
  if isinstance(last_message.content, str):
112
  t_payload = last_message.content
113
 
 
114
  signature_data = self._generate_signature(e_payload, t_payload)
115
  signature = signature_data["signature"]
116
  signature_timestamp = signature_data["timestamp"]
117
 
 
118
  url_params = {
119
  "requestId": payload_request_id,
120
  "timestamp": payload_timestamp,
@@ -122,14 +145,16 @@ class ProxyHandler:
122
  "signature_timestamp": str(signature_timestamp)
123
  }
124
 
 
 
125
  final_url = httpx.URL(settings.UPSTREAM_URL).copy_with(params=url_params)
126
 
127
  body = {
128
  "stream": True,
129
  "model": model,
130
  "messages": self._serialize_msgs(req.messages),
131
- "chat_id": str(uuid.uuid4()),
132
- "id": str(uuid.uuid4()),
133
  "features": {
134
  "image_generation": False,
135
  "web_search": False,
@@ -181,7 +206,6 @@ class ProxyHandler:
181
  if think_open:
182
  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"
183
  think_open = False
184
- # The _clean_answer_content is a general cleaner for subsequent chunks.
185
  cleaned_text = self._clean_answer_content(text)
186
  if cleaned_text:
187
  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"
@@ -200,6 +224,7 @@ class ProxyHandler:
200
  line = line.strip()
201
  if not line.startswith('data: '): continue
202
  payload_str = line[6:]
 
203
  if payload_str == '[DONE]':
204
  if think_open:
205
  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"
@@ -213,7 +238,11 @@ class ProxyHandler:
213
  phase = dat.get("phase")
214
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
215
  if not content_chunk:
216
- continue
 
 
 
 
217
 
218
  if phase == "thinking":
219
  current_raw_thinking = content_chunk if dat.get("edit_content") is not None else current_raw_thinking + content_chunk
@@ -222,15 +251,12 @@ class ProxyHandler:
222
  elif phase == "answer":
223
  content_to_process = content_chunk
224
  if is_first_answer_chunk:
225
- last_bracket_pos = content_to_process.rfind('>')
226
- if last_bracket_pos != -1:
227
- content_to_process = content_to_process[last_bracket_pos + 1:]
228
-
229
- # FINAL FIX: Remove any leading whitespace after slicing.
230
- content_to_process = content_to_process.lstrip()
231
-
232
  is_first_answer_chunk = False
233
-
234
  if content_to_process:
235
  async for item in yield_delta("answer", content_to_process):
236
  yield item
@@ -238,9 +264,12 @@ class ProxyHandler:
238
  logger.exception("Stream error"); raise
239
 
240
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
 
 
241
  ck = None
242
  try:
243
  body, headers, ck, url = await self._prep_upstream(req)
 
244
  body["stream"] = False
245
 
246
  async with self.client.post(url, json=body, headers=headers) as resp:
@@ -250,9 +279,13 @@ class ProxyHandler:
250
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
251
 
252
  await cookie_manager.mark_cookie_success(ck)
 
 
253
  response_data = resp.json()
 
 
254
  final_content = ""
255
- finish_reason = "stop"
256
 
257
  if "choices" in response_data and response_data["choices"]:
258
  first_choice = response_data["choices"][0]
@@ -261,9 +294,6 @@ class ProxyHandler:
261
  if "finish_reason" in first_choice:
262
  finish_reason = first_choice["finish_reason"]
263
 
264
- # Apply general cleaning to non-streamed responses as well.
265
- final_content = self._clean_answer_content(final_content)
266
-
267
  return ChatCompletionResponse(
268
  id=response_data.get("id", f"chatcmpl-{uuid.uuid4().hex[:29]}"),
269
  created=int(time.time()),
 
58
  Returns:
59
  A dictionary with 'signature' and 'timestamp'.
60
  """
61
+ # --- Updated logic from signature_generator.py ---
62
  timestamp_ms = self._get_timestamp_millis()
63
+
64
+ # 1. Base64 encode the message content (t_payload)
65
  b64_encoded_t = base64.b64encode(t_payload.encode("utf-8")).decode("utf-8")
66
+
67
+ # 2. Construct the message string for signing using the b64 encoded payload
68
  message_string = f"{e_payload}|{b64_encoded_t}|{timestamp_ms}"
69
+
70
+ # 3. Calculate the time bucket 'n' (5-minute intervals)
71
  n = timestamp_ms // (5 * 60 * 1000)
72
+
73
+ # 4. Intermediate key derivation
74
  msg1 = str(n).encode("utf-8")
75
  intermediate_key = hmac.new(self.primary_secret, msg1, hashlib.sha256).hexdigest()
76
+
77
+ # 5. Final signature
78
  msg2 = message_string.encode("utf-8")
79
  final_signature = hmac.new(intermediate_key.encode("utf-8"), msg2, hashlib.sha256).hexdigest()
80
+
81
  return {"signature": final_signature, "timestamp": timestamp_ms}
82
 
83
  def _clean_thinking_content(self, text: str) -> str:
 
91
 
92
  def _clean_answer_content(self, text: str) -> str:
93
  if not text: return ""
94
+ cleaned_text = re.sub(r'<glm_block.*?</glm_block>|<details[^>]*>.*?</details>|<summary>.*?</summary>', '', text, flags=re.DOTALL)
 
 
 
95
  return cleaned_text
96
 
97
  def _serialize_msgs(self, msgs) -> list:
98
  out = []
99
  for m in msgs:
100
+ # Adapting to Pydantic v1/v2 and dicts
101
  if hasattr(m, "dict"): out.append(m.dict())
102
  elif hasattr(m, "model_dump"): out.append(m.model_dump())
103
  elif isinstance(m, dict): out.append(m)
 
105
  return out
106
 
107
  async def _prep_upstream(self, req: ChatCompletionRequest) -> Tuple[Dict[str, Any], Dict[str, str], str, str]:
108
+ """Prepares the request body, headers, cookie, and URL for the upstream API."""
109
  ck = await cookie_manager.get_next_cookie()
110
  if not ck: raise HTTPException(503, "No available cookies")
111
 
112
  model = settings.UPSTREAM_MODEL if req.model == settings.MODEL_NAME else req.model
113
+ chat_id = str(uuid.uuid4())
114
+ request_id = str(uuid.uuid4())
115
 
116
+ # --- NEW Simplified Signature Payload Logic ---
117
+ user_info = self._parse_jwt_token(ck)
118
+ user_id = user_info.get("user_id", "")
119
+ # The reference code uses a separate UUID for user_id in payload, let's follow that.
120
+ # This seems strange, but let's replicate the reference code exactly.
121
  payload_user_id = str(uuid.uuid4())
122
  payload_request_id = str(uuid.uuid4())
123
  payload_timestamp = str(self._get_timestamp_millis())
124
 
125
+ # e: The simplified payload for the signature
126
  e_payload = f"requestId,{payload_request_id},timestamp,{payload_timestamp},user_id,{payload_user_id}"
127
+
128
+ # t: The last message content
129
  t_payload = ""
130
  if req.messages:
131
  last_message = req.messages[-1]
132
  if isinstance(last_message.content, str):
133
  t_payload = last_message.content
134
 
135
+ # Generate the signature
136
  signature_data = self._generate_signature(e_payload, t_payload)
137
  signature = signature_data["signature"]
138
  signature_timestamp = signature_data["timestamp"]
139
 
140
+ # The reference code sends these as URL parameters, not in the body.
141
  url_params = {
142
  "requestId": payload_request_id,
143
  "timestamp": payload_timestamp,
 
145
  "signature_timestamp": str(signature_timestamp)
146
  }
147
 
148
+ # Construct URL with query parameters
149
+ # Note: The reference code has a typo `f"{BASE_URL}/api/chat/completions"`, it should be `z.ai`
150
  final_url = httpx.URL(settings.UPSTREAM_URL).copy_with(params=url_params)
151
 
152
  body = {
153
  "stream": True,
154
  "model": model,
155
  "messages": self._serialize_msgs(req.messages),
156
+ "chat_id": chat_id,
157
+ "id": request_id,
158
  "features": {
159
  "image_generation": False,
160
  "web_search": False,
 
206
  if think_open:
207
  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"
208
  think_open = False
 
209
  cleaned_text = self._clean_answer_content(text)
210
  if cleaned_text:
211
  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"
 
224
  line = line.strip()
225
  if not line.startswith('data: '): continue
226
  payload_str = line[6:]
227
+ # The reference code has a special 'done' phase, but the original Z.AI uses [DONE]
228
  if payload_str == '[DONE]':
229
  if think_open:
230
  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"
 
238
  phase = dat.get("phase")
239
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
240
  if not content_chunk:
241
+ # Handle case where chunk is just usage info, etc.
242
+ if phase == 'other' and dat.get('usage'):
243
+ pass # In streaming, usage might come with the final chunk
244
+ else:
245
+ continue
246
 
247
  if phase == "thinking":
248
  current_raw_thinking = content_chunk if dat.get("edit_content") is not None else current_raw_thinking + content_chunk
 
251
  elif phase == "answer":
252
  content_to_process = content_chunk
253
  if is_first_answer_chunk:
254
+ # MODIFICATION START: Fix for lost characters after thinking phase
255
+ # Use a robust regex to remove everything up to and including the first '</details>' tag.
256
+ # This is more reliable than split() for handling stream boundaries.
257
+ content_to_process = re.sub(r'^.*?</details>', '', content_to_process, count=1, flags=re.DOTALL)
258
+ # MODIFICATION END
 
 
259
  is_first_answer_chunk = False
 
260
  if content_to_process:
261
  async for item in yield_delta("answer", content_to_process):
262
  yield item
 
264
  logger.exception("Stream error"); raise
265
 
266
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
267
+ # This part of the code can be simplified as well, but let's focus on fixing the streaming first.
268
+ # The logic will be almost identical to the streaming one.
269
  ck = None
270
  try:
271
  body, headers, ck, url = await self._prep_upstream(req)
272
+ # For non-stream, set stream to False in the body
273
  body["stream"] = False
274
 
275
  async with self.client.post(url, json=body, headers=headers) as resp:
 
279
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
280
 
281
  await cookie_manager.mark_cookie_success(ck)
282
+
283
+ # Z.AI non-stream response is a single JSON object
284
  response_data = resp.json()
285
+
286
+ # We need to adapt Z.AI's response format to OpenAI's format
287
  final_content = ""
288
+ finish_reason = "stop" # Default
289
 
290
  if "choices" in response_data and response_data["choices"]:
291
  first_choice = response_data["choices"][0]
 
294
  if "finish_reason" in first_choice:
295
  finish_reason = first_choice["finish_reason"]
296
 
 
 
 
297
  return ChatCompletionResponse(
298
  id=response_data.get("id", f"chatcmpl-{uuid.uuid4().hex[:29]}"),
299
  created=int(time.time()),