fix(streaming): route minimax/* on OpenRouter away from broken direct endpoint

The direct Minimax OpenRouter endpoint silently drops tool-call streams on
tool-calling workflows (MiniMax-M2#109, reproduced 4/4 times on 2026-04-18:
zero content, no finish_reason, silent close at ~40s). PR #12072 surfaced
the failure to the user; this PR avoids it entirely by routing minimax/*
requests to Fireworks / NovitaAI / Google-Vertex / AtlasCloud / Together
by default.

New module agent/provider_tweaks.py centralizes known-broken-endpoint
avoidance with a single registry entry per upstream bug. User-supplied
provider preferences (provider_sort, providers_allowed/ignored/order)
always win — tweaks only fill in defaults where absent, and a user who
sets 'only' is fully opted out.

Wired into both provider_preferences build sites in run_agent.py (main
chat loop + iteration-summary call). Only applies when base_url targets
openrouter.ai.

Validation
| | Before | After |
|---|---|---|
| minimax/minimax-m2.7 tool-call stream on OR (direct endpoint) | 0/4 success | 4/4 on Fireworks |
| extra_body.provider injected for minimax/* on OpenRouter | no | ignore=[minimax] order=[fireworks,novitaai,google-vertex,atlascloud,together] |
| extra_body.provider for anthropic/* on OpenRouter | unchanged | unchanged |
| extra_body.provider for minimax/* on api.minimax.io | unchanged | unchanged |
| User-supplied {only:[minimax]} | unchanged | unchanged (explicit opt-in honoured) |
| tests/agent/test_provider_tweaks.py | n/a | 23 passed |
| tests/run_agent/test_streaming.py | 26 passed | 26 passed |

Live e2e sanity (real OpenRouter call): 89.6s clean response via Fireworks,
with `extra_body.provider={'ignore': ['minimax'], 'order': ['fireworks',...]}`
confirmed in the outgoing request.
This commit is contained in:
Teknium 2026-04-18 02:24:10 -07:00
parent 3b69b2fd61
commit c4cc0f3bd0
No known key found for this signature in database
3 changed files with 321 additions and 0 deletions

135
agent/provider_tweaks.py Normal file
View file

@ -0,0 +1,135 @@
"""Provider-specific OpenRouter routing tweaks.
Central registry for known-buggy OpenRouter endpoints whose upstream providers
silently drop tool-call streams, stall mid-arguments, or otherwise fail in ways
that are not user-configurable. Consumed by ``run_agent.py`` when building
``provider_preferences`` for chat completion requests against OpenRouter.
Design principles:
* Only applies to OpenRouter ``base_url`` other provider chains route through
different infrastructure and may not have the same endpoint issues.
* User-provided preferences always win. We only layer defaults in where the
user hasn't specified ``only``, ``order``, or ``ignore``.
* Additions must be backed by a concrete upstream-bug reference (vendor repo
issue, reproducible empirical evidence) this is not for speculative
provider preferences.
Registry format (``_KNOWN_BROKEN_ROUTES``):
key: lowercase model-slug substring that identifies the affected family
value: {
"ignore": [list of OpenRouter provider tags to skip, e.g. "minimax"],
"order": [list of OpenRouter provider tags to prefer in order],
"reason": "human-readable one-liner used in logs",
"ref": "issue/PR reference for the upstream bug",
}
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Ordered list: first matching entry wins. Match is substring-in-lower-model.
_KNOWN_BROKEN_ROUTES: List[Dict[str, Any]] = [
{
# MiniMax direct OpenRouter endpoint has documented non-terminating
# streams on tool-calling workflows (MiniMax-M2 issue #109, Apr 2026;
# OpenClaw #1622). Empirically reproduced 4/4 times on 2026-04-18:
# streaming a write_file tool call returned zero bytes and closed
# silently at ~40s from both minimax/fp8 and minimax/highspeed tags.
# Fireworks, Together, NovitaAI, Google-Vertex, AtlasCloud all work.
"match": "minimax/",
"ignore": ["minimax"],
"order": [
"fireworks", # m2.7: best throughput + uptime
"novitaai", # m2: best tool-call error rate (0.19%)
"google-vertex", # m2: fastest latency
"atlascloud",
"together", # fp4 quant — last resort
],
"reason": "Minimax direct endpoint drops tool-call streams",
"ref": "MiniMax-M2#109, OpenClaw#1622, Hermes-PR#12072",
},
]
def get_provider_tweaks(model: Optional[str], base_url: Optional[str]) -> Dict[str, Any]:
"""Return known-broken-endpoint tweaks for a given model/base_url pair.
Returns an empty dict when no tweaks apply (non-OpenRouter endpoint,
unknown model, etc.) so callers can do ``if tweaks:`` cheaply.
Returned keys when applicable:
ignore: list[str] OpenRouter provider tags to exclude
order: list[str] OpenRouter provider tags to prefer in order
reason: str human-readable reason (for logging)
ref: str upstream bug reference (for logging)
"""
if not model or not base_url:
return {}
url_lower = base_url.lower()
# Only OpenRouter-compatible endpoints understand the ``provider`` object.
if "openrouter.ai" not in url_lower:
return {}
model_lower = model.lower()
for entry in _KNOWN_BROKEN_ROUTES:
if entry["match"] in model_lower:
return {
"ignore": list(entry.get("ignore") or []),
"order": list(entry.get("order") or []),
"reason": entry.get("reason", ""),
"ref": entry.get("ref", ""),
}
return {}
def merge_provider_tweaks(
provider_preferences: Dict[str, Any],
tweaks: Dict[str, Any],
*,
log_label: str = "",
) -> Dict[str, Any]:
"""Merge auto-tweaks into user-supplied provider preferences.
User-provided fields always win this function never overrides ``only``,
``ignore``, or ``order`` that the user has already set. It only supplies
defaults where those fields are absent.
When the user has set ``only`` (whitelist mode), the tweaks are fully
ignored: a whitelist already constrains routing to a known-good subset,
and layering ``ignore``/``order`` on top would be confusing.
Emits a single INFO log line when tweaks are actually applied so the
behaviour is visible in agent.log without spamming every request.
"""
if not tweaks:
return provider_preferences or {}
result = dict(provider_preferences or {})
# Whitelist already narrows routing — don't layer on.
if result.get("only"):
return result
applied: List[str] = []
if tweaks.get("ignore") and "ignore" not in result:
result["ignore"] = list(tweaks["ignore"])
applied.append(f"ignore={tweaks['ignore']}")
if tweaks.get("order") and "order" not in result:
result["order"] = list(tweaks["order"])
applied.append(f"order={tweaks['order']}")
if applied:
logger.info(
"Provider tweaks applied%s: %s (reason: %s; ref: %s)",
f" [{log_label}]" if log_label else "",
", ".join(applied),
tweaks.get("reason", "?"),
tweaks.get("ref", "?"),
)
return result
__all__ = ["get_provider_tweaks", "merge_provider_tweaks"]

View file

@ -96,6 +96,7 @@ from agent.subdirectory_hints import SubdirectoryHintTracker
from agent.prompt_caching import apply_anthropic_cache_control
from agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt, build_environment_hints, load_soul_md, TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, DEVELOPER_ROLE_MODELS, GOOGLE_MODEL_OPERATIONAL_GUIDANCE, OPENAI_MODEL_EXECUTION_GUIDANCE
from agent.usage_pricing import estimate_usage_cost, normalize_usage
from agent.provider_tweaks import get_provider_tweaks, merge_provider_tweaks
from agent.display import (
KawaiiSpinner, build_tool_preview as _build_tool_preview,
get_cute_tool_message as _get_cute_tool_message_impl,
@ -6958,6 +6959,16 @@ class AIAgent:
# specific. Only send to OpenRouter-compatible endpoints.
# TODO: Nous Portal will add transparent proxy support — re-enable
# for _is_nous when their backend is updated.
if _is_openrouter:
# Apply known-broken-endpoint tweaks (e.g. skip Minimax direct
# endpoint on minimax/* models where tool-call streams stall —
# MiniMax-M2#109, Hermes-PR#12072). User-supplied preferences
# always win; these only supply defaults where absent.
_tweaks = get_provider_tweaks(self.model, self._base_url)
if _tweaks:
provider_preferences = merge_provider_tweaks(
provider_preferences, _tweaks,
)
if provider_preferences and _is_openrouter:
extra_body["provider"] = provider_preferences
_is_nous = "nousresearch" in self._base_url_lower
@ -8414,6 +8425,14 @@ class AIAgent:
provider_preferences["order"] = self.providers_order
if self.provider_sort:
provider_preferences["sort"] = self.provider_sort
# Apply known-broken-endpoint tweaks (OpenRouter only).
if "openrouter.ai" in self._base_url_lower:
_tweaks = get_provider_tweaks(self.model, self._base_url)
if _tweaks:
provider_preferences = merge_provider_tweaks(
provider_preferences, _tweaks,
log_label="iteration_summary",
)
if provider_preferences:
summary_extra_body["provider"] = provider_preferences

View file

@ -0,0 +1,167 @@
"""Unit tests for agent.provider_tweaks."""
import pytest
from agent.provider_tweaks import get_provider_tweaks, merge_provider_tweaks
# ── get_provider_tweaks ────────────────────────────────────────────────────
class TestGetProviderTweaks:
def test_returns_empty_for_non_openrouter_base_url(self):
assert get_provider_tweaks("minimax/minimax-m2.7", "https://api.minimax.io/v1") == {}
def test_returns_empty_for_missing_base_url(self):
assert get_provider_tweaks("minimax/minimax-m2.7", None) == {}
assert get_provider_tweaks("minimax/minimax-m2.7", "") == {}
def test_returns_empty_for_missing_model(self):
assert get_provider_tweaks(None, "https://openrouter.ai/api/v1") == {}
assert get_provider_tweaks("", "https://openrouter.ai/api/v1") == {}
def test_returns_empty_for_unmatched_model_on_openrouter(self):
assert get_provider_tweaks("anthropic/claude-sonnet-4.6", "https://openrouter.ai/api/v1") == {}
assert get_provider_tweaks("openai/gpt-5.4", "https://openrouter.ai/api/v1") == {}
assert get_provider_tweaks("deepseek/deepseek-chat", "https://openrouter.ai/api/v1") == {}
def test_minimax_m27_on_openrouter_gets_ignore_and_order(self):
t = get_provider_tweaks("minimax/minimax-m2.7", "https://openrouter.ai/api/v1")
assert t != {}
assert t["ignore"] == ["minimax"]
assert "fireworks" in t["order"]
assert "novitaai" in t["order"]
assert t["ref"]
assert t["reason"]
def test_minimax_m2_base_also_matches(self):
t = get_provider_tweaks("minimax/minimax-m2", "https://openrouter.ai/api/v1")
assert t["ignore"] == ["minimax"]
def test_minimax_m21_also_matches(self):
t = get_provider_tweaks("minimax/minimax-m2.1", "https://openrouter.ai/api/v1")
assert t["ignore"] == ["minimax"]
def test_case_insensitive_model_match(self):
t = get_provider_tweaks("MiniMax/MiniMax-M2.7", "https://openrouter.ai/api/v1")
assert t["ignore"] == ["minimax"]
def test_case_insensitive_base_url_match(self):
t = get_provider_tweaks("minimax/minimax-m2.7", "https://OpenRouter.AI/api/v1")
assert t["ignore"] == ["minimax"]
def test_openrouter_subpath_still_matched(self):
# Proxied OpenRouter deployments still route through openrouter.ai
t = get_provider_tweaks("minimax/minimax-m2.7", "https://proxy.example.com/openrouter.ai/api/v1")
assert t["ignore"] == ["minimax"]
def test_returned_lists_are_copies_not_references(self):
t1 = get_provider_tweaks("minimax/minimax-m2.7", "https://openrouter.ai/api/v1")
t1["ignore"].append("pwned")
t2 = get_provider_tweaks("minimax/minimax-m2.7", "https://openrouter.ai/api/v1")
assert "pwned" not in t2["ignore"]
# ── merge_provider_tweaks ──────────────────────────────────────────────────
class TestMergeProviderTweaks:
def test_empty_tweaks_returns_input_unchanged(self):
assert merge_provider_tweaks({"order": ["x"]}, {}) == {"order": ["x"]}
assert merge_provider_tweaks(None, {}) == {}
assert merge_provider_tweaks({}, {}) == {}
def test_empty_preferences_gets_tweak_defaults(self):
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks({}, t)
assert merged["ignore"] == ["minimax"]
assert merged["order"] == ["fireworks"]
def test_none_preferences_gets_tweak_defaults(self):
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks(None, t)
assert merged["ignore"] == ["minimax"]
assert merged["order"] == ["fireworks"]
def test_user_ignore_wins_over_tweaks(self):
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks({"ignore": ["together"]}, t)
assert merged["ignore"] == ["together"]
# But order still filled from tweaks since user didn't set it
assert merged["order"] == ["fireworks"]
def test_user_order_wins_over_tweaks(self):
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks({"order": ["novitaai"]}, t)
assert merged["order"] == ["novitaai"]
# But ignore still filled from tweaks
assert merged["ignore"] == ["minimax"]
def test_user_only_disables_all_tweaks(self):
"""When user whitelists specific providers, don't layer ignore/order on top."""
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks({"only": ["minimax"]}, t)
assert merged == {"only": ["minimax"]}
assert "ignore" not in merged
assert "order" not in merged
def test_does_not_mutate_input_preferences(self):
prefs = {"order": ["novitaai"]}
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merge_provider_tweaks(prefs, t)
# Input wasn't modified
assert prefs == {"order": ["novitaai"]}
assert "ignore" not in prefs
def test_does_not_mutate_input_tweaks(self):
prefs = {}
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks(prefs, t)
merged["ignore"].append("pwned")
assert t["ignore"] == ["minimax"]
def test_preserves_unrelated_preference_keys(self):
t = {"ignore": ["minimax"], "order": ["fireworks"], "reason": "x", "ref": "y"}
merged = merge_provider_tweaks(
{"sort": "throughput", "data_collection": "deny"},
t,
)
assert merged["sort"] == "throughput"
assert merged["data_collection"] == "deny"
assert merged["ignore"] == ["minimax"]
assert merged["order"] == ["fireworks"]
# ── Integration: the concrete MiniMax case from PR #12072 ──────────────────
class TestMinimaxConcreteCase:
"""End-to-end behaviour for the exact scenario that motivated this module.
MiniMax direct OpenRouter endpoint has documented non-terminating streams
on tool-calling workloads (MiniMax-M2 issue #109). The tweaks module
must automatically route `minimax/*` requests away from that endpoint
on OpenRouter, while leaving user-supplied preferences untouched.
"""
def test_fresh_minimax_request_gets_fireworks_routing(self):
t = get_provider_tweaks("minimax/minimax-m2.7", "https://openrouter.ai/api/v1")
merged = merge_provider_tweaks({}, t)
assert "minimax" in merged["ignore"]
# Fireworks is the empirically-best provider for m2.7 (confirmed
# 2026-04-18: 99% uptime, 75 tok/s p50, clean tool-call streams)
assert merged["order"][0] == "fireworks"
def test_user_force_minimax_still_honoured(self):
"""User who explicitly wants to test the broken endpoint gets it."""
t = get_provider_tweaks("minimax/minimax-m2.7", "https://openrouter.ai/api/v1")
merged = merge_provider_tweaks({"only": ["minimax"]}, t)
# User's explicit whitelist wins, even though we "know better".
assert merged == {"only": ["minimax"]}
def test_anthropic_model_on_openrouter_unaffected(self):
"""Tweaks only trigger on the specific buggy model family."""
t = get_provider_tweaks("anthropic/claude-sonnet-4.6", "https://openrouter.ai/api/v1")
assert t == {}
merged = merge_provider_tweaks({}, t)
assert merged == {}