fix: close provider-anthropic MiniMax proxy bypass + rework cache observability
Follow-up fixes on top of the salvaged #83678 commit: 1. Hoist the MiniMax-M3 marker exclusion ABOVE the native-Anthropic early return. provider="anthropic" pointed at a MiniMax /anthropic proxy is a supported override (_anthropic_base_url_override_ok), and the is_native_anthropic branch matched on provider alone — returning (True, True) before the M3 exclusion was reached. Two regression tests pin the proxy route (M3 off, M2.7 still on). 2. Reuse the existing _model_name_suggests_minimax_m3() helper from agent/model_metadata.py instead of a second inline substring copy. 3. Drop the debug kwarg on normalize_usage() — it had zero production callers and duplicated standard logging level gating. The cache-observability line is now a plain logger.debug scoped to MiniMax providers on the Anthropic wire only, so the "+128 floor" note can no longer appear for native Anthropic where it is false. Tests updated accordingly (MiniMax logs, native Anthropic does not).
This commit is contained in:
parent
c1e2529ae2
commit
223f703012
|
|
@ -2181,6 +2181,27 @@ def anthropic_prompt_cache_policy(
|
|||
and (eff_provider == "anthropic" or base_url_hostname(eff_base_url) == "api.anthropic.com")
|
||||
)
|
||||
|
||||
# MiniMax-M3 rides MiniMax's server-side automatic prefix cache on the
|
||||
# Anthropic wire (content-keyed, no marker needed); explicit cache_control
|
||||
# is documented for M2.7/M2.5/M2.1/M2 only, so markers on M3 are dead
|
||||
# weight — never observable (cache_creation always 0) nor billable.
|
||||
# Checked BEFORE the native-Anthropic return: provider="anthropic"
|
||||
# pointed at a MiniMax /anthropic proxy is a supported override
|
||||
# (_anthropic_base_url_override_ok) that would otherwise return
|
||||
# (True, True) above this exclusion.
|
||||
# Docs: https://platform.minimax.io/docs/api-reference/text-prompt-caching
|
||||
is_minimax_provider = provider_lower in {"minimax", "minimax-cn"}
|
||||
is_minimax_host = (
|
||||
base_url_host_matches(eff_base_url, "api.minimax.io")
|
||||
or base_url_host_matches(eff_base_url, "api.minimaxi.com")
|
||||
)
|
||||
is_minimax_route = is_minimax_provider or is_minimax_host
|
||||
if is_anthropic_wire and is_minimax_route:
|
||||
from agent.model_metadata import _model_name_suggests_minimax_m3
|
||||
|
||||
if _model_name_suggests_minimax_m3(eff_model):
|
||||
return False, False
|
||||
|
||||
if is_native_anthropic:
|
||||
return True, True
|
||||
# Envelope layout is an OpenAI-wire construct. Portal Claude on the native
|
||||
|
|
@ -2213,29 +2234,11 @@ def anthropic_prompt_cache_policy(
|
|||
# explicitly via provider id or host match so users on
|
||||
# provider=minimax / minimax-cn (or custom endpoints pointing at
|
||||
# api.minimax.io/anthropic / api.minimaxi.com/anthropic) get the
|
||||
# same cost reduction as Claude traffic.
|
||||
# same cost reduction as Claude traffic. MiniMax-M3 never reaches
|
||||
# here — it is excluded before the native-Anthropic return above.
|
||||
# Docs: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache
|
||||
#
|
||||
# MiniMax-M3 is intentionally excluded: M3 ships server-side automatic
|
||||
# prefix caching on this wire format (content-keyed, no marker needed —
|
||||
# see https://platform.minimax.io/docs/api-reference/text-prompt-caching),
|
||||
# and cache_control markers are NOT on its explicit-cache support list
|
||||
# (M2.7/M2.5/M2.1/M2 only). Emitting markers on M3 wasted serialization
|
||||
# overhead, risked perturbing the server-side prefix hash, and gave users
|
||||
# a false sense of explicit-cache savings. Empirically verified against
|
||||
# api.minimaxi.com/anthropic/v1/messages with MiniMax-M3[1m]: identical
|
||||
# system prompt hit-rate with and without markers; cache_read field has
|
||||
# a +128 floor and cache_creation is always 0, so the marker path is
|
||||
# neither observable nor billable for M3 users.
|
||||
if is_anthropic_wire:
|
||||
is_minimax_provider = provider_lower in {"minimax", "minimax-cn"}
|
||||
is_minimax_host = (
|
||||
base_url_host_matches(eff_base_url, "api.minimax.io")
|
||||
or base_url_host_matches(eff_base_url, "api.minimaxi.com")
|
||||
)
|
||||
is_minimax_m3 = "minimax-m3" in model_lower
|
||||
if (is_minimax_provider or is_minimax_host) and not is_minimax_m3:
|
||||
return True, True
|
||||
if is_anthropic_wire and is_minimax_route:
|
||||
return True, True
|
||||
|
||||
# Qwen/Alibaba on OpenCode (Zen/Go) and native DashScope: OpenAI-wire
|
||||
# transport that accepts Anthropic-style cache_control markers and
|
||||
|
|
|
|||
|
|
@ -1210,7 +1210,6 @@ def normalize_usage(
|
|||
*,
|
||||
provider: Optional[str] = None,
|
||||
api_mode: Optional[str] = None,
|
||||
debug: bool = False,
|
||||
) -> CanonicalUsage:
|
||||
"""Normalize raw API response usage into canonical token buckets.
|
||||
|
||||
|
|
@ -1292,20 +1291,18 @@ def normalize_usage(
|
|||
getattr(completion_details, "reasoning_tokens", 0)
|
||||
)
|
||||
|
||||
# NOTE: opt-in cache observability for MiniMax-M3 (and similar providers
|
||||
# whose usage.cache_read_input_tokens carries a constant +128 floor and
|
||||
# whose usage.cache_creation_input_tokens is always 0). See
|
||||
# https://platform.minimax.io/docs/api-reference/text-prompt-caching
|
||||
# (Automatic Caching table). On M3, the cache_read field is NOT a
|
||||
# reliable hit signal; the only signal that survives is the input_tokens
|
||||
# drop between consecutive calls. This debug block logs the
|
||||
# observable-only fields so an operator can confirm cache is working
|
||||
# without relying on the misleading cache_read number.
|
||||
if debug and (mode == "anthropic_messages" or provider_name in {"minimax", "minimax-cn"}):
|
||||
# Cache observability for MiniMax's Anthropic wire: on MiniMax-M3,
|
||||
# usage.cache_read_input_tokens carries a constant +128 floor and
|
||||
# cache_creation_input_tokens is always 0, so cache_read is NOT a
|
||||
# reliable hit signal — the signal that survives is the input_tokens
|
||||
# drop between consecutive calls. Standard level-gated logger.debug;
|
||||
# enable via logging config to confirm cache behavior.
|
||||
# Docs: https://platform.minimax.io/docs/api-reference/text-prompt-caching
|
||||
if provider_name in {"minimax", "minimax-cn"} and mode == "anthropic_messages":
|
||||
logger.debug(
|
||||
"cache_observability provider=%s mode=%s input_tokens=%s "
|
||||
"output_tokens=%s cache_read_tokens=%s cache_write_tokens=%s "
|
||||
"(note: cache_read on this provider carries a +128 constant "
|
||||
"(note: on MiniMax-M3 cache_read carries a +128 constant "
|
||||
"floor and is not a reliable hit signal — track input_tokens "
|
||||
"drops across calls instead)",
|
||||
provider_name, mode, input_tokens, output_tokens,
|
||||
|
|
|
|||
|
|
@ -314,32 +314,13 @@ def test_vertex_default_model_estimates_cached_usage(monkeypatch):
|
|||
assert result.amount_usd is not None and result.amount_usd > 0
|
||||
|
||||
|
||||
def test_normalize_usage_debug_off_emits_no_log(caplog):
|
||||
"""The opt-in cache observability block must NOT log when debug=False
|
||||
(the default). Production callers should never see this log line —
|
||||
it is only useful when an operator explicitly opts in to investigate
|
||||
cache behavior on MiniMax-M3 or any other provider whose
|
||||
cache_read_input_tokens carries a misleading constant offset.
|
||||
"""
|
||||
usage = SimpleNamespace(
|
||||
input_tokens=53,
|
||||
output_tokens=10,
|
||||
cache_read_input_tokens=128,
|
||||
cache_creation_input_tokens=0,
|
||||
)
|
||||
|
||||
with caplog.at_level("DEBUG", logger="agent.usage_pricing"):
|
||||
normalize_usage(usage, provider="minimax-cn", api_mode="anthropic_messages")
|
||||
|
||||
assert all("cache_observability" not in rec.message for rec in caplog.records)
|
||||
|
||||
|
||||
def test_normalize_usage_debug_on_minimax_logs_cache_observability(caplog):
|
||||
"""When debug=True is passed and the provider is MiniMax/M3,
|
||||
normalize_usage emits a debug log line that records the observable-only
|
||||
fields (input_tokens, output_tokens, cache_read_tokens,
|
||||
cache_write_tokens) so an operator can see real cache behavior
|
||||
without trusting the misleading cache_read number.
|
||||
def test_normalize_usage_minimax_logs_cache_observability(caplog):
|
||||
"""MiniMax providers on the Anthropic wire emit a debug-level
|
||||
cache-observability line recording the observable fields
|
||||
(input_tokens, output_tokens, cache_read_tokens, cache_write_tokens),
|
||||
so an operator can see real cache behavior without trusting the
|
||||
misleading cache_read number (constant +128 floor on MiniMax-M3).
|
||||
Standard logging level gating applies — no separate opt-in flag.
|
||||
"""
|
||||
usage = SimpleNamespace(
|
||||
input_tokens=1,
|
||||
|
|
@ -353,7 +334,6 @@ def test_normalize_usage_debug_on_minimax_logs_cache_observability(caplog):
|
|||
usage,
|
||||
provider="minimax-cn",
|
||||
api_mode="anthropic_messages",
|
||||
debug=True,
|
||||
)
|
||||
|
||||
cache_obs_records = [r for r in caplog.records if "cache_observability" in r.message]
|
||||
|
|
@ -363,17 +343,13 @@ def test_normalize_usage_debug_on_minimax_logs_cache_observability(caplog):
|
|||
assert "output_tokens=11" in record.message
|
||||
assert "cache_read_tokens=8594" in record.message
|
||||
assert "cache_write_tokens=0" in record.message
|
||||
assert "+128 constant floor" in record.message
|
||||
|
||||
|
||||
def test_normalize_usage_debug_on_claude_also_logs_cache_observability(caplog):
|
||||
"""The opt-in observability block fires for every anthropic_messages
|
||||
response — not just MiniMax — because the input_tokens-vs-cache_read
|
||||
framing is useful diagnostic information on any Anthropic-compatible
|
||||
wire (e.g. OpenRouter Claude, Bedrock Claude, GLM Claude) where a
|
||||
provider's cache_read_input_tokens semantics may differ from
|
||||
Anthropic's native contract. The block is debug-level and off by
|
||||
default, so emitting it for Claude traffic has zero production cost.
|
||||
def test_normalize_usage_native_anthropic_no_cache_observability(caplog):
|
||||
"""The MiniMax cache-observability line must NOT fire for native
|
||||
Anthropic: there cache_read_input_tokens is exact and billable, so
|
||||
the MiniMax-specific "+128 floor / unreliable hit signal" note would
|
||||
be false and misleading in the logs.
|
||||
"""
|
||||
usage = SimpleNamespace(
|
||||
input_tokens=100,
|
||||
|
|
@ -383,14 +359,14 @@ def test_normalize_usage_debug_on_claude_also_logs_cache_observability(caplog):
|
|||
)
|
||||
|
||||
with caplog.at_level("DEBUG", logger="agent.usage_pricing"):
|
||||
normalize_usage(
|
||||
result = normalize_usage(
|
||||
usage,
|
||||
provider="anthropic",
|
||||
api_mode="anthropic_messages",
|
||||
debug=True,
|
||||
)
|
||||
|
||||
cache_obs_records = [r for r in caplog.records if "cache_observability" in r.message]
|
||||
assert len(cache_obs_records) == 1
|
||||
assert "input_tokens=100" in cache_obs_records[0].message
|
||||
assert "cache_read_tokens=50" in cache_obs_records[0].message
|
||||
assert all("cache_observability" not in rec.message for rec in caplog.records)
|
||||
# Token normalization itself is unaffected.
|
||||
assert result.input_tokens == 100
|
||||
assert result.cache_read_tokens == 50
|
||||
assert result.cache_write_tokens == 10
|
||||
|
|
|
|||
|
|
@ -252,6 +252,34 @@ class TestMiniMaxAnthropicWire:
|
|||
)
|
||||
assert agent._anthropic_prompt_cache_policy() == (False, False)
|
||||
|
||||
def test_minimax_m3_via_provider_anthropic_proxy_does_not_cache(self):
|
||||
# provider="anthropic" pointed at a MiniMax /anthropic proxy is a
|
||||
# supported override (_anthropic_base_url_override_ok accepts
|
||||
# MiniMax-style /anthropic hosts and _resolve_explicit_runtime
|
||||
# preserves provider="anthropic"). The M3 exclusion must run
|
||||
# BEFORE the native-Anthropic early return, or this route keeps
|
||||
# emitting markers while the direct minimax/minimax-cn routes
|
||||
# don't.
|
||||
agent = _make_agent(
|
||||
provider="anthropic",
|
||||
base_url="https://api.minimax.io/anthropic",
|
||||
api_mode="anthropic_messages",
|
||||
model="MiniMax-M3",
|
||||
)
|
||||
assert agent._anthropic_prompt_cache_policy() == (False, False)
|
||||
|
||||
def test_minimax_m27_via_provider_anthropic_proxy_still_caches(self):
|
||||
# The proxy-route exclusion is M3-only: M2.x through the same
|
||||
# provider="anthropic" MiniMax proxy keeps explicit cache_control
|
||||
# (the native-Anthropic return still applies).
|
||||
agent = _make_agent(
|
||||
provider="anthropic",
|
||||
base_url="https://api.minimax.io/anthropic",
|
||||
api_mode="anthropic_messages",
|
||||
model="MiniMax-M2.7",
|
||||
)
|
||||
assert agent._anthropic_prompt_cache_policy() == (True, True)
|
||||
|
||||
def test_minimax_m27_still_caches_after_m3_opt_out(self):
|
||||
# Regression guard: the M3 substring check must not collide with
|
||||
# M2.7 / M2.5 / M2.1 / M2 model names. "minimax-m3" is not a
|
||||
|
|
|
|||
Loading…
Reference in New Issue