tamar-model-client 0.1.22__tar.gz → 0.1.24__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.22 → tamar_model_client-0.1.24}/PKG-INFO +1 -1
  2. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/setup.py +1 -1
  3. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/async_client.py +1 -1
  4. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/error_handler.py +60 -60
  5. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/exceptions.py +2 -2
  6. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/sync_client.py +1 -1
  7. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client.egg-info/PKG-INFO +1 -1
  8. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client.egg-info/SOURCES.txt +1 -4
  9. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tests/test_google_azure_final.py +4 -4
  10. tamar_model_client-0.1.22/tests/stream_hanging_analysis.py +0 -357
  11. tamar_model_client-0.1.22/tests/test_logging_issue.py +0 -75
  12. tamar_model_client-0.1.22/tests/test_simple.py +0 -235
  13. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/README.md +0 -0
  14. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/setup.cfg +0 -0
  15. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/__init__.py +0 -0
  16. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/auth.py +0 -0
  17. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/circuit_breaker.py +0 -0
  18. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/__init__.py +0 -0
  19. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/base_client.py +0 -0
  20. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/http_fallback.py +0 -0
  21. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/logging_setup.py +0 -0
  22. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/request_builder.py +0 -0
  23. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/response_handler.py +0 -0
  24. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/core/utils.py +0 -0
  25. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/enums/__init__.py +0 -0
  26. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/enums/channel.py +0 -0
  27. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/enums/invoke.py +0 -0
  28. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/enums/providers.py +0 -0
  29. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/generated/__init__.py +0 -0
  30. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/generated/model_service_pb2.py +0 -0
  31. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/generated/model_service_pb2_grpc.py +0 -0
  32. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/json_formatter.py +0 -0
  33. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/logging_icons.py +0 -0
  34. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/schemas/__init__.py +0 -0
  35. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/schemas/inputs.py +0 -0
  36. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/schemas/outputs.py +0 -0
  37. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client/utils.py +0 -0
  38. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client.egg-info/dependency_links.txt +0 -0
  39. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client.egg-info/requires.txt +0 -0
  40. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/tamar_model_client.egg-info/top_level.txt +0 -0
  41. {tamar_model_client-0.1.22 → tamar_model_client-0.1.24}/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.22
3
+ Version: 0.1.24
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.22",
5
+ version="0.1.24",
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",
@@ -33,7 +33,7 @@ from .core import (
33
33
  generate_request_id,
34
34
  set_request_id,
35
35
  get_protected_logger,
36
- MAX_MESSAGE_LENGTH
36
+ MAX_MESSAGE_LENGTH, get_request_id
37
37
  )
38
38
  from .core.base_client import BaseClient
39
39
  from .core.request_builder import RequestBuilder
@@ -11,6 +11,7 @@ import logging
11
11
  from typing import Optional, Dict, Any, Callable, Union
12
12
  from collections import defaultdict
13
13
 
14
+ from .core import get_protected_logger
14
15
  from .exceptions import (
15
16
  ErrorContext, TamarModelException,
16
17
  NetworkException, ConnectionException, TimeoutException,
@@ -20,17 +21,16 @@ from .exceptions import (
20
21
  ERROR_CATEGORIES, RETRY_POLICY, ErrorStats
21
22
  )
22
23
 
23
-
24
- logger = logging.getLogger(__name__)
24
+ logger = get_protected_logger(__name__)
25
25
 
26
26
 
27
27
  class GrpcErrorHandler:
28
28
  """统一的 gRPC 错误处理器"""
29
-
29
+
30
30
  def __init__(self, client_logger: Optional[logging.Logger] = None):
31
31
  self.logger = client_logger or logger
32
32
  self.error_stats = ErrorStats()
33
-
33
+
34
34
  def handle_error(self, error: Union[grpc.RpcError, Exception], context: dict) -> TamarModelException:
35
35
  """
36
36
  统一错误处理流程:
@@ -41,7 +41,7 @@ class GrpcErrorHandler:
41
41
  5. 返回相应异常
42
42
  """
43
43
  error_context = ErrorContext(error, context)
44
-
44
+
45
45
  # 记录详细错误日志
46
46
  # 将error_context的重要信息平铺到日志的data字段中
47
47
  log_data = {
@@ -61,64 +61,64 @@ class GrpcErrorHandler:
61
61
  "is_network_cancelled": error_context.is_network_cancelled() if error_context.error_code == grpc.StatusCode.CANCELLED else None
62
62
  }
63
63
  }
