bluewinliang commited on
Commit
56e545a
·
verified ·
1 Parent(s): c394f8c

Update proxy_handler.py

Browse files
Files changed (1) hide show
  1. proxy_handler.py +37 -30
proxy_handler.py CHANGED
@@ -29,36 +29,35 @@ class ProxyHandler:
29
  def _clean_thinking_content(self, text: str) -> str:
30
  """
31
  Aggressively cleans raw thinking content strings based on observed patterns
32
- from the Z.AI API, inspired by a reference Cloudflare implementation.
33
- Removes tool calls, specific HTML-like tags, and other metadata while preserving
34
- the core thought process content.
35
  """
36
  if not text:
37
  return ""
38
 
39
  cleaned_text = text
40
 
41
- # 1. Remove entire blocks where the content is also unwanted metadata.
42
- # e.g., <summary>Thinking...</summary> or <glm_block>...</glm_block>
43
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
44
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', cleaned_text, flags=re.DOTALL)
45
 
46
- # 2. Remove specific structural tags, but keep the content between them.
47
- # Inspired by the reference implementation's targeted replaces.
48
- # e.g., <details> content </details> becomes just 'content'
 
 
49
  cleaned_text = cleaned_text.replace("</thinking>", "")
50
  cleaned_text = cleaned_text.replace("<Full>", "")
51
  cleaned_text = cleaned_text.replace("</Full>", "")
52
  # This regex handles <details>, <details open>, and </details>
53
  cleaned_text = re.sub(r'</?details[^>]*>', '', cleaned_text)
54
 
55
- # 3. Handle markdown blockquotes.
56
  cleaned_text = re.sub(r'^\s*>\s*(?!>)', '', cleaned_text, flags=re.MULTILINE)
57
 
58
- # 4. Remove other known text artifacts.
59
  cleaned_text = cleaned_text.replace("Thinking…", "")
60
 
61
- # 5. Final strip to clean up residual whitespace from removed elements.
62
  return cleaned_text.strip()
63
 
64
  def _clean_answer_content(self, text: str) -> str:
@@ -102,6 +101,8 @@ class ProxyHandler:
102
  think_open = False
103
  yielded_think_buffer = ""
104
  current_raw_thinking = ""
 
 
105
 
106
  async def yield_delta(content_type: str, text: str):
107
  nonlocal think_open, yielded_think_buffer
@@ -151,7 +152,6 @@ class ProxyHandler:
151
  except (json.JSONDecodeError, AttributeError):
152
  continue
153
 
154
- # --- START OF REFACTORED LOGIC ---
155
  phase = dat.get("phase")
156
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
157
 
@@ -159,21 +159,27 @@ class ProxyHandler:
159
  continue
160
 
161
  if phase == "thinking":
162
- # Accumulate raw thinking content. `edit_content` replaces the buffer.
163
  if dat.get("edit_content") is not None:
164
  current_raw_thinking = content_chunk
165
  else:
166
  current_raw_thinking += content_chunk
167
- # Yield the processed delta of the accumulated thinking content
168
  async for item in yield_delta("thinking", current_raw_thinking):
169
  yield item
170
 
171
  elif phase == "answer":
172
- # Directly yield the answer chunk for processing
173
- async for item in yield_delta("answer", content_chunk):
174
- yield item
175
- # --- END OF REFACTORED LOGIC ---
176
-
 
 
 
 
 
 
 
 
177
  except Exception:
178
  logger.exception("Stream error"); raise
179
 
@@ -190,7 +196,7 @@ class ProxyHandler:
190
  await cookie_manager.mark_cookie_success(ck)
191
 
192
  current_raw_thinking = ""
193
- answer_started = False
194
 
195
  async for raw in resp.aiter_text():
196
  for line in raw.strip().split('\n'):
@@ -202,7 +208,6 @@ class ProxyHandler:
202
  dat = json.loads(payload_str).get("data", {})
203
  except (json.JSONDecodeError, AttributeError): continue
204
 
205
- # Use the more robust phase-based logic for non-stream as well
206
  phase = dat.get("phase")
207
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
208
 
@@ -210,7 +215,6 @@ class ProxyHandler:
210
  continue
211
 
212
  if phase == "thinking":
213
- answer_started = False # Ensure we are in thinking mode
214
  if dat.get("edit_content") is not None:
215
  current_raw_thinking = content_chunk
216
  else:
@@ -218,19 +222,22 @@ class ProxyHandler:
218
  last_thinking_content = current_raw_thinking
219
 
220
  elif phase == "answer":
221
- if not answer_started:
222
- # First answer chunk might contain leftover thinking part, clean it.
223
- cleaned_chunk = self._clean_answer_content(content_chunk)
224
- if cleaned_chunk:
225
- raw_answer_parts.append(cleaned_chunk)
226
- answer_started = True
227
- else:
228
- raw_answer_parts.append(content_chunk)
 
 
229
  else:
230
  continue
231
  break
232
 
233
  full_answer = ''.join(raw_answer_parts)
 
234
  cleaned_ans_text = self._clean_answer_content(full_answer).strip()
