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