tamar-model-client 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl

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.
@@ -1,11 +1,392 @@
1
- class ModelManagerClientError(Exception):
2
- """Base exception for Model Manager Client errors"""
1
+ """
2
+ Tamar Model Client 异常定义
3
+
4
+ 提供了完整的错误分类体系,支持结构化错误信息和恢复策略。
5
+ """
6
+
7
+ import grpc
8
+ from datetime import datetime
9
+ from typing import Optional, Dict, Any, Union
10
+ from collections import defaultdict
11
+
12
+
13
+ # ===== 错误分类定义 =====
14
+
15
+ # 错误类别映射
16
+ ERROR_CATEGORIES = {
17
+ 'NETWORK': [
18
+ grpc.StatusCode.UNAVAILABLE,
19
+ grpc.StatusCode.DEADLINE_EXCEEDED,
20
+ grpc.StatusCode.CANCELLED, # 网络中断导致的取消
21
+ ],
22
+ 'CONCURRENCY': [
23
+ grpc.StatusCode.ABORTED, # 并发冲突,单独分类便于监控
24
+ ],
25
+ 'AUTH': [
26
+ grpc.StatusCode.UNAUTHENTICATED,
27
+ grpc.StatusCode.PERMISSION_DENIED,
28
+ ],
29
+ 'VALIDATION': [
30
+ grpc.StatusCode.INVALID_ARGUMENT,
31
+ grpc.StatusCode.OUT_OF_RANGE,
32
+ grpc.StatusCode.FAILED_PRECONDITION,
33
+ ],
34
+ 'RESOURCE': [
35
+ grpc.StatusCode.RESOURCE_EXHAUSTED,
36
+ grpc.StatusCode.NOT_FOUND,
37
+ grpc.StatusCode.ALREADY_EXISTS,
38
+ ],
39
+ 'PROVIDER': [
40
+ grpc.StatusCode.INTERNAL,
41
+ grpc.StatusCode.UNKNOWN,
42
+ grpc.StatusCode.UNIMPLEMENTED,
43
+ ],
44
+ 'DATA': [
45
+ grpc.StatusCode.DATA_LOSS,
46
+ ]
47
+ }
48
+
49
+ # 详细的重试策略
50
+ RETRY_POLICY = {
51
+ grpc.StatusCode.UNAVAILABLE: {
52
+ 'retryable': True,
53
+ 'backoff': 'exponential',
54
+ 'max_attempts': 5
55
+ },
56
+ grpc.StatusCode.DEADLINE_EXCEEDED: {
57
+ 'retryable': True,
58
+ 'backoff': 'linear',
59
+ 'max_attempts': 3
60
+ },
61
+ grpc.StatusCode.RESOURCE_EXHAUSTED: {
62
+ 'retryable': True,
63
+ 'backoff': 'exponential',
64
+ 'check_details': True, # 检查具体错误信息
65
+ 'max_attempts': 3
66
+ },
67
+ grpc.StatusCode.INTERNAL: {
68
+ 'retryable': 'conditional', # 条件重试
69
+ 'check_details': True,
70
+ 'max_attempts': 2
71
+ },
72
+ grpc.StatusCode.UNAUTHENTICATED: {
73
+ 'retryable': True,
74
+ 'action': 'refresh_token', # 特殊动作
75
+ 'max_attempts': 1
76
+ },
77
+ grpc.StatusCode.CANCELLED: {
78
+ 'retryable': True,
79
+ 'backoff': 'linear', # 线性退避,网络问题通常不需要指数退避
80
+ 'max_attempts': 2, # 限制重试次数,避免过度重试
81
+ 'check_details': False # 不检查详细信息,统一重试
82
+ },
83
+ grpc.StatusCode.ABORTED: {
84
+ 'retryable': True,
85
+ 'backoff': 'exponential', # 指数退避,避免加剧并发竞争
86
+ 'max_attempts': 3, # 适中的重试次数
87
+ 'jitter': True, # 添加随机延迟,减少竞争
88
+ 'check_details': False
89
+ },
90
+ # 不可重试的错误
91
+ grpc.StatusCode.INVALID_ARGUMENT: {'retryable': False},
92
+ grpc.StatusCode.NOT_FOUND: {'retryable': False},
93
+ grpc.StatusCode.ALREADY_EXISTS: {'retryable': False},
94
+ grpc.StatusCode.PERMISSION_DENIED: {'retryable': False},
95
+ }
96
+
97
+
98
+ # ===== 错误上下文类 =====
99
+
100
+ class ErrorContext:
101
+ """增强的错误上下文信息"""
102
+
103
+ def __init__(self, error: Optional[grpc.RpcError] = None, request_context: Optional[dict] = None):
104
+ self.error_code = error.code() if error else None
105
+ self.error_message = error.details() if error else ""
106
+ self.error_debug_string = error.debug_error_string() if error and hasattr(error, 'debug_error_string') else ""
107
+
108
+ # 请求上下文
109
+ request_context = request_context or {}
110
+ self.request_id = request_context.get('request_id')
111
+ self.timestamp = datetime.utcnow().isoformat()
112
+ self.provider = request_context.get('provider')
113
+ self.model = request_context.get('model')
114
+ self.method = request_context.get('method')
115
+
116
+ # 重试信息
117
+ self.retry_count = request_context.get('retry_count', 0)
118
+ self.total_duration = request_context.get('duration')
119
+
120
+ # 额外的诊断信息
121
+ self.client_version = request_context.get('client_version')
122
+ self.server_info = self._extract_server_info(error) if error else None
123
+
124
+ def _extract_server_info(self, error) -> Optional[Dict[str, Any]]:
125
+ """从错误中提取服务端信息"""
126
+ try:
127
+ # 尝试从 trailing metadata 获取服务端信息
128
+ if hasattr(error, 'trailing_metadata') and error.trailing_metadata():
129
+ metadata = {}
130
+ for key, value in error.trailing_metadata():
131
+ metadata[key] = value
132
+ return metadata
133
+ except:
134
+ pass
135
+ return None
136
+
137
+ def to_dict(self) -> dict:
138
+ """转换为字典格式"""
139
+ return {
140
+ 'error_code': self.error_code.name if self.error_code else 'UNKNOWN',
141
+ 'error_message': self.error_message,
142
+ 'request_id': self.request_id,
143
+ 'timestamp': self.timestamp,
144
+ 'provider': self.provider,
145
+ 'model': self.model,
146
+ 'method': self.method,
147
+ 'retry_count': self.retry_count,
148
+ 'total_duration': self.total_duration,
149
+ 'category': self._get_error_category(),
150
+ 'is_retryable': self._is_retryable(),
151
+ 'suggested_action': self._get_suggested_action(),
152
+ 'debug_info': {
153
+ 'client_version': self.client_version,
154
+ 'server_info': self.server_info,
155
+ 'debug_string': self.error_debug_string
156
+ }
157
+ }
158
+
159
+ def _get_error_category(self) -> str:
160
+ """获取错误类别"""
161
+ if not self.error_code:
162
+ return 'UNKNOWN'
163
+ for category, codes in ERROR_CATEGORIES.items():
164
+ if self.error_code in codes:
165
+ return category
166
+ return 'UNKNOWN'
167
+
168
+ def _is_retryable(self) -> bool:
169
+ """判断是否可重试"""
170
+ if not self.error_code:
171
+ return False
172
+ policy = RETRY_POLICY.get(self.error_code, {})
173
+ return policy.get('retryable', False) == True
174
+
175
+ def _get_suggested_action(self) -> str:
176
+ """获取建议的处理动作"""
177
+ suggestions = {
178
+ 'NETWORK': '检查网络连接,稍后重试',
179
+ 'CONCURRENCY': '并发冲突,系统会自动重试',
180
+ 'AUTH': '检查认证信息,可能需要刷新 Token',
181
+ 'VALIDATION': '检查请求参数是否正确',
182
+ 'RESOURCE': '检查资源限制或等待一段时间',
183
+ 'PROVIDER': '服务端错误,联系技术支持',
184
+ 'DATA': '数据损坏或丢失,请检查输入数据',
185
+ }
186
+ return suggestions.get(self._get_error_category(), '未知错误,请联系技术支持')
187
+
188
+
189
+ # ===== 异常类层级 =====
190
+
191
+ class TamarModelException(Exception):
192
+ """Tamar Model Client 基础异常类"""
193
+
194
+ def __init__(self, error_context: Union[ErrorContext, str, Exception]):
195
+ if isinstance(error_context, str):
196
+ # 简单字符串消息
197
+ self.context = ErrorContext()
198
+ self.context.error_message = error_context
199
+ super().__init__(error_context)
200
+ elif isinstance(error_context, Exception):
201
+ # 从其他异常创建
202
+ self.context = ErrorContext()
203
+ self.context.error_message = str(error_context)
204
+ super().__init__(str(error_context))
205
+ else:
206
+ # ErrorContext 对象
207
+ self.context = error_context
208
+ super().__init__(error_context.error_message)
209
+
210
+ @property
211
+ def request_id(self) -> Optional[str]:
212
+ """获取请求ID"""
213
+ return self.context.request_id
214
+
215
+ @property
216
+ def error_code(self) -> Optional[grpc.StatusCode]:
217
+ """获取错误码"""
218
+ return self.context.error_code
219
+
220
+ @property
221
+ def category(self) -> str:
222
+ """获取错误类别"""
223
+ return self.context._get_error_category()
224
+
225
+ @property
226
+ def is_retryable(self) -> bool:
227
+ """是否可重试"""
228
+ return self.context._is_retryable()
229
+
230
+ def to_dict(self) -> dict:
231
+ """转换为字典格式"""
232
+ return self.context.to_dict()
233
+
234
+
235
+ # ===== 网络相关异常 =====
236
+
237
+ class NetworkException(TamarModelException):
238
+ """网络相关异常基类"""
239
+ pass
240
+
241
+
242
+ class ConnectionException(NetworkException):
243
+ """连接异常"""
244
+ pass
245
+
246
+
247
+ class TimeoutException(NetworkException):
248
+ """超时异常"""
249
+ pass
250
+
251
+
252
+ class DNSException(NetworkException):
253
+ """DNS 解析异常"""
254
+ pass
255
+
256
+
257
+ # ===== 认证相关异常 =====
258
+
259
+ class AuthenticationException(TamarModelException):
260
+ """认证相关异常基类"""
261
+ pass
262
+
263
+
264
+ class TokenExpiredException(AuthenticationException):
265
+ """Token 过期异常"""
266
+ pass
267
+
268
+
269
+ class InvalidTokenException(AuthenticationException):
270
+ """无效 Token 异常"""
271
+ pass
272
+
273
+
274
+ class PermissionDeniedException(AuthenticationException):
275
+ """权限拒绝异常"""
276
+ pass
277
+
278
+
279
+ # ===== 限流相关异常 =====
280
+
281
+ class RateLimitException(TamarModelException):
282
+ """限流相关异常基类"""
283
+ pass
284
+
285
+
286
+ class QuotaExceededException(RateLimitException):
287
+ """配额超限异常"""
288
+ pass
289
+
290
+
291
+ class TooManyRequestsException(RateLimitException):
292
+ """请求过多异常"""
293
+ pass
294
+
295
+
296
+ # ===== 服务商相关异常 =====
297
+
298
+ class ProviderException(TamarModelException):
299
+ """服务商相关异常基类"""
300
+ pass
301
+
302
+
303
+ class ModelNotFoundException(ProviderException):
304
+ """模型未找到异常"""
305
+ pass
306
+
307
+
308
+ class ProviderUnavailableException(ProviderException):
309
+ """服务商不可用异常"""
310
+ pass
311
+
312
+
313
+ class InvalidResponseException(ProviderException):
314
+ """无效响应异常"""
3
315
  pass
