tamar-model-client 0.1.20__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.
@@ -30,22 +30,23 @@ import grpc
30
30
  from .core import (
31
31
  generate_request_id,
32
32
  set_request_id,
33
- setup_logger,
33
+ get_protected_logger,
34
34
  MAX_MESSAGE_LENGTH
35
35
  )
36
36
  from .core.base_client import BaseClient
37
37
  from .core.request_builder import RequestBuilder
38
38
  from .core.response_handler import ResponseHandler
39
- from .exceptions import ConnectionError, TamarModelException, is_retryable_error
39
+ from .exceptions import ConnectionError, TamarModelException
40
40
  from .generated import model_service_pb2, model_service_pb2_grpc
41
41
  from .schemas import BatchModelResponse, ModelResponse
42
42
  from .schemas.inputs import BatchModelRequest, ModelRequest
43
+ from .core.http_fallback import HttpFallbackMixin
43
44
 
44
- # 配置日志记录器
45
- logger = setup_logger(__name__)
45
+ # 配置日志记录器(使用受保护的logger)
46
+ logger = get_protected_logger(__name__)
46
47
 
47
48
 
48
- class TamarModelClient(BaseClient):
49
+ class TamarModelClient(BaseClient, HttpFallbackMixin):
49
50
  """
50
51
  Tamar Model Client 同步客户端
51
52
 
@@ -218,20 +219,31 @@ class TamarModelClient(BaseClient):
218
219
  context['retry_count'] = attempt
219
220
 
220
221
  # 判断是否可以重试
221
- if not is_retryable_error(e.code()) or attempt >= self.max_retries:
222
+ should_retry = self._should_retry(e, attempt)
223
+ if not should_retry or attempt >= self.max_retries:
222
224
  # 不可重试或已达到最大重试次数
223
225
  last_exception = self.error_handler.handle_error(e, context)
224
226
  break
225
227
 
226
228
  # 记录重试日志
229
+ log_data = {
230
+ "log_type": "info",
231
+ "request_id": context.get('request_id'),
232
+ "data": {
233
+ "error_code": e.code().name if e.code() else 'UNKNOWN',
234
+ "retry_count": attempt,
235
+ "max_retries": self.max_retries,
236
+ "method": context.get('method', 'unknown')
237
+ }
238
+ }
227
239
  logger.warning(
228
240
  f"Attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()}",
229
- extra=context
241
+ extra=log_data
230
242
  )
231
243
 
232
244
  # 执行退避等待
233
245
  if attempt < self.max_retries:
234
- delay = self._calculate_backoff(attempt)
246
+ delay = self._calculate_backoff(attempt, e.code())
235
247
  time.sleep(delay)
236
248
 
237
249
  last_exception = self.error_handler.handle_error(e, context)
@@ -248,14 +260,47 @@ class TamarModelClient(BaseClient):
248
260
  else:
249
261
  raise TamarModelException("Unknown error occurred")
250
262
 
251
- def _calculate_backoff(self, attempt: int) -> float:
252
- """计算退避时间"""
263
+ def _calculate_backoff(self, attempt: int, error_code: grpc.StatusCode = None) -> float:
264
+ """
265
+ 计算退避时间,支持不同的退避策略
266
+
267
+ Args:
268
+ attempt: 当前重试次数
269
+ error_code: gRPC错误码,用于确定退避策略
270
+ """
253
271
  max_delay = 60.0
254
- jitter_factor = 0.1
255
-
256
- delay = min(self.retry_delay * (2 ** attempt), max_delay)
257
- jitter = random.uniform(0, delay * jitter_factor)
258
- return delay + jitter
272
+ base_delay = self.retry_delay
273
+
274
+ # 获取错误的重试策略
275
+ if error_code:
276
+ from .exceptions import get_retry_policy
277
+ policy = get_retry_policy(error_code)
278
+ backoff_type = policy.get('backoff', 'exponential')
279
+ use_jitter = policy.get('jitter', False)
280
+ else:
281
+ backoff_type = 'exponential'
282
+ use_jitter = False
283
+
284
+ # 根据退避类型计算延迟
285
+ if backoff_type == 'linear':
286
+ # 线性退避:delay * (attempt + 1)
287
+ delay = min(base_delay * (attempt + 1), max_delay)
288
+ else:
289
+ # 指数退避:delay * 2^attempt
290
+ delay = min(base_delay * (2 ** attempt), max_delay)
291
+
292
+ # 添加抖动
293
+ if use_jitter:
294
+ jitter_factor = 0.2 # 增加抖动范围,减少竞争
295
+ jitter = random.uniform(0, delay * jitter_factor)
296
+ delay += jitter
297
+ else:
298
+ # 默认的小量抖动,避免完全同步
299
+ jitter_factor = 0.05
300
+ jitter = random.uniform(0, delay * jitter_factor)
301
+ delay += jitter
302
+
303
+ return delay
259
304
 
260
305
  def _retry_request_stream(self, func, *args, **kwargs):
261
306
  """