235
  final_content = cleaned_ans_text
236
 
 
29
  def _clean_thinking_content(self, text: str) -> str:
30
  """
31
  Aggressively cleans raw thinking content strings based on observed patterns
32
+ from the Z.AI API.
 
 
33
  """
34
  if not text:
35
  return ""
36
 
37
  cleaned_text = text
38
 
39
+ # 1. Remove specific unwanted blocks like tool calls and summaries.
 
40
  cleaned_text = re.sub(r'<summary>.*?</summary>', '', cleaned_text, flags=re.DOTALL)
41
  cleaned_text = re.sub(r'<glm_block.*?</glm_block>', '', cleaned_text, flags=re.DOTALL)
42
 
43
+ # 2. **FIX**: Remove tag-like metadata containing `duration` attribute.
44
+ # This handles the reported issue: `true" duration="0" ... >`
45
+ cleaned_text = re.sub(r'<[^>]*duration="[^"]*"[^>]*>', '', cleaned_text)
46
+
47
+ # 3. Remove specific structural tags, but keep the content between them.
48
  cleaned_text = cleaned_text.replace("</thinking>", "")
49
  cleaned_text = cleaned_text.replace("<Full>", "")
50
  cleaned_text = cleaned_text.replace("</Full>", "")
51
  # This regex handles <details>, <details open>, and </details>
52
  cleaned_text = re.sub(r'</?details[^>]*>', '', cleaned_text)
53
 
54
+ # 4. Handle markdown blockquotes, preserving multi-level ones.
55
  cleaned_text = re.sub(r'^\s*>\s*(?!>)', '', cleaned_text, flags=re.MULTILINE)
56
 
57
+ # 5. Remove other known text artifacts.
58
  cleaned_text = cleaned_text.replace("Thinking…", "")
59
 
60
+ # 6. Final strip to clean up residual whitespace.
61
  return cleaned_text.strip()
62
 
63
  def _clean_answer_content(self, text: str) -> str:
 
101
  think_open = False
102
  yielded_think_buffer = ""
103
  current_raw_thinking = ""
104
+ # **FIX**: State to handle the transition from thinking to answer
105
+ is_first_answer_chunk = True
106
 
107
  async def yield_delta(content_type: str, text: str):
108
  nonlocal think_open, yielded_think_buffer
 
152
  except (json.JSONDecodeError, AttributeError):
153
  continue
154
 
 
155
  phase = dat.get("phase")
156
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
157
 
 
159
  continue
160
 
161
  if phase == "thinking":
 
162
  if dat.get("edit_content") is not None:
163
  current_raw_thinking = content_chunk
164
  else:
165
  current_raw_thinking += content_chunk
 
166
  async for item in yield_delta("thinking", current_raw_thinking):
167
  yield item
168
 
169
  elif phase == "answer":
170
+ content_to_process = content_chunk
171
+ # **FIX**: Special handling for the first answer chunk
172
+ if is_first_answer_chunk:
173
+ # The first answer chunk often contains leftover thinking content.
174
+ # We split by '</details>' and only use the part after it.
175
+ if '</details>' in content_to_process:
176
+ parts = content_to_process.split('</details>', 1)
177
+ content_to_process = parts[1] if len(parts) > 1 else ""
178
+ is_first_answer_chunk = False
179
+
180
+ if content_to_process:
181
+ async for item in yield_delta("answer", content_to_process):
182
+ yield item
183
  except Exception:
184
  logger.exception("Stream error"); raise
185
 
 
196
  await cookie_manager.mark_cookie_success(ck)
197
 
198
  current_raw_thinking = ""
199
+ is_first_answer_chunk = True
200
 
201
  async for raw in resp.aiter_text():
202
  for line in raw.strip().split('\n'):
 
208
  dat = json.loads(payload_str).get("data", {})
209
  except (json.JSONDecodeError, AttributeError): continue
210
 
 
211
  phase = dat.get("phase")
212
  content_chunk = dat.get("delta_content") or dat.get("edit_content")
213
 
 
215
  continue
216
 
217
  if phase == "thinking":
 
218
  if dat.get("edit_content") is not None:
219
  current_raw_thinking = content_chunk
220
  else:
 
222
  last_thinking_content = current_raw_thinking
223
 
224
  elif phase == "answer":
225
+ content_to_process = content_chunk
226
+ # **FIX**: Apply same logic to non-stream mode
227
+ if is_first_answer_chunk:
228
+ if '</details>' in content_to_process:
229
+ parts = content_to_process.split('</details>', 1)
230
+ content_to_process = parts[1] if len(parts) > 1 else ""
231
+ is_first_answer_chunk = False
232
+
233
+ if content_to_process:
234
+ raw_answer_parts.append(content_to_process)
235
  else:
236
  continue
237
  break
238
 
239
  full_answer = ''.join(raw_answer_parts)
240
+ # The final cleaning is still useful for any other residual tags
241
  cleaned_ans_text = self._clean_answer_content(full_answer).strip()
242
  final_content = cleaned_ans_text
243