Skip to content

Commit d5da788

Browse files
Sanitize message name at OpenAI wire format (defense in depth)
Production still hits 400 BadRequest on messages[N].name even though _inner_get_response runs _sanitize_author_names on incoming Messages. The framework's _prepare_options/_prepare_messages_for_openai layer or agent-internal compaction can materialize messages with author_name set AFTER our early sanitization, leaving the dict 'name' field unsanitized on the wire. Override _prepare_messages_for_openai (the parent method that builds the final OpenAI dict payload) to sanitize each dict's 'name' field as a last-mile pass. This is the single chokepoint guaranteed to be on every Chat Completions request, regardless of upstream message-construction path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6d371fd commit d5da788

1 file changed

Lines changed: 47 additions & 0 deletions

File tree

src/processor/src/libs/agent_framework/azure_openai_response_retry.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,12 @@ def _inner_get_response(
801801
# OpenAI Chat Completions validates message `name` against ^[^\s<|\\/>]+$.
802802
# Sanitize before sending so agent display names like "Chief Architect"
803803
# don't trip a 400 BadRequest. Originals are shallow-copied, not mutated.
804+
# NOTE: this is a defense-in-depth pass on ``Message.author_name``.
805+
# The authoritative sanitization happens in ``_prepare_messages_for_openai``
806+
# below, which sanitizes the FINAL dict ``name`` field right before the
807+
# request is sent — catching any name that slips in via framework-internal
808+
# message construction (e.g. compaction, memory context providers,
809+
# orchestrator-injected messages) that bypasses this early pass.
804810
effective_messages = _sanitize_author_names(effective_messages)
805811

806812
if stream:
@@ -821,6 +827,47 @@ def _inner_get_response(
821827
**kwargs,
822828
)
823829

830+
def _prepare_messages_for_openai(self, chat_messages, *args: Any, **kwargs: Any): # type: ignore[override]
831+
"""Sanitize message ``name`` fields after framework conversion to wire format.
832+
833+
The parent ``_prepare_messages_for_openai`` walks ``Message`` objects and
834+
builds the OpenAI dict payload (``{"role": ..., "name": ..., "content": ...}``).
835+
The ``name`` field is copied from ``Message.author_name`` and is validated
836+
by the OpenAI Chat Completions API against ``^[^\\s<|\\\\/>]+$``.
837+
838+
We override here as a final, authoritative sanitization point. Even though
839+
``_inner_get_response`` already sanitizes ``Message.author_name``, names
840+
can still reach this layer unsanitized from:
841+
842+
* ``OpenAIChatCompletionClient._prepare_options`` calling
843+
``prepend_instructions_to_messages`` (which does not author_name, but
844+
downstream callers may add named messages).
845+
* ``ChatAgent`` / memory context providers materializing messages with
846+
``author_name`` set inside the agent run loop, after the client receives
847+
the original sequence.
848+
* Any framework-internal compaction or message-rewriting path that
849+
constructs new ``Message`` objects.
850+
851+
Sanitizing the dict output is the single chokepoint guaranteed to be
852+
on every Chat Completions request, regardless of how the messages were
853+
assembled upstream.
854+
"""
855+
result = super()._prepare_messages_for_openai(chat_messages, *args, **kwargs)
856+
for msg in result:
857+
if not isinstance(msg, dict):
858+
continue
859+
name = msg.get("name")
860+
if not isinstance(name, str):
861+
continue
862+
sanitized = _sanitize_author_name(name)
863+
if sanitized == name:
864+
continue
865+
if sanitized:
866+
msg["name"] = sanitized
867+
else:
868+
msg.pop("name", None)
869+
return result
870+
824871
def _maybe_trim_messages(
825872
self, messages: MutableSequence[Any]
826873
) -> MutableSequence[Any] | list[Any]:

0 commit comments

Comments
 (0)