4
316
 
5
- class ConnectionError(ModelManagerClientError):
6
- """Raised when connection to gRPC server fails"""
317
+
318
+ # ===== 验证相关异常 =====
319
+
320
+ class ValidationException(TamarModelException):
321
+ """验证相关异常基类"""
322
+ pass
323
+
324
+
325
+ class InvalidParameterException(ValidationException):
326
+ """无效参数异常"""
7
327
  pass
8
328
 
9
- class ValidationError(ModelManagerClientError):
10
- """Raised when input validation fails"""
11
- pass
329
+
330
+ class MissingParameterException(ValidationException):
331
+ """缺少参数异常"""
332
+ pass
333
+
334
+
335
+ # ===== 向后兼容的别名 =====
336
+
337
+ # 保持与原有代码的兼容性
338
+ ModelManagerClientError = TamarModelException
339
+ ConnectionError = ConnectionException
340
+ ValidationError = ValidationException
341
+
342
+
343
+ # ===== 错误处理工具函数 =====
344
+
345
+ def categorize_grpc_error(error_code: grpc.StatusCode) -> str:
346
+ """根据 gRPC 错误码分类错误"""
347
+ for category, codes in ERROR_CATEGORIES.items():
348
+ if error_code in codes:
349
+ return category
350
+ return 'UNKNOWN'
351
+
352
+
353
+ def is_retryable_error(error_code: grpc.StatusCode) -> bool:
354
+ """判断 gRPC 错误是否可重试"""
355
+ policy = RETRY_POLICY.get(error_code, {})
356
+ return policy.get('retryable', False) == True
357
+
358
+
359
+ def get_retry_policy(error_code: grpc.StatusCode) -> Dict[str, Any]:
360
+ """获取错误的重试策略"""
361
+ return RETRY_POLICY.get(error_code, {'retryable': False})
362
+
363
+
364
+ # ===== 错误统计 =====
365
+
366
+ class ErrorStats:
367
+ """错误统计工具"""
368
+
369
+ def __init__(self):
370
+ self.error_counts = defaultdict(int)
371
+ self.category_counts = defaultdict(int)
372
+ self.total_errors = 0
373
+
374
+ def record_error(self, error_code: grpc.StatusCode):
375
+ """记录错误统计"""
376
+ self.error_counts[error_code.name] += 1
377
+ self.category_counts[categorize_grpc_error(error_code)] += 1
378
+ self.total_errors += 1
379
+
380
+ def get_stats(self) -> Dict[str, Any]:
381
+ """获取统计信息"""
382
+ return {
383
+ 'total_errors': self.total_errors,
384
+ 'error_counts': dict(self.error_counts),
385
+ 'category_counts': dict(self.category_counts)
386
+ }
387
+
388
+ def reset(self):
389
+ """重置统计"""
390
+ self.error_counts.clear()
391
+ self.category_counts.clear()
392
+ self.total_errors = 0
@@ -1,6 +1,39 @@
1
1
  import json
