tamar-model-client 0.1.23__tar.gz → 0.1.25__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/PKG-INFO +1 -1
  2. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/setup.py +1 -1
  3. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/async_client.py +156 -2
  4. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/exceptions.py +2 -2
  5. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/sync_client.py +155 -1
  6. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client.egg-info/PKG-INFO +1 -1
  7. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client.egg-info/SOURCES.txt +1 -4
  8. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tests/test_google_azure_final.py +4 -4
  9. tamar_model_client-0.1.23/tests/stream_hanging_analysis.py +0 -357
  10. tamar_model_client-0.1.23/tests/test_logging_issue.py +0 -75
  11. tamar_model_client-0.1.23/tests/test_simple.py +0 -235
  12. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/README.md +0 -0
  13. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/setup.cfg +0 -0
  14. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/__init__.py +0 -0
  15. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/auth.py +0 -0
  16. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/circuit_breaker.py +0 -0
  17. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/__init__.py +0 -0
  18. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/base_client.py +0 -0
  19. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/http_fallback.py +0 -0
  20. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/logging_setup.py +0 -0
  21. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/request_builder.py +0 -0
  22. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/response_handler.py +0 -0
  23. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/core/utils.py +0 -0
  24. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/enums/__init__.py +0 -0
  25. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/enums/channel.py +0 -0
  26. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/enums/invoke.py +0 -0
  27. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/enums/providers.py +0 -0
  28. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/error_handler.py +0 -0
  29. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/generated/__init__.py +0 -0
  30. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/generated/model_service_pb2.py +0 -0
  31. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/generated/model_service_pb2_grpc.py +0 -0
  32. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/json_formatter.py +0 -0
  33. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/logging_icons.py +0 -0
  34. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/schemas/__init__.py +0 -0
  35. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/schemas/inputs.py +0 -0
  36. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/schemas/outputs.py +0 -0
  37. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client/utils.py +0 -0
  38. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client.egg-info/dependency_links.txt +0 -0
  39. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client.egg-info/requires.txt +0 -0
  40. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tamar_model_client.egg-info/top_level.txt +0 -0
  41. {tamar_model_client-0.1.23 → tamar_model_client-0.1.25}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-model-client
3
- Version: 0.1.23
3
+ Version: 0.1.25
4
4
  Summary: A Python SDK for interacting with the Model Manager gRPC service
5
5
  Home-page: http://gitlab.tamaredge.top/project-tap/AgentOS/model-manager-client
6
6
  Author: Oscar Ou
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="tamar-model-client",
5
- version="0.1.23",
5
+ version="0.1.25",
6
6
  description="A Python SDK for interacting with the Model Manager gRPC service",
7
7
  author="Oscar Ou",
8
8
  author_email="oscar.ou@tamaredge.ai",
@@ -98,6 +98,9 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
98
98
  # === gRPC 通道和连接管理 ===
99
99
  self.channel: Optional[grpc.aio.Channel] = None
100
100
  self.stub: Optional[model_service_pb2_grpc.ModelServiceStub] = None
101
+ self._channel_error_count = 0
102
+ self._last_channel_error_time = None
103
+ self._channel_lock = asyncio.Lock() # 异步锁
101
104
 
102
105
  # === 增强的重试处理器 ===
