baichuan-mcp-server 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: baichuan-mcp-server
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Baichuan Router — Multi-model AI routing MCP server. One endpoint, every model.
|
|
5
|
+
Project-URL: Repository, https://github.com/baichuan-alliance/router
|
|
6
|
+
Author: Baichuan Alliance
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Keywords: agent,ai-router,baichuan,cross-model,llm,mcp,model-routing
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: mcp<2
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Baichuan Router (MCP Server)
|
|
18
|
+
|
|
19
|
+
**One prompt, every model — route, compare, and save.**
|
|
20
|
+
|
|
21
|
+
Baichuan Router is an open-source MCP server that routes prompts across multiple AI models.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uvx baichuan-mcp-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install baichuan-mcp-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
Apache-2.0 © Baichuan Alliance
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Baichuan Router (MCP Server)
|
|
2
|
+
|
|
3
|
+
**One prompt, every model — route, compare, and save.**
|
|
4
|
+
|
|
5
|
+
Baichuan Router is an open-source MCP server that routes prompts across multiple AI models.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uvx baichuan-mcp-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install baichuan-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
Apache-2.0 © Baichuan Alliance
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "baichuan-mcp-server"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Baichuan Router — Multi-model AI routing MCP server. One endpoint, every model."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27.0",
|
|
14
|
+
"mcp<2",
|
|
15
|
+
]
|
|
16
|
+
keywords = ["mcp", "model-routing", "cross-model", "llm", "ai-router", "agent", "baichuan"]
|
|
17
|
+
authors = [{ name = "Baichuan Alliance" }]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/baichuan-alliance/router"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
baichuan-router = "baichuan_router.server:run"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/baichuan_router"]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Baichuan Router MCP server (self-hosted, BYOK).
|
|
2
|
+
|
|
3
|
+
Route prompts across 9 AI model providers through a single MCP server.
|
|
4
|
+
Bring your own keys (BYOK) via model_api_key, or set environment variables.
|
|
5
|
+
No lock-in, no vendor bias.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
|
|
17
|
+
_port = int(os.environ.get("BAICHUAN_PORT", "47832"))
|
|
18
|
+
mcp = FastMCP("Baichuan Router", host="127.0.0.1", port=_port)
|
|
19
|
+
|
|
20
|
+
SUPPORTED_MODELS = (
|
|
21
|
+
"chatgpt", "grok", "claude", "gemini", "llama",
|
|
22
|
+
"deepseek", "qwen", "silicon", "groq",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _reply(model, status, text=None, error=None, usage=None):
|
|
27
|
+
return {
|
|
28
|
+
"model": model,
|
|
29
|
+
"status": status,
|
|
30
|
+
"text": text,
|
|
31
|
+
"usage": usage or {"input_tokens": 0, "output_tokens": 0},
|
|
32
|
+
"task_id": str(uuid.uuid4()),
|
|
33
|
+
"error": error,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _secret(name):
|
|
38
|
+
value = (os.environ.get(name) or "").strip()
|
|
39
|
+
if not value:
|
|
40
|
+
raise RuntimeError(f"missing credential: {name}")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _key(name, override=None):
|
|
45
|
+
"""Use a caller-supplied key (BYOK) when present, else the server env key."""
|
|
46
|
+
if override and str(override).strip():
|
|
47
|
+
return str(override).strip()
|
|
48
|
+
return _secret(name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _post_json(url, headers, payload):
|
|
52
|
+
with httpx.Client(timeout=180) as client:
|
|
53
|
+
resp = client.post(url, headers=headers, json=payload)
|
|
54
|
+
resp.raise_for_status()
|
|
55
|
+
return resp.json()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _call_openai_compat(default_model, default_base, prompt, system, max_tokens,
|
|
59
|
+
temperature, key_env, base_env, key_optional=False, key_override=None):
|
|
60
|
+
if key_override:
|
|
61
|
+
key = key_override
|
|
62
|
+
elif key_optional:
|
|
63
|
+
key = (os.environ.get(key_env) or "").strip() or "local"
|
|
64
|
+
else:
|
|
65
|
+
key = _secret(key_env)
|
|
66
|
+
base = (os.environ.get(base_env) or default_base).rstrip("/")
|
|
67
|
+
payload = {
|
|
68
|
+
"model": os.environ.get(key_env.replace("_API_KEY", "") + "_MODEL") or default_model,
|
|
69
|
+
"messages": [],
|
|
70
|
+
}
|
|
71
|
+
if system:
|
|
72
|
+
payload["messages"].append({"role": "system", "content": system})
|
|
73
|
+
payload["messages"].append({"role": "user", "content": prompt})
|
|
74
|
+
if max_tokens:
|
|
75
|
+
payload["max_tokens"] = max_tokens
|
|
76
|
+
if temperature is not None:
|
|
77
|
+
payload["temperature"] = temperature
|
|
78
|
+
data = _post_json(f"{base}/chat/completions", {"Authorization": f"Bearer {key}"}, payload)
|
|
79
|
+
text = data["choices"][0]["message"]["content"]
|
|
80
|
+
usage = {
|
|
81
|
+
"input_tokens": data.get("usage", {}).get("prompt_tokens", 0),
|
|
82
|
+
"output_tokens": data.get("usage", {}).get("completion_tokens", 0),
|
|
83
|
+
}
|
|
84
|
+
return text, usage
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _call_claude(prompt, system, max_tokens, temperature, key_override=None):
|
|
88
|
+
key = _key("ANTHROPIC_API_KEY", key_override)
|
|
89
|
+
base = (os.environ.get("ANTHROPIC_BASE_URL") or "https://api.anthropic.com").rstrip("/")
|
|
90
|
+
model = os.environ.get("CLAUDE_MODEL") or "claude-opus-5"
|
|
91
|
+
payload = {
|
|
92
|
+
"model": model,
|
|
93
|
+
"max_tokens": max_tokens or 4096,
|
|
94
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
95
|
+
}
|
|
96
|
+
if system:
|
|
97
|
+
payload["system"] = system
|
|
98
|
+
if temperature is not None:
|
|
99
|
+
payload["temperature"] = temperature
|
|
100
|
+
headers = {"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
|
|
101
|
+
data = _post_json(f"{base}/v1/messages", headers, payload)
|
|
102
|
+
text = "".join(b.get("text", "") for b in data.get("content", []) if b.get("type") == "text")
|
|
103
|
+
usage = {
|
|
104
|
+
"input_tokens": data.get("usage", {}).get("input_tokens", 0),
|
|
105
|
+
"output_tokens": data.get("usage", {}).get("output_tokens", 0),
|
|
106
|
+
}
|
|
107
|
+
return text, usage
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _call_gemini(prompt, system, max_tokens, temperature, key_override=None):
|
|
111
|
+
key = _key("GEMINI_API_KEY", key_override)
|
|
112
|
+
model = os.environ.get("GEMINI_MODEL") or "gemini-2.5-pro"
|
|
113
|
+
base = (os.environ.get("GEMINI_BASE_URL") or "https://generativelanguage.googleapis.com").rstrip("/")
|
|
114
|
+
url = f"{base}/v1beta/models/{model}:generateContent?key={key}"
|
|
115
|
+
contents = []
|
|
116
|
+
if system:
|
|
117
|
+
contents.append({"role": "user", "parts": [{"text": system}]})
|
|
118
|
+
contents.append({"role": "user", "parts": [{"text": prompt}]})
|
|
119
|
+
payload = {"contents": contents}
|
|
120
|
+
gen = {}
|
|
121
|
+
if max_tokens:
|
|
122
|
+
gen["maxOutputTokens"] = max_tokens
|
|
123
|
+
if temperature is not None:
|
|
124
|
+
gen["temperature"] = temperature
|
|
125
|
+
if gen:
|
|
126
|
+
payload["generationConfig"] = gen
|
|
127
|
+
data = _post_json(url, {"content-type": "application/json"}, payload)
|
|
128
|
+
try:
|
|
129
|
+
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
130
|
+
except (KeyError, IndexError):
|
|
131
|
+
text = ""
|
|
132
|
+
usage = {
|
|
133
|
+
"input_tokens": data.get("usageMetadata", {}).get("promptTokenCount", 0),
|
|
134
|
+
"output_tokens": data.get("usageMetadata", {}).get("candidatesTokenCount", 0),
|
|
135
|
+
}
|
|
136
|
+
return text, usage
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _route(model, prompt, system, max_tokens, temperature, key_override=None):
|
|
140
|
+
if model == "chatgpt":
|
|
141
|
+
return _call_openai_compat("gpt-6-astra", "https://api.openai.com/v1", prompt, system, max_tokens, temperature, "OPENAI_API_KEY", "OPENAI_BASE_URL", key_override=key_override)
|
|
142
|
+
if model == "grok":
|
|
143
|
+
return _call_openai_compat("grok-4.6", "https://api.x.ai/v1", prompt, system, max_tokens, temperature, "GROK_API_KEY", "GROK_BASE_URL", key_override=key_override)
|
|
144
|
+
if model == "llama":
|
|
145
|
+
return _call_openai_compat("qwen2.5:7b", "http://127.0.0.1:11434/v1", prompt, system, max_tokens, temperature, "LLAMA_API_KEY", "LLAMA_BASE_URL", key_optional=True, key_override=key_override)
|
|
146
|
+
if model == "claude":
|
|
147
|
+
return _call_claude(prompt, system, max_tokens, temperature, key_override=key_override)
|
|
148
|
+
if model == "gemini":
|
|
149
|
+
return _call_gemini(prompt, system, max_tokens, temperature, key_override=key_override)
|
|
150
|
+
if model == "deepseek":
|
|
151
|
+
return _call_openai_compat("deepseek-chat", "https://api.deepseek.com/v1", prompt, system, max_tokens, temperature, "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL", key_override=key_override)
|
|
152
|
+
if model == "qwen":
|
|
153
|
+
return _call_openai_compat("qwen-plus", "https://dashscope.aliyuncs.com/compatible-mode/v1", prompt, system, max_tokens, temperature, "DASHSCOPE_API_KEY", "DASHSCOPE_BASE_URL", key_override=key_override)
|
|
154
|
+
if model == "silicon":
|
|
155
|
+
return _call_openai_compat("Qwen/Qwen3-235B-A22B", "https://api.siliconflow.cn/v1", prompt, system, max_tokens, temperature, "SILICONFLOW_API_KEY", "SILICONFLOW_BASE_URL", key_override=key_override)
|
|
156
|
+
if model == "groq":
|
|
157
|
+
return _call_openai_compat("llama-4-scout-17b-16e-instruct", "https://api.groq.com/openai/v1", prompt, system, max_tokens, temperature, "GROQ_API_KEY", "GROQ_BASE_URL", key_override=key_override)
|
|
158
|
+
raise ValueError(f"unsupported model: {model}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def route_to_model(
|
|
163
|
+
model: str,
|
|
164
|
+
prompt: str,
|
|
165
|
+
system: str = "",
|
|
166
|
+
max_tokens: int = 0,
|
|
167
|
+
temperature: float = 0.7,
|
|
168
|
+
context_id: str = "",
|
|
169
|
+
api_key: str = "",
|
|
170
|
+
model_api_key: str = "",
|
|
171
|
+
) -> dict:
|
|
172
|
+
"""Route prompt to a model, return unified response.
|
|
173
|
+
|
|
174
|
+
model: chatgpt|grok|claude|gemini|llama|deepseek|qwen|silicon|groq
|
|
175
|
+
model_api_key (BYOK): optional caller-owned key for the target provider.
|
|
176
|
+
When provided it is used instead of the server-side credential.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
if model not in SUPPORTED_MODELS:
|
|
180
|
+
return _reply(model, "failed", error=f"unsupported model: {model}")
|
|
181
|
+
text, usage = _route(model, prompt, system or None, max_tokens or None, temperature, key_override=(model_api_key or None))
|
|
182
|
+
return _reply(model, "completed", text=text, usage=usage)
|
|
183
|
+
except Exception as exc: # noqa: BLE001
|
|
184
|
+
return _reply(model, "failed", error=str(exc))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@mcp.tool()
|
|
188
|
+
def list_models() -> dict:
|
|
189
|
+
"""List available models."""
|
|
190
|
+
return {"models": [{"model": m} for m in SUPPORTED_MODELS]}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@mcp.tool()
|
|
194
|
+
def health_check() -> dict:
|
|
195
|
+
"""Health check: backend env status."""
|
|
196
|
+
backends = {}
|
|
197
|
+
for model in SUPPORTED_MODELS:
|
|
198
|
+
env_var = f"{model.upper()}_API_KEY"
|
|
199
|
+
backends[model] = {"env_set": bool((os.environ.get(env_var) or "").strip())}
|
|
200
|
+
return {
|
|
201
|
+
"status": "ok",
|
|
202
|
+
"host": platform.node(),
|
|
203
|
+
"python": platform.python_version(),
|
|
204
|
+
"backends": backends,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def run():
|
|
209
|
+
"""Run the MCP server over SSE transport."""
|
|
210
|
+
mcp.run(transport="sse")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
run()
|