64
-
64
+
65
65
  # 如果上下文中有 duration,添加到日志中
66
66
  if 'duration' in context:
67
67
  log_data['duration'] = context['duration']
68
-
68
+
69
69
  self.logger.error(
70
70
  f"gRPC Error occurred: {error_context.error_code.name if error_context.error_code else 'UNKNOWN'}",
71
71
  extra=log_data
72
72
  )
73
-
73
+
74
74
  # 更新错误统计
75
75
  if error_context.error_code:
76
76
  self.error_stats.record_error(error_context.error_code)
77
-
77
+
78
78
  # 根据错误类型返回相应异常
79
79
  return self._create_exception(error_context)
80
-
80
+
81
81
  def _create_exception(self, error_context: ErrorContext) -> TamarModelException:
82
82
  """根据错误上下文创建相应的异常"""
83
83
  error_code = error_context.error_code
84
-
84
+
85
85
  if not error_code:
86
86
  return TamarModelException(error_context)
87
-
87
+
88
88
  # 认证相关错误
89
89
  if error_code in ERROR_CATEGORIES['AUTH']:
90
90
  if error_code == grpc.StatusCode.UNAUTHENTICATED:
91
91
  return TokenExpiredException(error_context)
92
92
  else:
93
93
  return PermissionDeniedException(error_context)
94
-
94
+
95
95
  # 网络相关错误
96
96
  elif error_code in ERROR_CATEGORIES['NETWORK']:
97
97
  if error_code == grpc.StatusCode.DEADLINE_EXCEEDED:
98
98
  return TimeoutException(error_context)
99
99
  else:
100
100
  return ConnectionException(error_context)
101
-
101
+
102
102
  # 验证相关错误
103
103
  elif error_code in ERROR_CATEGORIES['VALIDATION']:
104
104
  return InvalidParameterException(error_context)
105
-
105
+
106
106
  # 资源相关错误
107
107
  elif error_code == grpc.StatusCode.RESOURCE_EXHAUSTED:
108
108
  return RateLimitException(error_context)
109
-
109
+
110
110
  # 服务商相关错误
111
111
  elif error_code in ERROR_CATEGORIES['PROVIDER']:
112
112
  return ProviderException(error_context)
113
-
113
+
114
114
  # 默认错误
115
115
  else:
116
116
  return TamarModelException(error_context)
117
-
117
+
118
118
  def get_error_stats(self) -> Dict[str, Any]:
119
119
  """获取错误统计信息"""
120
120
  return self.error_stats.get_stats()
121
-
121
+
122
122
  def reset_stats(self):
123
123
  """重置错误统计"""
124
124
  self.error_stats.reset()
@@ -126,60 +126,60 @@ class GrpcErrorHandler:
126
126
 
127
127
  class ErrorRecoveryStrategy:
128
128
  """错误恢复策略"""
129
-
129
+
130
130
  RECOVERY_ACTIONS = {
131
131
  'refresh_token': 'handle_token_refresh',
132
132
  'reconnect': 'handle_reconnect',
133
133
  'backoff': 'handle_backoff',
134
134
  'circuit_break': 'handle_circuit_break',
135
135
  }
136
-
136
+
137
137
  def __init__(self, client):
138
138
  self.client = client
139
-
139
+
140
140
  async def recover_from_error(self, error_context: ErrorContext):
141
141
  """根据错误类型执行恢复动作"""
142
142
  if not error_context.error_code:
143
143
  return
144
-
144
+
145
145
  policy = RETRY_POLICY.get(error_context.error_code, {})
146
-
146
+
147
147
  if action := policy.get('action'):
148
148
  if action in self.RECOVERY_ACTIONS:
149
149
  handler = getattr(self, self.RECOVERY_ACTIONS[action])
150
150
  await handler(error_context)
151
-
151
+
152
152
  async def handle_token_refresh(self, error_context: ErrorContext):
153
153
  """处理 Token 刷新"""
154
154
  self.client.logger.info("Attempting to refresh JWT token")
