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