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,3 @@
1
+ """Baichuan Router — Multi-model AI routing MCP server."""
2
+
3
+ __version__ = "1.0.0"
@@ -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()