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
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
12 changes: 11 additions & 1 deletion tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ProviderEntry,
add_provider,
load_providers,
resolve_upstream_model,
remove_provider,
select_preferred_model,
)
Expand Down Expand Up @@ -41,12 +42,21 @@ 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")
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",
Expand Down
19 changes: 17 additions & 2 deletions uncommon_route/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -48,7 +52,7 @@
}

PROVIDER_MODELS: dict[str, list[str]] = {
"minimax": ["minimax/minimax-m2.5"],
"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"],
Expand All @@ -57,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:
Expand Down
5 changes: 4 additions & 1 deletion uncommon_route/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
ProvidersConfig,
add_provider,
load_providers,
resolve_upstream_model,
remove_provider,
verify_key,
)
Expand Down Expand Up @@ -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

Expand Down