bluewinliang commited on
Commit
1f98263
·
verified ·
1 Parent(s): c2a4afc

Upload proxy_handler.py

Browse files
Files changed (1) hide show
  1. proxy_handler.py +69 -87
proxy_handler.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Proxy handler for Z.AI API requests - DEBUG VERSION
3
  """
4
  import json, logging, re, time, uuid
5
  from typing import AsyncGenerator, Dict, Any, Tuple
@@ -13,8 +13,6 @@ from cookie_manager import cookie_manager
13
  from models import ChatCompletionRequest, ChatCompletionResponse
14
 
15
  logger = logging.getLogger(__name__)
16
- # Set logger to DEBUG level to ensure all our messages are captured
17
- logger.setLevel(logging.DEBUG)
18
 
19
 
20
  class ProxyHandler:
@@ -29,7 +27,7 @@ class ProxyHandler:
29
  if not self.client.is_closed:
30
  await self.client.aclose()
31
 
32
- # --- Text utilities ---
33
  def _clean_thinking(self, s: str) -> str:
34
  if not s: return ""
35
  s = re.sub(r'<details[^>]*>.*?</details>', '', s, flags=re.DOTALL)
@@ -40,8 +38,6 @@ class ProxyHandler:
40
  def _clean_answer(self, s: str) -> str:
41
  if not s: return ""
42
  return re.sub(r"<details[^>]*>.*?</details>", "", s, flags=re.DOTALL)
43
-
44
- # --- Other methods ---
45
  def _serialize_msgs(self, msgs) -> list:
46
  out = []
47
  for m in msgs:
@@ -58,14 +54,15 @@ class ProxyHandler:
58
  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/138.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="138", "Google Chrome";v="138"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "x-fe-version": "prod-fe-1.0.53", "Origin": "https://chat.z.ai", "Referer": "https://chat.z.ai/",}
59
  return body, headers, ck
60
 
61
- # Streaming response is left as-is for now, we focus on non-stream for debugging
62
  async def stream_proxy_response(self, req: ChatCompletionRequest) -> AsyncGenerator[str, None]:
63
- # This function remains the same as the previous correct version.
64
- # The focus of debugging is on the non-stream version.
65
  ck = None
66
  try:
67
  body, headers, ck = await self._prep_upstream(req)
68
- comp_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"; think_open = False; phase_cur = None
 
 
 
69
  async with self.client.stream("POST", settings.UPSTREAM_URL, json=body, headers=headers) as resp:
70
  if resp.status_code != 200:
71
  await cookie_manager.mark_cookie_failed(ck)
@@ -73,42 +70,60 @@ class ProxyHandler:
73
  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"}],}
74
  yield f"data: {json.dumps(err)}\n\n"; yield "data: [DONE]\n\n"; return
75
  await cookie_manager.mark_cookie_success(ck)
 
76
  async for raw in resp.aiter_text():
77
  for line in raw.strip().split('\n'):
78
  line = line.strip()
79
  if not line or not line.startswith('data: '): continue
80
  payload_str = line[6:]
81
  if payload_str == '[DONE]':
82
- if think_open: 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"
83
- yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"; yield "data: [DONE]\n\n"; return
84
- try: dat = json.loads(payload_str).get("data", {})
 
 
 
85
  except (json.JSONDecodeError, AttributeError): continue
86
- delta = dat.get("delta_content", ""); new_phase = dat.get("phase")
 
 
 
 
87
  is_transition = new_phase and new_phase != phase_cur
88
  if is_transition:
89
  if phase_cur == "thinking" and new_phase == "answer" and think_open and settings.SHOW_THINK_TAGS:
90
- 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"
 
91
  think_open = False
92
  phase_cur = new_phase
 
93
  current_content_phase = phase_cur or new_phase
 
94
  text_to_yield = ""
95
  if current_content_phase == "thinking":
96
- if delta and settings.SHOW_THINK_TAGS:
97
  if not think_open:
98
- 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"
 
99
  think_open = True
100
- text_to_yield = self._clean_thinking(delta)
101
- elif current_content_phase == "answer": text_to_yield = self._clean_answer(delta)
102
- if text_to_yield: yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': text_to_yield}, 'finish_reason': None}]})}\n\n"
103
- except Exception: logger.exception("Stream error"); raise
 
 
 
 
 
 
 
104
 
105
- # ---------- NON-STREAM (DEBUG VERSION) ----------
106
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
107
- logger.debug("="*20 + " STARTING NON-STREAM DEBUG " + "="*20)
108
  ck = None
109
  try:
110
  body, headers, ck = await self._prep_upstream(req)
111
- full_content_stream = []
112
  phase_cur = None
113
 
114
  async with self.client.stream("POST", settings.UPSTREAM_URL, json=body, headers=headers) as resp:
@@ -116,9 +131,8 @@ class ProxyHandler:
116
  await cookie_manager.mark_cookie_failed(ck)
117
  error_detail = await resp.text()
118
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
119
-
120
  await cookie_manager.mark_cookie_success(ck)
121
-
122
  async for raw in resp.aiter_text():
123
  for line in raw.strip().split('\n'):
124
  line = line.strip()
@@ -127,68 +141,38 @@ class ProxyHandler:
127
  if payload_str == '[DONE]': break
128
  try:
129
  dat = json.loads(payload_str).get("data", {})
130
- full_content_stream.append(dat) # Store the raw parsed data
131
  except (json.JSONDecodeError, AttributeError): continue
 
 
 
 
 
 
 
 
 
 
132
  else: continue
133
  break
134
-
135
- # --- STAGE 1: LOG RAW COLLECTED DATA ---
136
- logger.debug("-" * 10 + " STAGE 1: RAW DATA FROM Z.AI " + "-" * 10)
137
- for i, dat in enumerate(full_content_stream):
138
- # Use !r to get an unambiguous representation of the string
139
- logger.debug(f"Chunk {i}: {dat!r}")
140
- logger.debug("-" * 10 + " END STAGE 1 " + "-" * 10)
141
-
142
-
143
- # --- STAGE 2: PROCESS AND LOG RAW JOINED STRINGS ---
144
- raw_think_str = ''.join([d.get("delta_content", "") for d in full_content_stream if d.get("phase") == "thinking"])
145
- raw_answer_str = ''.join([d.get("delta_content", "") for d in full_content_stream if d.get("phase") == "answer"])
146
 
147
- # This is a fallback for chunks that might not have a phase.
148
- # It's more complex but might catch the edge case.
149
- phase_aware_think = []
150
- phase_aware_answer = []
151
- current_phase_for_build = None
152
- for d in full_content_stream:
153
- if 'phase' in d:
154
- current_phase_for_build = d['phase']
155
- if 'delta_content' in d:
156
- if current_phase_for_build == 'thinking':
157
- phase_aware_think.append(d['delta_content'])
158
- elif current_phase_for_build == 'answer':
159
- phase_aware_answer.append(d['delta_content'])
160
 
161
- phase_aware_raw_answer_str = ''.join(phase_aware_answer)
162
-
163
-
164
- logger.debug("-" * 10 + " STAGE 2: RAW JOINED STRINGS " + "-" * 10)
165
- logger.debug(f"Phase-unaware Think String: {raw_think_str!r}")
166
- logger.debug(f"Phase-unaware Answer String: {raw_answer_str!r}")
167
- logger.debug(f"Phase-aware Answer String: {phase_aware_raw_answer_str!r}")
168
- logger.debug("-" * 10 + " END STAGE 2 " + "-" * 10)
169
-
170
-
171
- # --- STAGE 3: PROCESS AND LOG FINAL CLEANED TEXT ---
172
- # We will use the more robust phase-aware string for the final result
173
- think_text = self._clean_thinking(''.join(phase_aware_think)).strip()
174
- ans_text = self._clean_answer(''.join(phase_aware_answer))
175
-
176
-
177
- logger.debug("-" * 10 + " STAGE 3: FINAL CLEANED TEXT " + "-" * 10)
178
- logger.debug(f"Final Think Text: {think_text!r}")
179
- logger.debug(f"Final Answer Text: {ans_text!r}")
180
- logger.debug("-" * 10 + " END STAGE 3 " + "-" * 10)
181
-
182
-
183
- # Final construction
184
  final_content = ans_text
185
- if settings.SHOW_THINK_TAGS and think_text:
186
- newline = "\n" if ans_text and not ans_text.startswith(('\n', '\r')) else ""
187
- final_content = f"<think>{think_text}</think>{newline}{ans_text}"
188
-
189
- logger.debug(f"Final constructed content to be returned: {final_content!r}")
190
- logger.debug("="*20 + " END NON-STREAM DEBUG " + "="*20)
191
 
 
 
 
 
 
192
 
193
  return ChatCompletionResponse(
194
  id=f"chatcmpl-{uuid.uuid4().hex[:29]}", created=int(time.time()), model=req.model,
@@ -200,10 +184,8 @@ class ProxyHandler:
200
 
201
  # ---------- FastAPI entry ----------
202
  async def handle_chat_completion(self, req: ChatCompletionRequest):
203
- # Force non-stream mode for this debug session
204
- if req.stream:
205
- logger.warning("Request specified streaming, but DEBUG handler is forcing non-stream mode.")
206
-
207
- # Make sure stream is False to trigger the debug path
208
- req.stream = False
209
  return await self.non_stream_proxy_response(req)
 
1
  """
