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