From 110ef579e7adf7761abf17d37fbb343c450687e2 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 15:42:04 +0900 Subject: [PATCH 1/2] release: v2.25.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4e8bbd5c9..178d6a9c7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.24.2", + "version": "2.25.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 8c1abc46a8a1d6afb54eb94799e38e908c35fb76 Mon Sep 17 00:00:00 2001 From: yzxcj797 Date: Wed, 19 Aug 2026 09:35:14 +0800 Subject: [PATCH 2/2] fix(adapters): frame AgentRouter first messages to pass the language filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentRouter's gateway applies a language filter to the first user message and hard-fails non-English prompts with 400 content-blocked (#2074) — the 400 surfaced mid-session as a hard failure for any Portuguese/Spanish/etc. first prompt routed to an AgentRouter-backed provider. Prepend an explicit English instruction frame to the first user message when the provider's baseUrl resolves to AgentRouter. The frame tells the model to respond in the appropriate language, so the original request and the output language are preserved while the boundary filter passes. The helper is idempotent (marker check) and handles string content, structured content, and content with no text part; applied at the single request-build site so raw requests and tool-call ids are untouched. --- src/adapters/anthropic.ts | 50 +++++++++++++++++++++++ tests/anthropic-agr-preamble.test.ts | 59 ++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tests/anthropic-agr-preamble.test.ts diff --git a/src/adapters/anthropic.ts b/src/adapters/anthropic.ts index fd141ccfb8..3075d72287 100644 --- a/src/adapters/anthropic.ts +++ b/src/adapters/anthropic.ts @@ -816,6 +816,53 @@ function normalizeAnthropicInputSchema(schema: unknown): Record return normalized; } +/** + * AgentRouter's gateway applies a language filter to the FIRST user message + * content and hard-fails non-English prompts with 400 content-blocked + * (#2074). Prepending an explicit English instruction frame lets the filter + * pass while the model still answers in the user's language — the frame says + * "respond in the appropriate language", it does not force English output. + */ +export const AGR_PREAMBLE_MARKER = + "[Instruction: Process the user request below and respond in the appropriate language.]"; + +export function isAgentRouterBaseUrl(baseUrl: string): boolean { + try { + return new URL(baseUrl).hostname.includes("agentrouter"); + } catch { + return false; + } +} + +/** + * Prepend the AgentRouter language frame to the first user message, once. + * Idempotent: a message already carrying the marker is left untouched (retries + * and replays must not stack frames). + */ +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 }); + } + } +} + export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long"): ProviderAdapter { const isOAuth = provider.authMode === "oauth"; const toolNames = buildToolNameTransforms(provider); @@ -833,6 +880,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti } const { system, messages } = messagesToAnthropicFormat(parsed, toolNames); + if (isAgentRouterBaseUrl(provider.baseUrl)) { + applyAgrLanguagePreamble(messages); + } // Primary image layer: resize/re-encode to fit Anthropic limits without dropping // (anthropic-image-normalize.ts); the guard below remains the deterministic backstop. // imageTierBias > 0 = upstream-413 tightened retry (030): start every image one tier lower. diff --git a/tests/anthropic-agr-preamble.test.ts b/tests/anthropic-agr-preamble.test.ts new file mode 100644 index 0000000000..f6d5341b61 --- /dev/null +++ b/tests/anthropic-agr-preamble.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import { + AGR_PREAMBLE_MARKER, + applyAgrLanguagePreamble, + isAgentRouterBaseUrl, +} from "../src/adapters/anthropic"; + +describe("AgentRouter language preamble (#2074)", () => { + test("detects AgentRouter base URLs by hostname", () => { + expect(isAgentRouterBaseUrl("https://agentrouter.org")).toBe(true); + expect(isAgentRouterBaseUrl("https://agentrouter.org/v1")).toBe(true); + expect(isAgentRouterBaseUrl("https://api.anthropic.com")).toBe(false); + expect(isAgentRouterBaseUrl("not a url")).toBe(false); + }); + + test("prepends the frame to a string first user message", () => { + const messages = [ + { role: "user", content: "responda apenas: OK" }, + { role: "assistant", content: "OK" }, + ]; + applyAgrLanguagePreamble(messages); + const first = messages[0] as { content: string }; + expect(first.content.startsWith(AGR_PREAMBLE_MARKER)).toBe(true); + expect(first.content).toContain("responda apenas: OK"); + }); + + test("prepends into the first text part of structured content", () => { + const messages = [ + { role: "user", content: [{ type: "image", source: {} }, { type: "text", text: "hola" }] }, + ]; + applyAgrLanguagePreamble(messages); + const parts = (messages[0] as { content: Array<{ type: string; text?: string }> }).content; + const text = parts.find(p => p.type === "text"); + expect(text?.text?.startsWith(AGR_PREAMBLE_MARKER)).toBe(true); + expect(text?.text).toContain("hola"); + }); + + test("inserts a text part when the first message has none", () => { + const messages = [{ role: "user", content: [{ type: "image", source: {} }] }]; + applyAgrLanguagePreamble(messages); + const parts = (messages[0] as { content: Array<{ type: string; text?: string }> }).content; + expect(parts[0]?.type).toBe("text"); + expect(parts[0]?.text).toContain(AGR_PREAMBLE_MARKER); + }); + + test("is idempotent — replays do not stack frames", () => { + const messages = [{ role: "user", content: "bonjour" }]; + applyAgrLanguagePreamble(messages); + applyAgrLanguagePreamble(messages); + const content = (messages[0] as { content: string }).content; + expect(content.split(AGR_PREAMBLE_MARKER).length - 1).toBe(1); + }); + + test("no user message is a no-op", () => { + const messages = [{ role: "assistant", content: "hi" }]; + applyAgrLanguagePreamble(messages); + expect(messages).toEqual([{ role: "assistant", content: "hi" }]); + }); +});