9Router fails to trigger model combo fallback when upstream providers return HTTP 200 with safety refusals (finish_reason: "content_filter" / stop_reason: "refusal"), causing automated workflows to abort fatally instead of failing over to subsequent combo targets.
To solve this, 9Router's combo execution loop needs to evaluate response completion metadata (finish_reason === 'content_filter' / provider refusal payloads) alongside HTTP status codes when determining candidate failure.
Related Issues
In decolua/9router
Upstream Ecosystem References
anthropics/claude-code#95670: [Bug] Safeguard blocks firing on all requests with identical prompt templates (reasoning_extraction / duplicate model outputs).
simonw/llm-anthropic#90: Handle Fable refusals: Claude refused this request (reasoning_extraction / Terms of Service).
Problem Description
When using a fallback combo (e.g., genius configured with [cc/claude-sonnet-5-5(high), cx/gpt-6-astra(high)]), 9Router currently checks whether upstream returns HTTP 4xx (e.g. 429) or 5xx. If so, it logs [COMBO] Trying model 2/N and fails over.
However, providers like Anthropic return heuristic safeguard blocks (such as multi-agent burst detection tripping reasoning_extraction) as an HTTP 200 OK payload containing:
{
"choices": [
{
"finish_reason": "content_filter",
"index": 0,
"message": {
"role": "assistant",
"content": "This request was blocked as it seems to violate Anthropic's Terms of Service restrictions on reverse engineering or duplicating model outputs. To learn more, visit https://www.anthropic.com/legal/commercial-terms."
}
}
]
}
Or native Anthropic Messages API:
{
"type": "message",
"stop_reason": "refusal",
"stop_details": { "type": "content_filter", "category": "reasoning_extraction" },
"content": [{ "type": "text", "text": "This request was blocked as it seems to violate Anthropic's Terms of Service..." }]
}
Because the HTTP status is 200, 9Router logs:
[COMBO] Model cc/claude-sonnet-5-5 succeeded
It immediately flushes this refusal downstream. Client agents (OpenCode, Claude Code, Cursor, Cline) treat the response as a terminal provider refusal and crash or stop the session, completely ignoring the second model in the combo (cx/gpt-6-astra).
Steps to Reproduce
- Define a 9Router fallback combo with two distinct providers:
{
"id": "test-fallback",
"name": "Test Fallback",
"strategy": "fallback",
"models": [
"cc/claude-sonnet-5-5",
"cx/gpt-6-astra"
]
}
- Send a request to
http://localhost:20128/v1/chat/completions pointing to model test-fallback.
- Simulate or trigger an upstream Anthropic content/ToS filter response (HTTP 200 with
finish_reason: "content_filter").
- Observed Behavior: 9Router marks
cc/claude-sonnet-5-5 as succeeded, forwards the content filter message to the caller, and never attempts cx/gpt-6-astra.
- Expected Behavior: 9Router detects
finish_reason: "content_filter" / stop_reason: "refusal", marks candidate 1 as failed, and falls back to candidate 2 (cx/gpt-6-astra).
Root Cause Analysis
In 9Router's proxy layer:
- Candidate success is evaluated solely on
response.ok (HTTP status code 200-299).
- Neither the non-streaming JSON body nor the opening chunks of a streaming SSE response are inspected for provider-level refusal finish reasons.
- The combo router lacks an evaluation hook to treat
content_filter / refusal as a retriable failure condition.
Proposed Solution
1. Non-Streaming Requests
In the combo execution handler, after parsing the upstream JSON body:
const isContentFilter =
data?.choices?.[0]?.finish_reason === "content_filter" ||
data?.stop_reason === "refusal" ||
data?.stop_details?.type === "content_filter";
if (isContentFilter && combo.fallbackOnContentFilter !== false) {
logger.warn(`[COMBO] Model ${model} returned content_filter/refusal. Failing over to next candidate.`);
// Trigger next candidate in combo loop instead of returning 200 to client
continue;
}
2. Streaming (SSE) Requests
Anthropic and OpenAI send content_filter either:
- In the initial response payload before text generation starts, or
- As an immediate single-event block (
message_start followed directly by message_delta with stop_reason: "refusal" and 0 tokens of generated code).
Implementation:
- Buffer the stream until the first substantive delta or terminal stop reason event before piping headers to the downstream client.
- If the stream terminates immediately with
finish_reason: "content_filter" and zero valid completion output, abort the stream upstream without flushing to client, increment combo candidate index, and route to the next provider.
3. Combo Configuration Setting (Opt-in / Configurable)
Add a flag to combo options so users can control whether content filters trigger fallback:
{
"id": "genius",
"strategy": "fallback",
"fallbackOnContentFilter": true,
"models": ["cc/claude-sonnet-5-5(high)", "cx/gpt-6-astra(high)"]
}
Environment
- 9Router Version:
v0.5.65 / v0.5.91
- Clients Tested: OpenCode CLI, Antigravity
- Affected Models:
cc/* (Claude Code OAuth proxy) failing over to cx/* (Codex)
9Router fails to trigger model combo fallback when upstream providers return HTTP 200 with safety refusals (
finish_reason: "content_filter"/stop_reason: "refusal"), causing automated workflows to abort fatally instead of failing over to subsequent combo targets.To solve this, 9Router's combo execution loop needs to evaluate response completion metadata (
finish_reason === 'content_filter'/ provider refusal payloads) alongside HTTP status codes when determining candidate failure.Related Issues
In
decolua/9routerUpstream Ecosystem References
anthropics/claude-code#95670: [Bug] Safeguard blocks firing on all requests with identical prompt templates (reasoning_extraction / duplicate model outputs).simonw/llm-anthropic#90: Handle Fable refusals: Claude refused this request (reasoning_extraction/ Terms of Service).Problem Description
When using a fallback combo (e.g.,
geniusconfigured with[cc/claude-sonnet-5-5(high), cx/gpt-6-astra(high)]), 9Router currently checks whether upstream returns HTTP 4xx (e.g. 429) or 5xx. If so, it logs[COMBO] Trying model 2/Nand fails over.However, providers like Anthropic return heuristic safeguard blocks (such as multi-agent burst detection tripping
reasoning_extraction) as an HTTP 200 OK payload containing:{ "choices": [ { "finish_reason": "content_filter", "index": 0, "message": { "role": "assistant", "content": "This request was blocked as it seems to violate Anthropic's Terms of Service restrictions on reverse engineering or duplicating model outputs. To learn more, visit https://www.anthropic.com/legal/commercial-terms." } } ] }Or native Anthropic Messages API:
{ "type": "message", "stop_reason": "refusal", "stop_details": { "type": "content_filter", "category": "reasoning_extraction" }, "content": [{ "type": "text", "text": "This request was blocked as it seems to violate Anthropic's Terms of Service..." }] }Because the HTTP status is 200, 9Router logs:
It immediately flushes this refusal downstream. Client agents (OpenCode, Claude Code, Cursor, Cline) treat the response as a terminal provider refusal and crash or stop the session, completely ignoring the second model in the combo (
cx/gpt-6-astra).Steps to Reproduce
{ "id": "test-fallback", "name": "Test Fallback", "strategy": "fallback", "models": [ "cc/claude-sonnet-5-5", "cx/gpt-6-astra" ] }http://localhost:20128/v1/chat/completionspointing to modeltest-fallback.finish_reason: "content_filter").cc/claude-sonnet-5-5as succeeded, forwards the content filter message to the caller, and never attemptscx/gpt-6-astra.finish_reason: "content_filter"/stop_reason: "refusal", marks candidate 1 as failed, and falls back to candidate 2 (cx/gpt-6-astra).Root Cause Analysis
In 9Router's proxy layer:
response.ok(HTTP status code200-299).content_filter/refusalas a retriable failure condition.Proposed Solution
1. Non-Streaming Requests
In the combo execution handler, after parsing the upstream JSON body:
2. Streaming (SSE) Requests
Anthropic and OpenAI send
content_filtereither:message_startfollowed directly bymessage_deltawithstop_reason: "refusal"and 0 tokens of generated code).Implementation:
finish_reason: "content_filter"and zero valid completion output, abort the stream upstream without flushing to client, increment combo candidate index, and route to the next provider.3. Combo Configuration Setting (Opt-in / Configurable)
Add a flag to combo options so users can control whether content filters trigger fallback:
{ "id": "genius", "strategy": "fallback", "fallbackOnContentFilter": true, "models": ["cc/claude-sonnet-5-5(high)", "cx/gpt-6-astra(high)"] }Environment
v0.5.65/v0.5.91cc/*(Claude Code OAuth proxy) failing over tocx/*(Codex)