2
2
  import logging
3
3
  from datetime import datetime
4
+ from typing import Any
5
+
6
+ # 尝试导入 NotGiven,如果失败则定义一个占位类
7
+ try:
8
+ from openai import NOT_GIVEN
9
+ NotGiven = type(NOT_GIVEN)
10
+ except ImportError:
11
+ class NotGiven:
12
+ pass
13
+
14
+
15
+ class SafeJSONEncoder(json.JSONEncoder):
16
+ """安全的 JSON 编码器,能处理特殊类型"""
17
+
18
+ def default(self, obj):
19
+ # 处理 NotGiven 类型
20
+ if isinstance(obj, NotGiven):
21
+ return None
22
+
23
+ # 处理 datetime 类型
24
+ if isinstance(obj, datetime):
25
+ return obj.isoformat()
26
+
27
+ # 处理 bytes 类型
28
+ if isinstance(obj, bytes):
29
+ return obj.decode('utf-8', errors='replace')
30
+
31
+ # 处理其他不可序列化的对象
32
+ try:
33
+ return super().default(obj)
34
+ except TypeError:
35
+ # 返回对象的字符串表示
36
+ return str(obj)
4
37
 
5
38
 
6
39
  class JSONFormatter(logging.Formatter):
@@ -23,4 +56,6 @@ class JSONFormatter(logging.Formatter):
23
56
  # 增加 trace 支持
24
57
  if hasattr(record, "trace"):
25
58
  log_data["trace"] = getattr(record, "trace")
26
- return json.dumps(log_data, ensure_ascii=False)
59
+
60
+ # 使用安全的 JSON 编码器
61
+ return json.dumps(log_data, ensure_ascii=False, cls=SafeJSONEncoder)