@@ -272,29 +317,77 @@ class TamarModelClient(BaseClient):
272
317
  流式响应的每个元素
273
318
  """
274
319
  last_exception = None
320
+ context = {
321
+ 'method': 'stream',
322
+ 'client_version': 'sync',
323
+ }
275
324
 
276
325
  for attempt in range(self.max_retries + 1):
277
326
  try:
327
+ context['retry_count'] = attempt
278
328
  # 尝试创建流
279
329
  for item in func(*args, **kwargs):
280
330
  yield item
281
331
  return
282
332
 
283
333
  except grpc.RpcError as e:
284
- last_exception = e
285
- if attempt < self.max_retries:
286
- logger.warning(
287
- f"Stream attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()}",
288
- extra={"retry_count": attempt, "error_code": str(e.code())}
334
+ # 使用智能重试判断
335
+ context['retry_count'] = attempt
336
+
337
+ # 判断是否应该重试
338
+ should_retry = self._should_retry(e, attempt)
339
+ if not should_retry or attempt >= self.max_retries:
340
+ # 不重试或已达到最大重试次数
341
+ log_data = {
342
+ "log_type": "info",
343
+ "request_id": context.get('request_id'),
344
+ "data": {
345
+ "error_code": e.code().name if e.code() else 'UNKNOWN',
346
+ "retry_count": attempt,
347
+ "max_retries": self.max_retries,
348
+ "method": "stream",
349
+ "will_retry": False
350
+ }
351
+ }
352
+ logger.error(
353
+ f"Stream failed: {e.code()} (no retry)",
354
+ extra=log_data
289
355
  )
290
- time.sleep(self.retry_delay * (attempt + 1))
291
- else:
356
+ last_exception = self.error_handler.handle_error(e, context)
292
357
  break
358
+
359
+ # 记录重试日志
360
+ log_data = {
361
+ "log_type": "info",
362
+ "request_id": context.get('request_id'),
363
+ "data": {
364
+ "error_code": e.code().name if e.code() else 'UNKNOWN',
365
+ "retry_count": attempt,
366
+ "max_retries": self.max_retries,
367
+ "method": "stream"
368
+ }
369
+ }
370
+ logger.warning(
371
+ f"Stream attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()} (will retry)",
372
+ extra=log_data
373
+ )
374
+
375
+ # 执行退避等待
376
+ if attempt < self.max_retries:
377
+ delay = self._calculate_backoff(attempt, e.code())
378
+ time.sleep(delay)
379
+
380
+ last_exception = e
381
+
293
382
  except Exception as e:
383
+ context['retry_count'] = attempt
294
384
  raise TamarModelException(str(e)) from e
295
385
 
296
386
  if last_exception:
297
- raise self.error_handler.handle_error(last_exception, {"retry_count": self.max_retries})
387
+ if isinstance(last_exception, TamarModelException):
388
+ raise last_exception
389
+ else:
390
+ raise self.error_handler.handle_error(last_exception, context)
298
391
  else:
299
392
  raise TamarModelException("Unknown streaming error occurred")
300
393
 
@@ -457,6 +550,12 @@ class TamarModelClient(BaseClient):
457
550
  ValidationError: 输入验证失败。
458
551
  ConnectionError: 连接服务端失败。
459
552
  """