103
106
  self.retry_handler = EnhancedRetryHandler(
@@ -176,9 +179,23 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
176
179
  Raises:
177
180
  ConnectionError: 当达到最大重试次数仍无法连接时
178
181
  """
179
- if self.channel and self.stub:
182
+ if self.channel and self.stub and await self._is_channel_healthy():
180
183
  return
181
184
 
185
+ # 如果 channel 存在但不健康,记录日志
186
+ if self.channel and self.stub:
187
+ logger.warning(
188
+ "Channel exists but unhealthy, will recreate",
189
+ extra={
190
+ "log_type": "channel_recreate",
191
+ "data": {
192
+ "channel_error_count": self._channel_error_count,
193
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else None
194
+ }
195
+ }
196
+ )
197
+ await self._recreate_channel()
198
+
182
199
  retry_count = 0
183
200
  options = self.build_channel_options()
184
201
 
@@ -228,6 +245,111 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
228
245
  await asyncio.sleep(self.retry_delay * retry_count)
229
246
 
230
247
  raise ConnectionError(f"Failed to connect to {self.server_address} after {self.max_retries} retries")
248
+
249
+ async def _is_channel_healthy(self) -> bool:
250
+ """
251
+ 检查 channel 是否健康
252
+
253
+ Returns:
254
+ bool: True 如果 channel 健康,False 如果需要重建
255
+ """
256
+ if not self.channel:
257
+ return False
258
+
259
+ try:
260
+ # 检查 channel 状态
261
+ state = self.channel.get_state()
262
+
263
+ # 如果处于关闭或失败状态,需要重建
264
+ if state in [grpc.ChannelConnectivity.SHUTDOWN,
265
+ grpc.ChannelConnectivity.TRANSIENT_FAILURE]:
266
+ logger.warning(f"Channel in unhealthy state: {state}",
267
+ extra={"log_type": "info",
268
+ "data": {"channel_state": str(state)}})
269
+ return False
270
+
271
+ # 如果最近有多次错误,也需要重建
272
+ if self._channel_error_count > 3 and self._last_channel_error_time:
273
+ if time.time() - self._last_channel_error_time < 60: # 60秒内
274
+ logger.warning("Too many channel errors recently, marking as unhealthy",
275
+ extra={"log_type": "info",
276
+ "data": {"error_count": self._channel_error_count}})
277
+ return False
278
+
279
+ return True
280
+
281
+ except Exception as e:
282
+ logger.error(f"Error checking channel health: {e}",
283
+ extra={"log_type": "info",
284
+ "data": {"error": str(e)}})
285
+ return False
286
+
287
+ async def _recreate_channel(self):
288
+ """
289
+ 重建 gRPC channel
290
+
291
+ 关闭旧的 channel 并创建新的连接
292
+ """
293
+ async with self._channel_lock:
294
+ # 关闭旧 channel
295
+ if self.channel:
296
+ try:
297
+ await self.channel.close()
298
+ logger.info("Closed unhealthy channel",
299
+ extra={"log_type": "info"})
300
+ except Exception as e:
301
+ logger.warning(f"Error closing channel: {e}",
302
+ extra={"log_type": "info"})
303
+
304
+ # 清空引用
305
+ self.channel = None
306
+ self.stub = None
307
+
308
+ # 重置错误计数
309
+ self._channel_error_count = 0
310
+ self._last_channel_error_time = None
311
+
312
+ logger.info("Recreating gRPC channel...",
313
+ extra={"log_type": "info"})
314
+
315
+ def _record_channel_error(self, error: grpc.RpcError):
316
+ """
317
+ 记录 channel 错误,用于健康检查
318
+
319
+ Args:
320
+ error: gRPC 错误
321
+ """
322
+ self._channel_error_count += 1
323
+ self._last_channel_error_time = time.time()
324
+
325
+ # 获取当前 channel 状态
326
+ channel_state = None
327
+ if self.channel:
328
+ try:
329
+ channel_state = self.channel.get_state()
330
+ except:
331
+ channel_state = "UNKNOWN"
332
+
333
+ # 对于严重错误,增加错误权重
334
+ if error.code() in [grpc.StatusCode.INTERNAL,
335
+ grpc.StatusCode.UNAVAILABLE]:
336
+ self._channel_error_count += 2
337
+
338
+ # 记录详细的错误信息
339
+ logger.warning(
340
+ f"Channel error recorded: {error.code().name}",
341
+ extra={
342
+ "log_type": "channel_error",
343
+ "data": {
344
+ "error_code": error.code().name,
345
+ "error_count": self._channel_error_count,
346
+ "channel_state": str(channel_state) if channel_state else "NO_CHANNEL",
347
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else 0,
348
+ "error_details": error.details() if hasattr(error, 'details') else "",
349
+ "debug_string": error.debug_error_string() if hasattr(error, 'debug_error_string') else ""
350
+ }
351
+ }
352
+ )
231
353
 
232
354
  async def _retry_request(self, func, *args, **kwargs):
233
355
  """
@@ -315,7 +437,33 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
315
437
  elif retryable == 'conditional':
316
438
  # 条件重试,特殊处理 CANCELLED
317
439
  if error_code == grpc.StatusCode.CANCELLED:
318
- should_retry = error_context.is_network_cancelled()
440
+ # 获取 channel 状态信息
441
+ channel_state = None
442
+ if self.channel:
443
+ try:
444
+ channel_state = self.channel.get_state()
445
+ except:
446
+ channel_state = "UNKNOWN"
447
+
448
+ is_network_cancelled = error_context.is_network_cancelled()
449
+
450
+ logger.warning(
451
+ f"CANCELLED error in stream, channel state: {channel_state}",
452
+ extra={
453
+ "log_type": "cancelled_debug",
454
+ "request_id": context.get('request_id'),
455
+ "data": {
456
+ "channel_state": str(channel_state) if channel_state else "NO_CHANNEL",
457
+ "channel_error_count": self._channel_error_count,
458
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else None,
459
+ "channel_healthy": await self._is_channel_healthy(),
460
+ "is_network_cancelled": is_network_cancelled,
461
+ "debug_string": e.debug_error_string() if hasattr(e, 'debug_error_string') else ""
462
+ }
463
+ }
464
+ )
465
+
466
+ should_retry = is_network_cancelled
319
467
  else:
320
468
  should_retry = self._check_error_details_for_retry(e)
321
469
  else:
@@ -363,6 +511,8 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
363
511
  )
364
512
  context['duration'] = current_duration
365
513
  last_exception = self.error_handler.handle_error(e, context)
514
+ # 记录 channel 错误
515
+ self._record_channel_error(e)
366
516
  break
367
517
 
368
518
  last_exception = e
@@ -674,6 +824,10 @@ class AsyncTamarModelClient(BaseClient, AsyncHttpFallbackMixin):
674
824
  )
675
825
  })
676
826
 
827
+ # 记录 channel 错误
828
+ if isinstance(e, grpc.RpcError):
829
+ self._record_channel_error(e)
830
+
677
831
  # 记录失败并尝试降级(如果启用了熔断)
678
832
  if self.resilient_enabled and self.circuit_breaker:
679
833
  # 将错误码传递给熔断器,用于智能失败统计
@@ -65,9 +65,9 @@ RETRY_POLICY = {
65
65
  'max_attempts': 3
66
66
  },
67
67
  grpc.StatusCode.INTERNAL: {
68
- 'retryable': 'conditional', # 条件重试
68
+ 'retryable': False, # 内部错误通常不应重试
69
69
  'check_details': True,
70
- 'max_attempts': 2
70
+ 'max_attempts': 0
71
71
  },
