Files
writer-work-flow/packages/llm_gateway/ww_llm_gateway/adapters/base.py
Yaojia Wang 016509c5c6 fix(gateway): 熔断计入持续 4xx + 去 assert + Protocol/transient 去重
P1-2 非可重试错误(持续 401/403)也 record_failure,坏 key 可触发熔断。
P1-9 _complete_structured 的 assert 改显式 raise ValueError(-O 安全)。
P2 GatewayRun 抽到 orchestrator/_protocols.py 单点(去 4 处重复);
  _is_transient 抽到 adapters/base.py is_transient_by_name(去 3 处重复);
  Gemini Protocol 改 async def;gateway._retrying 去无用 async。
2026-06-21 19:32:49 +02:00

80 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""适配器接口与中间数据形ARCH §4.2/§4.4)。
适配器把 `LlmRequest` 翻译成目标厂商请求,并把响应/流/usage 翻译回统一中间形。
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from typing import Protocol, runtime_checkable
from pydantic import BaseModel, ConfigDict
from ..types import LlmRequest
# 瞬时(可退避重试)的 HTTP 状态码判定429 限流 或 5xx 服务端错误。
_RATE_LIMITED_STATUS = 429
_SERVER_ERROR_MIN_STATUS = 500
def _is_transient_status(status: object) -> bool:
return isinstance(status, int) and (
status == _RATE_LIMITED_STATUS or status >= _SERVER_ERROR_MIN_STATUS
)
def is_transient_by_name(
exc: Exception, names: frozenset[str], *, extra_codes: bool = False
) -> bool:
"""统一的瞬时错误判定DRY三个适配器共用
① 异常类名命中 `names`(按名匹配,避免硬依赖各厂商 SDK 异常类型);或
② `status_code` 属性是 429/5xx
③ `extra_codes=True` 时额外检查 `code` 属性是否为 429/5xxGemini 用 `exc.code`)。
"""
if type(exc).__name__ in names:
return True
if _is_transient_status(getattr(exc, "status_code", None)):
return True
if extra_codes and _is_transient_status(getattr(exc, "code", None)):
return True
return False
class Capabilities(BaseModel):
structured_output: bool = False
prefix_cache: bool = False
thinking: bool = False
class ProviderUsage(BaseModel):
input_tokens: int
output_tokens: int
cache_read_tokens: int = 0
class ProviderResult(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
text: str
usage: ProviderUsage
parsed: BaseModel | None = None # output_schema 命中时的结构化结果§4.4
class StreamChunk(BaseModel):
"""流式块文本增量usage=None或末尾用量块text="")。"""
text: str = ""
usage: ProviderUsage | None = None
@runtime_checkable
class ProviderAdapter(Protocol):
provider: str
def capabilities(self) -> Capabilities: ...
async def complete(self, req: LlmRequest, model: str) -> ProviderResult: ...
def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]: ...