Skip to content

[Bug]: AgentRouter 400 content-blocked still affects openai-chat adapter (AGR-OAI) — apply language framing on Chat/Responses #2415

Description

@rrmlima

Summary

#2074 fixed the non-English content-blocked 400 for the anthropic adapter only. The openai-chat adapter (AGR-OAI provider with adapter: "openai-chat") still 400s on non-English first user messages on every wire/v1/responses, /v1/chat/completions, and even a raw Anthropic-format call to the gateway.

Affected: gpt-5.6-sol (and any OpenAI-format model routed through AgentRouter) used by Codex CLI (wire_api = "responses") and chat-style clients. The model is listed in subagentModels, so subagent turns in PT fail hard.

Reproduction (verified against agentrouter.org with a real key)

All requests use model: "gpt-5.6-sol" (endpoint support: ["openai"] only per /v1/models), base https://agentrouter.org:

# Wire First message Result
1 /v1/chat/completions (via proxy) "What is the capital of Brazil?" (EN) 200
2 /v1/chat/completions (via proxy) "Qual a capital do Brasil?" (PT) 400 content-blocked
3 /v1/responses (via proxy) "Qual a capital do Brasil?" (PT) 400 content-blocked
4 Anthropic /v1/messages direct with claude-cli headers PT, no framing 400 content-blocked
5 Anthropic /v1/messages direct with claude-cli headers PT + [Instruction: ...] framing 200
6 /v1/chat/completions direct with all claude-cli/X-Stainless headers PT, no framing 400 content-blocked (headers do NOT bypass the filter)
7 /v1/responses via proxy + local framing (patch) PT 200

So: the gateway validates the opening user turn language on all wires, regardless of adapter, endpoint, or client headers. See #1804 for the original correlation research.

Note on the "model is openai-only" fact

GET /v1/models (with recognized User-Agent) reports:

{"id":"gpt-5.6-sol","model":"gpt-5.6-sol","supported_endpoint_types":["openai"]}

A raw Anthropic-format call does work for it today (test #4/#5), but that is undocumented/unofficial behavior — the supported surface is OpenAI-compatible. So the fix must live on the openai-chat path, not by re-routing users to the anthropic adapter.

Proposed fix

Extend the framing from #2074 to the openai-chat adapter. Mirror the existing applyAgentRouterLanguageFraming (src/adapters/anthropic.ts) with:

  • Host exact match on agentrouter.org / *.agentrouter.org (never substring — same rationale as fix(anthropic): frame the opening turn so AgentRouter stops blocking non-English #2162's comment about notagentrouter.example).
  • Idempotent: keyed on the leading block/content being exactly the marker, not includes() (a user quoting the marker later in their prompt must not suppress the frame, and retries/replays must not stack frames).
  • Frame as its own text block (or prefix for string content) — the user's original text survives byte-for-byte.
  • Hook points: messagesToChatFormat() result in buildRequest() for the translated Chat path, and buildOpenAIChatPassthroughRequest() for /v1/chat/completions passthrough — both are needed; patching only one leaves the other wire 400ing.
  • Only the first user role message is framed (the gateway inspects the opening turn; system/developer prefixes do not help — verified: a system EN + user PT request still 400s).

Test matrix

Case Expected
AGR-OAI PT via /v1/responses and /v1/chat/completions 200, answer in PT
AGR-OAI EN (no frame needed) 200, no double frame
AGR-CLA (anthropic) unchanged (#2074 already handles it)
Non-AGR openai-chat providers (e.g. OpenAI, command-code, Z.AI) byte-for-byte unchanged — no framing
Retry/replay of a framed turn single marker, no stacking
Subsequent non-English messages after the first unchanged (only first user turn framed)
String content AND content-part arrays (Claude Code style) covered

Alternative considered

Shipping a provider-level flag (e.g. languagePreamble: true) instead of host-derived detection. Rejected for scope: the transform is safe, idempotent, and host-scoped, matching the #2074 decision; a flag adds config surface for no behavioral benefit.

References: #2074 (closed, anthropic-only), #2162 (merged PR), #1804 (correlation research), #1689 (closed by research).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions