From 35ad72e2410de5dce4aad5a75becbd3f2d09fb1b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:57:33 +0000 Subject: [PATCH 1/8] Add MiniMax M3 to provider defaults --- uncommon_route/providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 338c3de..9e79eb9 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -48,7 +48,7 @@ } PROVIDER_MODELS: dict[str, list[str]] = { - "minimax": ["minimax/minimax-m2.5"], + "minimax": ["minimax/minimax-m2.5", "minimax/minimax-m3"], "deepseek": ["deepseek/deepseek-chat", "deepseek/deepseek-reasoner"], "openai": ["openai/gpt-4o-mini", "openai/gpt-4o", "openai/gpt-5.2", "openai/gpt-5.2-codex", "openai/o1-mini", "openai/o3", "openai/o4-mini"], "anthropic": ["anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.6"], From 93dc418e1d49c29a002cbdeb0cc5d70023771762 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:42:20 +0800 Subject: [PATCH 2/8] Add MiniMax M2.7 to provider defaults --- uncommon_route/providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 9e79eb9..d3cf13d 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -48,7 +48,7 @@ } PROVIDER_MODELS: dict[str, list[str]] = { - "minimax": ["minimax/minimax-m2.5", "minimax/minimax-m3"], + "minimax": ["minimax/minimax-m2.5", "minimax/minimax-m3", "minimax/minimax-m2.7"], "deepseek": ["deepseek/deepseek-chat", "deepseek/deepseek-reasoner"], "openai": ["openai/gpt-4o-mini", "openai/gpt-4o", "openai/gpt-5.2", "openai/gpt-5.2-codex", "openai/o1-mini", "openai/o3", "openai/o4-mini"], "anthropic": ["anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.6"], From ec695e63e28cc42f9d62fc3cf64e9edccec7cfb7 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:20:22 +0800 Subject: [PATCH 3/8] Document and test MiniMax provider defaults --- tests/test_providers.py | 6 +++++- uncommon_route/providers.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_providers.py b/tests/test_providers.py index 352f7e2..d32662b 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -41,7 +41,11 @@ def test_add_provider_with_plan(self) -> None: cfg = add_provider("minimax", "eyJ-test", plan="coding-plan") entry = cfg.providers["minimax"] assert entry.plan == "coding-plan" - assert "minimax/minimax-m2.5" in entry.models + assert entry.models == [ + "minimax/minimax-m2.5", + "minimax/minimax-m3", + "minimax/minimax-m2.7", + ] def test_add_provider_custom_url(self) -> None: cfg = add_provider("openai", "sk-openai", base_url="https://my-proxy.com/v1") diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index d3cf13d..236e1ad 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -12,7 +12,11 @@ "minimax": { "api_key": "eyJ...", "base_url": "https://api.minimax.io/v1", - "models": ["minimax/minimax-m2.5"], + "models": [ + "minimax/minimax-m2.5", + "minimax/minimax-m3", + "minimax/minimax-m2.7" + ], "plan": "coding-plan" }, "deepseek": { From 097c150fe4704c5cc4196be6699db82e4b9715ad Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:21:21 +0800 Subject: [PATCH 4/8] Document MiniMax regional endpoints --- README.md | 17 ++++++++++ tests/test_anthropic_compat.py | 57 ++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 018d432..5606d53 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,23 @@ uncommon-route provider add google AIza... uncommon-route serve ``` +MiniMax uses the global OpenAI-compatible endpoint by default. Pass the China +endpoint explicitly when needed: + +```bash +uncommon-route provider add minimax eyJ-... +uncommon-route provider add minimax eyJ-... --url https://api.minimaxi.com/v1 +``` + +| Region | OpenAI-compatible base URL | Anthropic-compatible base URL | +|---|---|---| +| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` | +| China | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` | + +Provider configuration stores the OpenAI-compatible base URL. Anthropic +requests use the corresponding `/anthropic` base and append `/v1/messages` +internally. + > UncommonRoute doesn't automatically read `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Use `init`, a saved connection, or one of the manual setup paths above. ### Routing Modes diff --git a/tests/test_anthropic_compat.py b/tests/test_anthropic_compat.py index a1d462d..dcf434a 100644 --- a/tests/test_anthropic_compat.py +++ b/tests/test_anthropic_compat.py @@ -1931,9 +1931,18 @@ def handler(request: httpx.Request) -> httpx.Response: finally: asyncio.run(async_client.aclose()) + @pytest.mark.parametrize( + ("base_url", "expected_url"), + [ + ("https://api.minimax.io/v1", "https://api.minimax.io/anthropic/v1/messages"), + ("https://api.minimaxi.com/v1", "https://api.minimaxi.com/anthropic/v1/messages"), + ], + ) def test_messages_use_minimax_anthropic_endpoint_for_direct_provider( self, monkeypatch: pytest.MonkeyPatch, + base_url: str, + expected_url: str, ) -> None: captured: dict[str, object] = {} @@ -1947,7 +1956,7 @@ def handler(request: httpx.Request) -> httpx.Response: "id": "msg_minimax_byok", "type": "message", "role": "assistant", - "model": "minimax/minimax-m2.1", + "model": "minimax/minimax-m3", "content": [{"type": "text", "text": "ok"}], "stop_reason": "end_turn", "stop_sequence": None, @@ -1964,32 +1973,41 @@ def handler(request: httpx.Request) -> httpx.Response: "minimax": ProviderEntry( name="minimax", api_key="mm-key-123", - base_url="https://api.minimax.io/v1", - models=["minimax/minimax-m2.1"], + base_url=base_url, + models=["minimax/minimax-m3"], ), }) app = create_app( - upstream="https://api.commonstack.ai/v1", + upstream="https://primary.example/v1", providers_config=providers, ) client = TestClient(app, raise_server_exceptions=False) resp = client.post("/v1/messages", json={ - "model": "minimax/minimax-m2.1", + "model": "minimax/minimax-m3", "max_tokens": 32, "messages": [{"role": "user", "content": "hello"}], }) assert resp.status_code == 200 - assert captured["url"] == "https://api.minimax.io/anthropic/v1/messages" + assert captured["url"] == expected_url headers = captured["headers"] assert isinstance(headers, dict) assert headers["x-api-key"] == "mm-key-123" finally: asyncio.run(async_client.aclose()) - def test_chat_completions_keep_openai_transport_for_minimax_models( + @pytest.mark.parametrize( + ("base_url", "expected_url"), + [ + ("https://api.minimax.io/v1", "https://api.minimax.io/v1/chat/completions"), + ("https://api.minimaxi.com/v1", "https://api.minimaxi.com/v1/chat/completions"), + ], + ) + def test_chat_completions_use_minimax_openai_endpoint_for_direct_provider( self, monkeypatch: pytest.MonkeyPatch, + base_url: str, + expected_url: str, ) -> None: captured: dict[str, object] = {} @@ -2003,7 +2021,7 @@ def handler(request: httpx.Request) -> httpx.Response: "id": "chatcmpl_minimax", "object": "chat.completion", "created": 1, - "model": "minimax/minimax-m2.1", + "model": "minimax/minimax-m3", "choices": [{ "index": 0, "message": {"role": "assistant", "content": "done"}, @@ -2016,13 +2034,23 @@ def handler(request: httpx.Request) -> httpx.Response: async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: async_client) - monkeypatch.setenv("UNCOMMON_ROUTE_API_KEY", "env-key-123") try: - app = create_app(upstream="https://api.commonstack.ai/v1") + providers = ProvidersConfig(providers={ + "minimax": ProviderEntry( + name="minimax", + api_key="mm-key-123", + base_url=base_url, + models=["minimax/minimax-m3"], + ), + }) + app = create_app( + upstream="https://primary.example/v1", + providers_config=providers, + ) client = TestClient(app, raise_server_exceptions=False) resp = client.post("/v1/chat/completions", json={ - "model": "minimax/minimax-m2.1", + "model": "minimax/minimax-m3", "max_tokens": 32, "messages": [{"role": "user", "content": "hello"}], }) @@ -2031,10 +2059,13 @@ def handler(request: httpx.Request) -> httpx.Response: assert resp.headers["x-uncommon-route-requested-transport"] == "openai-chat" assert resp.headers["x-uncommon-route-transport"] == "openai-chat" assert resp.headers["x-uncommon-route-transport-source"] == "ingress-policy" - assert captured["url"] == "https://api.commonstack.ai/v1/chat/completions" + assert captured["url"] == expected_url + headers = captured["headers"] + assert isinstance(headers, dict) + assert headers["authorization"] == "Bearer mm-key-123" body = captured["body"] assert isinstance(body, dict) - assert body["model"] == "minimax/minimax-m2.1" + assert body["model"] == "minimax/minimax-m3" finally: asyncio.run(async_client.aclose()) From 7ed31d93297da5c7c3c69e351a37ed0f48a32050 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:27:46 +0800 Subject: [PATCH 5/8] Translate MiniMax model IDs for direct requests --- tests/test_anthropic_compat.py | 9 ++++++--- tests/test_providers.py | 6 ++++++ uncommon_route/providers.py | 11 +++++++++++ uncommon_route/proxy.py | 5 ++++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/test_anthropic_compat.py b/tests/test_anthropic_compat.py index dcf434a..519b1ee 100644 --- a/tests/test_anthropic_compat.py +++ b/tests/test_anthropic_compat.py @@ -1956,7 +1956,7 @@ def handler(request: httpx.Request) -> httpx.Response: "id": "msg_minimax_byok", "type": "message", "role": "assistant", - "model": "minimax/minimax-m3", + "model": "MiniMax-M3", "content": [{"type": "text", "text": "ok"}], "stop_reason": "end_turn", "stop_sequence": None, @@ -1993,6 +1993,9 @@ def handler(request: httpx.Request) -> httpx.Response: headers = captured["headers"] assert isinstance(headers, dict) assert headers["x-api-key"] == "mm-key-123" + body = captured["body"] + assert isinstance(body, dict) + assert body["model"] == "MiniMax-M3" finally: asyncio.run(async_client.aclose()) @@ -2021,7 +2024,7 @@ def handler(request: httpx.Request) -> httpx.Response: "id": "chatcmpl_minimax", "object": "chat.completion", "created": 1, - "model": "minimax/minimax-m3", + "model": "MiniMax-M3", "choices": [{ "index": 0, "message": {"role": "assistant", "content": "done"}, @@ -2065,7 +2068,7 @@ def handler(request: httpx.Request) -> httpx.Response: assert headers["authorization"] == "Bearer mm-key-123" body = captured["body"] assert isinstance(body, dict) - assert body["model"] == "minimax/minimax-m3" + assert body["model"] == "MiniMax-M3" finally: asyncio.run(async_client.aclose()) diff --git a/tests/test_providers.py b/tests/test_providers.py index d32662b..528a840 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -11,6 +11,7 @@ ProviderEntry, add_provider, load_providers, + resolve_upstream_model, remove_provider, select_preferred_model, ) @@ -51,6 +52,11 @@ def test_add_provider_custom_url(self) -> None: cfg = add_provider("openai", "sk-openai", base_url="https://my-proxy.com/v1") assert cfg.providers["openai"].base_url == "https://my-proxy.com/v1" + def test_resolve_minimax_upstream_model_ids(self) -> None: + assert resolve_upstream_model("minimax", "minimax/minimax-m3") == "MiniMax-M3" + assert resolve_upstream_model("minimax", "minimax/minimax-m2.7") == "MiniMax-M2.7" + assert resolve_upstream_model("minimax", "minimax/unknown") == "minimax/unknown" + def test_add_provider_custom_models(self) -> None: cfg = add_provider( "custom", diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 236e1ad..8adfccd 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -61,6 +61,17 @@ "moonshot": ["moonshot/kimi-k2.5"], } +UPSTREAM_MODEL_IDS: dict[str, dict[str, str]] = { + "minimax": { + "minimax/minimax-m3": "MiniMax-M3", + "minimax/minimax-m2.7": "MiniMax-M2.7", + }, +} + + +def resolve_upstream_model(provider_name: str, model_id: str) -> str: + return UPSTREAM_MODEL_IDS.get(provider_name, {}).get(model_id, model_id) + @dataclass class ProviderEntry: diff --git a/uncommon_route/proxy.py b/uncommon_route/proxy.py index fc341aa..4ef4b0c 100644 --- a/uncommon_route/proxy.py +++ b/uncommon_route/proxy.py @@ -106,6 +106,7 @@ ProvidersConfig, add_provider, load_providers, + resolve_upstream_model, remove_provider, verify_key, ) @@ -4635,7 +4636,9 @@ def _prepare_attempt(model_name: str) -> dict[str, Any]: attempt_headers[_ORIGINAL_MODEL_HEADER] = requested_model resolved_model = model_name - if not attempt_provider_entry: + if attempt_provider_entry: + resolved_model = resolve_upstream_model(attempt_provider_entry.name, model_name) + else: resolved_model = _mapper.resolve(model_name) attempt_upstream_body["model"] = resolved_model From ea3d0e4cb991fb6f0a0db594e809a957cc0f0393 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:40:22 +0800 Subject: [PATCH 6/8] Resolve MiniMax IDs for semantic requests --- tests/test_proxy.py | 31 +++++++++++++++++++++++++++++++ uncommon_route/proxy.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 08427f0..9387d04 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -17,6 +17,7 @@ from uncommon_route.model_experience import InMemoryModelExperienceStorage, ModelExperienceStore from uncommon_route.providers import ProviderEntry, ProvidersConfig from uncommon_route.proxy import ( + UpstreamSemanticCompressor, _extract_current_message, _extract_prompt, _normalize_reasoning_content_chunk, @@ -77,6 +78,36 @@ def _build_test_mapper(*model_ids: str) -> ModelMapper: return mapper +class TestUpstreamSemanticCompressor: + def test_direct_minimax_provider_uses_upstream_model_id(self) -> None: + providers = ProvidersConfig(providers={ + "minimax": ProviderEntry( + name="minimax", + api_key="test-key", + base_url="https://api.minimax.io/v1", + models=["minimax/minimax-m3"], + ), + }) + compressor = UpstreamSemanticCompressor( + upstream_chat="https://primary.example/v1/chat/completions", + primary_api_key="primary-test-key", + providers_config=providers, + model_mapper=_build_test_mapper("openai/gpt-4o-mini"), + composition_policy=CompositionPolicy(), + ) + + resolved = compressor._resolve_request( + "minimax/minimax-m3", + httpx.Request("POST", "https://local.example/v1/chat/completions"), + ) + + assert resolved is not None + target_url, headers, upstream_model = resolved + assert target_url == "https://api.minimax.io/v1/chat/completions" + assert headers["authorization"] == "Bearer test-key" + assert upstream_model == "MiniMax-M3" + + class TestPromptExtraction: def test_extract_prompt_ignores_claude_code_wrapper_blocks(self) -> None: prompt, system_prompt, max_tokens = _extract_prompt({ diff --git a/uncommon_route/proxy.py b/uncommon_route/proxy.py index 4ef4b0c..de6f775 100644 --- a/uncommon_route/proxy.py +++ b/uncommon_route/proxy.py @@ -915,7 +915,7 @@ def _resolve_request(self, model_id: str, request: Request) -> tuple[str, dict[s provider_entry = self._providers.get_for_model(model_id) if provider_entry and provider_entry.base_url: target_chat_url = f"{provider_entry.base_url.rstrip('/')}/chat/completions" - upstream_model = model_id + upstream_model = resolve_upstream_model(provider_entry.name, model_id) elif self._upstream_chat: target_chat_url = self._upstream_chat upstream_model = self._mapper.resolve(model_id) From 801912f68b730da53bf799a92e35c293fffb098d Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:44:20 +0800 Subject: [PATCH 7/8] Keep semantic regression target-specific --- tests/test_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 9387d04..1b7043b 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -92,7 +92,7 @@ def test_direct_minimax_provider_uses_upstream_model_id(self) -> None: upstream_chat="https://primary.example/v1/chat/completions", primary_api_key="primary-test-key", providers_config=providers, - model_mapper=_build_test_mapper("openai/gpt-4o-mini"), + model_mapper=ModelMapper("https://primary.example/v1"), composition_policy=CompositionPolicy(), ) From 93395ce3d393a4119a636c3552e410bc98ed5c26 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:56:23 +0800 Subject: [PATCH 8/8] Add MiniMax provider routing metadata --- tests/test_cache_support.py | 25 ++++++++- tests/test_providers.py | 27 ++++++++++ tests/test_proxy.py | 87 +++++++++++++++++++++++++++++++ uncommon_route/cache_support.py | 7 ++- uncommon_route/providers.py | 1 + uncommon_route/proxy.py | 19 ++++++- uncommon_route/router/config.py | 63 ++++++++++++++++++++++ uncommon_route/router/selector.py | 2 +- uncommon_route/router/types.py | 36 +++++++++++++ uncommon_route/stats.py | 9 +++- 10 files changed, 269 insertions(+), 7 deletions(-) diff --git a/tests/test_cache_support.py b/tests/test_cache_support.py index adbe483..ef534f5 100644 --- a/tests/test_cache_support.py +++ b/tests/test_cache_support.py @@ -2,8 +2,8 @@ import json -from uncommon_route.cache_support import apply_anthropic_cache_breakpoints, parse_stream_usage_metrics -from uncommon_route.router.types import ModelPricing +from uncommon_route.cache_support import apply_anthropic_cache_breakpoints, estimate_usage_cost, parse_stream_usage_metrics +from uncommon_route.router.types import ModelPricing, ModelPricingTier def test_anthropic_cache_breakpoints_do_not_upgrade_after_existing_5m() -> None: @@ -88,3 +88,24 @@ def test_stream_usage_falls_back_to_reasoning_content_tokens_when_usage_missing( assert usage.output_tokens > 0 assert usage.total_tokens == usage.output_tokens assert usage.actual_cost is not None + + +def test_usage_cost_applies_long_context_pricing_tier() -> None: + pricing = ModelPricing( + 0.30, + 1.20, + pricing_tiers=( + ModelPricingTier("standard", 0.30, 1.20, input_tokens_lte=512_000), + ModelPricingTier("standard", 0.60, 2.40, input_tokens_gt=512_000), + ), + ) + + cost = estimate_usage_cost( + input_tokens_uncached=600_000, + output_tokens=100_000, + cache_read_input_tokens=0, + cache_write_input_tokens=0, + pricing=pricing, + ) + + assert cost == 0.60 diff --git a/tests/test_providers.py b/tests/test_providers.py index 528a840..baccd6e 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -15,6 +15,7 @@ remove_provider, select_preferred_model, ) +from uncommon_route.router.config import PROVIDER_MODEL_CAPABILITIES, PROVIDER_MODEL_PRICING from uncommon_route.router.types import RoutingFeatures, Tier @@ -53,10 +54,36 @@ def test_add_provider_custom_url(self) -> None: assert cfg.providers["openai"].base_url == "https://my-proxy.com/v1" def test_resolve_minimax_upstream_model_ids(self) -> None: + assert resolve_upstream_model("minimax", "minimax/minimax-m2.5") == "MiniMax-M2.5" assert resolve_upstream_model("minimax", "minimax/minimax-m3") == "MiniMax-M3" assert resolve_upstream_model("minimax", "minimax/minimax-m2.7") == "MiniMax-M2.7" assert resolve_upstream_model("minimax", "minimax/unknown") == "minimax/unknown" + def test_minimax_provider_model_metadata(self) -> None: + m3_pricing = PROVIDER_MODEL_PRICING["minimax/minimax-m3"] + assert m3_pricing.for_usage(512_000).input_price == 0.30 + assert m3_pricing.for_usage(512_001).input_price == 0.60 + assert m3_pricing.for_usage(512_000, "priority").input_price == 0.45 + assert m3_pricing.for_usage(512_001, "priority").input_price == 0.90 + + m27_pricing = PROVIDER_MODEL_PRICING["minimax/minimax-m2.7"] + assert m27_pricing.cached_input_price == 0.06 + assert m27_pricing.cache_write_price == 0.375 + + m3_capabilities = PROVIDER_MODEL_CAPABILITIES["minimax/minimax-m3"] + assert m3_capabilities.context_window == 1_000_000 + assert m3_capabilities.input_modalities == ("text", "image", "video") + assert m3_capabilities.thinking_modes == ("adaptive", "disabled") + assert m3_capabilities.vision is True + assert m3_capabilities.reasoning is True + + m27_capabilities = PROVIDER_MODEL_CAPABILITIES["minimax/minimax-m2.7"] + assert m27_capabilities.context_window == 204_800 + assert m27_capabilities.input_modalities == ("text",) + assert m27_capabilities.thinking_modes == ("always_on",) + assert m27_capabilities.vision is False + assert m27_capabilities.reasoning is True + def test_add_provider_custom_models(self) -> None: cfg = add_provider( "custom", diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 1b7043b..5c0f0a6 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -108,6 +108,93 @@ def test_direct_minimax_provider_uses_upstream_model_id(self) -> None: assert upstream_model == "MiniMax-M3" +class TestProviderModelMetadata: + def test_minimax_byok_metadata_is_available_to_virtual_routing( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + routed: dict[str, object] = {} + + def fake_route(*_args, **kwargs) -> RoutingDecision: + routed["pricing"] = kwargs["pricing"] + routed["capabilities"] = kwargs["model_capabilities"] + return RoutingDecision( + model="minimax/minimax-m3", + tier=Tier.COMPLEX, + capability_lane=CapabilityLane.VISION, + served_quality=ServedQuality.PREMIUM, + served_quality_target=ServedQuality.PREMIUM, + served_quality_floor=ServedQuality.PREMIUM, + continuity_quality_floor=kwargs["routing_features"].continuity_quality_floor, + mode=RoutingMode.AUTO, + confidence=0.9, + method="provider metadata test", + reasoning="provider metadata test", + cost_estimate=0.001, + baseline_cost=0.002, + savings=0.5, + routing_features=kwargs["routing_features"], + ) + + captured: dict[str, object] = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured["body"] = json.loads(request.content.decode("utf-8")) + return httpx.Response( + 200, + json={ + "id": "chatcmpl_minimax_metadata", + "object": "chat.completion", + "created": 1, + "model": "MiniMax-M3", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + }], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + }, + headers={"content-type": "application/json"}, + ) + + providers = ProvidersConfig(providers={ + "minimax": ProviderEntry( + name="minimax", + api_key="test-key", + base_url="https://api.minimax.io/v1", + models=["minimax/minimax-m3", "minimax/minimax-m2.7"], + ), + }) + async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: async_client) + monkeypatch.setattr("uncommon_route.proxy.route", fake_route) + + try: + app = create_app( + upstream="https://primary.example/v1", + providers_config=providers, + spend_control=SpendControl(storage=InMemorySpendControlStorage()), + ) + client = TestClient(app, raise_server_exceptions=False) + resp = client.post("/v1/chat/completions", json={ + "model": "uncommon-route/auto", + "messages": [{"role": "user", "content": "describe this image"}], + }) + + assert resp.status_code == 200 + pricing = routed["pricing"] + capabilities = routed["capabilities"] + assert isinstance(pricing, dict) + assert isinstance(capabilities, dict) + assert pricing["minimax/minimax-m3"].cached_input_price == 0.06 + assert pricing["minimax/minimax-m2.7"].cache_write_price == 0.375 + assert capabilities["minimax/minimax-m3"].vision is True + assert capabilities["minimax/minimax-m2.7"].thinking_modes == ("always_on",) + assert captured["body"]["model"] == "MiniMax-M3" + finally: + asyncio.run(async_client.aclose()) + + class TestPromptExtraction: def test_extract_prompt_ignores_claude_code_wrapper_blocks(self) -> None: prompt, system_prompt, max_tokens = _extract_prompt({ diff --git a/uncommon_route/cache_support.py b/uncommon_route/cache_support.py index 78de6a8..32b3480 100644 --- a/uncommon_route/cache_support.py +++ b/uncommon_route/cache_support.py @@ -268,6 +268,7 @@ def parse_usage_metrics( actual_cost: float | None = None input_cost_multiplier = 1.0 if mp is not None: + effective_pricing = mp.for_usage(input_tokens_total) actual_cost = estimate_usage_cost( input_tokens_uncached=input_tokens_uncached, output_tokens=output_tokens, @@ -275,7 +276,7 @@ def parse_usage_metrics( cache_write_input_tokens=cache_write_input_tokens, pricing=mp, ) - baseline_input_cost = (input_tokens_total / 1_000_000) * mp.input_price + baseline_input_cost = (input_tokens_total / 1_000_000) * effective_pricing.input_price effective_input_cost = estimate_input_cost( input_tokens_uncached=input_tokens_uncached, cache_read_input_tokens=cache_read_input_tokens, @@ -411,6 +412,8 @@ def estimate_input_cost( cache_write_input_tokens: int, pricing: ModelPricing, ) -> float: + total_input_tokens = input_tokens_uncached + cache_read_input_tokens + cache_write_input_tokens + pricing = pricing.for_usage(total_input_tokens) cached_read_price = ( pricing.cached_input_price if pricing.cached_input_price is not None @@ -436,6 +439,8 @@ def estimate_usage_cost( cache_write_input_tokens: int, pricing: ModelPricing, ) -> float: + total_input_tokens = input_tokens_uncached + cache_read_input_tokens + cache_write_input_tokens + pricing = pricing.for_usage(total_input_tokens) return estimate_input_cost( input_tokens_uncached=input_tokens_uncached, cache_read_input_tokens=cache_read_input_tokens, diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 8adfccd..1cbb392 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -63,6 +63,7 @@ UPSTREAM_MODEL_IDS: dict[str, dict[str, str]] = { "minimax": { + "minimax/minimax-m2.5": "MiniMax-M2.5", "minimax/minimax-m3": "MiniMax-M3", "minimax/minimax-m2.7": "MiniMax-M2.7", }, diff --git a/uncommon_route/proxy.py b/uncommon_route/proxy.py index de6f775..d9b2013 100644 --- a/uncommon_route/proxy.py +++ b/uncommon_route/proxy.py @@ -59,6 +59,8 @@ BASELINE_MODEL, DEFAULT_CONFIG, DEFAULT_MODEL_PRICING, + PROVIDER_MODEL_CAPABILITIES, + PROVIDER_MODEL_PRICING, VIRTUAL_MODEL_IDS, routing_mode_from_model, virtual_model_entries, @@ -321,6 +323,7 @@ def _estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float: mp = _get_pricing().get(model) if mp is None: return 0.0 + mp = mp.for_usage(input_tokens) return (input_tokens / 1_000_000) * mp.input_price + (output_tokens / 1_000_000) * mp.output_price @@ -2847,6 +2850,11 @@ def _refresh_active_pricing() -> None: dynamic = _mapper.dynamic_pricing if dynamic: merged.update(dynamic) + keyed_models = _providers.keyed_models() + for model_id in keyed_models: + provider_pricing = PROVIDER_MODEL_PRICING.get(model_id) + if provider_pricing is not None: + merged[model_id] = provider_pricing _active_pricing = merged import copy @@ -2855,7 +2863,13 @@ def _refresh_active_pricing() -> None: if dynamic_caps: merged_caps = dict(updated.model_capabilities) merged_caps.update(dynamic_caps) - updated.model_capabilities = merged_caps + else: + merged_caps = dict(updated.model_capabilities) + for model_id in keyed_models: + provider_capabilities = PROVIDER_MODEL_CAPABILITIES.get(model_id) + if provider_capabilities is not None: + merged_caps[model_id] = provider_capabilities + updated.model_capabilities = merged_caps _routing_config = updated def _reload_providers() -> ProvidersConfig: @@ -2863,8 +2877,11 @@ def _reload_providers() -> ProvidersConfig: _providers = load_providers() if isinstance(_semantic, UpstreamSemanticCompressor): _semantic.rebind_providers(_providers) + _refresh_active_pricing() return _providers + _refresh_active_pricing() + def _current_connection_payload() -> dict[str, Any]: effective = resolve_primary_connection( cli_upstream=_cli_upstream_override, diff --git a/uncommon_route/router/config.py b/uncommon_route/router/config.py index f3863f1..a90316a 100644 --- a/uncommon_route/router/config.py +++ b/uncommon_route/router/config.py @@ -6,6 +6,7 @@ ModeConfig, ModelCapabilities, ModelPricing, + ModelPricingTier, RoutingConfig, RoutingMode, ScoringConfig, @@ -42,6 +43,68 @@ "anthropic/claude-opus-4-7": ModelPricing(5.00, 25.00, cached_input_price=0.50, cache_write_price=6.25), } +PROVIDER_MODEL_PRICING: dict[str, ModelPricing] = { + "minimax/minimax-m3": ModelPricing( + 0.30, + 1.20, + cached_input_price=0.06, + pricing_tiers=( + ModelPricingTier( + service_tier="standard", + input_tokens_lte=512_000, + input_price=0.30, + output_price=1.20, + cached_input_price=0.06, + ), + ModelPricingTier( + service_tier="standard", + input_tokens_gt=512_000, + input_price=0.60, + output_price=2.40, + cached_input_price=0.12, + ), + ModelPricingTier( + service_tier="priority", + input_tokens_lte=512_000, + input_price=0.45, + output_price=1.80, + cached_input_price=0.09, + ), + ModelPricingTier( + service_tier="priority", + input_tokens_gt=512_000, + input_price=0.90, + output_price=3.60, + cached_input_price=0.18, + ), + ), + ), + "minimax/minimax-m2.7": ModelPricing( + 0.30, + 1.20, + cached_input_price=0.06, + cache_write_price=0.375, + ), +} + +PROVIDER_MODEL_CAPABILITIES: dict[str, ModelCapabilities] = { + "minimax/minimax-m3": ModelCapabilities( + tool_calling=True, + vision=True, + reasoning=True, + context_window=1_000_000, + input_modalities=("text", "image", "video"), + thinking_modes=("adaptive", "disabled"), + ), + "minimax/minimax-m2.7": ModelCapabilities( + tool_calling=True, + reasoning=True, + context_window=204_800, + input_modalities=("text",), + thinking_modes=("always_on",), + ), +} + BASELINE_MODEL = "anthropic/claude-opus-4-7" VIRTUAL_MODEL_IDS: dict[RoutingMode, str] = { diff --git a/uncommon_route/router/selector.py b/uncommon_route/router/selector.py index 9803192..7f42b60 100644 --- a/uncommon_route/router/selector.py +++ b/uncommon_route/router/selector.py @@ -94,7 +94,7 @@ def _calc_cost( *, input_cost_multiplier: float = 1.0, ) -> float: - mp = _pricing_for_model(model, pricing) + mp = _pricing_for_model(model, pricing).for_usage(input_tokens) effective_multiplier = max(0.1, min(2.0, input_cost_multiplier)) return ( (input_tokens / 1_000_000) * mp.input_price * effective_multiplier diff --git a/uncommon_route/router/types.py b/uncommon_route/router/types.py index 4a9a319..ccf2a9e 100644 --- a/uncommon_route/router/types.py +++ b/uncommon_route/router/types.py @@ -60,6 +60,9 @@ class ModelCapabilities: free: bool = False local: bool = False responses: bool = False + context_window: int | None = None + input_modalities: tuple[str, ...] = () + thinking_modes: tuple[str, ...] = () @dataclass(frozen=True, slots=True) @@ -416,12 +419,45 @@ class TierConfig: hard_pin: bool = False +@dataclass(frozen=True, slots=True) +class ModelPricingTier: + service_tier: str + input_price: float + output_price: float + cached_input_price: float | None = None + cache_write_price: float | None = None + input_tokens_lte: int | None = None + input_tokens_gt: int | None = None + + def matches(self, input_tokens: int, service_tier: str) -> bool: + if self.service_tier != service_tier: + return False + if self.input_tokens_lte is not None and input_tokens > self.input_tokens_lte: + return False + if self.input_tokens_gt is not None and input_tokens <= self.input_tokens_gt: + return False + return True + + @dataclass(frozen=True, slots=True) class ModelPricing: input_price: float # per 1M tokens output_price: float # per 1M tokens cached_input_price: float | None = None # per 1M cached-read tokens cache_write_price: float | None = None # per 1M cache-write / cache-create tokens + pricing_tiers: tuple[ModelPricingTier, ...] = () + + def for_usage(self, input_tokens: int, service_tier: str = "standard") -> ModelPricing: + normalized_tier = str(service_tier or "standard").strip().lower() + for tier in self.pricing_tiers: + if tier.matches(max(0, input_tokens), normalized_tier): + return ModelPricing( + input_price=tier.input_price, + output_price=tier.output_price, + cached_input_price=tier.cached_input_price, + cache_write_price=tier.cache_write_price, + ) + return self @dataclass(frozen=True, slots=True) diff --git a/uncommon_route/stats.py b/uncommon_route/stats.py index e59ba87..dce6b4e 100644 --- a/uncommon_route/stats.py +++ b/uncommon_route/stats.py @@ -256,6 +256,8 @@ def _cache_savings(r: RouteRecord) -> float: pricing = _get_stats_pricing().get(r.model) if pricing is None: return 0.0 + billed_input_tokens = r.usage_input_tokens or r.input_tokens_after + pricing = pricing.for_usage(billed_input_tokens) cached_input_price = pricing.cached_input_price if pricing.cached_input_price is not None else pricing.input_price cache_write_price = pricing.cache_write_price if pricing.cache_write_price is not None else pricing.input_price read_delta = ((pricing.input_price - cached_input_price) * r.cache_read_input_tokens) / 1_000_000 @@ -269,8 +271,11 @@ def _compaction_savings(r: RouteRecord) -> float: pricing = _get_stats_pricing().get(r.model) if pricing is None: return 0.0 - reduced_tokens = r.input_tokens_before - r.input_tokens_after - return (reduced_tokens / 1_000_000) * pricing.input_price + before_pricing = pricing.for_usage(r.input_tokens_before) + after_pricing = pricing.for_usage(r.input_tokens_after) + before_cost = (r.input_tokens_before / 1_000_000) * before_pricing.input_price + after_cost = (r.input_tokens_after / 1_000_000) * after_pricing.input_price + return max(0.0, before_cost - after_cost) class RouteStats: