Summary
AgentRouter-backed providers (AGR-OAI, AGR-CLA, or any provider whose baseUrl contains agentrouter) reject non-English first user messages with HTTP 400 content-blocked. This is a regional/language filter applied by the AgentRouter gateway to the first user message content.
Affected users: anyone routing to AgentRouter with Portuguese, Spanish, or other non-English prompts — the 400 surfaces mid-session as a hard failure, and it correlates with the non-English first prompt (see #1804, which closed #1689 as research).
The fix is to prepend an explicit English instruction frame to the first user message when the provider is AgentRouter, so the boundary filter passes while preserving the original request and the model's output language.
Reproduction
# 1. Configure an AgentRouter provider:
# providers: { "AGR-CLA": { adapter: "anthropic", baseUrl: "https://agentrouter.org", ... } }
# 2. Send a Portuguese first message:
curl -X POST http://127.0.0.1:10100/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $OPENCODEX_API_AUTH_TOKEN" \
-d '{"model":"claude-opus-4-8","messages":[{"role":"user","content":"responda apenas: OK"}]}'
# → 400 content-blocked (only when the FIRST user message is non-English)
# 3. Same request with an English first message:
# → 200
Root cause
The Anthropic adapter builds messages with messagesToAnthropicFormat(parsed, toolNames) and sends them verbatim. AgentRouter runs a language/regional filter on the first user message content; a non-English first prompt trips it.
Proposed diff
src/adapters/anthropic.ts — add a small idempotent helper and call it when the provider is AgentRouter:
const AGR_PREAMBLE_MARKER =
"[Instruction: Process the user request below and respond in the appropriate language.]";
export function applyAgrLanguagePreamble(messages: unknown[]): void {
const firstUser = messages.find(
m => typeof m === "object" && m !== null && (m as { role?: string }).role === "user",
) as { content?: unknown } | undefined;
if (!firstUser) return;
if (typeof firstUser.content === "string") {
if (!firstUser.content.includes(AGR_PREAMBLE_MARKER)) {
firstUser.content = `${AGR_PREAMBLE_MARKER}\n\n${firstUser.content}`;
}
} else if (Array.isArray(firstUser.content)) {
const textPart = firstUser.content.find(
p => typeof p === "object" && p !== null && (p as { type?: string }).type === "text",
) as { text?: string } | undefined;
if (textPart && typeof textPart.text === "string") {
if (!textPart.text.includes(AGR_PREAMBLE_MARKER)) {
textPart.text = `${AGR_PREAMBLE_MARKER}\n\n${textPart.text}`;
}
} else {
firstUser.content.unshift({ type: "text", text: `${AGR_PREAMBLE_MARKER}\n\n` });
}
}
}
And at the message-build site:
const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
const isAgr =
(typeof provider.name === "string" && provider.name.startsWith("AGR")) ||
(typeof provider.baseUrl === "string" && provider.baseUrl.includes("agentrouter"));
if (isAgr) {
applyAgrLanguagePreamble(messages);
}
Note: provider.name can be undefined at runtime for custom providers, so the reliable signal is the baseUrl containing agentrouter (or the configured name starting with AGR).
Alternatives considered
- Do nothing — non-English AgentRouter users keep hitting 400; workaround is forcing English first prompts, which degrades UX for PT/ES.
- Client-side only — users prepend the frame manually in their prompts; fragile, not enforceable.
- Provider-level config flag (e.g.
agrLanguagePreamble: true) — more flexible but adds config surface for what is a single, safe, idempotent transform scoped to AgentRouter destinations.
Test matrix
| Case |
Expected |
| First user message in PT via AGR-CLA |
200, response in PT |
| First user message in ES via AGR-CLA |
200, response in ES |
| First user message in EN via AGR-CLA |
200, no double preamble (idempotent) |
| Non-AGR provider (anthropic direct) |
Unchanged — no preamble |
| Multi-turn session (subsequent messages non-English) |
Unchanged — only first message gets the frame |
| Content as text-part array (Claude Code format) |
Frame prepended to the text part |
| Re-injection after upstream retry |
No duplicate marker (idempotent includes check) |
Summary
AgentRouter-backed providers (
AGR-OAI,AGR-CLA, or any provider whosebaseUrlcontainsagentrouter) reject non-English first user messages with HTTP 400content-blocked. This is a regional/language filter applied by the AgentRouter gateway to the first user message content.Affected users: anyone routing to AgentRouter with Portuguese, Spanish, or other non-English prompts — the 400 surfaces mid-session as a hard failure, and it correlates with the non-English first prompt (see #1804, which closed #1689 as research).
The fix is to prepend an explicit English instruction frame to the first user message when the provider is AgentRouter, so the boundary filter passes while preserving the original request and the model's output language.
Reproduction
Root cause
The Anthropic adapter builds messages with
messagesToAnthropicFormat(parsed, toolNames)and sends them verbatim. AgentRouter runs a language/regional filter on the first user message content; a non-English first prompt trips it.Proposed diff
src/adapters/anthropic.ts— add a small idempotent helper and call it when the provider is AgentRouter:And at the message-build site:
Note:
provider.namecan beundefinedat runtime for custom providers, so the reliable signal is thebaseUrlcontainingagentrouter(or the configured name starting withAGR).Alternatives considered
agrLanguagePreamble: true) — more flexible but adds config surface for what is a single, safe, idempotent transform scoped to AgentRouter destinations.Test matrix
includescheck)