155
155
  # 这里需要客户端实现 _refresh_jwt_token 方法
156
156
  if hasattr(self.client, '_refresh_jwt_token'):
157
157
  await self.client._refresh_jwt_token()
158
-
158
+
159
159
  async def handle_reconnect(self, error_context: ErrorContext):
160
160
  """处理重连"""
161
161
  self.client.logger.info("Attempting to reconnect channel")
162
162
  # 这里需要客户端实现 _reconnect_channel 方法
163
163
  if hasattr(self.client, '_reconnect_channel'):
164
164
  await self.client._reconnect_channel()
165
-
165
+
166
166
  async def handle_backoff(self, error_context: ErrorContext):
167
167
  """处理退避等待"""
168
168
  wait_time = self._calculate_backoff(error_context.retry_count)
169
169
  await asyncio.sleep(wait_time)
170
-
170
+
171
171
  async def handle_circuit_break(self, error_context: ErrorContext):
172
172
  """处理熔断"""
173
173
  self.client.logger.warning("Circuit breaker activated")
174
174
  # 这里可以实现熔断逻辑
175
175
  pass
176
-
176
+
177
177
  def _calculate_backoff(self, retry_count: int) -> float:
178
178
  """计算退避时间"""
179
179
  base_delay = 1.0
180
180
  max_delay = 60.0
181
181
  jitter_factor = 0.1
182
-
182
+
183
183
  delay = min(base_delay * (2 ** retry_count), max_delay)
184
184
  jitter = random.uniform(0, delay * jitter_factor)
185
185
  return delay + jitter
@@ -187,18 +187,18 @@ class ErrorRecoveryStrategy:
187
187
 
188
188
  class EnhancedRetryHandler:
189
189
  """增强的重试处理器"""
190
-
190
+
191
191
  def __init__(self, max_retries: int = 3, base_delay: float = 1.0):
192
192
  self.max_retries = max_retries
193
193
  self.base_delay = base_delay
194
194
  self.error_handler = GrpcErrorHandler()
195
-
195
+
196
196
  async def execute_with_retry(
197
- self,
198
- func: Callable,
199
- *args,
200
- context: Optional[Dict[str, Any]] = None,
201
- **kwargs
197
+ self,
198
+ func: Callable,
199
+ *args,
200
+ context: Optional[Dict[str, Any]] = None,
201
+ **kwargs
202
202
  ):
