Is this a request payload issue?
Note on #5837: I opened that PR first, not knowing about translator-path-guard. It is kept open as a reviewed reference implementation (5 commits hardening the same function against 5 adversarial cases); this issue is the request for your team to land the change.
Describe the bug
When a Codex client sends namespace-grouped tools, the OpenaiResponse → OpenAI (Responses → Chat Completions) translation flattens each {"type":"namespace","name":…,"tools":[…]} child into a tools[].function.name by prefixing the namespace, without any length cap. Chat Completions upstreams that enforce the documented name limit reject the whole request:
Invalid 'tools[27].function.name': string too long; expected 1-64 ASCII letters,
digits, underscores, or hyphens
One rejected name fails the entire request — not just that tool — so Codex Desktop becomes unusable against such upstreams.
CLI Type: codex (wire_api = "responses") → openai-compatibility provider
Model Name: z-ai/glm-5.3-free (any strict-name upstream; the limit is spec'd, several vendors enforce it)
LLM Client: Codex Desktop. The long names come from bundled plugins, e.g. codex-app-tools@openai-bundled and documents/pdf/spreadsheets/presentations@openai-primary-runtime, whose MCP namespaces flatten to names longer than the limit. In the captured error-v1-responses log, 3 of the 42 flattened names were over the limit (66, 67 and 66 bytes), which is enough to fail the entire call. (That log has since been rotated out, so the exact names are not reproduced here.)
Reproduce
A synthetic minimal payload in the shape Codex Desktop sends (namespace + child, 69 bytes once flattened):
Translate with ConvertOpenAIResponsesRequestToOpenAIChatCompletions and inspect tools[0].function.name — it is the uncapped 69-byte mcp__codex_apps__spreadsheets__get_cell_values_from_spreadsheet_range. The same children also arrive through Codex Desktop's input[].additional_tools items, so both sources need to be handled (they already share one walk).
This is not a missing concept in the codebase: the X → Codex translators already enforce the same limit when they generate Codex-side names — sanitizeToolName + shortenNameIfNeeded (keep the mcp__ prefix and the last segment, otherwise truncate to 64) + buildShortNameMap (numeric dedup suffixes), driven by collectRequestToolNames, which deliberately walks tool declarations, tool_choice and historical assistant tool_calls together so all of them agree on one short name. The gap is that none of that is applied on the OpenaiResponse → OpenAI side, which is the path a Codex Desktop client lands on whenever the upstream is an openai-compatibility provider.
Why a one-line truncate(name, 64) is not enough
The name is used in four places that must agree, or a session desyncs mid-conversation: the outgoing tools array, replayed function_call / custom_tool_call history items, tool_choice, and reverse resolution of the upstream's tool call back to a Responses (namespace, name) identity. Naive capping breaks each of them in a way that silently invokes the wrong tool rather than erroring. The six requirements below are what #5837 hardened, in order:
- Determinism. Cap in one shared helper used by all four paths; a per-call-site truncation drifts as soon as history was written by an older build.
- Which end to keep, and the resulting collisions. Keep the tail (the local name is the identifying part; namespace-qualified siblings share a long prefix) and strip leading
_/-, since some upstreams also reject names not starting with an alphanumeric. Tail-keeping makes distinct declarations collide (two siblings differing only before byte −64), so collisions need suffixes — while declarations that qualified to the same name before the cap must keep merging first-wins, or legitimate same-tool duplicates (tools + additional_tools, flat tool shadowing a namespace child) get spuriously renamed.
- A capped alias must not displace a short original. Given flat
pdf_fetch_summary (17) declared before a long tool whose tail truncates to that exact string, awarding the exact name to the long tool and renaming the flat one leaves the flat tool's replayed calls and tool_choice matching the long tool's alias as an "exact emitted name" → the wrong tool runs. Short originals must claim first, so the collision is visible to capping.
- An unresolved name must not be blind-capped onto a declared alias. A name no declaration backs (unknown namespace pair, history from an older build, foreign client) previously truncated onto a real declaration's name and thus dispatched to it; it now stays unresolved. Relatedly, a unique local-name match is checked against emitted names, so a local name longer than the cap that collides with a different declaration must not be renamed onto that declaration either.
- A capped alias must not occupy another declaration's local name. A long declaration truncating onto the ≤64-byte local name of a namespaced declaration hijacks namespace-less replays, which carry the bare local name. Shared local names must additionally be burned rather than awarded first-wins: two namespaces declaring the same exactly-64-byte child both cap onto that bare name, and if the first keeps it, the exact-emitted-alias match outranks the ambiguity check and every namespace-less call silently goes to the first namespace.
- Qualified-name provenance outranks the local-name guess. A name can be simultaneously declaration A's uncapped qualified identity and declaration B's own child name (B literally named
alpha_ns__read_…). Local-name recovery running first dispatches A's history to B.
Expected behavior
Namespace-flattened names are capped to the Chat Completions limit deterministically, collision-free, and reversibly — replayed calls, tool_choice and reverse resolution keep addressing the same declaration, and genuinely ambiguous identities stay unresolved instead of silently invoking another tool.
Environment
- Version: v7.3.2, re-checked against current
main (8335eac7, v7.3.4) — qualifyResponsesNamespaceToolName still returns an uncapped name.
- OS: macOS (arm64)
Additional context
#5837 implements the above in internal/translator/openai/openai/responses/openai_openai-responses_tools.go by routing every declaration through one shared walk that caps and then disambiguates the flattened names; all four derivation paths ask that walk instead of re-deriving. It adds 7 regression tests (+781/−20 across 3 files); each of the hardened cases fails on the pre-fix code with a mis-attributed tool call, and the whole ./internal/translator/... suite passes.
If you would rather converge on the existing house style than take that patch, the useful port is collectRequestToolNames' single-pass-over-all-name-sites idea; the shortening rule itself still needs the reservation rules in items 3–6, because buildShortNameMap dedups among the names it generates but does not stop a generated name from landing on a different tool's original name, which is what mis-attributes replayed calls and tool_choice. Happy to rework it to whichever shape you prefer.
Is this a request payload issue?
Note on #5837: I opened that PR first, not knowing about
translator-path-guard. It is kept open as a reviewed reference implementation (5 commits hardening the same function against 5 adversarial cases); this issue is the request for your team to land the change.Describe the bug
When a Codex client sends namespace-grouped tools, the
OpenaiResponse → OpenAI(Responses → Chat Completions) translation flattens each{"type":"namespace","name":…,"tools":[…]}child into atools[].function.nameby prefixing the namespace, without any length cap. Chat Completions upstreams that enforce the documented name limit reject the whole request:One rejected name fails the entire request — not just that tool — so Codex Desktop becomes unusable against such upstreams.
CLI Type: codex (
wire_api = "responses") →openai-compatibilityproviderModel Name:
z-ai/glm-5.3-free(any strict-name upstream; the limit is spec'd, several vendors enforce it)LLM Client: Codex Desktop. The long names come from bundled plugins, e.g.
codex-app-tools@openai-bundledanddocuments/pdf/spreadsheets/presentations@openai-primary-runtime, whose MCP namespaces flatten to names longer than the limit. In the capturederror-v1-responseslog, 3 of the 42 flattened names were over the limit (66, 67 and 66 bytes), which is enough to fail the entire call. (That log has since been rotated out, so the exact names are not reproduced here.)Reproduce
A synthetic minimal payload in the shape Codex Desktop sends (namespace + child, 69 bytes once flattened):
{ "model": "any-strict-upstream", "tools": [ {"type": "namespace", "name": "mcp__codex_apps__spreadsheets", "tools": [ {"type": "function", "name": "get_cell_values_from_spreadsheet_range", "parameters": {"type": "object"}} ]} ], "input": [{"role": "user", "content": "hi"}] }Translate with
ConvertOpenAIResponsesRequestToOpenAIChatCompletionsand inspecttools[0].function.name— it is the uncapped 69-bytemcp__codex_apps__spreadsheets__get_cell_values_from_spreadsheet_range. The same children also arrive through Codex Desktop'sinput[].additional_toolsitems, so both sources need to be handled (they already share one walk).This is not a missing concept in the codebase: the
X → Codextranslators already enforce the same limit when they generate Codex-side names —sanitizeToolName+shortenNameIfNeeded(keep themcp__prefix and the last segment, otherwise truncate to 64) +buildShortNameMap(numeric dedup suffixes), driven bycollectRequestToolNames, which deliberately walks tool declarations,tool_choiceand historical assistanttool_callstogether so all of them agree on one short name. The gap is that none of that is applied on theOpenaiResponse → OpenAIside, which is the path a Codex Desktop client lands on whenever the upstream is anopenai-compatibilityprovider.Why a one-line
truncate(name, 64)is not enoughThe name is used in four places that must agree, or a session desyncs mid-conversation: the outgoing
toolsarray, replayedfunction_call/custom_tool_callhistory items,tool_choice, and reverse resolution of the upstream's tool call back to a Responses(namespace, name)identity. Naive capping breaks each of them in a way that silently invokes the wrong tool rather than erroring. The six requirements below are what #5837 hardened, in order:_/-, since some upstreams also reject names not starting with an alphanumeric. Tail-keeping makes distinct declarations collide (two siblings differing only before byte −64), so collisions need suffixes — while declarations that qualified to the same name before the cap must keep merging first-wins, or legitimate same-tool duplicates (tools+additional_tools, flat tool shadowing a namespace child) get spuriously renamed.pdf_fetch_summary(17) declared before a long tool whose tail truncates to that exact string, awarding the exact name to the long tool and renaming the flat one leaves the flat tool's replayed calls andtool_choicematching the long tool's alias as an "exact emitted name" → the wrong tool runs. Short originals must claim first, so the collision is visible to capping.alpha_ns__read_…). Local-name recovery running first dispatches A's history to B.Expected behavior
Namespace-flattened names are capped to the Chat Completions limit deterministically, collision-free, and reversibly — replayed calls,
tool_choiceand reverse resolution keep addressing the same declaration, and genuinely ambiguous identities stay unresolved instead of silently invoking another tool.Environment
main(8335eac7, v7.3.4) —qualifyResponsesNamespaceToolNamestill returns an uncapped name.Additional context
#5837 implements the above in
internal/translator/openai/openai/responses/openai_openai-responses_tools.goby routing every declaration through one shared walk that caps and then disambiguates the flattened names; all four derivation paths ask that walk instead of re-deriving. It adds 7 regression tests (+781/−20 across 3 files); each of the hardened cases fails on the pre-fix code with a mis-attributed tool call, and the whole./internal/translator/...suite passes.If you would rather converge on the existing house style than take that patch, the useful port is
collectRequestToolNames' single-pass-over-all-name-sites idea; the shortening rule itself still needs the reservation rules in items 3–6, becausebuildShortNameMapdedups among the names it generates but does not stop a generated name from landing on a different tool's original name, which is what mis-attributes replayed calls andtool_choice. Happy to rework it to whichever shape you prefer.