feat: add scoped model runtime configuration
Build / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled

Introduce provider, model, and invocation contracts with encrypted configuration persistence. Add web runtime fencing, route fallback, recovery middleware, workspace scoping, and comprehensive tests.
This commit is contained in:
m4
2026-08-14 22:03:04 +08:00
parent 3ce5614254
commit 5a581c78a2
97 changed files with 26670 additions and 454 deletions
+96 -5
View File
@@ -11,6 +11,8 @@ Patches:
- _patch_openai_capture_reasoning_content: capture provider
reasoning_content into AIMessage.additional_kwargs (module-level,
applied at import)
- _patch_openai_empty_sse_keepalive: ignore blank SSE keepalive events
emitted by some OpenAI-compatible Responses endpoints
- _patch_deepseek_reasoning_passback: re-inject reasoning_content into
outgoing DeepSeek assistant messages for thinking-mode multi-turn /
tool_use scenarios
@@ -70,6 +72,65 @@ def _patch_anthropic_proxy_compat() -> None:
_patch_anthropic_proxy_compat()
# ---------------------------------------------------------------------------
# Patch (module-level): some OpenAI-compatible endpoints emit SSE keepalive
# frames with an empty ``data`` field. OpenAI SDK 2.x unconditionally passes
# every event to ``json.loads``, so an otherwise harmless keepalive becomes a
# JSONDecodeError and aborts the stream. Filter only blank data frames before
# the SDK's parser; JSON events, error events, and [DONE] are unchanged.
# ---------------------------------------------------------------------------
_openai_empty_sse_keepalive_patched = False
def _is_blank_sse_keepalive(event: Any) -> bool:
"""Return whether an SSE event has no JSON payload to parse."""
data = getattr(event, "data", None)
return data is None or (isinstance(data, str) and not data.strip())
def _patch_openai_empty_sse_keepalive() -> None:
global _openai_empty_sse_keepalive_patched
if _openai_empty_sse_keepalive_patched:
return
try:
import functools
from openai._streaming import AsyncStream as _AsyncStream
from openai._streaming import Stream as _Stream
original_async = _AsyncStream._iter_events
if not getattr(original_async, "_evoscientist_skips_blank_sse", False):
@functools.wraps(original_async)
async def _filtered_async_events(self: Any) -> Any:
async for event in original_async(self):
if not _is_blank_sse_keepalive(event):
yield event
_filtered_async_events._evoscientist_skips_blank_sse = True # type: ignore[attr-defined]
_AsyncStream._iter_events = _filtered_async_events
original_sync = _Stream._iter_events
if not getattr(original_sync, "_evoscientist_skips_blank_sse", False):
@functools.wraps(original_sync)
def _filtered_sync_events(self: Any) -> Any:
for event in original_sync(self):
if not _is_blank_sse_keepalive(event):
yield event
_filtered_sync_events._evoscientist_skips_blank_sse = True # type: ignore[attr-defined]
_Stream._iter_events = _filtered_sync_events
_openai_empty_sse_keepalive_patched = True
except Exception:
# The patch is only needed when the optional OpenAI SDK is available.
pass
_patch_openai_empty_sse_keepalive()
# ---------------------------------------------------------------------------
# Patch: ccproxy-api 0.2.7 Codex compatibility.
#
@@ -207,6 +268,9 @@ def _is_ccproxy_codex(
# preserved, not flattened away.
# ---------------------------------------------------------------------------
_SKIP_CONTENT_TYPES = frozenset({"thinking", "reasoning", "reasoning_content"})
_NONPORTABLE_REASONING_METADATA = frozenset(
{"reasoning_content", "reasoning_details"}
)
# Media block types preserved when flattening (positive allowlist;
# thinking/reasoning still dropped). Images + files (PDF/documents): both
@@ -407,6 +471,8 @@ def _copy_ai_message_with_tool_pairs(
# LangChain content blocks use id; the Responses converter later
# maps it to call_id.
block["id"] = call_id
if "call_id" in block:
block["call_id"] = call_id
block["name"] = call_name
if isinstance(block.get("function"), dict):
block["function"] = {**block["function"], "name": call_name}
@@ -540,7 +606,7 @@ def _validate_openai_tool_history(messages: list[Any]) -> None:
}:
continue
block_id = str(
block.get("id") or block.get("call_id") or ""
block.get("call_id") or block.get("id") or ""
).strip()
block_name = block.get("name") or block.get("tool_name")
function = block.get("function")
@@ -566,7 +632,12 @@ def _validate_openai_tool_history(messages: list[Any]) -> None:
raise ValueError("assistant tool call is missing its tool result")
def _sanitize_messages(messages: list[Any], hoist_tool_media: bool = True) -> list[Any]:
def _sanitize_messages(
messages: list[Any],
hoist_tool_media: bool = True,
*,
drop_reasoning_metadata: bool = False,
) -> list[Any]:
"""Flatten list content for OpenAI-compatible APIs, preserving media.
Text/reasoning content is flattened to a string; image blocks are
@@ -593,6 +664,15 @@ def _sanitize_messages(messages: list[Any], hoist_tool_media: bool = True) -> li
pending_media.clear()
for msg in messages:
if drop_reasoning_metadata:
additional_kwargs = getattr(msg, "additional_kwargs", None) or {}
if set(additional_kwargs) & _NONPORTABLE_REASONING_METADATA:
msg = copy.copy(msg)
msg.additional_kwargs = {
key: value
for key, value in additional_kwargs.items()
if key not in _NONPORTABLE_REASONING_METADATA
}
is_tool = getattr(msg, "type", None) == "tool"
if not is_tool:
_flush() # emit hoisted media before any non-tool message
@@ -714,7 +794,12 @@ def _strip_media_types(messages: list[Any], types: set[str]) -> list[Any]:
return out
def _patch_openai_compat_content(model: Any, hoist_tool_media: bool = True) -> None:
def _patch_openai_compat_content(
model: Any,
hoist_tool_media: bool = True,
*,
drop_reasoning_metadata: bool = False,
) -> None:
"""Flatten list content to strings before OpenAI-compatible API calls.
Wraps ``_generate`` / ``_agenerate`` / ``_stream`` / ``_astream`` to prevent
@@ -749,11 +834,17 @@ def _patch_openai_compat_content(model: Any, hoist_tool_media: bool = True) -> N
def _prepare(messages: list[BaseMessage]) -> list[BaseMessage]:
msgs = _strip_media_types(messages, blocked) if blocked else messages
return _sanitize_messages(msgs, hoist_tool_media)
return _sanitize_messages(
msgs,
hoist_tool_media,
drop_reasoning_metadata=drop_reasoning_metadata,
)
def _stripped(messages: list[BaseMessage], suspects: set[str]) -> list[BaseMessage]:
return _sanitize_messages(
_strip_media_types(messages, blocked | suspects), hoist_tool_media
_strip_media_types(messages, blocked | suspects),
hoist_tool_media,
drop_reasoning_metadata=drop_reasoning_metadata,
)
orig_generate = getattr(model, "_generate", None)