Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,53 @@ 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.

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
Expand Down
72 changes: 49 additions & 23 deletions tests/test_anthropic_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -1931,9 +1923,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 +1948,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 +1965,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 +2016,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 +2029,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 +2054,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)
Loading