553
+ # 如果启用了熔断且熔断器打开,直接走 HTTP
554
+ if self.resilient_enabled and self.circuit_breaker and self.circuit_breaker.is_open:
555
+ if self.http_fallback_url:
556
+ logger.warning("🔻 Circuit breaker is OPEN, using HTTP fallback")
557
+ return self._invoke_http_fallback(model_request, timeout, request_id)
558
+
460
559
  self._ensure_initialized()
461
560
 
462
561
  if not self.default_payload:
@@ -527,9 +626,14 @@ class TamarModelClient(BaseClient):
527
626
  "data": ResponseHandler.build_log_data(model_request, result)
528
627
  }
529
628
  )
629
+
630
+ # 记录成功(如果启用了熔断)
631
+ if self.resilient_enabled and self.circuit_breaker:
632
+ self.circuit_breaker.record_success()
633
+
530
634
  return result
531
635
 
532
- except grpc.RpcError as e:
636
+ except (ConnectionError, grpc.RpcError) as e:
533
637
  duration = time.time() - start_time
534
638
  error_message = f"❌ Invoke gRPC failed: {str(e)}"
535
639
  logger.error(error_message, exc_info=True,
@@ -542,6 +646,18 @@ class TamarModelClient(BaseClient):
542
646
  error=e
543
647
  )
544
648
  })
649
+
650
+ # 记录失败并尝试降级(如果启用了熔断)
651
+ if self.resilient_enabled and self.circuit_breaker:
652
+ # 将错误码传递给熔断器,用于智能失败统计
653
+ error_code = e.code() if hasattr(e, 'code') else None
654
+ self.circuit_breaker.record_failure(error_code)
655
+
656
+ # 如果可以降级,则降级
657
+ if self.http_fallback_url and self.circuit_breaker.should_fallback():
658
+ logger.warning(f"🔻 gRPC failed, falling back to HTTP: {str(e)}")
659
+ return self._invoke_http_fallback(model_request, timeout, request_id)
660
+
545
661
  raise e
546
662
  except Exception as e:
