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
+3 -1
View File
@@ -33,7 +33,9 @@ class BreakerGate:
except Exception as exc:
raise GovernanceBackendError(f"熔断后端故障(record_success): {exc}") from exc
async def record_failure(self, entry: GateDecision, reason: str, force_open: bool) -> GateUpdate:
async def record_failure(
self, entry: GateDecision, reason: str, force_open: bool
) -> GateUpdate:
try:
return await self._gate.record_failure(entry, reason, force_open)
except GovernanceBackendError:
+6 -2
View File
@@ -111,8 +111,12 @@ class CacheMW:
fields = {k: v for k, v in data.items() if k in _RESPONSE_FIELDS}
structured_data = self._rebuild_structured(fields.get("content", ""), request)
fields.update(
cache_hit=True, latency_ms=0, ttft_ms=None, max_inter_token_ms=None,
call_id=str(uuid.uuid4()), structured_data=structured_data,
cache_hit=True,
latency_ms=0,
ttft_ms=None,
max_inter_token_ms=None,
call_id=str(uuid.uuid4()),
structured_data=structured_data,
)
return LLMResponse(**fields)
except Exception as exc:
+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),
+8 -4
View File
@@ -166,15 +166,19 @@ class TelemetryMW:
response = await call_next(request)
except (GatewayUnavailableError, GovernanceBackendError) as exc:
await self._emitter.emit_terminal_failure(
request=request, call_id=str(uuid.uuid4()),
latency_ms=int((self._now() - started) * 1000), error=str(exc),
request=request,
call_id=str(uuid.uuid4()),
latency_ms=int((self._now() - started) * 1000),
error=str(exc),
)
raise
except asyncio.CancelledError:
# 尽力而为: 取消也留痕(§5.1 约定④);随后立即重抛
await self._emitter.emit_terminal_failure(
request=request, call_id=str(uuid.uuid4()),
latency_ms=int((self._now() - started) * 1000), error="cancelled",
request=request,
call_id=str(uuid.uuid4()),
latency_ms=int((self._now() - started) * 1000),
error="cancelled",
)
raise
if response.cache_hit: