Skip to content
Open
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 47 additions & 13 deletions tests/test_anthropic_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}

Expand All @@ -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-M3",
"content": [{"type": "text", "text": "ok"}],
"stop_reason": "end_turn",
"stop_sequence": None,
Expand All @@ -1964,32 +1973,44 @@ 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"
body = captured["body"]
assert isinstance(body, dict)
assert body["model"] == "MiniMax-M3"
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] = {}

Expand All @@ -2003,7 +2024,7 @@ def handler(request: httpx.Request) -> httpx.Response:
"id": "chatcmpl_minimax",
"object": "chat.completion",
"created": 1,
"model": "minimax/minimax-m2.1",
"model": "MiniMax-M3",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "done"},
Expand All @@ -2016,13 +2037,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"}],
})
Expand All @@ -2031,10 +2062,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-M3"
finally:
asyncio.run(async_client.aclose())

Expand Down
82 changes: 80 additions & 2 deletions tests/test_cache_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

import json

from uncommon_route.cache_support import apply_anthropic_cache_breakpoints, parse_stream_usage_metrics
from uncommon_route.router.types import ModelPricing
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


def test_anthropic_cache_breakpoints_do_not_upgrade_after_existing_5m() -> None:
Expand Down Expand Up @@ -88,3 +95,74 @@ 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


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)
51 changes: 49 additions & 2 deletions tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
ProviderEntry,
add_provider,
load_providers,
resolve_upstream_model,
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


Expand Down Expand Up @@ -41,12 +44,55 @@ 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-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")
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"

@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
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
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",
Expand Down Expand Up @@ -81,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")
Expand Down
Loading