feat: let chat() take a tenant and caller-defined dimensions

Validation runs before the request enters the onion: every failure inside it
is downgraded to a warning by the telemetry layer, so validating in there
would not validate anything.

The dimensions stay out of the cache key — cache_namespace already carries
tenant isolation, and folding meta in would cold-start every existing entry.
This commit is contained in:
2026-08-17 09:43:51 -04:00
parent dba706b59c
commit 4be2b4f287
2 changed files with 76 additions and 1 deletions
+20 -1
View File
@@ -34,7 +34,12 @@ from polygateway.sources import (
SourceCooldownMemo,
)
from polygateway.transports.openai_compat import OpenAICompatTransport
from polygateway.types import ChatRequest, LLMResponse, validate_request_overlay
from polygateway.types import (
ChatRequest,
LLMResponse,
validate_caller_dimensions,
validate_request_overlay,
)
if TYPE_CHECKING:
from collections.abc import Awaitable, Iterable, Mapping
@@ -205,12 +210,18 @@ class GatewayClient:
structured: type[BaseModel] | Literal["json"] | None = None,
stream: bool = True,
overlay: Mapping[str, Any] | None = None,
tenant_id: str | None = None,
meta: Mapping[str, Any] | None = None,
) -> LLMResponse:
"""一次治理调用(签名冻结,ARCH §5.2;与三项目 LLMProvider 协议兼容)。
`overlay` 是采样参数覆盖层(`temperature`/`seed`/`max_tokens` 等),优先级
高于源级 `extra_body`、低于结构化输出的注入。带默认值的 keyword-only
参数不影响既有调用点(issue #4)。
`tenant_id` 与 `meta` 是调用方自定义维度,只进遥测、**不进缓存 key**
(租户隔离由 `cache_namespace` 负责,ARCH §7.5);前者享有真实列待遇
(可挂 RLS、可进复合索引),后者是任意 KV 容器(issue #11)。
"""
if structured is not None and not self._structured_available:
raise ImportError(
@@ -221,6 +232,12 @@ class GatewayClient:
# 造成的竞态。同一份快照填 overlay 与 sampling——前者会被结构化注入,
# 后者跨层恒定,供缓存 key 与遥测读取(设计决策 A/B/E)
sampling = validate_request_overlay(overlay or {}, origin="chat(overlay=...)")
# 同理必须在洋葱之外: 洋葱内的一切失败都被遥测层降级成 warning(库铁律
# 「遥测写失败降级不冒泡」),校验放里面等于没有校验——非法维度会变成
# 静默丢失的遥测行,而调用照常发出(issue #11 §4.2)
dimension_tenant_id, dimensions = validate_caller_dimensions(
tenant_id, meta, origin="chat(tenant_id=..., meta=...)"
)
request = ChatRequest(
messages=messages,
session_id=session_id,
@@ -231,6 +248,8 @@ class GatewayClient:
stream=stream,
overlay=sampling,
sampling=sampling,
tenant_id=dimension_tenant_id,
meta=dimensions,
)
return await self._handler(request)