feat: add gateway client with env-driven assembly

Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.
This commit is contained in:
2026-07-20 07:47:05 -04:00
parent 936895919c
commit 7b9815f4bc
30 changed files with 1701 additions and 253 deletions
+55 -19
View File
@@ -67,7 +67,9 @@ async def _iter_sse_deltas(
try:
chunk = json.loads(data)
except json.JSONDecodeError as exc:
raise TransientError(f"SSE 帧畸形(malformed_json): {data[:80]!r}", operation="chat") from exc
raise TransientError(
f"SSE 帧畸形(malformed_json): {data[:80]!r}", operation="chat"
) from exc
delta = _sse_delta(chunk, usage_sink)
if delta is not None:
yield delta
@@ -95,12 +97,16 @@ def _translate_429(source: SourceConfig, body_text: str, headers: Mapping[str, s
if err_type == "insufficient_quota":
return SourceDeadError(
f"{source.name} 配额耗尽(insufficient_quota)",
source_name=source.name, status_code=429, operation="chat",
source_name=source.name,
status_code=429,
operation="chat",
)
return TransientError(
f"{source.name} 限速: 429",
retry_after_s=_parse_retry_after(headers.get("retry-after")),
source_name=source.name, status_code=429, operation="chat",
source_name=source.name,
status_code=429,
operation="chat",
)
@@ -164,8 +170,13 @@ class OpenAICompatTransport:
return client
def _build_payload(
self, *, messages: list[dict[str, Any]], source: SourceConfig,
profile: ProviderProfile, stream: bool, overlay: dict[str, Any],
self,
*,
messages: list[dict[str, Any]],
source: SourceConfig,
profile: ProviderProfile,
stream: bool,
overlay: dict[str, Any],
) -> dict[str, Any]:
payload: dict[str, Any] = {"model": source.model, "messages": messages, "stream": stream}
if stream:
@@ -178,8 +189,13 @@ class OpenAICompatTransport:
return payload
async def complete(
self, *, messages: list[dict[str, Any]], source: SourceConfig,
stream: bool, overlay: dict[str, Any], call_id: str,
self,
*,
messages: list[dict[str, Any]],
source: SourceConfig,
stream: bool,
overlay: dict[str, Any],
call_id: str,
) -> TransportResult:
"""一次原始调用;HTTP/线路/流式异常按 ARCH §6.2 翻译为领域错误。"""
profile = get_provider(source.provider, registry=self._registry)
@@ -202,8 +218,12 @@ class OpenAICompatTransport:
raise TransientError(f"{source.name} 网络错误: {exc}", **ctx) from exc
async def _complete_stream(
self, client: httpx.AsyncClient, url: str, payload: dict[str, Any],
source: SourceConfig, profile: ProviderProfile,
self,
client: httpx.AsyncClient,
url: str,
payload: dict[str, Any],
source: SourceConfig,
profile: ProviderProfile,
) -> TransportResult:
started = time.monotonic()
async with client.stream("POST", url, json=payload) as resp:
@@ -236,15 +256,22 @@ class OpenAICompatTransport:
if salvaged:
usage_source = "estimated" # 打捞路径强制 estimated(设计 §6)
return TransportResult(
content=content, thinking=thinking, prompt_tokens=prompt,
completion_tokens=completion, usage_source=usage_source,
ttft_ms=ttft_ms, max_inter_token_ms=(max_gap if ttft_ms is not None else None),
content=content,
thinking=thinking,
prompt_tokens=prompt,
completion_tokens=completion,
usage_source=usage_source,
ttft_ms=ttft_ms,
max_inter_token_ms=(max_gap if ttft_ms is not None else None),
raw={"usage": sink.get("usage")},
)
def _check_done(
self, sink: dict[str, Any], content_parts: list[str],
thinking_parts: list[str], source: SourceConfig,
self,
sink: dict[str, Any],
content_parts: list[str],
thinking_parts: list[str],
source: SourceConfig,
) -> bool:
"""缺 [DONE] 语义(设计 §6): 零内容恒 retry;有内容按 missing_done 策略。"""
if sink.get("done"):
@@ -268,8 +295,12 @@ class OpenAICompatTransport:
return content, thinking
async def _complete_once(
self, client: httpx.AsyncClient, url: str, payload: dict[str, Any],
source: SourceConfig, profile: ProviderProfile,
self,
client: httpx.AsyncClient,
url: str,
payload: dict[str, Any],
source: SourceConfig,
profile: ProviderProfile,
) -> TransportResult:
"""非流式快路径(三项目均无,库新增): 单 JSON 响应,仅 total 超时。"""
resp = await client.post(url, json=payload)
@@ -292,9 +323,14 @@ class OpenAICompatTransport:
)
prompt, completion, usage_source = _resolve_usage(body.get("usage") or {}, source)
return TransportResult(
content=content, thinking=thinking, prompt_tokens=prompt,
completion_tokens=completion, usage_source=usage_source,
ttft_ms=None, max_inter_token_ms=None, raw={"usage": body.get("usage")},
content=content,
thinking=thinking,
prompt_tokens=prompt,
completion_tokens=completion,
usage_source=usage_source,
ttft_ms=None,
max_inter_token_ms=None,
raw={"usage": body.get("usage")},
)
async def aclose(self) -> None: