Repository navigation
[Bug]: AgentRouter 400 content-blocked on non-English first messages; add language preamble for AGR providers #2074
Description
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:
# → 200Root 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) |
Activity
coderabbitai commentedon Aug 18, 2026
🔗 Related PRs
#626 - [WRONG BRANCH] fix: stabilize cache inputs [open]
#757 - feat: route GPT-5.6 Pro through standard ChatGPT [open]
#889 - fix(providers): Copilot mixed-wire routing, DeepSeek service_tier + reasoning replay, Claude 4.6/4.7 1M windows [open]
#896 - fix(anthropic): complete AgentRouter streams that end before terminal frames [open]
#1859 - fix(chat): preserve OpenRouter routing in native passthrough [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
yzxcj797 commentedon Aug 19, 2026
Implementing the proposed diff with three small hardenings:
isAgentRouterBaseUrlon the provider's hostname, so any configured AgentRouter-backed provider (AGR-OAI,AGR-CLA, or a custombaseUrlcontainingagentrouter) gets the frame — not just the registry preset;messagesToAnthropicFormatoutput), so the raw request and the tool-call id allocation are untouched.Tests cover hostname detection, all three content shapes, idempotence, and the no-user-message no-op. Note the OpenAI adapter path doesn't need this — per the issue's reproduction the filter is on the Anthropic-format gateway; if OAI-format AgentRouter routing shows the same 400, the same helper can be wired there in a follow-up.
(Couldn't run the bun suite locally on this Windows checkout; relying on CI.)
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 50 / 80
AgentRouter(
baseUrl에agentrouter, 또는AGR-*이름)로 나가는 첫 사용자 메시지가 영어가 아니면 게이트웨이가 HTTP 400content-blocked를 준다. 포르투갈어/스페인어 첫 프롬프트에서 세션이 끊긴다. 영어 첫 문장이면 200이다. 기본 native Codex 경로가 아니라 옵트인 프로바이더 호환이라 50이다. 매일 쓰는 AGR 사용자에게는 재현되고, 영어 한 줄을 앞에 다는 우회가 있다.src/adapters/anthropic.ts의buildRequest()는messagesToAnthropicFormat(parsed, toolNames)결과를 그대로 보낸다. 그 직후는 이미지 정규화/enforceAnthropicImageLimits/toolsToAnthropicFormat뿐이다. 언어 프리앰블은 없다.src/providers/free-directory.ts의agentrouter항목은adapter: "anthropic",baseUrl: "https://agentrouter.org"다. AGR-CLA 재현과 맞다.제안된
applyAgrLanguagePreamble()는 이 파일의 메시지 빌드 지점에 붙는 형태다.provider.name은 커스텀 프로바이더에서 undefined일 수 있으니, 리포터 말대로baseUrl에agentrouter가 있는지가 믿을 신호다. 마커를includes로 보면 재시도와 영어 첫 메시지에서 이중 삽입을 피한다. 첫 user 메시지에만 넣는 조건도 재현(첫 메시지만 필터)과 같다.AGR-OAI처럼 OpenAI 호환 어댑터를 쓰는 이름은 anthropic 헬퍼만으로 안 닫힌다. openai-chat 경로에 같은 함수를 있다고 지어 넣지는 않는다. AgentRouter로 가는 어댑터마다 첫 user 텍스트에 같은 마커를 달지 확인해야 한다. 직접 Anthropic(
api.anthropic.com)에는 넣으면 안 된다. 테스트 표의 Non-AGR 행이 그 가드다.프리앰블은 필터 우회이지 모델 출력 언어를 영어로 고정하면 안 된다. 마커 문구가 “respond in the appropriate language”인 이유가 그것이다. 프롬프트/토큰을 로그에 남기지 말고, 400
content-blocked만 진단에 남긴다.해결방안
src/adapters/anthropic.ts의messagesToAnthropicFormat()직후, AgentRouter일 때만 첫 user 메시지에 영어 instruction frame을 붙인다. 판별은provider.baseUrl에agentrouter포함(필요하면 이름AGR접두). 문자열 content와 text-part 배열 둘 다 다루고, 마커가 있으면 no-op. 직접 Anthropic OAuth/키 경로와 멀티턴 후속 메시지는 그대로 둔다. AGR-OAI가 다른 어댑터면 그buildRequest에도 같은 마커를 재사용하되, 없는 함수를 새로 지어 호출하지는 않는다. 회귀는 PT/ES 첫 메시지 200, EN 첫 메시지 이중 프리앰블 없음, non-AGR 불변이다.이 댓글은 grok-bot이 작성했습니다
fix(anthropic): frame the opening turn so AgentRouter stops blocking …
fix(anthropic): frame the opening turn so AgentRouter stops blocking …
lidge-jun commentedon Aug 20, 2026
Fixed on
devby #2162, merged as 087c3c3.Verified present on the branch rather than assumed:
git log origin/devshows the merge commit above. These PRs targetdevrather thanmain, so GitHub does not auto-close the linked issue — this closure is manual.It ships in the next release cut from
dev. Reopen if you still see it after updating.fix(anthropic): frame the opening turn so AgentRouter stops blocking …
fix(anthropic): frame the opening turn so AgentRouter stops blocking …