72
72
  grpc.StatusCode.UNAUTHENTICATED: {
73
73
  'retryable': True,
@@ -22,6 +22,7 @@ Tamar Model Client 同步客户端实现
22
22
  import json
23
23
  import logging
24
24
  import random
25
+ import threading
25
26
  import time
26
27
  from typing import Optional, Union, Iterator
27
28
 
@@ -95,6 +96,9 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
95
96
  # === gRPC 通道和连接管理 ===
96
97
  self.channel: Optional[grpc.Channel] = None
97
98
  self.stub: Optional[model_service_pb2_grpc.ModelServiceStub] = None
99
+ self._channel_error_count = 0
100
+ self._last_channel_error_time = None
101
+ self._channel_lock = threading.Lock() # 线程安全的channel操作
98
102
 
99
103
  def close(self):
100
104
  """
@@ -143,8 +147,22 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
143
147
  Raises:
144
148
  ConnectionError: 当达到最大重试次数仍无法连接时
145
149
  """
146
- if self.channel and self.stub:
150
+ if self.channel and self.stub and self._is_channel_healthy():
147
151
  return
152
+
153
+ # 如果 channel 存在但不健康,记录日志
154
+ if self.channel and self.stub:
155
+ logger.warning(
156
+ "Channel exists but unhealthy, will recreate",
157
+ extra={
158
+ "log_type": "channel_recreate",
159
+ "data": {
160
+ "channel_error_count": self._channel_error_count,
161
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else None
162
+ }
163
+ }
164
+ )
165
+ self._recreate_channel()
148
166
 
149
167
  retry_count = 0
150
168
  options = self.build_channel_options()
@@ -196,6 +214,111 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
196
214
  time.sleep(self.retry_delay * retry_count)
197
215
 
198
216
  raise ConnectionError(f"Failed to connect to {self.server_address} after {self.max_retries} retries")
217
+
218
+ def _is_channel_healthy(self) -> bool:
219
+ """
220
+ 检查 channel 是否健康
221
+
222
+ Returns:
223
+ bool: True 如果 channel 健康,False 如果需要重建
224
+ """
225
+ if not self.channel:
226
+ return False
227
+
228
+ try:
229
+ # 检查 channel 状态
230
+ state = self.channel._channel.check_connectivity_state(False)
231
+
232
+ # 如果处于关闭或失败状态,需要重建
233
+ if state in [grpc.ChannelConnectivity.SHUTDOWN,
234
+ grpc.ChannelConnectivity.TRANSIENT_FAILURE]:
235
+ logger.warning(f"Channel in unhealthy state: {state}",
236
+ extra={"log_type": "info",
237
+ "data": {"channel_state": str(state)}})
238
+ return False
239
+
240
+ # 如果最近有多次错误,也需要重建
241
+ if self._channel_error_count > 3 and self._last_channel_error_time:
242
+ if time.time() - self._last_channel_error_time < 60: # 60秒内
243
+ logger.warning("Too many channel errors recently, marking as unhealthy",
244
+ extra={"log_type": "info",
245
+ "data": {"error_count": self._channel_error_count}})
246
+ return False
247
+
248
+ return True
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error checking channel health: {e}",
252
+ extra={"log_type": "info",
253
+ "data": {"error": str(e)}})
254
+ return False
255
+
256
+ def _recreate_channel(self):
257
+ """
258
+ 重建 gRPC channel
259
+
260
+ 关闭旧的 channel 并创建新的连接
261
+ """
262
+ with self._channel_lock:
263
+ # 关闭旧 channel
264
+ if self.channel:
265
+ try:
266
+ self.channel.close()
267
+ logger.info("Closed unhealthy channel",
268
+ extra={"log_type": "info"})
269
+ except Exception as e:
270
+ logger.warning(f"Error closing channel: {e}",
271
+ extra={"log_type": "info"})
272
+
273
+ # 清空引用
274
+ self.channel = None
275
+ self.stub = None
276
+
277
+ # 重置错误计数
278
+ self._channel_error_count = 0
279
+ self._last_channel_error_time = None
280
+
281
+ logger.info("Recreating gRPC channel...",
282
+ extra={"log_type": "info"})
283
+
284
+ def _record_channel_error(self, error: grpc.RpcError):
285
+ """
286
+ 记录 channel 错误,用于健康检查
287
+
288
+ Args:
289
+ error: gRPC 错误
290
+ """
291
+ self._channel_error_count += 1
292
+ self._last_channel_error_time = time.time()
293
+
294
+ # 获取当前 channel 状态
295
+ channel_state = None
296
+ if self.channel:
297
+ try:
298
+ channel_state = self.channel._channel.check_connectivity_state(False)
299
+ except:
300
+ channel_state = "UNKNOWN"
301
+
302
+ # 对于严重错误,增加错误权重
303
+ if error.code() in [grpc.StatusCode.INTERNAL,
304
+ grpc.StatusCode.UNAVAILABLE]:
305
+ self._channel_error_count += 2
306
+
307
+ # 记录详细的错误信息
308
+ logger.warning(
309
+ f"Channel error recorded: {error.code().name}",
310
+ extra={
311
+ "log_type": "channel_error",
312
+ "data": {
313
+ "error_code": error.code().name,
314
+ "error_count": self._channel_error_count,
315
+ "channel_state": str(channel_state) if channel_state else "NO_CHANNEL",
316
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else 0,
317
+ "error_details": error.details() if hasattr(error, 'details') else "",
318
+ "debug_string": error.debug_error_string() if hasattr(error, 'debug_error_string') else ""
319
+ }
320
+ }
321
+ )
199
322
 
200
323
  def _retry_request(self, func, *args, **kwargs):
