feat: accept sampling overlay on chat() and per-source extra_body
Priority is structured injection > per-call overlay > per-source config, and extra_body now takes part in the cache fingerprint.
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
||||
@@ -32,7 +34,7 @@ from polygateway.sources import (
|
||||
SourceCooldownMemo,
|
||||
)
|
||||
from polygateway.transports.openai_compat import OpenAICompatTransport
|
||||
from polygateway.types import ChatRequest, LLMResponse
|
||||
from polygateway.types import ChatRequest, LLMResponse, validate_request_overlay
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable, Iterable, Mapping
|
||||
@@ -59,6 +61,29 @@ if TYPE_CHECKING:
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def build_model_fingerprint(sources: Iterable[SourceConfig]) -> str:
|
||||
"""缓存 key 的模型身份: 多源 scope = 排序去重的 model 合集。
|
||||
|
||||
配置级采样参数(`extra_body`)必须参与,否则把 temperature 从 0 改成 1
|
||||
后重启仍会读到旧缓存(issue #4 设计决策 C)。全源 `extra_body` 皆空时
|
||||
字面量与历史实现逐字相同,不触发存量缓存冷启动。
|
||||
"""
|
||||
fingerprint = ",".join(sorted({s.model for s in sources}))
|
||||
# 按 (model, extra_body) 而非源名摘要: 语义是"本 scope 会用哪些
|
||||
# (模型, 解码参数)组合",改源名不该误触全量冷启动
|
||||
marks = sorted(
|
||||
{
|
||||
json.dumps([s.model, dict(s.extra_body)], sort_keys=True, ensure_ascii=False)
|
||||
for s in sources
|
||||
if s.extra_body
|
||||
}
|
||||
)
|
||||
if marks:
|
||||
digest = hashlib.sha256("".join(marks).encode("utf-8")).hexdigest()
|
||||
fingerprint = f"{fingerprint}|{digest}"
|
||||
return fingerprint
|
||||
|
||||
|
||||
class GatewayClient:
|
||||
"""统一治理入口;构造函数全量注入(测试/高级),工厂覆盖 90% 场景。"""
|
||||
|
||||
@@ -113,12 +138,11 @@ class GatewayClient:
|
||||
if cache is not None:
|
||||
if cache_namespace is None or cache_ttl_s is None:
|
||||
raise ValueError("启用缓存必须提供 cache_namespace 与 cache_ttl_s")
|
||||
# 多源 scope 的 key 身份 = 排序去重的 model 合集;源集合变化 → 一次性冷启动
|
||||
fingerprint = ",".join(sorted({s.model for s in sources}))
|
||||
# 多源 scope 的 key 身份;源集合或其 extra_body 变化 → 一次性冷启动
|
||||
middlewares.append(
|
||||
CacheMW(
|
||||
backend=cache,
|
||||
model_fingerprint=fingerprint,
|
||||
model_fingerprint=build_model_fingerprint(sources),
|
||||
default_namespace=cache_namespace,
|
||||
ttl_s=cache_ttl_s,
|
||||
strategy=structured_strategy,
|
||||
@@ -150,12 +174,23 @@ class GatewayClient:
|
||||
cache_namespace: str | None = None,
|
||||
structured: type[BaseModel] | Literal["json"] | None = None,
|
||||
stream: bool = True,
|
||||
overlay: Mapping[str, Any] | None = None,
|
||||
) -> LLMResponse:
|
||||
"""一次治理调用(签名冻结,ARCH §5.2;与三项目 LLMProvider 协议兼容)。"""
|
||||
"""一次治理调用(签名冻结,ARCH §5.2;与三项目 LLMProvider 协议兼容)。
|
||||
|
||||
`overlay` 是采样参数覆盖层(`temperature`/`seed`/`max_tokens` 等),优先级
|
||||
高于源级 `extra_body`、低于结构化输出的注入。带默认值的 keyword-only
|
||||
参数不影响既有调用点(issue #4)。
|
||||
"""
|
||||
if structured is not None and not self._structured_available:
|
||||
raise ImportError(
|
||||
"结构化输出未启用: 安装 pip install 'polygateway[structured]' 后重新装配"
|
||||
)
|
||||
# 进洋葱之前校验并拷贝: 保护键/不可序列化值在此收口(否则会在 CacheMW
|
||||
# 的降级 try 之外抛裸 TypeError);拷贝防调用方复用同一 dict 逐次改 seed
|
||||
# 造成的竞态。同一份快照填 overlay 与 sampling——前者会被结构化注入,
|
||||
# 后者跨层恒定,供缓存 key 与遥测读取(设计决策 A/B/E)
|
||||
sampling = validate_request_overlay(overlay or {}, origin="chat(overlay=...)")
|
||||
request = ChatRequest(
|
||||
messages=messages,
|
||||
session_id=session_id,
|
||||
@@ -164,6 +199,8 @@ class GatewayClient:
|
||||
cache_namespace=cache_namespace,
|
||||
structured=structured,
|
||||
stream=stream,
|
||||
overlay=sampling,
|
||||
sampling=sampling,
|
||||
)
|
||||
return await self._handler(request)
|
||||
|
||||
|
||||
@@ -294,6 +294,9 @@ class OpenAICompatTransport:
|
||||
payload.update(profile.thinking_on)
|
||||
elif source.enable_thinking is False:
|
||||
payload.update(profile.thinking_off)
|
||||
# 顺序即优先级(issue #4 设计决策 A): 配置级 extra_body 在前,调用级
|
||||
# overlay(含结构化注入)在后覆盖之。两行不可调换
|
||||
payload.update(source.extra_body)
|
||||
payload.update(overlay)
|
||||
return payload
|
||||
|
||||
|
||||
Reference in New Issue
Block a user