2
+ Proxy handler for Z.AI API requests
3
  """
4
  import json, logging, re, time, uuid
5
  from typing import AsyncGenerator, Dict, Any, Tuple
 
13
  from models import ChatCompletionRequest, ChatCompletionResponse
14
 
15
  logger = logging.getLogger(__name__)
 
 
16
 
17
 
18
  class ProxyHandler:
 
27
  if not self.client.is_closed:
28
  await self.client.aclose()
29
 
30
+ # --- Text utilities and other methods remain the same ---
31
  def _clean_thinking(self, s: str) -> str:
32
  if not s: return ""
33
  s = re.sub(r'<details[^>]*>.*?</details>', '', s, flags=re.DOTALL)
 
38
  def _clean_answer(self, s: str) -> str:
39
  if not s: return ""
40
  return re.sub(r"<details[^>]*>.*?</details>", "", s, flags=re.DOTALL)
 
 
41
  def _serialize_msgs(self, msgs) -> list:
42
  out = []
43
  for m in msgs:
 
54
  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/138.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="138", "Google Chrome";v="138"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "x-fe-version": "prod-fe-1.0.53", "Origin": "https://chat.z.ai", "Referer": "https://chat.z.ai/",}
55
  return body, headers, ck
56
 
57
+ # ---------- stream ----------
58
  async def stream_proxy_response(self, req: ChatCompletionRequest) -> AsyncGenerator[str, None]:
 
 
59
  ck = None
60
  try:
61
  body, headers, ck = await self._prep_upstream(req)
62
+ comp_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
63
+ think_open = False
64
+ phase_cur = None
65
+
66
  async with self.client.stream("POST", settings.UPSTREAM_URL, json=body, headers=headers) as resp:
67
  if resp.status_code != 200:
68
  await cookie_manager.mark_cookie_failed(ck)
 
70
  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"}],}
71
  yield f"data: {json.dumps(err)}\n\n"; yield "data: [DONE]\n\n"; return
72
  await cookie_manager.mark_cookie_success(ck)
73
+
74
  async for raw in resp.aiter_text():
75
  for line in raw.strip().split('\n'):
76
  line = line.strip()
77
  if not line or not line.startswith('data: '): continue
78
  payload_str = line[6:]
79
  if payload_str == '[DONE]':
80
+ if think_open:
81
+ 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"
82
+ yield f"data: {json.dumps({'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
83
+ yield "data: [DONE]\n\n"; return
84
+ try:
85
+ dat = json.loads(payload_str).get("data", {})
86
  except (json.JSONDecodeError, AttributeError): continue
87
+
88
+ # --- FINAL FIX: Handle both delta_content and edit_content ---
89
+ content = dat.get("delta_content", "") or dat.get("edit_content", "")
90
+ new_phase = dat.get("phase")
91
+
92
  is_transition = new_phase and new_phase != phase_cur
93
  if is_transition:
94
  if phase_cur == "thinking" and new_phase == "answer" and think_open and settings.SHOW_THINK_TAGS:
95
+ close_payload = {'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '</think>'}, 'finish_reason': None}]}
96
+ yield f"data: {json.dumps(close_payload)}\n\n"
97
  think_open = False
98
  phase_cur = new_phase
99
+
100
  current_content_phase = phase_cur or new_phase
101
+
102
  text_to_yield = ""
103
  if current_content_phase == "thinking":
104
+ if content and settings.SHOW_THINK_TAGS:
105
  if not think_open:
106
+ open_payload = {'id': comp_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': req.model, 'choices': [{'index': 0, 'delta': {'content': '<think>'}, 'finish_reason': None}]}
107
+ yield f"data: {json.dumps(open_payload)}\n\n"
108
  think_open = True
109
+ text_to_yield = self._clean_thinking(content)
110
+ elif current_content_phase == "answer":
111
+ text_to_yield = self._clean_answer(content)
112
+
113
+ if text_to_yield:
114
+ content_payload = {"id": comp_id, "object": "chat.completion.chunk", "created": int(time.time()), "model": req.model, "choices": [{"index": 0, "delta": {"content": text_to_yield}, "finish_reason": None}],}
115
+ yield f"data: {json.dumps(content_payload)}\n\n"
116
+ except Exception:
117
+ logger.exception("Stream error")
118
+ # You might want to yield an error to the client here as well
119
+ raise
120
 
121
+ # ---------- non-stream ----------
122
  async def non_stream_proxy_response(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
 
123
  ck = None
124
  try:
125
  body, headers, ck = await self._prep_upstream(req)
126
+ full_content = []
127
  phase_cur = None
128
 
129
  async with self.client.stream("POST", settings.UPSTREAM_URL, json=body, headers=headers) as resp:
 
131
  await cookie_manager.mark_cookie_failed(ck)
132
  error_detail = await resp.text()
133
  raise HTTPException(resp.status_code, f"Upstream error: {error_detail}")
 
134
  await cookie_manager.mark_cookie_success(ck)
135
+
136
  async for raw in resp.aiter_text():
137
  for line in raw.strip().split('\n'):
138
  line = line.strip()
 
141
  if payload_str == '[DONE]': break
142
  try:
143
  dat = json.loads(payload_str).get("data", {})
 
144
  except (json.JSONDecodeError, AttributeError): continue
145
+
146
+ # --- FINAL FIX: Handle both delta_content and edit_content ---
147
+ content = dat.get("delta_content") or dat.get("edit_content")
148
+ new_phase = dat.get("phase")
149
+
150
+ if new_phase:
151
+ phase_cur = new_phase
152
+
153
+ if content and phase_cur:
154
+ full_content.append((phase_cur, content))
155
  else: continue
156
  break
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ think_buf = []
159
+ answer_buf = []
160
+ for phase, content in full_content:
161
+ if phase == "thinking":
162
+ think_buf.append(self._clean_thinking(content))
163
+ elif phase == "answer":
164
+ # For edit_content, it can contain both thinking and answer parts.
165
+ # We only want the answer part. Let's clean it again.
166
+ answer_buf.append(self._clean_answer(content))
 
 
 
 
167
 
168
+ ans_text = ''.join(answer_buf)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  final_content = ans_text
 
 
 
 
 
 
170
 
171
+ if settings.SHOW_THINK_TAGS and think_buf:
172
+ think_text = ''.join(think_buf).strip()
173
+ if think_text:
174
+ newline = "\n" if ans_text and not ans_text.startswith(('\n', '\r')) else ""
175
+ final_content = f"<think>{think_text}</think>{newline}{ans_text}"
176
 
177
  return ChatCompletionResponse(
178
  id=f"chatcmpl-{uuid.uuid4().hex[:29]}", created=int(time.time()), model=req.model,
 
184
 
185
  # ---------- FastAPI entry ----------
186
  async def handle_chat_completion(self, req: ChatCompletionRequest):
187
+ stream = bool(req.stream) if req.stream is not None else settings.DEFAULT_STREAM
188
+ if stream:
189
+ return StreamingResponse(self.stream_proxy_response(req), media_type="text/event-stream",
190
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"})
 
 
191
  return await self.non_stream_proxy_response(req)