201
324
  """
@@ -237,6 +360,30 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
237
360
  # 计算当前的耗时
238
361
  current_duration = time.time() - method_start_time
239
362
 
363
+ # 特殊处理 CANCELLED 错误
364
+ if e.code() == grpc.StatusCode.CANCELLED:
365
+ channel_state = None
366
+ if self.channel:
367
+ try:
368
+ channel_state = self.channel._channel.check_connectivity_state(False)
369
+ except:
370
+ channel_state = "UNKNOWN"
371
+
372
+ logger.warning(
373
+ f"CANCELLED error detected, channel state: {channel_state}",
374
+ extra={
375
+ "log_type": "cancelled_debug",
376
+ "request_id": context.get('request_id'),
377
+ "data": {
378
+ "channel_state": str(channel_state) if channel_state else "NO_CHANNEL",
379
+ "channel_error_count": self._channel_error_count,
380
+ "time_since_last_error": time.time() - self._last_channel_error_time if self._last_channel_error_time else None,
381
+ "channel_healthy": self._is_channel_healthy(),
382
+ "debug_string": e.debug_error_string() if hasattr(e, 'debug_error_string') else ""
383
+ }
384
+ }
385
+ )
386
+
240
387
  # 记录重试日志
241
388
  log_data = {
242
389
  "log_type": "info",
@@ -261,6 +408,9 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
261
408
 
262
409
  context['duration'] = current_duration
263
410
  last_exception = self.error_handler.handle_error(e, context)
411
+
412
+ # 记录 channel 错误
413
+ self._record_channel_error(e)
264
414
 
265
415
  except Exception as e:
266
416
  # 非 gRPC 错误,直接包装抛出
@@ -742,6 +892,10 @@ class TamarModelClient(BaseClient, HttpFallbackMixin):
742
892
  )
743
893
  })
744
894
 
895
+ # 记录 channel 错误
896
+ if isinstance(e, grpc.RpcError):
897
+ self._record_channel_error(e)
898
+
745
899
  # 记录失败并尝试降级(如果启用了熔断)
746
900
  if self.resilient_enabled and self.circuit_breaker:
747
901
  # 将错误码传递给熔断器,用于智能失败统计
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-model-client
3
- Version: 0.1.23
3
+ Version: 0.1.25
4
4
  Summary: A Python SDK for interacting with the Model Manager gRPC service
5
5
  Home-page: http://gitlab.tamaredge.top/project-tap/AgentOS/model-manager-client
6
6
  Author: Oscar Ou
@@ -33,7 +33,4 @@ tamar_model_client/schemas/__init__.py
33
33
  tamar_model_client/schemas/inputs.py
34
34
  tamar_model_client/schemas/outputs.py
35
35
  tests/__init__.py
36
- tests/stream_hanging_analysis.py
37
- tests/test_google_azure_final.py
38
- tests/test_logging_issue.py
39
- tests/test_simple.py
36
+ tests/test_google_azure_final.py
@@ -414,7 +414,7 @@ def test_concurrent_requests(num_requests: int = 150):
414
414
  model="tamar-google-gemini-flash-lite",
415
415
  contents="1+1等于几?",
416
416
  user_context=UserContext(
417
- user_id=f"concurrent_user_{request_id:03d}",
417
+ user_id=f"{os.environ.get('INSTANCE_ID', '0')}_{request_id:03d}",
418
418
  org_id="test_org",
419
419
  client_type="concurrent_test"
420
420
  ),
@@ -533,7 +533,7 @@ async def test_async_concurrent_requests(num_requests: int = 150):
533
533
  model="tamar-google-gemini-flash-lite",
534
534
  contents="1+1等于几?",
535
535
  user_context=UserContext(
536
- user_id=f"async_concurrent_user_{request_id:03d}",
536
+ user_id=f"{os.environ.get('INSTANCE_ID', '0')}_{request_id:03d}",
537
537
  org_id="test_org",
538
538
  client_type="async_concurrent_test"
539
539
  ),
@@ -645,10 +645,10 @@ async def main():
645
645
  # await asyncio.wait_for(test_batch_requests(), timeout=120.0)
646
646
 
647
647
  # 同步并发测试
648
- #test_concurrent_requests(150) # 测试150个并发请求
648
+ test_concurrent_requests(150) # 测试150个并发请求
649
649
 
650
650
  # 异步并发测试
651
- await test_async_concurrent_requests(2) # 测试150个异步并发请求
651
+ await test_async_concurrent_requests(150) # 测试150个异步并发请求
652
652
 
653
653
  print("\n✅ 测试完成")
654
654
 
@@ -1,357 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 流式响应挂起分析和解决方案演示
4
-
5
- 这个脚本模拟各种流式响应挂起场景,并展示解决方案。
6
- """
7
-
8
- import asyncio
9
- import time
10
- import logging
11
- from typing import AsyncIterator, Optional
12
- from dataclasses import dataclass
13
- from enum import Enum
14
-
15
- # 配置日志
16
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class StreamingFailureType(Enum):
21
- """流式响应失败类型"""
22
- PARTIAL_DATA_THEN_HANG = "partial_data_then_hang" # 发送部分数据后挂起
23
- NETWORK_INTERRUPTION = "network_interruption" # 网络中断
24
- SERVER_CRASH = "server_crash" # 服务器崩溃
25
- SLOW_RESPONSE = "slow_response" # 响应过慢
26
- CONNECTION_RESET = "connection_reset" # 连接重置
27
-
28
-
29
- @dataclass
30
- class StreamChunk:
31
- """流式数据块"""
32
- content: str
33
- chunk_id: int
34
- is_last: bool = False
35
- error: Optional[str] = None
36
-
37
-
38
- class MockStreamingServer:
39
- """模拟流式服务器的各种故障场景"""
40
-
41
- def __init__(self, failure_type: StreamingFailureType, failure_at_chunk: int = 3):
42
- self.failure_type = failure_type
43
- self.failure_at_chunk = failure_at_chunk
44
- self.chunks_sent = 0
45
-
46
- async def generate_stream(self) -> AsyncIterator[StreamChunk]:
47
- """生成流式数据"""
48
- try:
49
- while True:
50
- self.chunks_sent += 1
51
-
52
- # 正常发送数据
53
- if self.chunks_sent <= self.failure_at_chunk:
54
- chunk = StreamChunk(
55
- content=f"数据块 {self.chunks_sent}",
56
- chunk_id=self.chunks_sent,
57
- is_last=(self.chunks_sent == 10) # 假设10个块就结束
58
- )
59
- logger.info(f"📦 发送数据块 {self.chunks_sent}: {chunk.content}")
60
- yield chunk
61
-
62
- # 模拟正常的块间延迟
63
- await asyncio.sleep(0.1)
64
-
65
- if chunk.is_last:
66
- logger.info("✅ 流式传输正常完成")
67
- return
68
-
69
- # 在指定位置触发故障
70
- elif self.chunks_sent == self.failure_at_chunk + 1:
71
- await self._trigger_failure()
72
- # 故障后就不再发送数据
73
- return
74
-
75
- except Exception as e:
76
- logger.error(f"❌ 流式传输异常: {e}")
77
- yield StreamChunk(
78
- content="",
79
- chunk_id=self.chunks_sent,
80
- error=str(e)
81
- )
82
-
83
- async def _trigger_failure(self):
84
- """触发特定类型的故障"""
85
- logger.warning(f"⚠️ 触发故障类型: {self.failure_type.value}")
86
-
87
- if self.failure_type == StreamingFailureType.PARTIAL_DATA_THEN_HANG:
88
- logger.warning("🔄 服务器发送部分数据后挂起...")
89
- # 无限等待,模拟服务器挂起
90
- await asyncio.sleep(3600) # 等待1小时(实际会被超时机制打断)
91
-
92
- elif self.failure_type == StreamingFailureType.NETWORK_INTERRUPTION:
93
- logger.warning("📡 模拟网络中断...")
94
- await asyncio.sleep(2) # 短暂延迟后
95
- raise ConnectionError("网络连接中断")
96
-
97
- elif self.failure_type == StreamingFailureType.SERVER_CRASH:
98
- logger.warning("💥 模拟服务器崩溃...")
99
- raise RuntimeError("服务器内部错误")
100
-
101
- elif self.failure_type == StreamingFailureType.SLOW_RESPONSE:
102
- logger.warning("🐌 模拟服务器响应过慢...")
103
- await asyncio.sleep(30) # 30秒延迟
104
-
105
- elif self.failure_type == StreamingFailureType.CONNECTION_RESET:
106
- logger.warning("🔌 模拟连接重置...")
107
- raise ConnectionResetError("连接被重置")
108
-
109
-
110
- class StreamConsumer:
111
- """流式数据消费者,演示不同的处理策略"""
112
-
113
- def __init__(self, name: str):
114
- self.name = name
115
- self.chunks_received = 0
116
- self.start_time = time.time()
117
-
118
- async def consume_stream_basic(self, stream: AsyncIterator[StreamChunk]) -> bool:
119
- """基础流消费(容易挂起的版本)"""
120
- logger.info(f"🔄 {self.name}: 开始基础流消费...")
121
-
122
- try:
123
- async for chunk in stream:
124
- self.chunks_received += 1
125
- logger.info(f"📥 {self.name}: 收到数据块 {chunk.chunk_id}: {chunk.content}")
126
-
127
- if chunk.error:
128
- logger.error(f"❌ {self.name}: 数据块包含错误: {chunk.error}")
129
- return False
130
-
131
- if chunk.is_last:
132
- logger.info(f"✅ {self.name}: 流正常结束")
133
- return True
134
-
135
- logger.warning(f"⚠️ {self.name}: 流意外结束")
136
- return False
137
-
138
- except Exception as e:
139
- logger.error(f"❌ {self.name}: 流消费异常: {e}")
140
- return False
141
-
142
- async def consume_stream_with_timeout(self, stream: AsyncIterator[StreamChunk],
143
- chunk_timeout: float = 5.0) -> bool:
144
- """带超时保护的流消费"""
145
- logger.info(f"🔄 {self.name}: 开始带超时保护的流消费 (块超时: {chunk_timeout}s)...")
146
-
147
- try:
148
- # 注意:这种方法仍然有问题,因为 async for 本身不能被超时保护
149
- async for chunk in stream:
150
- self.chunks_received += 1
151
- logger.info(f"📥 {self.name}: 收到数据块 {chunk.chunk_id}: {chunk.content}")
152
-
153
- if chunk.error:
154
- logger.error(f"❌ {self.name}: 数据块包含错误: {chunk.error}")
155
- return False
156
-
157
- if chunk.is_last:
158
- logger.info(f"✅ {self.name}: 流正常结束")
159
- return True
160
-
161
- logger.warning(f"⚠️ {self.name}: 流意外结束")
162
- return False
163
-
164
- except asyncio.TimeoutError:
165
- logger.error(f"⏰ {self.name}: 流消费超时")
166
- return False
167
- except Exception as e:
168
- logger.error(f"❌ {self.name}: 流消费异常: {e}")
169
- return False
170
-
171
- async def consume_stream_with_chunk_timeout(self, stream: AsyncIterator[StreamChunk],
172
- chunk_timeout: float = 5.0,
173
- total_timeout: float = 60.0) -> bool:
174
- """正确的超时保护方案"""
175
- logger.info(f"🔄 {self.name}: 开始改进的流消费 (块超时: {chunk_timeout}s, 总超时: {total_timeout}s)...")
176
-
177
- stream_iter = stream.__aiter__()
178
- overall_start = time.time()
179
-
180
- try:
181
- while True:
182
- # 检查总体超时
183
- if time.time() - overall_start > total_timeout:
184
- logger.error(f"⏰ {self.name}: 总体超时 ({total_timeout}s)")
185
- return False
186
-
187
- # 对单个数据块获取进行超时保护
188
- try:
189
- chunk = await asyncio.wait_for(
190
- stream_iter.__anext__(),
191
- timeout=chunk_timeout
192
- )
193
-
194
- self.chunks_received += 1
195
- logger.info(f"📥 {self.name}: 收到数据块 {chunk.chunk_id}: {chunk.content}")
196
-
197
- if chunk.error:
198
- logger.error(f"❌ {self.name}: 数据块包含错误: {chunk.error}")
199
- return False
200
-
201
- if chunk.is_last:
202
- logger.info(f"✅ {self.name}: 流正常结束")
203
- return True
204
-
205
- except asyncio.TimeoutError:
206
- logger.error(f"⏰ {self.name}: 等待下一个数据块超时 ({chunk_timeout}s)")
207
- return False
208
-
209
- except StopAsyncIteration:
210
- logger.warning(f"⚠️ {self.name}: 流意外结束")
211
- return False
212
-
213
- except Exception as e:
214
- logger.error(f"❌ {self.name}: 流消费异常: {e}")
215
- return False
216
-
217
- async def consume_stream_with_heartbeat(self, stream: AsyncIterator[StreamChunk],
218
- heartbeat_interval: float = 2.0) -> bool:
219
- """带心跳检测的流消费"""
220
- logger.info(f"🔄 {self.name}: 开始带心跳检测的流消费...")
221
-
222
- stream_iter = stream.__aiter__()
223
- last_heartbeat = time.time()
224
-
225
- async def heartbeat_monitor():
226
- """心跳监控任务"""
227
- while True:
228
- await asyncio.sleep(heartbeat_interval)
229
- if time.time() - last_heartbeat > heartbeat_interval * 3:
230
- logger.warning(f"💓 {self.name}: 心跳超时,可能存在问题")
231
-
232
- # 启动心跳监控
233
- heartbeat_task = asyncio.create_task(heartbeat_monitor())
234
-
235
- try:
236
- while True:
237
- try:
238
- chunk = await asyncio.wait_for(
239
- stream_iter.__anext__(),
240
- timeout=10.0 # 10秒超时
241
- )
242
-
243
- last_heartbeat = time.time() # 更新心跳时间
244
- self.chunks_received += 1
245
- logger.info(f"📥 {self.name}: 收到数据块 {chunk.chunk_id}: {chunk.content}")
246
-
247
- if chunk.error:
248
- logger.error(f"❌ {self.name}: 数据块包含错误: {chunk.error}")
249
- return False
250
-
251
- if chunk.is_last:
252
- logger.info(f"✅ {self.name}: 流正常结束")
253
- return True
254
-
255
- except asyncio.TimeoutError:
256
- logger.error(f"⏰ {self.name}: 等待数据块超时")
257
- return False
258
-
259
- except StopAsyncIteration:
260
- logger.warning(f"⚠️ {self.name}: 流意外结束")
261
- return False
262
-
263
- finally:
264
- heartbeat_task.cancel()
265
- try:
266
- await heartbeat_task
267
- except asyncio.CancelledError:
268
- pass
269
-
270
-
271
- async def test_streaming_failure_scenario(failure_type: StreamingFailureType):
272
- """测试特定的流式失败场景"""
273
- logger.info(f"\n{'='*60}")
274
- logger.info(f"🧪 测试场景: {failure_type.value}")
275
- logger.info(f"{'='*60}")
276
-
277
- # 创建模拟服务器
278
- server = MockStreamingServer(failure_type, failure_at_chunk=3)
279
-
280
- # 创建不同策略的消费者
281
- consumers = [
282
- ("基础消费者", "consume_stream_basic"),
283
- ("改进的超时消费者", "consume_stream_with_chunk_timeout"),
284
- ("心跳检测消费者", "consume_stream_with_heartbeat")
285
- ]
286
-
287
- for consumer_name, method_name in consumers:
288
- logger.info(f"\n🔍 测试 {consumer_name}...")
289
-
290
- consumer = StreamConsumer(consumer_name)
291
- stream = server.generate_stream()
292
-
293
- start_time = time.time()
294
-
295
- try:
296
- # 根据方法名调用不同的消费策略
297
- method = getattr(consumer, method_name)
298
-
299
- if method_name == "consume_stream_basic":
300
- # 基础方法需要额外的超时保护
301
- success = await asyncio.wait_for(method(stream), timeout=15.0)
302
- else:
303
- success = await method(stream)
304
-
305
- duration = time.time() - start_time
306
-
307
- if success:
308
- logger.info(f"✅ {consumer_name} 成功完成,耗时: {duration:.2f}s,收到 {consumer.chunks_received} 个数据块")
309
- else:
310
- logger.warning(f"⚠️ {consumer_name} 未能成功完成,耗时: {duration:.2f}s,收到 {consumer.chunks_received} 个数据块")
311
-
312
- except asyncio.TimeoutError:
313
- duration = time.time() - start_time
314
- logger.error(f"⏰ {consumer_name} 超时,耗时: {duration:.2f}s,收到 {consumer.chunks_received} 个数据块")
315
-
316
- except Exception as e:
317
- duration = time.time() - start_time
318
- logger.error(f"❌ {consumer_name} 异常: {e},耗时: {duration:.2f}s,收到 {consumer.chunks_received} 个数据块")
319
-
320
- # 重置服务器状态进行下一个测试
321
- server = MockStreamingServer(failure_type, failure_at_chunk=3)
322
-
323
-
324
- async def main():
325
- """主测试函数"""
326
- logger.info("🚀 开始流式响应挂起分析...")
327
-
328
- # 测试各种失败场景
329
- failure_scenarios = [
330
- StreamingFailureType.PARTIAL_DATA_THEN_HANG,
331
- StreamingFailureType.NETWORK_INTERRUPTION,
332
- StreamingFailureType.SERVER_CRASH,
333
- StreamingFailureType.SLOW_RESPONSE,
334
- ]
335
-
336
- for scenario in failure_scenarios:
337
- try:
338
- await test_streaming_failure_scenario(scenario)
339
- except Exception as e:
340
- logger.error(f"❌ 测试场景 {scenario.value} 时出错: {e}")
341
-
342
- logger.info(f"\n{'='*60}")
343
- logger.info("🎯 分析结论:")
344
- logger.info("1. 基础的 async for 循环容易在流中断时挂起")
345
- logger.info("2. 需要对单个数据块的获取进行超时保护")
346
- logger.info("3. 心跳检测可以提供额外的监控能力")
347
- logger.info("4. 总体超时 + 块超时的双重保护最为可靠")
348
- logger.info(f"{'='*60}")
349
-
350
-
351
- if __name__ == "__main__":
352
- try:
353
- asyncio.run(main())
354
- except KeyboardInterrupt:
355
- logger.info("\n⚠️ 用户中断测试")
356
- finally:
357
- logger.info("🏁 流式响应分析完成")
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 测试日志格式问题
4
- """
5
-
6
- import asyncio
7
- import logging
8
- import os
9
- import sys
10
-
11
- # 设置环境变量
12
- os.environ['MODEL_MANAGER_SERVER_GRPC_USE_TLS'] = "false"
13
- os.environ['MODEL_MANAGER_SERVER_ADDRESS'] = "localhost:50051"
14
- os.environ['MODEL_MANAGER_SERVER_JWT_SECRET_KEY'] = "model-manager-server-jwt-key"
15
-
16
- # 先导入 SDK
17
- from tamar_model_client import AsyncTamarModelClient
18
- from tamar_model_client.schemas import ModelRequest, UserContext
19
- from tamar_model_client.enums import ProviderType, InvokeType, Channel
20
-
21
- # 检查 SDK 的日志配置
22
- print("=== SDK Logger Configuration ===")
23
- sdk_loggers = [
24
- 'tamar_model_client',
25
- 'tamar_model_client.async_client',
26
- 'tamar_model_client.error_handler',
27
- 'tamar_model_client.core.base_client'
28
- ]
29
-
30
- for logger_name in sdk_loggers:
31
- logger = logging.getLogger(logger_name)
32
- print(f"\nLogger: {logger_name}")
33
- print(f" Level: {logging.getLevelName(logger.level)}")
34
- print(f" Handlers: {len(logger.handlers)}")
35
- for i, handler in enumerate(logger.handlers):
36
- print(f" Handler {i}: {type(handler).__name__}")
37
- if hasattr(handler, 'formatter'):
38
- print(f" Formatter: {type(handler.formatter).__name__ if handler.formatter else 'None'}")
39
- print(f" Propagate: {logger.propagate}")
40
-
41
-
42
- async def test_error_logging():
43
- """测试错误日志格式"""
44
- print("\n=== Testing Error Logging ===")
45
-
46
- try:
47
- async with AsyncTamarModelClient() as client:
48
- # 故意创建一个会失败的请求
49
- request = ModelRequest(
50
- provider=ProviderType.GOOGLE,
51
- channel=Channel.VERTEXAI,
52
- invoke_type=InvokeType.GENERATION,
53
- model="invalid-model",
54
- contents="test",
55
- user_context=UserContext(
56
- user_id="test_user",
57
- org_id="test_org",
58
- client_type="test_client"
59
- )
60
- )
61
-
62
- response = await client.invoke(request, timeout=5.0)
63
- print(f"Response: {response}")
64
-
65
- except Exception as e:
66
- print(f"Exception caught: {type(e).__name__}: {str(e)}")
67
-
68
-
69
- async def main():
70
- await test_error_logging()
71
-
72
-
73
- if __name__ == "__main__":
74
- print("Starting logging test...")
75
- asyncio.run(main())
@@ -1,235 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 简化版的 Google/Azure 场景测试脚本
4
- 只保留基本调用和打印功能
5
- """
6
-
7
- import asyncio
8
- import logging
9
- import os
10
- import sys
11
-
12
- # 配置日志
13
- logging.basicConfig(
14
- level=logging.INFO,
15
- format='%(asctime)s - %(levelname)s - %(message)s'
16
- )
17
- logger = logging.getLogger(__name__)
18
-
19
- os.environ['MODEL_MANAGER_SERVER_GRPC_USE_TLS'] = "false"
20
- os.environ['MODEL_MANAGER_SERVER_ADDRESS'] = "localhost:50051"
21
- os.environ['MODEL_MANAGER_SERVER_JWT_SECRET_KEY'] = "model-manager-server-jwt-key"
22
-
23
- # 导入客户端模块
24
- try:
25
- from tamar_model_client import TamarModelClient, AsyncTamarModelClient
26
- from tamar_model_client.schemas import ModelRequest, UserContext
27
- from tamar_model_client.enums import ProviderType, InvokeType, Channel
28
- except ImportError as e:
29
- logger.error(f"导入模块失败: {e}")
30
- sys.exit(1)
31
-
32
-
33
- def test_google_ai_studio():
34
- """测试 Google AI Studio"""
35
- print("\n🔍 测试 Google AI Studio...")
36
-
37
- try:
38
- client = TamarModelClient()
39
-
40
- request = ModelRequest(
41
- provider=ProviderType.GOOGLE,
42
- channel=Channel.AI_STUDIO,
43
- invoke_type=InvokeType.GENERATION,
44
- model="gemini-pro",
45
- contents=[
46
- {"role": "user", "parts": [{"text": "Hello, how are you?"}]}
47
- ],
48
- user_context=UserContext(
49
- user_id="test_user",
50
- org_id="test_org",
51
- client_type="test_client"
52
- ),
53
- config={
54
- "temperature": 0.7,
55
- "maxOutputTokens": 100
56
- }
57
- )
58
-
59
- response = client.invoke(request)
60
- print(f"✅ Google AI Studio 成功")
61
- print(f" 响应类型: {type(response)}")
62
- print(f" 响应内容: {str(response)[:200]}...")
63
-
64
- except Exception as e:
65
- print(f"❌ Google AI Studio 失败: {str(e)}")
66
-
67
-
68
- def test_google_vertex_ai():
69
- """测试 Google Vertex AI"""
70
- print("\n🔍 测试 Google Vertex AI...")
71
-
72
- try:
73
- client = TamarModelClient()
74
-
75
- request = ModelRequest(
76
- provider=ProviderType.GOOGLE,
77
- channel=Channel.VERTEXAI,
78
- invoke_type=InvokeType.GENERATION,
79
- model="gemini-1.5-flash",
80
- contents=[
81
- {"role": "user", "parts": [{"text": "What is AI?"}]}
82
- ],
83
- user_context=UserContext(
84
- user_id="test_user",
85
- org_id="test_org",
86
- client_type="test_client"
87
- ),
88
- config={
89
- "temperature": 0.5
90
- }
91
- )
92
-
93
- response = client.invoke(request)
94
- print(f"✅ Google Vertex AI 成功")
95
- print(f" 响应类型: {type(response)}")
96
- print(f" 响应内容: {str(response)[:200]}...")
97
-
98
- except Exception as e:
99
- print(f"❌ Google Vertex AI 失败: {str(e)}")
100
-
101
-
102
- def test_azure_openai():
103
- """测试 Azure OpenAI"""
104
- print("\n☁️ 测试 Azure OpenAI...")
105
-
106
- try:
107
- client = TamarModelClient()
108
-
109
- request = ModelRequest(
110
- provider=ProviderType.AZURE,
111
- channel=Channel.OPENAI,
112
- invoke_type=InvokeType.CHAT_COMPLETIONS,
113
- model="gpt-4o-mini",
114
- messages=[
115
- {"role": "user", "content": "Hello, how are you?"}
116
- ],
117
- user_context=UserContext(
118
- user_id="test_user",
119
- org_id="test_org",
120
- client_type="test_client"
121
- ),
122
- temperature=0.7,
123
- max_tokens=100
124
- )
125
-
126
- response = client.invoke(request)
127
- print(f"✅ Azure OpenAI 成功")
128
- print(f" 响应类型: {type(response)}")
129
- print(f" 响应内容: {str(response)[:200]}...")
130
-
131
- except Exception as e:
132
- print(f"❌ Azure OpenAI 失败: {str(e)}")
133
-
134
-
135
- async def test_google_streaming():
136
- """测试 Google 流式响应"""
137
- print("\n📡 测试 Google 流式响应...")
138
-
139
- try:
140
- client = AsyncTamarModelClient()
141
-
142
- request = ModelRequest(
143
- provider=ProviderType.GOOGLE,
144
- channel=Channel.AI_STUDIO,
145
- invoke_type=InvokeType.GENERATION,
146
- model="gemini-pro",
147
- contents=[
148
- {"role": "user", "parts": [{"text": "Count 1 to 5"}]}
149
- ],
150
- user_context=UserContext(
151
- user_id="test_user",
152
- org_id="test_org",
153
- client_type="test_client"
154
- ),
155
- stream=True,
156
- config={
157
- "temperature": 0.1,
158
- "maxOutputTokens": 50
159
- }
160
- )
161
-
162
- response_gen = await client.invoke(request)
163
- print(f"✅ Google 流式调用成功")
164
- print(f" 响应类型: {type(response_gen)}")
165
-
166
- chunk_count = 0
167
- async for chunk in response_gen:
168
- chunk_count += 1
169
- print(f" 数据块 {chunk_count}: {type(chunk)} - {str(chunk)[:100]}...")
170
- if chunk_count >= 3: # 只显示前3个数据块
171
- break
172
-
173
- except Exception as e:
174
- print(f"❌ Google 流式响应失败: {str(e)}")
175
-
176
-
177
- async def test_azure_streaming():
178
- """测试 Azure 流式响应"""
179
- print("\n📡 测试 Azure 流式响应...")
180
-
181
- try:
182
- client = AsyncTamarModelClient()
183
-
184
- request = ModelRequest(
185
- provider=ProviderType.AZURE,
186
- channel=Channel.OPENAI,
187
- invoke_type=InvokeType.CHAT_COMPLETIONS,
188
- model="gpt-4o-mini",
189
- messages=[
190
- {"role": "user", "content": "Count 1 to 5"}
191
- ],
192
- user_context=UserContext(
193
- user_id="test_user",
194
- org_id="test_org",
195
- client_type="test_client"
196
- ),
197
- stream=True,
198
- temperature=0.1,
199
- max_tokens=50
200
- )
201
-
202
- response_gen = await client.invoke(request)
203
- print(f"✅ Azure 流式调用成功")
204
- print(f" 响应类型: {type(response_gen)}")
205
-
206
- chunk_count = 0
207
- async for chunk in response_gen:
208
- chunk_count += 1
209
- print(f" 数据块 {chunk_count}: {type(chunk)} - {str(chunk)[:100]}...")
210
- if chunk_count >= 3: # 只显示前3个数据块
211
- break
212
-
213
- except Exception as e:
214
- print(f"❌ Azure 流式响应失败: {str(e)}")
215
-
216
-
217
- async def main():
218
- """主函数"""
219
- print("🚀 简化版 Google/Azure 测试")
220
- print("=" * 50)
221
-
222
- # 同步测试
223
- test_google_ai_studio()
224
- test_google_vertex_ai()
225
- test_azure_openai()
226
-
227
- # 异步流式测试
228
- await test_google_streaming()
229
- await test_azure_streaming()
230
-
231
- print("\n✅ 测试完成")
232
-
233
-
234
- if __name__ == "__main__":
235
- asyncio.run(main())