tamar-model-client 0.1.18__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.
- tamar_model_client/__init__.py +2 -0
- tamar_model_client/async_client.py +430 -539
- tamar_model_client/core/__init__.py +34 -0
- tamar_model_client/core/base_client.py +168 -0
- tamar_model_client/core/logging_setup.py +84 -0
- tamar_model_client/core/request_builder.py +221 -0
- tamar_model_client/core/response_handler.py +136 -0
- tamar_model_client/core/utils.py +171 -0
- tamar_model_client/error_handler.py +283 -0
- tamar_model_client/exceptions.py +371 -7
- tamar_model_client/json_formatter.py +36 -1
- tamar_model_client/logging_icons.py +60 -0
- tamar_model_client/sync_client.py +473 -485
- {tamar_model_client-0.1.18.dist-info → tamar_model_client-0.1.20.dist-info}/METADATA +217 -61
- tamar_model_client-0.1.20.dist-info/RECORD +33 -0
- {tamar_model_client-0.1.18.dist-info → tamar_model_client-0.1.20.dist-info}/top_level.txt +1 -0
- tests/__init__.py +1 -0
- tests/stream_hanging_analysis.py +357 -0
- tests/test_google_azure_final.py +448 -0
- tests/test_simple.py +235 -0
- tamar_model_client-0.1.18.dist-info/RECORD +0 -21
- {tamar_model_client-0.1.18.dist-info → tamar_model_client-0.1.20.dist-info}/WHEEL +0 -0
tamar_model_client/exceptions.py
CHANGED
@@ -1,11 +1,375 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
|
6
|
-
|
300
|
+
|
301
|
+
# ===== 验证相关异常 =====
|
302
|
+
|
303
|
+
class ValidationException(TamarModelException):
|
304
|
+
"""验证相关异常基类"""
|
305
|
+
pass
|
306
|
+
|
307
|
+
|
308
|
+
class InvalidParameterException(ValidationException):
|
309
|
+
"""无效参数异常"""
|
7
310
|
pass
|
8
311
|
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
59
|
+
|
60
|
+
# 使用安全的 JSON 编码器
|
61
|
+
return json.dumps(log_data, ensure_ascii=False, cls=SafeJSONEncoder)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""
|
2
|
+
日志图标规范
|
3
|
+
|
4
|
+
本模块定义了统一的日志图标标准,确保整个项目中日志消息的视觉一致性。
|
5
|
+
"""
|
6
|
+
|
7
|
+
# 请求生命周期图标
|
8
|
+
REQUEST_START = "🔵" # 请求开始
|
9
|
+
RESPONSE_SUCCESS = "✅" # 响应成功
|
10
|
+
RESPONSE_ERROR = "❌" # 响应错误
|
11
|
+
|
12
|
+
# 连接和网络图标
|
13
|
+
SECURE_CONNECTION = "🔐" # 安全连接 (TLS)
|
14
|
+
INSECURE_CONNECTION = "🔓" # 不安全连接 (无TLS)
|
15
|
+
CONNECTION_SUCCESS = "✅" # 连接成功
|
16
|
+
CONNECTION_RETRY = "🔄" # 连接重试
|
17
|
+
CONNECTION_ERROR = "❌" # 连接错误
|
18
|
+
|
19
|
+
# 操作状态图标
|
20
|
+
SUCCESS = "✅" # 成功
|
21
|
+
ERROR = "❌" # 错误
|
22
|
+
WARNING = "⚠️" # 警告
|
23
|
+
INFO = "ℹ️" # 信息
|
24
|
+
RETRY = "🔄" # 重试
|
25
|
+
PROCESSING = "⚙️" # 处理中
|
26
|
+
|
27
|
+
# 流式响应图标
|
28
|
+
STREAM_SUCCESS = "✅" # 流完成
|
29
|
+
STREAM_ERROR = "❌" # 流错误
|
30
|
+
STREAM_CHUNK = "📦" # 流数据块
|
31
|
+
|
32
|
+
# 批量操作图标
|
33
|
+
BATCH_START = "🔵" # 批量开始
|
34
|
+
BATCH_SUCCESS = "✅" # 批量成功
|
35
|
+
BATCH_ERROR = "❌" # 批量错误
|
36
|
+
|
37
|
+
# 系统操作图标
|
38
|
+
INIT = "🚀" # 初始化
|
39
|
+
CLOSE = "🔚" # 关闭
|
40
|
+
CLEANUP = "🧹" # 清理
|
41
|
+
|
42
|
+
def get_icon_for_log_type(log_type: str, is_success: bool = True) -> str:
|
43
|
+
"""
|
44
|
+
根据日志类型和状态获取合适的图标
|
45
|
+
|
46
|
+
Args:
|
47
|
+
log_type: 日志类型 (request, response, info)
|
48
|
+
is_success: 是否成功
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
对应的图标字符串
|
52
|
+
"""
|
53
|
+
if log_type == "request":
|
54
|
+
return REQUEST_START
|
55
|
+
elif log_type == "response":
|
56
|
+
return RESPONSE_SUCCESS if is_success else RESPONSE_ERROR
|
57
|
+
elif log_type == "info":
|
58
|
+
return INFO if is_success else WARNING
|
59
|
+
else:
|
60
|
+
return INFO
|