203
203
  """
204
204
  执行函数并处理重试
@@ -218,19 +218,19 @@ class EnhancedRetryHandler:
218
218
  # 记录开始时间
219
219
  import time
220
220
  method_start_time = time.time()
221
-
221
+
222
222
  context = context or {}
223
223
  last_exception = None
224
-
224
+
225
225
  for attempt in range(self.max_retries + 1):
226
226
  try:
227
227
  context['retry_count'] = attempt
228
228
  return await func(*args, **kwargs)
229
-
229
+
230
230
  except (grpc.RpcError, grpc.aio.AioRpcError) as e:
231
231
  # 创建错误上下文
232
232
  error_context = ErrorContext(e, context)
233
-
233
+
234
234
  # 判断是否可以重试
235
235
  if not self._should_retry(e, attempt):
236
236
  # 不可重试或已达到最大重试次数
@@ -238,10 +238,10 @@ class EnhancedRetryHandler:
238
238
  context['duration'] = current_duration
239
239
  last_exception = self.error_handler.handle_error(e, context)
240
240
  break
241
-
241
+
242
242
  # 计算当前耗时
243
243
  current_duration = time.time() - method_start_time
244
-
244
+
245
245
  # 记录重试日志
246
246
  log_data = {
247
247
  "log_type": "info",
@@ -261,15 +261,15 @@ class EnhancedRetryHandler:
261
261
  f"Attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()}",
262
262
  extra=log_data
263
263
  )
264
-
264
+
265
265
  # 执行退避等待
266
266
  if attempt < self.max_retries:
267
267
  delay = self._calculate_backoff(attempt)
268
268
  await asyncio.sleep(delay)
269
-
269
+
270
270
  context['duration'] = current_duration
271
271
  last_exception = self.error_handler.handle_error(e, context)
272
-
272
+
273
273
  except Exception as e:
274
274
  # 非 gRPC 错误,直接包装抛出
275
275
  context['retry_count'] = attempt
@@ -277,28 +277,28 @@ class EnhancedRetryHandler:
277
277
  error_context.error_message = str(e)
278
278
  last_exception = TamarModelException(error_context)
279
279
  break
280
-
280
+
281
281
  # 抛出最后的异常
282
282
  if last_exception:
283
283
  raise last_exception
284
284
  else:
285
285
  raise TamarModelException("Unknown error occurred")
286
-
286
+
287
287
  def _should_retry(self, error: grpc.RpcError, attempt: int) -> bool:
288
288
  """判断是否应该重试"""
289
289
  error_code = error.code()
290
290
  policy = RETRY_POLICY.get(error_code, {})
291
-
291
+
292
292
  # 先检查错误级别的 max_attempts 配置
293
293
  # max_attempts 表示最大重试次数(不包括初始请求)
294
294
  error_max_attempts = policy.get('max_attempts', self.max_retries)
295
295
  if attempt >= error_max_attempts:
296
296
  return False
297
-
297
+
298
298
  # 再检查全局的 max_retries
299
299
  if attempt >= self.max_retries:
300
300
  return False
301
-
301
+
302
302
  # 检查基本重试策略
303
303
  retryable = policy.get('retryable', False)
304
304
  if retryable == False:
@@ -308,30 +308,30 @@ class EnhancedRetryHandler:
308
308
  elif retryable == 'conditional':
309
309
  # 条件重试,需要检查错误详情
310
310
  return self._check_conditional_retry(error)
311
-
311
+
312
312
  return False
313
-
313
+
314
314
  def _check_conditional_retry(self, error: grpc.RpcError) -> bool:
315
315
  """检查条件重试"""
316
316
  error_message = error.details().lower() if error.details() else ""
317
-
317
+
318
318
  # 一些可重试的内部错误模式
319
319
  retryable_patterns = [
320
- 'temporary', 'timeout', 'unavailable',
320
+ 'temporary', 'timeout', 'unavailable',
321
321
  'connection', 'network', 'try again'
322
322
  ]
323
-
323
+
324
324
  for pattern in retryable_patterns:
325
325
  if pattern in error_message:
326
326
  return True
327
-
327
+
328
328
  return False
329
-
329
+
330
330
  def _calculate_backoff(self, attempt: int) -> float:
331
331
  """计算退避时间"""
332
332
  max_delay = 60.0
333
333
  jitter_factor = 0.1
334
-
334
+
335
335
  delay = min(self.base_delay * (2 ** attempt), max_delay)
336
336
  jitter = random.uniform(0, delay * jitter_factor)
337
- return delay + jitter
337
+ return delay + jitter
@@ -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,
@@ -31,7 +31,7 @@ from .core import (
31
31
  generate_request_id,
32
32
  set_request_id,
33
33
  get_protected_logger,
34
- MAX_MESSAGE_LENGTH
34
+ MAX_MESSAGE_LENGTH, get_request_id
35
35
  )
36
36
  from .core.base_client import BaseClient
37
37
  from .core.request_builder import RequestBuilder
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-model-client
3
- Version: 0.1.22
3
+ Version: 0.1.24
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
@@ -27,7 +27,7 @@ test_logger.addHandler(test_handler)
27
27
  logger = test_logger
28
28
 
29
29
  os.environ['MODEL_MANAGER_SERVER_GRPC_USE_TLS'] = "true"
30
- os.environ['MODEL_MANAGER_SERVER_ADDRESS'] = "model-manager-server-grpc-131786869360.asia-northeast1.run.app"
30
+ os.environ['MODEL_MANAGER_SERVER_ADDRESS'] = "localhost:50051"
31
31
  os.environ['MODEL_MANAGER_SERVER_JWT_SECRET_KEY'] = "model-manager-server-jwt-key"
32
32
 
33
33
  # 导入客户端模块
@@ -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
  ),
@@ -648,7 +648,7 @@ async def main():
648
648
  #test_concurrent_requests(150) # 测试150个并发请求
649
649
 
650
650
  # 异步并发测试
651
- await test_async_concurrent_requests(1000) # 测试150个异步并发请求
651
+ await test_async_concurrent_requests(50) # 测试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())