feat: preserve governed OpenAI tool messages in 1.4.0

Add faithful single-choice chat results, tool stream activity, error-frame retries, and isolated protocol caching.

Reject empty cached completions with the same value predicate as transport, add regression coverage, and adopt Apache-2.0.
This commit is contained in:
2026-10-01 14:32:19 -04:00
parent 0e29deac1a
commit 9185eab8c9
42 changed files with 2227 additions and 78 deletions
@@ -0,0 +1,174 @@
"""OpenAI 单 choice 消息聚合:仅协议值处理,不做 I/O 或治理。"""
from dataclasses import dataclass
from typing import Any
from polygateway.errors import TransientError
from polygateway.types import OpenAIChatCompletion, ResponseMode
def _invalid(reason: str) -> TransientError:
"""协议形态失败,不回显可能敏感的工具参数。"""
return TransientError(f"OpenAI 协议失败: {reason}", operation="chat")
def _object(value: Any, name: str) -> dict[str, Any]:
"""外部对象先校验再读取。"""
if not isinstance(value, dict):
raise _invalid(f"{name} 必须为对象")
return value
def _identity(target: dict[str, Any], key: str, value: Any) -> bool:
"""身份字段只接受同值重复,不把重复完整 ID 当分片拼接。"""
if value is None:
return False
if not isinstance(value, str):
raise _invalid(f"{key} 必须为字符串")
if key in target and target[key] != value:
raise _invalid(f"{key} 冲突")
changed = key not in target and bool(value)
target[key] = value
return changed
def _append(target: dict[str, Any], key: str, value: Any) -> str:
"""字符串片段逐字拼接,保留缺省/null/空串区别。"""
if value is None:
target.setdefault(key, None)
return ""
if not isinstance(value, str):
raise _invalid(f"{key} 必须为字符串或 null")
target[key] = (target.get(key) or "") + value
return value
def _completion(message: dict[str, Any], metadata: dict[str, Any]) -> OpenAIChatCompletion:
"""复用内核最终快照校验并翻译领域错误。"""
try:
return OpenAIChatCompletion(
message, metadata.get("finish_reason"), metadata.get("id"), metadata.get("usage")
)
except (ValueError, TypeError) as exc:
raise _invalid(str(exc)) from exc
@dataclass(frozen=True)
class ChatDelta:
"""一帧提取结果;progress 表示真实生成进展。"""
content: str
reasoning_content: str
progress: bool
class ChatCompletionAccumulator:
"""每次 transport 尝试独享的聚合器,文本模式不严格校验工具产物。"""
def __init__(self, *, response_mode: ResponseMode) -> None:
"""初始化尝试私有状态。"""
self.mode = response_mode
self.message: dict[str, Any] = {}
self.metadata: dict[str, Any] = {}
self.tools: dict[int, dict[str, Any]] = {}
def add_chunk(self, chunk: dict[str, Any]) -> ChatDelta:
"""聚合一个已解码帧,一帧至多一个活性事件。"""
chunk = _object(chunk, "chunk")
strict = self.mode == "openai"
if strict:
_identity(self.metadata, "id", chunk.get("id"))
if chunk.get("usage") is not None:
_object(chunk["usage"], "usage")
self.metadata["usage"] = chunk["usage"]
choices = chunk.get("choices", [])
if not isinstance(choices, list):
raise _invalid("choices 必须为数组")
if not choices:
return ChatDelta("", "", False)
if strict and len(choices) != 1:
raise _invalid("仅支持单 choice")
choice = _object(choices[0], "choice")
if strict:
if type(choice.get("index")) is not int or choice["index"] != 0:
raise _invalid("choice index 必须为 0")
_identity(self.metadata, "finish_reason", choice.get("finish_reason"))
delta = _object(choice.get("delta", {}), "delta")
if strict and delta.keys() - {"role", "content", "reasoning_content", "tool_calls"}:
raise _invalid("未知 delta 字段")
if "role" in delta and strict:
if delta["role"] != "assistant":
raise _invalid("role 必须为 assistant")
self.message["role"] = delta["role"]
content = _append(self.message, "content", delta["content"]) if "content" in delta else ""
reasoning = (
_append(self.message, "reasoning_content", delta["reasoning_content"])
if "reasoning_content" in delta
else ""
)
progress = bool(content or reasoning)
if "tool_calls" in delta:
tools = delta["tool_calls"]
if not isinstance(tools, list):
if strict:
raise _invalid("tool_calls 必须为数组")
return ChatDelta(content, reasoning, progress)
if strict:
self.message.setdefault("tool_calls", [])
for raw in tools:
if not strict:
# 文本只消费可识别的工具进展,不承诺工具完整性。
if isinstance(raw, dict) and type(raw.get("index")) is int:
index = raw["index"]
tool = self.tools.setdefault(index, {})
for key in ("id", "type"):
if isinstance(raw.get(key), str) and raw[key] and key not in tool:
tool[key] = raw[key]
progress = True
fn = raw.get("function")
if isinstance(fn, dict):
progress |= any(
isinstance(fn.get(k), str) and bool(fn[k])
for k in ("name", "arguments")
)
continue
raw = _object(raw, "tool")
index = raw.get("index")
if type(index) is not int or index < 0:
raise _invalid("tool index 必须为非负整数")
if raw.keys() - {"index", "id", "type", "function"}:
raise _invalid("未知工具增量字段")
tool = self.tools.setdefault(index, {})
for key in ("id", "type"):
progress |= _identity(tool, key, raw.get(key))
if "function" in raw:
fn = _object(raw["function"], "function")
if fn.keys() - {"name", "arguments"}:
raise _invalid("未知 function 增量字段")
target = tool.setdefault("function", {})
for key in ("name", "arguments"):
if key in fn:
if not isinstance(fn[key], str):
raise _invalid(f"function.{key} 必须为字符串")
progress |= bool(_append(target, key, fn[key]))
return ChatDelta(content, reasoning, progress)
def finish(self) -> OpenAIChatCompletion:
"""线路完整性由调用者保证;此处只校验最终消息。"""
message = dict(self.message)
if self.tools:
message["tool_calls"] = [self.tools[index] for index in sorted(self.tools)]
return _completion(message, self.metadata)
def parse_chat_completion(body: dict[str, Any]) -> OpenAIChatCompletion:
"""非流式完整消息保留扩展字段,拒绝静默截取多 choice。"""
body = _object(body, "body")
choices = body.get("choices")
if not isinstance(choices, list) or len(choices) != 1:
raise _invalid("仅支持单 choice")
choice = _object(choices[0], "choice")
if "index" in choice and (type(choice["index"]) is not int or choice["index"] != 0):
raise _invalid("choice index 必须为 0")
metadata = {**body, "finish_reason": choice.get("finish_reason")}
return _completion(_object(choice.get("message"), "message"), metadata)