Files
writer-work-flow/packages/llm_gateway/ww_llm_gateway/adapters/base.py

96 lines
3.0 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 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/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]: ...