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
+35 -14
View File
@@ -129,7 +129,8 @@ class RetryMW:
fails += 1
if fails >= self._retry.max_attempts:
raise AllSourcesExhausted(
scope=self._scope, reason="retry_exhausted",
scope=self._scope,
reason="retry_exhausted",
retry_after_s=self._retry.backoff_base_s,
per_source_reasons=reasons,
) from outcome.exc
@@ -177,8 +178,10 @@ class RetryMW:
)
if self._quota_full == "fail_fast":
raise AllSourcesExhausted(
scope=self._scope, reason="quota_exhausted",
retry_after_s=self._bp.poll_interval_s, per_source_reasons=reasons,
scope=self._scope,
reason="quota_exhausted",
retry_after_s=self._bp.poll_interval_s,
per_source_reasons=reasons,
)
await self._sleep(self._bp.poll_interval_s)
@@ -197,8 +200,11 @@ class RetryMW:
actual = 0
try:
result = await self._transport.complete(
messages=request.messages, source=source,
stream=request.stream, overlay=request.overlay, call_id=call_id,
messages=request.messages,
source=source,
stream=request.stream,
overlay=request.overlay,
call_id=call_id,
)
actual = result.prompt_tokens + result.completion_tokens
await self._breaker.record_success(entry)
@@ -254,13 +260,20 @@ class RetryMW:
self, source: SourceConfig, result: TransportResult, call_id: str, started: float
) -> LLMResponse:
return LLMResponse(
content=result.content, thinking=result.thinking,
model=source.model, provider=source.provider,
prompt_tokens=result.prompt_tokens, completion_tokens=result.completion_tokens,
content=result.content,
thinking=result.thinking,
model=source.model,
provider=source.provider,
prompt_tokens=result.prompt_tokens,
completion_tokens=result.completion_tokens,
latency_ms=int((self._now() - started) * 1000),
ttft_ms=result.ttft_ms, max_inter_token_ms=result.max_inter_token_ms,
cache_hit=False, call_id=call_id,
source_name=source.name, cost=None, usage_source=result.usage_source,
ttft_ms=result.ttft_ms,
max_inter_token_ms=result.max_inter_token_ms,
cache_hit=False,
call_id=call_id,
source_name=source.name,
cost=None,
usage_source=result.usage_source,
)
async def _settle_and_release(self, permit: Permit, actual: int) -> None:
@@ -276,15 +289,23 @@ class RetryMW:
logger.warning("permit 结算/释放失败(不掩盖主异常): {}", exc)
async def _emit(
self, request: ChatRequest, source: SourceConfig, call_id: str, started: float,
*, response: LLMResponse | None = None, error: object | None = None,
self,
request: ChatRequest,
source: SourceConfig,
call_id: str,
started: float,
*,
response: LLMResponse | None = None,
error: object | None = None,
) -> None:
"""逐次遥测(经注入的单一 Emitter);遥测失败不得影响调用(铁律)。"""
if self._emitter is None:
return
try:
await self._emitter.emit_attempt(
request=request, source=source, call_id=call_id,
request=request,
source=source,
call_id=call_id,
latency_ms=int((self._now() - started) * 1000),
response=response,
error=None if error is None else str(error),