Files
hermes-agent/agent/sdk_transform_bypass.py
kshitijk4poor af7b60e8ec fix(agent): keep moved chat fields as slot placeholders so the wire body is byte-identical; one shared escape hatch
The cherry-picked helper deleted 'tools' from the typed kwargs, so the SDK's
post-transform extra_body merge appended it after the caller's extra_body keys —
equal dict, different bytes (byte-keyed prompt caches would miss). keep_slots=True
leaves [] placeholders that the merge overwrites in place. Drop the invented
HERMES_CHAT_SDK_TRANSFORM env var; the pre-existing HERMES_CODEX_SDK_TRANSFORM
hatch from #93650 now disables both API families. Tests trimmed to the two
invariants (byte-identity incl. caller extra_body precedence; escape hatch).
2026-09-15 19:23:53 -07:00

81 lines
3.9 KiB
Python

"""Route bulk request payloads around the OpenAI SDK's request transform (#93650).
``responses.create`` and ``chat.completions.create`` both re-walk the whole request
body against their TypedDict/union param graph client-side, with the GIL held,
before any byte leaves the process. #93650 documents that walk wedging for 12+
hours on a ~1.4 MB conversation — starving the TTFB/stale watchdogs whose job is
to rescue this exact call; the hang is pre-network, so no socket kill helps.
Hermes assembles these payloads from JSON round-trips, so they are already wire
format and the walk has nothing to convert. The SDK merges ``extra_body`` into
the JSON body *after* the transform (``_base_client._build_request``), so moving
the bulk fields there skips the walk and yields the same request bytes.
"""
from __future__ import annotations
import os
from typing import Any
# Bulk request fields carrying the conversation payload, per API family. Everything
# else is scalar configuration the SDK transform handles in microseconds.
RESPONSES_BYPASS_FIELDS = ("input", "tools")
CHAT_COMPLETIONS_BYPASS_FIELDS = ("messages", "tools")
# One hatch for both API families (established by #93650); restores the typed SDK path.
ESCAPE_HATCH_ENV = "HERMES_CODEX_SDK_TRANSFORM"
def _is_plain_json_data(value: Any) -> bool:
"""True when ``value`` is purely JSON wire types; pydantic models / generators must keep the typed SDK path."""
if value is None or isinstance(value, (str, int, float, bool)):
return True
if isinstance(value, dict):
return all(isinstance(key, str) and _is_plain_json_data(item) for key, item in value.items())
if isinstance(value, list):
return all(_is_plain_json_data(item) for item in value)
return False
def bypass_sdk_request_transform(
request_kwargs: dict,
fields: tuple[str, ...] = RESPONSES_BYPASS_FIELDS,
*,
keep_slots: bool = False,
) -> dict:
"""Move wire-format bulk ``fields`` into ``extra_body``.
Returns ``request_kwargs`` itself when nothing is safe to move, so callers use
the result unconditionally. ``keep_slots`` leaves an empty-list placeholder in
the typed kwargs for each moved field: it satisfies ``@required_args``
(``messages`` on chat.completions) and keeps the field's position in the JSON
body, so the bytes — and therefore any byte-keyed prompt cache — are unchanged.
"""
if os.environ.get(ESCAPE_HATCH_ENV, "").strip().lower() in {"1", "true", "yes", "on"}:
return request_kwargs
moved = {f: request_kwargs[f] for f in fields
if isinstance(request_kwargs.get(f), (dict, list)) and _is_plain_json_data(request_kwargs[f])}
if not moved:
return request_kwargs
bypassed = {key: ([] if keep_slots else value) if key in moved else value
for key, value in request_kwargs.items() if keep_slots or key not in moved}
extra_body = bypassed.get("extra_body")
merged = dict(extra_body) if isinstance(extra_body, dict) else {}
# An explicit caller-provided extra_body entry keeps precedence (SDK post-transform merge).
bypassed["extra_body"] = {**merged, **{f: v for f, v in moved.items() if f not in merged}}
return bypassed
def bypass_chat_sdk_request_transform(request_kwargs: dict, client: Any) -> dict:
"""Chat-completions bypass, gated on the real OpenAI SDK.
Only the SDK performs the transform and only the SDK merges ``extra_body``
afterwards. Hermes also drives chat-shaped facades that are NOT the SDK (the
in-process MoA aggregator, test stand-ins); handing those an ``extra_body`` they
never merge would silently send an empty conversation.
"""
completions = getattr(getattr(client, "chat", None), "completions", None)
if completions is None or not type(completions).__module__.startswith("openai."):
return request_kwargs
return bypass_sdk_request_transform(request_kwargs, CHAT_COMPLETIONS_BYPASS_FIELDS, keep_slots=True)