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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user