96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
"""适配器接口与中间数据形(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 provider_error_summary(exc: Exception) -> str:
|
||
"""脱敏摘要:只含异常类名 + 可选状态码,绝不含原始厂商 HTTP 响应体(CR-M2-2)。
|
||
|
||
厂商异常的 `str(exc)` 常内嵌完整响应体(可能含请求内容/内部细节),不应进我们
|
||
构造并上抛的错误消息(防泄漏,§9.3)。完整细节仍可留在服务端日志(若已记)。
|
||
状态码优先取 `status_code`,回退取 `code`(Gemini 部分错误用 `exc.code`)。
|
||
"""
|
||
name = type(exc).__name__
|
||
status = getattr(exc, "status_code", None)
|
||
if not isinstance(status, int):
|
||
status = getattr(exc, "code", None)
|
||
if isinstance(status, int):
|
||
return f"{name} (status={status})"
|
||
return name
|
||
|
||
|
||
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/5xx(Gemini 用 `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]: ...
|