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 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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: From 26073fa5a01e92605660cb6895488a695436a04b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:41:41 +0800 Subject: [PATCH 09/12] Honor service tiers in pricing calculations --- tests/test_cache_support.py | 59 ++++++++++++++++++++++++++++++++- tests/test_proxy.py | 14 ++++++++ uncommon_route/cache_support.py | 30 ++++++++++++++--- uncommon_route/proxy.py | 54 +++++++++++++++++++++++++----- uncommon_route/router/types.py | 21 +++++++----- 5 files changed, 157 insertions(+), 21 deletions(-) diff --git a/tests/test_cache_support.py b/tests/test_cache_support.py index ef534f5..072921c 100644 --- a/tests/test_cache_support.py +++ b/tests/test_cache_support.py @@ -2,7 +2,14 @@ import json -from uncommon_route.cache_support import apply_anthropic_cache_breakpoints, estimate_usage_cost, parse_stream_usage_metrics +import pytest + +from uncommon_route.cache_support import ( + apply_anthropic_cache_breakpoints, + estimate_usage_cost, + parse_stream_usage_metrics, + parse_usage_metrics, +) from uncommon_route.router.types import ModelPricing, ModelPricingTier @@ -109,3 +116,53 @@ def test_usage_cost_applies_long_context_pricing_tier() -> None: ) assert cost == 0.60 + + +def test_usage_cost_applies_priority_pricing_tier() -> None: + pricing = ModelPricing( + 0.30, + 1.20, + pricing_tiers=( + ModelPricingTier("standard", 0.60, 2.40, input_tokens_gt=512_000), + ModelPricingTier("priority", 0.90, 3.60, 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, + service_tier="priority", + ) + + assert cost == pytest.approx(0.90) + + +def test_usage_metrics_prefers_response_service_tier() -> None: + pricing = ModelPricing( + 0.30, + 1.20, + pricing_tiers=( + ModelPricingTier("standard", 0.60, 2.40, input_tokens_gt=512_000), + ModelPricingTier("priority", 0.90, 3.60, input_tokens_gt=512_000), + ), + ) + content = json.dumps({ + "service_tier": "priority", + "usage": { + "prompt_tokens": 600_000, + "completion_tokens": 100_000, + "total_tokens": 700_000, + }, + }).encode() + + usage = parse_usage_metrics( + content, + "test/model", + {"test/model": pricing}, + ) + + assert usage is not None + assert usage.actual_cost == pytest.approx(0.90) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 5c0f0a6..50f6b2f 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -114,6 +114,16 @@ def test_minimax_byok_metadata_is_available_to_virtual_routing( monkeypatch: pytest.MonkeyPatch, ) -> None: routed: dict[str, object] = {} + estimated_tiers: list[str] = [] + + def fake_estimate_cost( + _model: str, + _input_tokens: int, + _output_tokens: int, + service_tier: str = "standard", + ) -> float: + estimated_tiers.append(service_tier) + return 0.001 def fake_route(*_args, **kwargs) -> RoutingDecision: routed["pricing"] = kwargs["pricing"] @@ -167,6 +177,7 @@ 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.setattr("uncommon_route.proxy._estimate_cost", fake_estimate_cost) monkeypatch.setattr("uncommon_route.proxy.route", fake_route) try: @@ -178,6 +189,7 @@ def handler(request: httpx.Request) -> httpx.Response: client = TestClient(app, raise_server_exceptions=False) resp = client.post("/v1/chat/completions", json={ "model": "uncommon-route/auto", + "service_tier": "priority", "messages": [{"role": "user", "content": "describe this image"}], }) @@ -191,6 +203,8 @@ def handler(request: httpx.Request) -> httpx.Response: assert capabilities["minimax/minimax-m3"].vision is True assert capabilities["minimax/minimax-m2.7"].thinking_modes == ("always_on",) assert captured["body"]["model"] == "MiniMax-M3" + assert captured["body"]["service_tier"] == "priority" + assert "priority" in estimated_tiers finally: asyncio.run(async_client.aclose()) diff --git a/uncommon_route/cache_support.py b/uncommon_route/cache_support.py index 32b3480..3416fd0 100644 --- a/uncommon_route/cache_support.py +++ b/uncommon_route/cache_support.py @@ -205,6 +205,7 @@ def parse_usage_metrics( content: bytes, model: str, pricing: dict[str, ModelPricing], + service_tier: str = "standard", ) -> UsageMetrics | None: try: data = json.loads(content) @@ -214,6 +215,9 @@ def parse_usage_metrics( usage = data.get("usage") or {} if not isinstance(usage, dict): return None + effective_service_tier = str( + data.get("service_tier") or usage.get("service_tier") or service_tier or "standard" + ).strip().lower() prompt_details = usage.get("prompt_tokens_details") or usage.get("input_tokens_details") or {} output_details = usage.get("completion_tokens_details") or usage.get("output_tokens_details") or {} @@ -268,13 +272,14 @@ 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) + effective_pricing = mp.for_usage(input_tokens_total, effective_service_tier) actual_cost = estimate_usage_cost( input_tokens_uncached=input_tokens_uncached, output_tokens=output_tokens, cache_read_input_tokens=cache_read_input_tokens, cache_write_input_tokens=cache_write_input_tokens, pricing=mp, + service_tier=effective_service_tier, ) baseline_input_cost = (input_tokens_total / 1_000_000) * effective_pricing.input_price effective_input_cost = estimate_input_cost( @@ -282,6 +287,7 @@ def parse_usage_metrics( cache_read_input_tokens=cache_read_input_tokens, cache_write_input_tokens=cache_write_input_tokens, pricing=mp, + service_tier=effective_service_tier, ) if baseline_input_cost > 0: input_cost_multiplier = max(0.05, min(2.0, effective_input_cost / baseline_input_cost)) @@ -318,6 +324,7 @@ def parse_stream_usage_metrics( chunks: list[bytes], model: str, pricing: dict[str, ModelPricing], + service_tier: str = "standard", ) -> UsageMetrics | None: if not chunks: return None @@ -331,6 +338,7 @@ def parse_stream_usage_metrics( fallback_output_tokens = 0 fallback_prompt_tokens = 0 fallback_has_content = False + effective_service_tier = service_tier for line in text.splitlines(): if not line.startswith("data:"): @@ -342,9 +350,16 @@ def parse_stream_usage_metrics( data = json.loads(payload) except json.JSONDecodeError: continue + if isinstance(data, dict) and data.get("service_tier"): + effective_service_tier = str(data["service_tier"]) if isinstance(data, dict) and isinstance(data.get("usage"), dict): anthropic_usage.update(data["usage"]) - parsed = parse_usage_metrics(json.dumps({"usage": anthropic_usage}).encode("utf-8"), model, pricing) + parsed = parse_usage_metrics( + json.dumps({"usage": anthropic_usage}).encode("utf-8"), + model, + pricing, + effective_service_tier, + ) if parsed is not None: latest = parsed continue @@ -353,6 +368,8 @@ def parse_stream_usage_metrics( message = data.get("message") if isinstance(message, dict) and isinstance(message.get("usage"), dict): anthropic_usage.update(message["usage"]) + if isinstance(message, dict) and message.get("service_tier"): + effective_service_tier = str(message["service_tier"]) if data.get("type") == "message_delta" and isinstance(data.get("usage"), dict): anthropic_usage.update(data["usage"]) if anthropic_usage: @@ -360,6 +377,7 @@ def parse_stream_usage_metrics( json.dumps({"usage": anthropic_usage}).encode("utf-8"), model, pricing, + effective_service_tier, ) if parsed is not None: latest = parsed @@ -388,6 +406,7 @@ def parse_stream_usage_metrics( cache_read_input_tokens=0, cache_write_input_tokens=0, pricing=mp, + service_tier=effective_service_tier, ) latest = UsageMetrics( input_tokens_total=fallback_prompt_tokens, @@ -411,9 +430,10 @@ def estimate_input_cost( cache_read_input_tokens: int, cache_write_input_tokens: int, pricing: ModelPricing, + service_tier: str = "standard", ) -> float: total_input_tokens = input_tokens_uncached + cache_read_input_tokens + cache_write_input_tokens - pricing = pricing.for_usage(total_input_tokens) + pricing = pricing.for_usage(total_input_tokens, service_tier) cached_read_price = ( pricing.cached_input_price if pricing.cached_input_price is not None @@ -438,14 +458,16 @@ def estimate_usage_cost( cache_read_input_tokens: int, cache_write_input_tokens: int, pricing: ModelPricing, + service_tier: str = "standard", ) -> float: total_input_tokens = input_tokens_uncached + cache_read_input_tokens + cache_write_input_tokens - pricing = pricing.for_usage(total_input_tokens) + pricing = pricing.for_usage(total_input_tokens, service_tier) return estimate_input_cost( input_tokens_uncached=input_tokens_uncached, cache_read_input_tokens=cache_read_input_tokens, cache_write_input_tokens=cache_write_input_tokens, pricing=pricing, + service_tier=service_tier, ) + ((output_tokens / 1_000_000) * pricing.output_price) diff --git a/uncommon_route/proxy.py b/uncommon_route/proxy.py index d9b2013..14d9074 100644 --- a/uncommon_route/proxy.py +++ b/uncommon_route/proxy.py @@ -318,12 +318,17 @@ def _get_pricing() -> dict[str, ModelPricing]: return _active_pricing or DEFAULT_MODEL_PRICING -def _estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float: +def _estimate_cost( + model: str, + input_tokens: int, + output_tokens: int, + service_tier: str = "standard", +) -> float: """Compute dollar cost from token counts using the model pricing table.""" mp = _get_pricing().get(model) if mp is None: return 0.0 - mp = mp.for_usage(input_tokens) + mp = mp.for_usage(input_tokens, service_tier) return (input_tokens / 1_000_000) * mp.input_price + (output_tokens / 1_000_000) * mp.output_price @@ -3952,6 +3957,7 @@ async def _handle_chat_core( model = (body.get("model") or "").strip().lower() is_streaming = body.get("stream", False) + request_service_tier = str(body.get("service_tier") or "standard").strip().lower() response_model = str(body.pop("_client_requested_model", "") or model).strip() recursion_guarded = _recursion_guard_enabled(request) @@ -4164,6 +4170,7 @@ async def _handle_chat_core( _active_scene.primary, input_token_estimate, scene_output_budget, + request_service_tier, ) scene_baseline = _estimate_baseline_cost( input_token_estimate, @@ -4421,6 +4428,7 @@ async def _handle_chat_core( selected_model, input_tokens_after, effective_output_tokens, + request_service_tier, ) baseline_cost = _estimate_baseline_cost( input_tokens_before if input_tokens_before > 0 else input_tokens_after, @@ -4578,7 +4586,12 @@ async def _handle_chat_core( full_text = f"{system_prompt or ''} {prompt}".strip() input_tokens_before = estimate_tokens(full_text) if full_text else 0 input_tokens_after = input_tokens_before - estimated_cost = _estimate_cost(selected_model, input_tokens_after, max_tokens) + estimated_cost = _estimate_cost( + selected_model, + input_tokens_after, + max_tokens, + request_service_tier, + ) baseline_cost = estimated_cost main_estimated_cost = estimated_cost effective_output_tokens = max_tokens @@ -4618,7 +4631,12 @@ async def _handle_chat_core( def _estimated_total_cost_for(model_name: str) -> tuple[float, float]: token_input = input_tokens_after if input_tokens_after > 0 else input_tokens_before - main_cost = _estimate_cost(model_name, token_input, effective_output_tokens) + main_cost = _estimate_cost( + model_name, + token_input, + effective_output_tokens, + request_service_tier, + ) total_cost = main_cost + (sidechannel_estimated_cost if is_virtual else 0.0) return main_cost, total_cost @@ -5630,7 +5648,12 @@ async def anthropic_native_sse() -> AsyncGenerator[bytes, None]: for ev in converter.finish(): yield ev await _record_stream_success( - parse_stream_usage_metrics(stream_chunks, selected_model, _get_pricing()), + parse_stream_usage_metrics( + stream_chunks, + selected_model, + _get_pricing(), + request_service_tier, + ), stream_chunks=stream_chunks, ) except Exception: @@ -5669,7 +5692,12 @@ async def anthropic_sse() -> AsyncGenerator[bytes, None]: for ev in converter.finish(): yield ev await _record_stream_success( - parse_stream_usage_metrics(stream_chunks, selected_model, _get_pricing()), + parse_stream_usage_metrics( + stream_chunks, + selected_model, + _get_pricing(), + request_service_tier, + ), stream_chunks=stream_chunks, ) except Exception: @@ -5697,7 +5725,12 @@ async def sse_passthrough() -> AsyncGenerator[bytes, None]: _mark_current_attempt_first_token() yield _normalize_reasoning_content_chunk(chunk) await _record_stream_success( - parse_stream_usage_metrics(stream_chunks, selected_model, _get_pricing()), + parse_stream_usage_metrics( + stream_chunks, + selected_model, + _get_pricing(), + request_service_tier, + ), stream_chunks=stream_chunks, ) except Exception: @@ -5771,7 +5804,12 @@ async def sse_passthrough() -> AsyncGenerator[bytes, None]: tps: float | None = None usage_metrics: UsageMetrics | None = None if resp.status_code == 200: - usage_metrics = parse_usage_metrics(resp.content, selected_model, _get_pricing()) + usage_metrics = parse_usage_metrics( + resp.content, + selected_model, + _get_pricing(), + request_service_tier, + ) if usage_metrics is not None: actual_cost = ( usage_metrics.actual_cost diff --git a/uncommon_route/router/types.py b/uncommon_route/router/types.py index ccf2a9e..01d42b4 100644 --- a/uncommon_route/router/types.py +++ b/uncommon_route/router/types.py @@ -449,14 +449,19 @@ class ModelPricing: 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, - ) + available_tiers = {tier.service_tier for tier in self.pricing_tiers} + tier_candidates = [normalized_tier] + if normalized_tier not in available_tiers and "standard" in available_tiers: + tier_candidates.append("standard") + for tier_name in tier_candidates: + for tier in self.pricing_tiers: + if tier.matches(max(0, input_tokens), tier_name): + 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 From c7eae75fbef43fc3bd0d5996a91dfa59b3c0d3c7 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:16:26 +0000 Subject: [PATCH 10/12] Align MiniMax M3 pricing with provider defaults --- tests/test_providers.py | 8 +++---- tests/test_proxy.py | 5 ++++- uncommon_route/router/config.py | 37 +++------------------------------ 3 files changed, 11 insertions(+), 39 deletions(-) diff --git a/tests/test_providers.py b/tests/test_providers.py index baccd6e..6086853 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -61,10 +61,10 @@ def test_resolve_minimax_upstream_model_ids(self) -> None: 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 + assert m3_pricing.input_price == 0.60 + assert m3_pricing.output_price == 2.40 + assert m3_pricing.cached_input_price == 0.12 + assert m3_pricing.cache_write_price is None m27_pricing = PROVIDER_MODEL_PRICING["minimax/minimax-m2.7"] assert m27_pricing.cached_input_price == 0.06 diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 50f6b2f..a3e7d5b 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -198,7 +198,10 @@ def handler(request: httpx.Request) -> httpx.Response: 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-m3"].input_price == 0.60 + assert pricing["minimax/minimax-m3"].output_price == 2.40 + assert pricing["minimax/minimax-m3"].cached_input_price == 0.12 + assert pricing["minimax/minimax-m3"].cache_write_price is None 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",) diff --git a/uncommon_route/router/config.py b/uncommon_route/router/config.py index a90316a..3131ecb 100644 --- a/uncommon_route/router/config.py +++ b/uncommon_route/router/config.py @@ -6,7 +6,6 @@ ModeConfig, ModelCapabilities, ModelPricing, - ModelPricingTier, RoutingConfig, RoutingMode, ScoringConfig, @@ -45,39 +44,9 @@ 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, - ), - ), + 0.60, + 2.40, + cached_input_price=0.12, ), "minimax/minimax-m2.7": ModelPricing( 0.30, From fa1f4663445573e0a9d8618ba2e4149ef6d3b0c9 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:46:36 +0800 Subject: [PATCH 11/12] Align MiniMax defaults with current models --- tests/test_providers.py | 16 +++++++++++++--- uncommon_route/model_map.py | 1 + uncommon_route/providers.py | 4 +--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/test_providers.py b/tests/test_providers.py index 6086853..fa4bf25 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -15,6 +15,7 @@ remove_provider, select_preferred_model, ) +from uncommon_route.model_map import detect_provider from uncommon_route.router.config import PROVIDER_MODEL_CAPABILITIES, PROVIDER_MODEL_PRICING from uncommon_route.router.types import RoutingFeatures, Tier @@ -44,7 +45,6 @@ def test_add_provider_with_plan(self) -> None: entry = cfg.providers["minimax"] assert entry.plan == "coding-plan" assert entry.models == [ - "minimax/minimax-m2.5", "minimax/minimax-m3", "minimax/minimax-m2.7", ] @@ -54,11 +54,20 @@ 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" + @pytest.mark.parametrize( + "base_url", + [ + "https://api.minimax.io/v1", + "https://api.minimaxi.com/v1", + ], + ) + def test_detect_minimax_regional_endpoints(self, base_url: str) -> None: + assert detect_provider(base_url) == ("minimax", False) + def test_minimax_provider_model_metadata(self) -> None: m3_pricing = PROVIDER_MODEL_PRICING["minimax/minimax-m3"] assert m3_pricing.input_price == 0.60 @@ -118,7 +127,8 @@ def test_keyed_models(self) -> None: keyed = cfg.keyed_models() assert "deepseek/deepseek-chat" in keyed assert "deepseek/deepseek-reasoner" in keyed - assert "minimax/minimax-m2.5" in keyed + assert "minimax/minimax-m3" in keyed + assert "minimax/minimax-m2.7" in keyed def test_get_for_model(self) -> None: add_provider("deepseek", "sk-1") diff --git a/uncommon_route/model_map.py b/uncommon_route/model_map.py index f84cc3d..0a19631 100644 --- a/uncommon_route/model_map.py +++ b/uncommon_route/model_map.py @@ -48,6 +48,7 @@ "api.anthropic.com": "anthropic", "api.deepseek.com": "deepseek", "api.minimax.io": "minimax", + "api.minimaxi.com": "minimax", "api.minimax.chat": "minimax", "generativelanguage.googleapis.com": "google", "api.x.ai": "xai", diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 1cbb392..786a900 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -13,7 +13,6 @@ "api_key": "eyJ...", "base_url": "https://api.minimax.io/v1", "models": [ - "minimax/minimax-m2.5", "minimax/minimax-m3", "minimax/minimax-m2.7" ], @@ -52,7 +51,7 @@ } PROVIDER_MODELS: dict[str, list[str]] = { - "minimax": ["minimax/minimax-m2.5", "minimax/minimax-m3", "minimax/minimax-m2.7"], + "minimax": ["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"], @@ -63,7 +62,6 @@ 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", }, From ff89fed4e741076429999490eb23e9fa30d7a048 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:10:33 +0800 Subject: [PATCH 12/12] feat(minimax): prefer Flash and preserve transport controls --- README.md | 30 ++++ tests/test_anthropic_compat.py | 12 +- tests/test_minimax_rollout.py | 223 +++++++++++++++++++++++++++++ tests/test_providers.py | 9 +- uncommon_route/minimax.py | 60 ++++++++ uncommon_route/model_map.py | 34 ++++- uncommon_route/providers.py | 10 +- uncommon_route/proxy.py | 32 ++++- uncommon_route/responses_compat.py | 70 ++++++++- uncommon_route/router/config.py | 13 ++ uncommon_route/router/types.py | 1 + 11 files changed, 462 insertions(+), 32 deletions(-) create mode 100644 tests/test_minimax_rollout.py create mode 100644 uncommon_route/minimax.py diff --git a/README.md b/README.md index 5606d53..56f9076 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,36 @@ Provider configuration stores the OpenAI-compatible base URL. Anthropic requests use the corresponding `/anthropic` base and append `/v1/messages` internally. +New MiniMax connections offer **MiniMax-M3.1-Flash**, then **MiniMax-M3**. +Saved custom and legacy model lists remain usable; older MiniMax text models +are omitted from ordinary discovery suggestions. Image, speech, and video +model entries are unaffected. + +Direct Flash requests use adaptive thinking with `max` effort by default; +`low`, `medium`, `high`, `xhigh`, and `max` are accepted. The proxy removes +unsupported `service_tier` and `reasoning_split=false` controls. Native +Anthropic requests carry effort in `output_config.effort`; OpenAI-compatible +requests use `reasoning_effort`. M3 supports adaptive/disabled thinking without +an effort control. Tools, reasoning history, and image/video content are kept. + +Direct Flash advertises a 1,000,000-token context and 524,288-token maximum +output. Flash costs $0.15 input / $0.015 cached input / $0.60 output per million +tokens. M3 standard bands are $0.30/$0.06/$1.20 and $0.60/$0.12/$2.40; priority +bands are $0.45/$0.09/$1.80 and $0.90/$0.18/$3.60 (input/cache/output). +Until the M3 input-band boundary is verified, local estimates conservatively +use the upper band. Flash has no priority tier or Coding Plan promise. + +Gateway catalogs remain authoritative for IDs, rates, context, and modalities. +For example, OpenRouter currently publishes `minimax/minimax-m3.1-flash:free`, +with zero catalog pricing and a 1,000,000-token context; requests use its +`reasoning.effort` field and preserve `reasoning_details`. Commonstack models +are taken from its live `/models` discovery; no unverified Flash slug is added. +These gateway values are never replaced with direct MiniMax prices. + +Sources: [MiniMax OpenAI API](https://platform.minimax.io/docs/api-reference/text-openai-api.md), +[MiniMax pricing](https://platform.minimax.io/docs/guides/pricing-paygo.md), +[OpenRouter model catalog](https://openrouter.ai/api/v1/models). + > 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 519b1ee..4f1b4dc 100644 --- a/tests/test_anthropic_compat.py +++ b/tests/test_anthropic_compat.py @@ -1181,10 +1181,7 @@ def handler(request: httpx.Request) -> httpx.Response: ) assert resp.status_code == 200 - assert set(routed["available_models"]) == { - "deepseek/deepseek-v3.2", - "minimax/minimax-m2.1", - } + assert set(routed["available_models"]) == {"deepseek/deepseek-v3.2"} assert routed["capability_lane"] == CapabilityLane.GENERAL assert resp.headers["x-uncommon-route-requested-transport"] == "anthropic-messages" assert resp.headers["x-uncommon-route-transport"] == "openai-chat" @@ -1675,10 +1672,7 @@ def handler(request: httpx.Request) -> httpx.Response: ) assert resp.status_code == 200 - assert set(routed["available_models"]) == { - "minimax/minimax-m2.7", - "deepseek/deepseek-v3.2", - } + assert set(routed["available_models"]) == {"deepseek/deepseek-v3.2"} assert captured["url"] == "https://api.commonstack.ai/v1/chat/completions" body = captured["body"] assert isinstance(body, dict) @@ -1779,8 +1773,6 @@ def handler(request: httpx.Request) -> httpx.Response: "deepseek/deepseek-v3.2", "anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-4-6", - "minimax/minimax-m2.1", - "minimax/minimax-m2.7", } assert captured["url"] == "https://api.commonstack.ai/v1/chat/completions" body = captured["body"] diff --git a/tests/test_minimax_rollout.py b/tests/test_minimax_rollout.py new file mode 100644 index 0000000..a8fcc1e --- /dev/null +++ b/tests/test_minimax_rollout.py @@ -0,0 +1,223 @@ +"""Exercise MiniMax selection and serialized HTTP requests without inference.""" + +import asyncio +import json + +import httpx +import pytest +from starlette.testclient import TestClient + +from uncommon_route.model_map import DiscoveredModel, ModelMapper, ordinary_model_choices +from uncommon_route.providers import ProviderEntry, ProvidersConfig, load_providers, save_providers +from uncommon_route.proxy import create_app +from uncommon_route.router.types import ModelCapabilities, ModelPricing + + +FLASH = "minimax/minimax-m3.1-flash" + + +def test_defaults_and_saved_models(tmp_path): + path = tmp_path / "providers.json" + path.write_text(json.dumps({"providers": {"minimax": {"api_key": "fixture"}}})) + assert load_providers(path).providers["minimax"].models == [FLASH, "minimax/minimax-m3"] + saved = ["minimax/minimax-m2.5", "minimax/custom-model"] + config = ProvidersConfig({"minimax": ProviderEntry("minimax", "fixture", "https://custom.test/v1", saved)}) + save_providers(config, path) + assert load_providers(path).providers["minimax"].models == saved + + +def test_ordinary_pool_preserves_explicit_legacy_and_media(): + models = ["minimax/minimax-m2.5", "minimax/minimax-m3", FLASH + ":free", "vendor/model", "minimax/image-01"] + mapper = ModelMapper("https://openrouter.ai/api/v1") + mapper._discovered = True + mapper._upstream_models = set(models) + for model in models: + mapper._pool[model] = DiscoveredModel(model, "minimax", "minimax", ModelPricing(0, 0), ModelCapabilities()) + assert ordinary_model_choices(models) == [FLASH + ":free", "minimax/minimax-m3", "vendor/model", "minimax/image-01"] + assert mapper.routable_models == [FLASH + ":free", "minimax/minimax-m3", "vendor/model"] + assert mapper.resolve(models[0]) == models[0] + assert models[0] not in [row["id"] for row in mapper.pool_table()] + assert "minimax/image-01" in [row["id"] for row in mapper.pool_table()] + + +@pytest.mark.parametrize("host", ["api.minimax.io", "api.minimaxi.com"]) +@pytest.mark.parametrize("effort", [None, "low", "medium", "high", "xhigh", "max", "none"]) +def test_flash_chat_http_boundary(monkeypatch, host, effort): + captured = [] + + def handler(request): + captured.append((str(request.url), json.loads(request.content))) + return httpx.Response(200, json={"id": "fixture", "object": "chat.completion", "choices": [ + {"index": 0, "message": {"role": "assistant", "content": "ok", "reasoning_content": "reason"}, "finish_reason": "stop"} + ]}) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: client) + providers = ProvidersConfig({"minimax": ProviderEntry("minimax", "fixture", f"https://{host}/v1", [FLASH])}) + app = create_app(upstream="https://unused.test/v1", providers_config=providers) + messages = [ + {"role": "user", "content": [{"type": "text", "text": "Inspect"}, + {"type": "image_url", "image_url": {"url": "https://example.test/image.png"}}, + {"type": "video_url", "video_url": {"url": "mm_file://fixture"}}]}, + {"role": "assistant", "content": "keep", "reasoning_content": "keep reasoning", + "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "inspect", "arguments": "{}"}}]}, + {"role": "tool", "tool_call_id": "call_1", "content": "done"}, + ] + tools = [{"type": "function", "function": {"name": "inspect", "parameters": {"type": "object", "properties": {}}}}] + body = {"model": FLASH, "messages": messages, "tools": tools, "thinking": {"type": "disabled"}, + "service_tier": "priority", "reasoning_split": False} + if effort is not None: + body["reasoning_effort"] = effort + try: + response = TestClient(app).post("/v1/chat/completions", json=body) + assert response.status_code == 200 + url, sent = captured[-1] + assert url == f"https://{host}/v1/chat/completions" + assert sent["model"] == "MiniMax-M3.1-Flash" + assert sent["thinking"] == {"type": "adaptive"} + assert sent["reasoning_effort"] == (effort if effort not in (None, "none") else "max") + assert "service_tier" not in sent and "reasoning_split" not in sent + assert sent["messages"] == messages + assert sent["tools"] == tools + finally: + asyncio.run(client.aclose()) + + +@pytest.mark.parametrize("model,base,expected", [ + (FLASH, "https://api.minimax.io/v1", "MiniMax-M3.1-Flash"), + ("minimax/minimax-m3", "https://api.minimax.io/v1", "MiniMax-M3"), + ("minimax/minimax-m2.7", "https://api.minimax.io/v1", "MiniMax-M2.7"), + (FLASH, "https://custom.test/v1", FLASH), + (FLASH, "https://api.minimax.io.example.test/v1", FLASH), +]) +def test_anthropic_http_history_and_scope(monkeypatch, model, base, expected): + captured = [] + + def handler(request): + captured.append(json.loads(request.content)) + return httpx.Response(200, json={"id": "fixture", "type": "message", "role": "assistant", "model": expected, + "content": [{"type": "text", "text": "ok"}], "stop_reason": "end_turn", "usage": {"input_tokens": 1, "output_tokens": 1}}) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: client) + providers = ProvidersConfig({"minimax": ProviderEntry("minimax", "fixture", base, [model])}) + app = create_app(upstream="https://unused.test/v1", providers_config=providers) + messages = [ + {"role": "user", "content": [{"type": "text", "text": "Inspect"}, {"type": "image", "source": {"type": "url", "url": "https://example.test/a.png"}}]}, + {"role": "assistant", "content": [{"type": "thinking", "thinking": "keep", "signature": "signed"}, + {"type": "tool_use", "id": "call_1", "name": "inspect", "input": {}}]}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "done"}]}, + ] + body = {"model": model, "messages": messages, "max_tokens": 1000, "thinking": {"type": "disabled"}, + "output_config": {"effort": "low"}, "service_tier": "priority", + "tools": [{"name": "inspect", "input_schema": {"type": "object", "properties": {}}}]} + try: + response = TestClient(app).post("/v1/messages", json=body) + assert response.status_code == 200 + sent = captured[-1] + assert sent["model"] == expected + if model == FLASH and base == "https://api.minimax.io/v1": + assert sent["thinking"] == {"type": "adaptive"} + assert sent["output_config"]["effort"] == "low" + assert "service_tier" not in sent + elif model == "minimax/minimax-m3": + assert sent["thinking"] == {"type": "disabled"} + assert "effort" not in sent.get("output_config", {}) + assert sent["service_tier"] == "priority" + # Native transport preserves signed reasoning and all tool/image blocks. + if base == "https://api.minimax.io/v1": + for actual, original in zip(sent["messages"], messages): + for actual_block, original_block in zip(actual["content"], original["content"]): + assert {key: actual_block[key] for key in original_block} == original_block + finally: + asyncio.run(client.aclose()) + + +@pytest.mark.parametrize("stream", [False, True]) +def test_openrouter_exact_id_and_reasoning_http(monkeypatch, stream): + captured = [] + delta = {"role": "assistant", "content": "ok", "reasoning_details": [{"type": "reasoning.text", "text": "kept"}]} + + def handler(request): + captured.append(json.loads(request.content)) + if stream: + chunk = {"id": "fixture", "choices": [{"index": 0, "delta": delta}]} + return httpx.Response(200, content=f"data: {json.dumps(chunk)}\n\ndata: [DONE]\n\n", headers={"content-type": "text/event-stream"}) + return httpx.Response(200, json={"id": "fixture", "choices": [{"index": 0, "message": delta, "finish_reason": "stop"}]}) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: client) + app = create_app(upstream="https://openrouter.ai/api/v1", providers_config=ProvidersConfig()) + message = {"role": "assistant", "content": "previous", "reasoning_details": delta["reasoning_details"]} + try: + response = TestClient(app).post("/v1/chat/completions", json={ + "model": FLASH + ":free", "messages": [message, {"role": "user", "content": "next"}], "stream": stream, + "reasoning": {"effort": "none", "enabled": False, "exclude": True}, "service_tier": "priority", + }) + assert response.status_code == 200 + sent = captured[-1] + assert sent["model"] == FLASH + ":free" + assert sent["reasoning"] == {"effort": "max", "exclude": False} + assert "thinking" not in sent and "service_tier" not in sent + assert sent["messages"][0] == message + assert "reasoning_details" in response.text + finally: + asyncio.run(client.aclose()) + + +def test_discovery_uses_gateway_metadata(monkeypatch): + model_id = FLASH + ":free" + client = httpx.AsyncClient(transport=httpx.MockTransport(lambda request: httpx.Response(200, json={"data": [{ + "id": model_id, "context_length": 1_048_576, + "architecture": {"input_modalities": ["text", "image", "video"]}, + "pricing": {"prompt": "0", "completion": "0"}, + }]}))) + monkeypatch.setattr("uncommon_route.model_map.httpx.AsyncClient", lambda **kwargs: client) + mapper = ModelMapper("https://openrouter.ai/api/v1") + asyncio.run(mapper.discover()) + caps = mapper.dynamic_capabilities[model_id] + assert caps.context_window == 1_048_576 + assert caps.input_modalities == ("text", "image", "video") + assert caps.vision is True + assert mapper.dynamic_pricing[model_id].input_price == 0 + assert mapper.dynamic_pricing[model_id].output_price == 0 + + +@pytest.mark.parametrize("stream", [False, True]) +def test_responses_adapter_preserves_images_tools_and_history(monkeypatch, stream): + captured = [] + message = {"role": "assistant", "content": "done", "reasoning_content": "kept reasoning"} + + def handler(request): + captured.append(json.loads(request.content)) + if captured[-1].get("stream"): + chunk = {"id": "fixture", "model": "MiniMax-M3.1-Flash", "choices": [{"index": 0, "delta": message}]} + return httpx.Response(200, content=f"data: {json.dumps(chunk)}\n\ndata: [DONE]\n\n", headers={"content-type": "text/event-stream"}) + return httpx.Response(200, json={"id": "fixture", "model": "MiniMax-M3.1-Flash", "choices": [{"index": 0, "message": message, "finish_reason": "stop"}]}) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: client) + providers = ProvidersConfig({"minimax": ProviderEntry("minimax", "fixture", "https://api.minimax.io/v1", [FLASH])}) + local = TestClient(create_app(upstream="https://unused.test/v1", providers_config=providers)) + try: + response = local.post("/v1/responses", json={"model": FLASH, "stream": stream, "reasoning": {"effort": "low"}, + "input": [{"role": "user", "content": [{"type": "input_text", "text": "inspect"}, + {"type": "input_image", "image_url": "https://example.test/a.png"}]}], + "tools": [{"type": "function", "name": "inspect", "parameters": {"type": "object", "properties": {}}}], + }) + assert response.status_code == 200 + sent = captured[-1] + assert sent["messages"][0]["content"][1]["image_url"]["url"] == "https://example.test/a.png" + assert sent["reasoning_effort"] == "low" + assert sent["tools"][0]["function"]["name"] == "inspect" + if stream: + completed = [json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: {")] + response_id = next(event["response"]["id"] for event in completed if event.get("type") == "response.completed") + else: + response_id = response.json()["id"] + followup = local.post("/v1/responses", json={"model": FLASH, "previous_response_id": response_id, "input": "continue"}) + assert followup.status_code == 200 + assert captured[-1]["messages"][-2]["reasoning_content"] == "kept reasoning" + assert "previous_response_id" not in captured[-1] + finally: + asyncio.run(client.aclose()) diff --git a/tests/test_providers.py b/tests/test_providers.py index fa4bf25..cbf0d26 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -45,8 +45,8 @@ def test_add_provider_with_plan(self) -> None: entry = cfg.providers["minimax"] assert entry.plan == "coding-plan" assert entry.models == [ + "minimax/minimax-m3.1-flash", "minimax/minimax-m3", - "minimax/minimax-m2.7", ] def test_add_provider_custom_url(self) -> None: @@ -73,6 +73,10 @@ def test_minimax_provider_model_metadata(self) -> None: assert m3_pricing.input_price == 0.60 assert m3_pricing.output_price == 2.40 assert m3_pricing.cached_input_price == 0.12 + assert m3_pricing.for_usage(1000, "priority").output_price == 3.60 + flash = PROVIDER_MODEL_PRICING["minimax/minimax-m3.1-flash"] + assert (flash.input_price, flash.output_price, flash.cached_input_price) == (0.15, 0.60, 0.015) + assert PROVIDER_MODEL_CAPABILITIES["minimax/minimax-m3.1-flash"].max_output_tokens == 524_288 assert m3_pricing.cache_write_price is None m27_pricing = PROVIDER_MODEL_PRICING["minimax/minimax-m2.7"] @@ -128,7 +132,8 @@ def test_keyed_models(self) -> None: assert "deepseek/deepseek-chat" in keyed assert "deepseek/deepseek-reasoner" in keyed assert "minimax/minimax-m3" in keyed - assert "minimax/minimax-m2.7" in keyed + assert "minimax/minimax-m3.1-flash" in keyed + assert "minimax/minimax-m2.7" not in keyed def test_get_for_model(self) -> None: add_provider("deepseek", "sk-1") diff --git a/uncommon_route/minimax.py b/uncommon_route/minimax.py new file mode 100644 index 0000000..a6067cf --- /dev/null +++ b/uncommon_route/minimax.py @@ -0,0 +1,60 @@ +"""MiniMax request controls, scoped to verified transports and exact model IDs.""" + +from urllib.parse import urlparse + + +FLASH = "minimax-m3.1-flash" +EFFORTS = {"low", "medium", "high", "xhigh", "max"} + + +def is_direct_minimax(url: str) -> bool: + return urlparse(url).hostname in {"api.minimax.io", "api.minimaxi.com"} + + +def apply_minimax_controls(body: dict, url: str, *, anthropic: bool) -> None: + """Normalize incompatible router controls without changing message history.""" + model = str(body.get("model", "")).lower().split("/", 1)[-1] + direct = is_direct_minimax(url) + openrouter_flash = ( + urlparse(url).hostname == "openrouter.ai" and model == f"{FLASH}:free" + ) + if not direct and not openrouter_flash: + return + if (direct and model not in {FLASH, "minimax-m3"}) or (not direct and not openrouter_flash): + return + if direct: + body["model"] = "MiniMax-M3.1-Flash" if model == FLASH else "MiniMax-M3" + if model == "minimax-m3": + # M3 supports thinking on/off but does not tune depth. + body.pop("reasoning_effort", None) + if isinstance(body.get("output_config"), dict): + body["output_config"].pop("effort", None) + if isinstance(body.get("reasoning"), dict): + body["reasoning"].pop("effort", None) + return + + body.pop("service_tier", None) + if body.get("reasoning_split") is False: + body.pop("reasoning_split") + output_config = body.get("output_config") or {} + reasoning = body.get("reasoning") or {} + effort = ( + output_config.get("effort") if anthropic else body.get("reasoning_effort") + ) or reasoning.get("effort") or "max" + if effort not in EFFORTS: + effort = "max" + if openrouter_flash: + body.pop("thinking", None) + body.pop("reasoning_effort", None) + # Keep the full reasoning response available for tool follow-ups. + body["reasoning"] = {**reasoning, "effort": effort, "exclude": False} + body["reasoning"].pop("enabled", None) + elif anthropic: + body["thinking"] = {"type": "adaptive"} + body["output_config"] = {**output_config, "effort": effort} + body.pop("reasoning_effort", None) + body.pop("reasoning", None) + else: + body["thinking"] = {"type": "adaptive"} + body["reasoning_effort"] = effort + body.pop("reasoning", None) diff --git a/uncommon_route/model_map.py b/uncommon_route/model_map.py index 0a19631..4be7e5c 100644 --- a/uncommon_route/model_map.py +++ b/uncommon_route/model_map.py @@ -25,7 +25,7 @@ import math import re import time -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from pathlib import Path import httpx @@ -219,6 +219,23 @@ def is_routable_chat_model(model_id: str) -> bool: return not any(marker in core for marker in _NON_CHAT_MODEL_MARKERS) +def ordinary_model_choices(model_ids: list[str]) -> list[str]: + """Keep current MiniMax text choices; explicit saved IDs remain resolvable.""" + current = {"minimax-m3.1-flash", "minimax-m3.1-flash:free", "minimax-m3"} + choices = [ + model for model in model_ids + if not re.match(r"(?:minimax-m\d|minimax-text-01|minimax-vl-01)", _core(model.lower())) + or not is_routable_chat_model(model) + or _core(model.lower()) in current + ] + # Stable ordering preserves other providers while putting Flash before M3. + positions = [i for i, model in enumerate(choices) if _core(model.lower()) in current] + ordered = sorted((choices[i] for i in positions), key=lambda model: "flash" not in model.lower()) + for index, model in zip(positions, ordered): + choices[index] = model + return choices + + def _superseded_routing_models(model_ids: set[str]) -> set[str]: """Return routable IDs to hide when their live successor is available.""" return { @@ -395,6 +412,17 @@ async def discover(self, api_key: str | None = None) -> int: pricing, has_explicit_pricing=isinstance(m.get("pricing"), dict) and bool(m.get("pricing")), ) + if _core(model_id.lower()) in {"minimax-m3", "minimax-m3.1-flash", "minimax-m3.1-flash:free"}: + # Gateway limits and modalities come from that gateway's catalog. + modalities = tuple((m.get("architecture") or {}).get("input_modalities") or ()) + capabilities = replace( + capabilities, + vision="image" in modalities, + reasoning=True, + context_window=m.get("context_length"), + max_output_tokens=(m.get("top_provider") or {}).get("max_completion_tokens"), + input_modalities=modalities, + ) provider_name = _provider_prefix(model_id) or m.get("owned_by", "unknown") self._pool[model_id] = DiscoveredModel( @@ -569,7 +597,7 @@ def routable_models(self) -> list[str]: for model in self.available_models if is_routable_chat_model(model) ] - return filter_superseded_routing_models(routable) + return ordinary_model_choices(filter_superseded_routing_models(routable)) def get_pricing(self, model_id: str) -> ModelPricing | None: """Look up pricing for a single model (internal or upstream ID).""" @@ -680,7 +708,7 @@ def mapping_table(self) -> list[dict[str, str | bool | None]]: def pool_table(self) -> list[dict]: """Full model pool for the new dashboard — provider-grouped view.""" rows: list[dict] = [] - for model_id in sorted(self._pool.keys()): + for model_id in ordinary_model_choices(sorted(self._pool.keys())): dm = self._pool[model_id] rows.append({ "id": dm.id, diff --git a/uncommon_route/providers.py b/uncommon_route/providers.py index 786a900..28b44bc 100644 --- a/uncommon_route/providers.py +++ b/uncommon_route/providers.py @@ -13,10 +13,9 @@ "api_key": "eyJ...", "base_url": "https://api.minimax.io/v1", "models": [ - "minimax/minimax-m3", - "minimax/minimax-m2.7" - ], - "plan": "coding-plan" + "minimax/minimax-m3.1-flash", + "minimax/minimax-m3" + ] }, "deepseek": { "api_key": "sk-...", @@ -51,7 +50,7 @@ } PROVIDER_MODELS: dict[str, list[str]] = { - "minimax": ["minimax/minimax-m3", "minimax/minimax-m2.7"], + "minimax": ["minimax/minimax-m3.1-flash", "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"], @@ -62,6 +61,7 @@ UPSTREAM_MODEL_IDS: dict[str, dict[str, str]] = { "minimax": { + "minimax/minimax-m3.1-flash": "MiniMax-M3.1-Flash", "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 14d9074..629b1cb 100644 --- a/uncommon_route/proxy.py +++ b/uncommon_route/proxy.py @@ -112,7 +112,8 @@ remove_provider, verify_key, ) -from uncommon_route.model_map import ModelMapper +from uncommon_route.model_map import ModelMapper, ordinary_model_choices +from uncommon_route.minimax import apply_minimax_controls, is_direct_minimax from uncommon_route.routing_config_store import RoutingConfigStore from uncommon_route.scene_store import SceneConfig, SceneStore, _serialize_scene from uncommon_route.connections_store import ConnectionsStore, mask_api_key, resolve_primary_connection @@ -886,6 +887,7 @@ async def _run_task( "max_tokens": task.max_tokens, "stream": False, } + apply_minimax_controls(payload, target_chat_url, anthropic=False) try: resp = await client.post(target_chat_url, json=payload, headers=headers) except (httpx.ConnectError, httpx.TimeoutException): @@ -923,7 +925,11 @@ 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 = resolve_upstream_model(provider_entry.name, model_id) + upstream_model = ( + resolve_upstream_model(provider_entry.name, model_id) + if is_direct_minimax(provider_entry.base_url) + else model_id + ) elif self._upstream_chat: target_chat_url = self._upstream_chat upstream_model = self._mapper.resolve(model_id) @@ -2857,6 +2863,9 @@ def _refresh_active_pricing() -> None: merged.update(dynamic) keyed_models = _providers.keyed_models() for model_id in keyed_models: + entry = _providers.get_for_model(model_id) + if entry is None or not is_direct_minimax(entry.base_url): + continue provider_pricing = PROVIDER_MODEL_PRICING.get(model_id) if provider_pricing is not None: merged[model_id] = provider_pricing @@ -2871,6 +2880,9 @@ def _refresh_active_pricing() -> None: else: merged_caps = dict(updated.model_capabilities) for model_id in keyed_models: + entry = _providers.get_for_model(model_id) + if entry is None or not is_direct_minimax(entry.base_url): + continue provider_capabilities = PROVIDER_MODEL_CAPABILITIES.get(model_id) if provider_capabilities is not None: merged_caps[model_id] = provider_capabilities @@ -3111,7 +3123,7 @@ def _build_selector_preview(body: dict[str, Any], request: Request) -> dict[str, ) ctx_features = extract_context_features(body, step_type, prompt) user_keyed = _providers.keyed_models() or None - base_available_models = _mapper.routable_models if _mapper.discovered else list(DEFAULT_MODEL_PRICING.keys()) + base_available_models = _mapper.routable_models if _mapper.discovered else ordinary_model_choices(list(DEFAULT_MODEL_PRICING)) if user_keyed: base_available_models = _merge_available_models(base_available_models, sorted(user_keyed)) available_models = _circuit_breaker.filter_available(base_available_models) @@ -4145,7 +4157,7 @@ async def _handle_chat_core( step_type = routing_features.step_type user_keyed = _providers.keyed_models() or None try: - base_available_models = _mapper.routable_models if _mapper.discovered else list(DEFAULT_MODEL_PRICING.keys()) + base_available_models = _mapper.routable_models if _mapper.discovered else ordinary_model_choices(list(DEFAULT_MODEL_PRICING)) if user_keyed: base_available_models = _merge_available_models(base_available_models, sorted(user_keyed)) route_available_models = _circuit_breaker.filter_available(base_available_models) @@ -4672,7 +4684,11 @@ def _prepare_attempt(model_name: str) -> dict[str, Any]: resolved_model = model_name if attempt_provider_entry: - resolved_model = resolve_upstream_model(attempt_provider_entry.name, model_name) + resolved_model = ( + resolve_upstream_model(attempt_provider_entry.name, model_name) + if is_direct_minimax(attempt_provider_entry.base_url) + else model_name + ) else: resolved_model = _mapper.resolve(model_name) attempt_upstream_body["model"] = resolved_model @@ -4753,6 +4769,12 @@ def _prepare_attempt(model_name: str) -> dict[str, Any]: upstream_provider=_mapper.provider, ) + apply_minimax_controls( + attempt_transport_body, + attempt_target_chat_url, + anthropic=attempt_native_anthropic_transport, + ) + if attempt_provider_entry: if attempt_native_anthropic_transport: attempt_headers.pop("authorization", None) diff --git a/uncommon_route/responses_compat.py b/uncommon_route/responses_compat.py index 706dd80..c3bf200 100644 --- a/uncommon_route/responses_compat.py +++ b/uncommon_route/responses_compat.py @@ -20,14 +20,16 @@ def responses_to_openai_chat_request( ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """Convert a Responses API request into a chat completions request.""" + preserve = str(raw.get("model") or "").lower().startswith(("minimax/", "minimax-m")) messages = json.loads(json.dumps(previous_messages or [])) if previous_messages: - input_messages = _responses_input_to_messages(raw.get("input")) + input_messages = _responses_input_to_messages(raw.get("input"), preserve=preserve) messages.extend(input_messages) else: initial_messages = _build_initial_messages( instructions=raw.get("instructions"), input_value=raw.get("input"), + preserve=preserve, ) messages.extend(initial_messages) @@ -37,6 +39,13 @@ def responses_to_openai_chat_request( "stream": bool(raw.get("stream")), } + if preserve: + for field in ("reasoning", "service_tier"): + if field in raw: + body[field] = raw[field] + if "max_output_tokens" in raw: + body["max_tokens"] = raw["max_output_tokens"] + if raw.get("prompt_cache_key"): body["prompt_cache_key"] = raw.get("prompt_cache_key") @@ -117,6 +126,8 @@ def __init__(self, *, request_body: dict[str, Any], response_id: str) -> None: self._model = str(request_body.get("model") or "") self._chat_response_id = "" self._text_parts: list[str] = [] + self._reasoning_parts: list[str] = [] + self._reasoning_details: list[dict[str, Any]] = [] self._usage: dict[str, Any] | None = None self._tool_calls: dict[int, dict[str, Any]] = {} @@ -162,6 +173,10 @@ def _consume_chunk(self, payload: dict[str, Any]) -> None: return choice = choices[0] if isinstance(choices[0], dict) else {} delta = choice.get("delta") if isinstance(choice.get("delta"), dict) else {} + if isinstance(delta.get("reasoning_content"), str): + self._reasoning_parts.append(delta["reasoning_content"]) + if isinstance(delta.get("reasoning_details"), list): + self._reasoning_details.extend(delta["reasoning_details"]) content = delta.get("content") if isinstance(content, str) and content: self._text_parts.append(content) @@ -230,6 +245,11 @@ def _build_output_items(self) -> tuple[dict[str, Any] | None, list[dict[str, Any if assistant_message is not None and normalized_tool_calls: assistant_message["tool_calls"] = normalized_tool_calls output_items.extend(tool_items) + if assistant_message is not None: + if self._reasoning_parts: + assistant_message["reasoning_content"] = "".join(self._reasoning_parts) + if self._reasoning_details: + assistant_message["reasoning_details"] = self._reasoning_details return assistant_message, output_items def _build_events(self, output_items: list[dict[str, Any]]) -> list[bytes]: @@ -426,14 +446,14 @@ def _build_events(self, output_items: list[dict[str, Any]]) -> list[bytes]: return events -def _build_initial_messages(*, instructions: Any, input_value: Any) -> list[dict[str, Any]]: +def _build_initial_messages(*, instructions: Any, input_value: Any, preserve: bool = False) -> list[dict[str, Any]]: messages: list[dict[str, Any]] = [] system_parts: list[str] = [] instructions_text = str(instructions or "").strip() if instructions_text: system_parts.append(instructions_text) - for message in _responses_input_to_messages(input_value): + for message in _responses_input_to_messages(input_value, preserve=preserve): role = str(message.get("role") or "") content = message.get("content") text = content if isinstance(content, str) else "" @@ -447,26 +467,41 @@ def _build_initial_messages(*, instructions: Any, input_value: Any) -> list[dict return messages -def _responses_input_to_messages(input_value: Any) -> list[dict[str, Any]]: +def _responses_input_to_messages(input_value: Any, *, preserve: bool = False) -> list[dict[str, Any]]: if input_value is None: return [] if isinstance(input_value, str): return [{"role": "user", "content": input_value}] if isinstance(input_value, dict): - return _responses_input_item_to_messages(input_value) + return _responses_input_item_to_messages(input_value, preserve=preserve) messages: list[dict[str, Any]] = [] if isinstance(input_value, list): for item in input_value: - messages.extend(_responses_input_item_to_messages(item)) + converted = _responses_input_item_to_messages(item, preserve=preserve) + if ( + preserve and converted and messages + and "reasoning_content" in messages[-1] + and messages[-1].get("content") is None + and not messages[-1].get("tool_calls") + and converted[0].get("role") == "assistant" + ): + messages[-1].update(converted[0]) + messages.extend(converted[1:]) + else: + messages.extend(converted) return messages -def _responses_input_item_to_messages(item: Any) -> list[dict[str, Any]]: +def _responses_input_item_to_messages(item: Any, *, preserve: bool = False) -> list[dict[str, Any]]: if not isinstance(item, dict): return [] item_type = str(item.get("type") or "message") + if preserve and item_type == "reasoning": + return [{"role": "assistant", "content": None, "reasoning_content": "\n".join( + str(part.get("text") or "") for part in item.get("summary", []) + )}] if item_type == "function_call_output": return [ { @@ -499,7 +534,25 @@ def _responses_input_item_to_messages(item: Any) -> list[dict[str, Any]]: if item_type == "message" or role: chat_role = "system" if role in {"developer", "system"} else (role or "user") content = _responses_content_to_text(item.get("content")) + if preserve and isinstance(item.get("content"), list): + blocks = [] + for block in item["content"]: + kind = block.get("type") + if kind in {"input_text", "output_text", "text"}: + blocks.append({"type": "text", "text": block.get("text", "")}) + elif kind in {"input_image", "input_video"}: + key = "image_url" if kind == "input_image" else "video_url" + value = block.get(key, block.get("file_id", "")) + blocks.append({"type": key, key: value if isinstance(value, dict) else {"url": value}}) + else: + blocks.append(block) + if any(block.get("type") != "text" for block in blocks): + content = blocks message: dict[str, Any] = {"role": chat_role, "content": content} + if preserve: + for key in ("reasoning_content", "reasoning_details"): + if key in item: + message[key] = item[key] tool_calls = _responses_message_tool_calls(item) if tool_calls: message["role"] = "assistant" @@ -658,6 +711,9 @@ def _openai_message_to_responses_output(message: dict[str, Any]) -> tuple[dict[s ) if assistant_message is not None and normalized_tool_calls: assistant_message["tool_calls"] = normalized_tool_calls + for key in ("reasoning_content", "reasoning_details"): + if assistant_message is not None and key in message: + assistant_message[key] = message[key] return assistant_message, output_items diff --git a/uncommon_route/router/config.py b/uncommon_route/router/config.py index 3131ecb..a98b2f4 100644 --- a/uncommon_route/router/config.py +++ b/uncommon_route/router/config.py @@ -6,6 +6,7 @@ ModeConfig, ModelCapabilities, ModelPricing, + ModelPricingTier, RoutingConfig, RoutingMode, ScoringConfig, @@ -43,10 +44,13 @@ } PROVIDER_MODEL_PRICING: dict[str, ModelPricing] = { + "minimax/minimax-m3.1-flash": ModelPricing(0.15, 0.60, cached_input_price=0.015), "minimax/minimax-m3": ModelPricing( + # Upper-band estimates: no unverified input-token boundary is assumed. 0.60, 2.40, cached_input_price=0.12, + pricing_tiers=(ModelPricingTier("priority", 0.90, 3.60, cached_input_price=0.18),), ), "minimax/minimax-m2.7": ModelPricing( 0.30, @@ -57,6 +61,15 @@ } PROVIDER_MODEL_CAPABILITIES: dict[str, ModelCapabilities] = { + "minimax/minimax-m3.1-flash": ModelCapabilities( + tool_calling=True, + vision=True, + reasoning=True, + context_window=1_000_000, + max_output_tokens=524_288, + input_modalities=("text", "image", "video"), + thinking_modes=("adaptive",), + ), "minimax/minimax-m3": ModelCapabilities( tool_calling=True, vision=True, diff --git a/uncommon_route/router/types.py b/uncommon_route/router/types.py index 01d42b4..a2dbf17 100644 --- a/uncommon_route/router/types.py +++ b/uncommon_route/router/types.py @@ -61,6 +61,7 @@ class ModelCapabilities: local: bool = False responses: bool = False context_window: int | None = None + max_output_tokens: int | None = None input_modalities: tuple[str, ...] = () thinking_modes: tuple[str, ...] = ()