547
663
  duration = time.time() - start_time
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-model-client
3
- Version: 0.1.20
3
+ Version: 0.1.21
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
@@ -532,6 +532,61 @@ response = client.invoke(
532
532
  - 启用流式响应减少首字延迟
533
533
  - 合理设置 max_tokens 避免浪费
534
534
 
535
+ ### 🛡️ 熔断降级功能(高可用保障)
536
+
537
+ SDK 内置了熔断降级机制,当 gRPC 服务不可用时自动切换到 HTTP 服务,确保业务连续性。
538
+
539
+ #### 工作原理
540
+ 1. **正常状态**:所有请求通过高性能的 gRPC 协议
541
+ 2. **熔断触发**:当连续失败达到阈值时,熔断器打开
542
+ 3. **自动降级**:切换到 HTTP 协议继续提供服务
543
+ 4. **定期恢复**:熔断器会定期尝试恢复到 gRPC
544
+
545
+ #### 启用方式
546
+ ```bash
547
+ # 设置环境变量
548
+ export MODEL_CLIENT_RESILIENT_ENABLED=true
549
+ export MODEL_CLIENT_HTTP_FALLBACK_URL=http://localhost:8080
550
+ export MODEL_CLIENT_CIRCUIT_BREAKER_THRESHOLD=5
551
+ export MODEL_CLIENT_CIRCUIT_BREAKER_TIMEOUT=60
552
+ ```
553
+
554
+ #### 使用示例
555
+ ```python
556
+ from tamar_model_client import TamarModelClient
557
+
558
+ # 客户端会自动处理熔断降级,对使用者透明
559
+ client = TamarModelClient()
560
+
561
+ # 正常使用,无需关心底层协议
562
+ response = client.invoke(request)
563
+
564
+ # 获取熔断器状态(可选)
565
+ metrics = client.get_resilient_metrics()
566
+ if metrics:
567
+ print(f"熔断器状态: {metrics['circuit_state']}")
568
+ print(f"失败次数: {metrics['failure_count']}")
569
+ ```
570
+
571
+ #### 熔断器状态
572
+ - **CLOSED**(关闭):正常工作状态,请求正常通过
573
+ - **OPEN**(打开):熔断状态,所有请求直接降级到 HTTP
574
+ - **HALF_OPEN**(半开):恢复测试状态,允许少量请求测试 gRPC 是否恢复
575
+
576
+ #### 监控指标
577
+ ```python
578
+ # 获取熔断降级指标
579
+ metrics = client.get_resilient_metrics()
580
+ # 返回示例:
581
+ # {
582
+ # "enabled": true,
583
+ # "circuit_state": "closed",
584
+ # "failure_count": 0,
585
+ # "last_failure_time": null,
586
+ # "http_fallback_url": "http://localhost:8080"
587
+ # }
588
+ ```
589
+
535
590
  ### ⚠️ 注意事项
536
591
 
537
592
  1. **参数说明**
@@ -595,6 +650,23 @@ MODEL_MANAGER_SERVER_GRPC_MAX_RETRIES=3
595
650
 
596
651
  # 初始重试延迟(秒,默认 1.0),指数退避
597
652
  MODEL_MANAGER_SERVER_GRPC_RETRY_DELAY=1.0
653
+
654
+
655
+ # ========================
656
+ # 🛡️ 熔断降级配置(可选)
657
+ # ========================
658
+
659
+ # 是否启用熔断降级功能(默认 false)
660
+ MODEL_CLIENT_RESILIENT_ENABLED=false
661
+
662
+ # HTTP 降级服务地址(当 gRPC 不可用时的备用地址)
663
+ MODEL_CLIENT_HTTP_FALLBACK_URL=http://localhost:8080
664
+
665
+ # 熔断器触发阈值(连续失败多少次后熔断,默认 5)
666
+ MODEL_CLIENT_CIRCUIT_BREAKER_THRESHOLD=5
667
+
668
+ # 熔断器恢复超时(秒,熔断后多久尝试恢复,默认 60)
669
+ MODEL_CLIENT_CIRCUIT_BREAKER_TIMEOUT=60
598
670
  ```
599
671
 
600
672
  加载后,初始化时无需传参:
@@ -1,15 +1,17 @@
1
1
  tamar_model_client/__init__.py,sha256=4DEIUGlLTeiaECjJQbGYik7C0JO6hHwwfbLYpYpMdzg,444
2
- tamar_model_client/async_client.py,sha256=t945Qyw2s2MQpj9ArxCSGrMH625Asmt52QXD8S5CEPM,26314
2
+ tamar_model_client/async_client.py,sha256=cU3cUrwP75zacyFa3KibcYfacJBsxRNLIu1vtQZrrcU,32836
3
3
  tamar_model_client/auth.py,sha256=gbwW5Aakeb49PMbmYvrYlVx1mfyn1LEDJ4qQVs-9DA4,438
4
- tamar_model_client/error_handler.py,sha256=_KUCTCpXhQm7_7a0-luClOW3e7FbaNC8gDHvGp0Wtxg,10019
5
- tamar_model_client/exceptions.py,sha256=vATn4LUNiD-0sL2Cn4E-HqQEKmIBd96xcoUlkArus7w,11188
4
+ tamar_model_client/circuit_breaker.py,sha256=0XHJXBYA4O8vwsDGwqNrae9zxNJphY5Rfucc9ytVFGA,5419
5
+ tamar_model_client/error_handler.py,sha256=kVfHL7DWvO3sIobjVuJbqjV4mtI4oqbS4Beax7Dmm9w,11788
6
+ tamar_model_client/exceptions.py,sha256=FImLCBpYQ8DpsNbH-ZttxyClEZCL6ICmQGESIlbI--s,12038
6
7
  tamar_model_client/json_formatter.py,sha256=IyBv_pEEzjF-KaMF-7rxRpNc_fxRYK2A-pu_2n4Liow,1990
7
8
  tamar_model_client/logging_icons.py,sha256=MRTZ1Xvkep9ce_jdltj54_XZUXvIpQ95soRNmLdJ4qw,1837
8
- tamar_model_client/sync_client.py,sha256=2LM3ZQ0M0MNE732sfwDeDruOAAiEEFB7BcjEeNmQIb8,27401
9
+ tamar_model_client/sync_client.py,sha256=AhNFlhk9aC7JhNrI2BEZJDLjXZwVT9pMy3u9jgjO1QU,32603
9
10
  tamar_model_client/utils.py,sha256=Kn6pFz9GEC96H4eejEax66AkzvsrXI3WCSDtgDjnVTI,5238
10
- tamar_model_client/core/__init__.py,sha256=PDQ2emPz3eHg_dhmvsd3pXlJf93A-Pl5Qh6E6mHO4XQ,670
11
- tamar_model_client/core/base_client.py,sha256=YogODGjDnQE2b2P_fcpgB1uYt0A88h_oUfFN63qZLf8,6699
12
- tamar_model_client/core/logging_setup.py,sha256=YXX0MEe83mpXwGH8A-D3bYOTNwX14NQbgfA7b3T_vbs,2626
11
+ tamar_model_client/core/__init__.py,sha256=bJRJllrp4Xc0g_qu1pW9G-lsXNB7c1r0NBIfb2Ypxe0,832
12
+ tamar_model_client/core/base_client.py,sha256=sYvJZsDu_66akddAMowSnihFtgOoVKaQJxxnVruF9Ms,8995
13
+ tamar_model_client/core/http_fallback.py,sha256=1OuSMxzhDyxy07JZa5artMTNdPNMyAhI7By3RUCSPDw,9872
14
+ tamar_model_client/core/logging_setup.py,sha256=h1aky1uslIQnx4NxMqjoDMxwlc4Vg46KYTjW9yPu2xQ,6032
13
15
  tamar_model_client/core/request_builder.py,sha256=yi8iy2Ps2m4d1YwIFiQLRxTvxQxgEGV576aXnNYRl7E,8507
14
16
  tamar_model_client/core/response_handler.py,sha256=_q5galAT0_RaUT5C_yZsjg-9VnT9CBjmIASOt28BUmQ,4616
15
17
  tamar_model_client/core/utils.py,sha256=8jSx8UOE6ukbiIgruCX7SXN8J5FyuGbqENOmJDsxaSM,5084
@@ -27,7 +29,7 @@ tests/__init__.py,sha256=kbmImddLDwdqlkkmkyKtl4bQy_ipe-R8eskpaBylU9w,38
27
29
  tests/stream_hanging_analysis.py,sha256=W3W48IhQbNAR6-xvMpoWZvnWOnr56CTaH4-aORNBuD4,14807
28
30
  tests/test_google_azure_final.py,sha256=wAnfodYCs8VIqYlgT6nm1YnLnufqSuYfXBaVqCXkmfU,17019
29
31
  tests/test_simple.py,sha256=Xf0U-J9_xn_LzUsmYu06suK0_7DrPeko8OHoHldsNxE,7169
30
- tamar_model_client-0.1.20.dist-info/METADATA,sha256=FDp1ZkTf3FJ08gVvIVAx2_CmGRz1aNqa7LJFH0WPqnY,21147
31
- tamar_model_client-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- tamar_model_client-0.1.20.dist-info/top_level.txt,sha256=f1I-S8iWN-cgv4gB8gxRg9jJOTJMumvm4oGKVPfGg6A,25
33
- tamar_model_client-0.1.20.dist-info/RECORD,,
32
+ tamar_model_client-0.1.21.dist-info/METADATA,sha256=gj8tUbP3goUZKi3pVVWMxEpmmK6W72IV23Ym2ohlcBs,23453
33
+ tamar_model_client-0.1.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ tamar_model_client-0.1.21.dist-info/top_level.txt,sha256=f1I-S8iWN-cgv4gB8gxRg9jJOTJMumvm4oGKVPfGg6A,25
35
+ tamar_model_client-0.1.21.dist-info/RECORD,,