tamar-model-client 0.1.27__py3-none-any.whl → 0.1.30__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/async_client.py +83 -40
- tamar_model_client/circuit_breaker.py +6 -3
- tamar_model_client/core/__init__.py +5 -1
- tamar_model_client/core/base_client.py +136 -40
- tamar_model_client/core/http_fallback.py +313 -31
- tamar_model_client/core/logging_setup.py +15 -1
- tamar_model_client/core/utils.py +27 -1
- tamar_model_client/error_handler.py +112 -17
- tamar_model_client/json_formatter.py +9 -0
- tamar_model_client/sync_client.py +177 -38
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/METADATA +588 -6
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/RECORD +16 -15
- tests/test_circuit_breaker.py +269 -0
- tests/test_google_azure_final.py +605 -21
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/WHEEL +0 -0
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,21 @@
|
|
1
1
|
tamar_model_client/__init__.py,sha256=4DEIUGlLTeiaECjJQbGYik7C0JO6hHwwfbLYpYpMdzg,444
|
2
|
-
tamar_model_client/async_client.py,sha256=
|
2
|
+
tamar_model_client/async_client.py,sha256=J3787otYGW2ycizLC5HDPibm_USNy4oKI5QQUo3L-aE,45328
|
3
3
|
tamar_model_client/auth.py,sha256=gbwW5Aakeb49PMbmYvrYlVx1mfyn1LEDJ4qQVs-9DA4,438
|
4
|
-
tamar_model_client/circuit_breaker.py,sha256=
|
5
|
-
tamar_model_client/error_handler.py,sha256=
|
4
|
+
tamar_model_client/circuit_breaker.py,sha256=Y3AVp7WzVYU-ubcmovKsJ8DRJbbO4G7vdZgSjnwcWJQ,5550
|
5
|
+
tamar_model_client/error_handler.py,sha256=y7EipcqkXbCecSAOsnoSP3SH7hvZSNF_NUHooTi3hP0,18364
|
6
6
|
tamar_model_client/exceptions.py,sha256=EOr4JMYI7hVszRvNYJ1JqsUNpVmd16T2KpJ0MkFTsUE,13073
|
7
|
-
tamar_model_client/json_formatter.py,sha256=
|
7
|
+
tamar_model_client/json_formatter.py,sha256=XT8XPMKKM2M22tuYR2e1rvWHcpz3UD9iLLgGPsGOjCI,2410
|
8
8
|
tamar_model_client/logging_icons.py,sha256=MRTZ1Xvkep9ce_jdltj54_XZUXvIpQ95soRNmLdJ4qw,1837
|
9
|
-
tamar_model_client/sync_client.py,sha256=
|
9
|
+
tamar_model_client/sync_client.py,sha256=vBxVvDFeY_Sd7JRLJwkdOcm6sCxmGaDW0tyCspp-n7E,52671
|
10
10
|
tamar_model_client/utils.py,sha256=Kn6pFz9GEC96H4eejEax66AkzvsrXI3WCSDtgDjnVTI,5238
|
11
|
-
tamar_model_client/core/__init__.py,sha256=
|
12
|
-
tamar_model_client/core/base_client.py,sha256=
|
13
|
-
tamar_model_client/core/http_fallback.py,sha256=
|
14
|
-
tamar_model_client/core/logging_setup.py,sha256
|
11
|
+
tamar_model_client/core/__init__.py,sha256=RMiZjV1S4csWPLxB_JfdOea8fYPz97Oj3humQSBw1OI,1054
|
12
|
+
tamar_model_client/core/base_client.py,sha256=XUbMDM6B3ZtAnAdgSDF-bdLgACY46igCcMVoiMC3faQ,13056
|
13
|
+
tamar_model_client/core/http_fallback.py,sha256=ULmHXfKPwP4T32xo7yQV_z2bGaI_L71BQIcylFs8dTM,21243
|
14
|
+
tamar_model_client/core/logging_setup.py,sha256=-MXzTR4Ax50H16cbq1jCXbxgayf5fZ0U3o0--fMmxD8,6692
|
15
15
|
tamar_model_client/core/request_builder.py,sha256=yi8iy2Ps2m4d1YwIFiQLRxTvxQxgEGV576aXnNYRl7E,8507
|
16
16
|
tamar_model_client/core/request_id_manager.py,sha256=S-Mliaby9zN_bx-B85FvVnttal-w0skkjy2ZvWoQ5vw,3689
|
17
17
|
tamar_model_client/core/response_handler.py,sha256=_q5galAT0_RaUT5C_yZsjg-9VnT9CBjmIASOt28BUmQ,4616
|
18
|
-
tamar_model_client/core/utils.py,sha256=
|
18
|
+
tamar_model_client/core/utils.py,sha256=AcbsGfNQEaZLYI4OZJs-BdmJgxAoLUC5LFoiYmji820,5875
|
19
19
|
tamar_model_client/enums/__init__.py,sha256=3cYYn8ztNGBa_pI_5JGRVYf2QX8fkBVWdjID1PLvoBQ,182
|
20
20
|
tamar_model_client/enums/channel.py,sha256=wCzX579nNpTtwzGeS6S3Ls0UzVAgsOlfy4fXMzQTCAw,199
|
21
21
|
tamar_model_client/enums/invoke.py,sha256=Up87myAg4-0SDJV5a82ggPDpYHSLEtIco8BF_5Ph1nY,322
|
@@ -28,10 +28,11 @@ tamar_model_client/schemas/inputs.py,sha256=dz1m8NbUIxA99JXZc8WlyzbKpDuz1lEzx3Vg
|
|
28
28
|
tamar_model_client/schemas/outputs.py,sha256=M_fcqUtXPJnfiLabHlyA8BorlC5pYkf5KLjXO1ysKIQ,1031
|
29
29
|
tests/__init__.py,sha256=kbmImddLDwdqlkkmkyKtl4bQy_ipe-R8eskpaBylU9w,38
|
30
30
|
tests/stream_hanging_analysis.py,sha256=W3W48IhQbNAR6-xvMpoWZvnWOnr56CTaH4-aORNBuD4,14807
|
31
|
-
tests/
|
31
|
+
tests/test_circuit_breaker.py,sha256=nhEBnyXFjIYjRWlUdu7Z9PnPq48ypbBK6fxN6deHedw,12172
|
32
|
+
tests/test_google_azure_final.py,sha256=Cx2lfnoj48_7pUjpCYbrx6OLJF4cI79McV24_EYt_8s,55093
|
32
33
|
tests/test_logging_issue.py,sha256=JTMbotfHpAEPMBj73pOwxPn-Zn4QVQJX6scMz48FRDQ,2427
|
33
34
|
tests/test_simple.py,sha256=Xf0U-J9_xn_LzUsmYu06suK0_7DrPeko8OHoHldsNxE,7169
|
34
|
-
tamar_model_client-0.1.
|
35
|
-
tamar_model_client-0.1.
|
36
|
-
tamar_model_client-0.1.
|
37
|
-
tamar_model_client-0.1.
|
35
|
+
tamar_model_client-0.1.30.dist-info/METADATA,sha256=QrJLOVUNNXKGNv4ZJRPKDFLP9d9JUlfrqQNN-FQuAvA,41310
|
36
|
+
tamar_model_client-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
37
|
+
tamar_model_client-0.1.30.dist-info/top_level.txt,sha256=f1I-S8iWN-cgv4gB8gxRg9jJOTJMumvm4oGKVPfGg6A,25
|
38
|
+
tamar_model_client-0.1.30.dist-info/RECORD,,
|
@@ -0,0 +1,269 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
熔断器测试脚本
|
4
|
+
专门用于测试熔断器和HTTP fallback功能
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
import time
|
12
|
+
from typing import List, Dict, Tuple
|
13
|
+
|
14
|
+
# 配置测试脚本专用的日志
|
15
|
+
test_logger = logging.getLogger('test_circuit_breaker')
|
16
|
+
test_logger.setLevel(logging.INFO)
|
17
|
+
test_logger.propagate = False
|
18
|
+
|
19
|
+
# 创建测试脚本专用的handler
|
20
|
+
test_handler = logging.StreamHandler()
|
21
|
+
test_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
22
|
+
test_logger.addHandler(test_handler)
|
23
|
+
|
24
|
+
logger = test_logger
|
25
|
+
|
26
|
+
# 导入客户端模块
|
27
|
+
try:
|
28
|
+
from tamar_model_client import AsyncTamarModelClient
|
29
|
+
from tamar_model_client.schemas import ModelRequest, UserContext
|
30
|
+
from tamar_model_client.enums import ProviderType, InvokeType
|
31
|
+
except ImportError as e:
|
32
|
+
logger.error(f"导入模块失败: {e}")
|
33
|
+
sys.exit(1)
|
34
|
+
|
35
|
+
|
36
|
+
async def test_circuit_breaker_with_single_requests(num_requests: int = 10):
|
37
|
+
"""
|
38
|
+
测试熔断器功能 - 使用单个请求触发熔断
|
39
|
+
|
40
|
+
Args:
|
41
|
+
num_requests: 要发送的请求数,默认10个
|
42
|
+
"""
|
43
|
+
print(f"\n🔥 测试熔断器功能 - 单请求模式 ({num_requests} 个请求)...")
|
44
|
+
|
45
|
+
# 保存原始环境变量
|
46
|
+
original_env = {}
|
47
|
+
env_vars = ['MODEL_CLIENT_RESILIENT_ENABLED', 'MODEL_CLIENT_HTTP_FALLBACK_URL',
|
48
|
+
'MODEL_CLIENT_CIRCUIT_BREAKER_THRESHOLD', 'MODEL_CLIENT_CIRCUIT_BREAKER_TIMEOUT',
|
49
|
+
'MODEL_MANAGER_SERVER_ADDRESS', 'MODEL_MANAGER_SERVER_GRPC_USE_TLS']
|
50
|
+
for var in env_vars:
|
51
|
+
original_env[var] = os.environ.get(var)
|
52
|
+
|
53
|
+
# 设置环境变量以启用熔断器和HTTP fallback
|
54
|
+
os.environ['MODEL_CLIENT_RESILIENT_ENABLED'] = 'true'
|
55
|
+
os.environ['MODEL_CLIENT_HTTP_FALLBACK_URL'] = 'http://localhost:8000' # 假设HTTP服务在8000端口
|
56
|
+
os.environ['MODEL_CLIENT_CIRCUIT_BREAKER_THRESHOLD'] = '3' # 3次失败后触发熔断
|
57
|
+
os.environ['MODEL_CLIENT_CIRCUIT_BREAKER_TIMEOUT'] = '30' # 熔断器30秒后恢复
|
58
|
+
|
59
|
+
# 使用一个不存在的服务器地址来触发连接错误
|
60
|
+
os.environ['MODEL_MANAGER_SERVER_ADDRESS'] = 'localhost:99999' # 无效端口
|
61
|
+
os.environ['MODEL_MANAGER_SERVER_GRPC_USE_TLS'] = 'false'
|
62
|
+
|
63
|
+
# 调试:打印环境变量确认设置成功
|
64
|
+
print(f" 环境变量设置:")
|
65
|
+
print(f" - MODEL_CLIENT_RESILIENT_ENABLED: {os.environ.get('MODEL_CLIENT_RESILIENT_ENABLED')}")
|
66
|
+
print(f" - MODEL_CLIENT_HTTP_FALLBACK_URL: {os.environ.get('MODEL_CLIENT_HTTP_FALLBACK_URL')}")
|
67
|
+
print(f" - 熔断阈值: {os.environ.get('MODEL_CLIENT_CIRCUIT_BREAKER_THRESHOLD')} 次失败")
|
68
|
+
print(f" - 熔断恢复时间: {os.environ.get('MODEL_CLIENT_CIRCUIT_BREAKER_TIMEOUT')} 秒")
|
69
|
+
print(f" - gRPC服务器: {os.environ.get('MODEL_MANAGER_SERVER_ADDRESS')} (故意使用无效地址)")
|
70
|
+
|
71
|
+
# 统计变量
|
72
|
+
total_requests = 0
|
73
|
+
successful_requests = 0
|
74
|
+
failed_requests = 0
|
75
|
+
http_fallback_requests = 0
|
76
|
+
circuit_breaker_opened = False
|
77
|
+
request_times: List[float] = []
|
78
|
+
errors: Dict[str, int] = {}
|
79
|
+
|
80
|
+
try:
|
81
|
+
# 创建一个共享的异步客户端(启用熔断器)
|
82
|
+
async with AsyncTamarModelClient() as client:
|
83
|
+
print(f"\n 熔断器初始配置:")
|
84
|
+
print(f" - 启用状态: {getattr(client, 'resilient_enabled', False)}")
|
85
|
+
print(f" - HTTP Fallback URL: {getattr(client, 'http_fallback_url', 'None')}")
|
86
|
+
|
87
|
+
# 获取初始熔断器状态
|
88
|
+
if hasattr(client, 'resilient_enabled') and client.resilient_enabled:
|
89
|
+
try:
|
90
|
+
metrics = client.get_resilient_metrics()
|
91
|
+
if metrics and 'circuit_breaker' in metrics:
|
92
|
+
print(f" - 初始状态: {metrics['circuit_breaker']['state']}")
|
93
|
+
print(f" - 失败阈值: {metrics['circuit_breaker']['failure_threshold']}")
|
94
|
+
print(f" - 恢复超时: {metrics['circuit_breaker']['recovery_timeout']}秒")
|
95
|
+
except Exception as e:
|
96
|
+
print(f" - 获取初始状态失败: {e}")
|
97
|
+
|
98
|
+
print(f"\n 开始发送请求...")
|
99
|
+
|
100
|
+
for i in range(num_requests):
|
101
|
+
start_time = time.time()
|
102
|
+
request_num = i + 1
|
103
|
+
|
104
|
+
try:
|
105
|
+
# 构建请求
|
106
|
+
request = ModelRequest(
|
107
|
+
provider=ProviderType.GOOGLE,
|
108
|
+
invoke_type=InvokeType.GENERATION,
|
109
|
+
model="tamar-google-gemini-flash-lite",
|
110
|
+
contents=f"测试请求 {request_num}: 1+1等于几?",
|
111
|
+
user_context=UserContext(
|
112
|
+
user_id=f"circuit_test_user_{i}",
|
113
|
+
org_id="circuit_test_org",
|
114
|
+
client_type="circuit_breaker_test"
|
115
|
+
),
|
116
|
+
config={"temperature": 0.1, "maxOutputTokens": 10}
|
117
|
+
)
|
118
|
+
|
119
|
+
print(f"\n 📤 请求 {request_num}/{num_requests}...")
|
120
|
+
|
121
|
+
# 发送请求
|
122
|
+
response = await client.invoke(
|
123
|
+
request,
|
124
|
+
timeout=5000, # 5秒超时
|
125
|
+
request_id=f"circuit_test_{i}"
|
126
|
+
)
|
127
|
+
|
128
|
+
duration = time.time() - start_time
|
129
|
+
request_times.append(duration)
|
130
|
+
total_requests += 1
|
131
|
+
successful_requests += 1
|
132
|
+
|
133
|
+
# 如果成功了,检查是否是通过HTTP fallback
|
134
|
+
if hasattr(client, 'resilient_enabled') and client.resilient_enabled:
|
135
|
+
metrics = client.get_resilient_metrics()
|
136
|
+
if metrics and metrics['circuit_breaker']['state'] == 'open':
|
137
|
+
http_fallback_requests += 1
|
138
|
+
print(f" ✅ 请求 {request_num} 成功 (通过HTTP fallback) - 耗时: {duration:.2f}秒")
|
139
|
+
else:
|
140
|
+
print(f" ✅ 请求 {request_num} 成功 (gRPC) - 耗时: {duration:.2f}秒")
|
141
|
+
else:
|
142
|
+
print(f" ✅ 请求 {request_num} 成功 - 耗时: {duration:.2f}秒")
|
143
|
+
|
144
|
+
# 打印响应内容的前100个字符
|
145
|
+
if response.content:
|
146
|
+
print(f" 响应: {response.content[:100]}...")
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
duration = time.time() - start_time
|
150
|
+
request_times.append(duration)
|
151
|
+
total_requests += 1
|
152
|
+
failed_requests += 1
|
153
|
+
|
154
|
+
error_type = type(e).__name__
|
155
|
+
error_msg = str(e)[:100]
|
156
|
+
errors[error_type] = errors.get(error_type, 0) + 1
|
157
|
+
|
158
|
+
print(f" ❌ 请求 {request_num} 失败 - {error_type}: {error_msg}")
|
159
|
+
print(f" 耗时: {duration:.2f}秒")
|
160
|
+
|
161
|
+
# 检查熔断器状态
|
162
|
+
if hasattr(client, 'resilient_enabled') and client.resilient_enabled:
|
163
|
+
try:
|
164
|
+
metrics = client.get_resilient_metrics()
|
165
|
+
if metrics and 'circuit_breaker' in metrics:
|
166
|
+
cb_state = metrics['circuit_breaker']['state']
|
167
|
+
cb_failures = metrics['circuit_breaker']['failure_count']
|
168
|
+
|
169
|
+
print(f" 熔断器状态: {cb_state}, 失败计数: {cb_failures}")
|
170
|
+
|
171
|
+
if cb_state == 'open' and not circuit_breaker_opened:
|
172
|
+
circuit_breaker_opened = True
|
173
|
+
print(f" 🔻 熔断器已打开!后续请求将使用HTTP fallback")
|
174
|
+
except Exception as e:
|
175
|
+
print(f" 获取熔断器状态失败: {e}")
|
176
|
+
|
177
|
+
# 请求之间短暂等待
|
178
|
+
if i < num_requests - 1:
|
179
|
+
await asyncio.sleep(0.5)
|
180
|
+
|
181
|
+
# 最终统计
|
182
|
+
print(f"\n📊 熔断器测试结果:")
|
183
|
+
print(f" 总请求数: {total_requests}")
|
184
|
+
print(f" 成功请求: {successful_requests} ({successful_requests / total_requests * 100:.1f}%)")
|
185
|
+
print(f" 失败请求: {failed_requests} ({failed_requests / total_requests * 100:.1f}%)")
|
186
|
+
|
187
|
+
if request_times:
|
188
|
+
avg_time = sum(request_times) / len(request_times)
|
189
|
+
print(f"\n 请求耗时统计:")
|
190
|
+
print(f" - 平均: {avg_time:.3f} 秒")
|
191
|
+
print(f" - 最小: {min(request_times):.3f} 秒")
|
192
|
+
print(f" - 最大: {max(request_times):.3f} 秒")
|
193
|
+
|
194
|
+
print(f"\n 🔥 熔断器统计:")
|
195
|
+
print(f" - 熔断器是否触发: {'是' if circuit_breaker_opened else '否'}")
|
196
|
+
print(f" - HTTP fallback请求数: {http_fallback_requests}")
|
197
|
+
|
198
|
+
# 获取最终的熔断器状态
|
199
|
+
if hasattr(client, 'resilient_enabled') and client.resilient_enabled:
|
200
|
+
try:
|
201
|
+
final_metrics = client.get_resilient_metrics()
|
202
|
+
if final_metrics and 'circuit_breaker' in final_metrics:
|
203
|
+
print(f" - 最终状态: {final_metrics['circuit_breaker']['state']}")
|
204
|
+
print(f" - 总失败次数: {final_metrics['circuit_breaker']['failure_count']}")
|
205
|
+
print(f" - 失败阈值: {final_metrics['circuit_breaker']['failure_threshold']}")
|
206
|
+
print(f" - 恢复超时: {final_metrics['circuit_breaker']['recovery_timeout']}秒")
|
207
|
+
except Exception as e:
|
208
|
+
print(f" - 获取最终状态失败: {e}")
|
209
|
+
|
210
|
+
if errors:
|
211
|
+
print(f"\n 错误类型统计:")
|
212
|
+
for error_type, count in sorted(errors.items(), key=lambda x: x[1], reverse=True):
|
213
|
+
print(f" - {error_type}: {count} 次")
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
print(f"\n❌ 测试失败: {str(e)}")
|
217
|
+
import traceback
|
218
|
+
traceback.print_exc()
|
219
|
+
|
220
|
+
finally:
|
221
|
+
# 恢复原始环境变量
|
222
|
+
print(f"\n 恢复环境变量...")
|
223
|
+
for var, value in original_env.items():
|
224
|
+
if value is None:
|
225
|
+
os.environ.pop(var, None)
|
226
|
+
else:
|
227
|
+
os.environ[var] = value
|
228
|
+
|
229
|
+
|
230
|
+
async def test_circuit_breaker_recovery():
|
231
|
+
"""测试熔断器恢复功能"""
|
232
|
+
print(f"\n🔄 测试熔断器恢复功能...")
|
233
|
+
|
234
|
+
# 这里可以先触发熔断,然后恢复正常服务,观察熔断器是否能自动恢复
|
235
|
+
# 实现略...
|
236
|
+
pass
|
237
|
+
|
238
|
+
|
239
|
+
async def main():
|
240
|
+
"""主函数"""
|
241
|
+
print("🚀 熔断器功能测试")
|
242
|
+
print("=" * 50)
|
243
|
+
|
244
|
+
try:
|
245
|
+
# 测试熔断器触发
|
246
|
+
await test_circuit_breaker_with_single_requests(10)
|
247
|
+
|
248
|
+
# 可选:测试熔断器恢复
|
249
|
+
# await test_circuit_breaker_recovery()
|
250
|
+
|
251
|
+
print("\n✅ 测试完成")
|
252
|
+
|
253
|
+
except KeyboardInterrupt:
|
254
|
+
print("\n⚠️ 用户中断测试")
|
255
|
+
except Exception as e:
|
256
|
+
print(f"\n❌ 测试执行出错: {e}")
|
257
|
+
import traceback
|
258
|
+
traceback.print_exc()
|
259
|
+
|
260
|
+
|
261
|
+
if __name__ == "__main__":
|
262
|
+
try:
|
263
|
+
asyncio.run(main())
|
264
|
+
except KeyboardInterrupt:
|
265
|
+
print("\n⚠️ 程序被用户中断")
|
266
|
+
except Exception as e:
|
267
|
+
print(f"\n❌ 程序执行出错: {e}")
|
268
|
+
finally:
|
269
|
+
print("🏁 程序已退出")
|