baiduan-router 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.
- baiduan_router-1.0.0/PKG-INFO +10 -0
- baiduan_router-1.0.0/README.md +42 -0
- baiduan_router-1.0.0/pyproject.toml +19 -0
- baiduan_router-1.0.0/src/baiduan_router/__init__.py +1 -0
- baiduan_router-1.0.0/src/baiduan_router/db.py +75 -0
- baiduan_router-1.0.0/src/baiduan_router/pricing.py +27 -0
- baiduan_router-1.0.0/src/baiduan_router/server.py +252 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: baiduan-router
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Route prompts across multiple leading AI models through a single MCP server
|
|
5
|
+
Author: Baichuan
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: ai-router,cross-model,llm,mcp,model-routing
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: mcp>=1.0.0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Baichuan Router
|
|
2
|
+
|
|
3
|
+
**One prompt, every model — route, compare, and save.**
|
|
4
|
+
|
|
5
|
+
Baichuan Router is an open-source MCP server that routes your prompts across multiple leading AI models. Instead of being locked into a single provider, get side-by-side comparison, cost optimization, and smart model selection — all through one neutral interface.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx baiduan-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install baiduan-router
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Multi-model routing** — send one prompt to multiple models, compare results
|
|
22
|
+
- **Smart selection** — auto-route simple tasks to cost-effective models, complex tasks to premium ones
|
|
23
|
+
- **MCP-native** — works with Claude Desktop, VS Code, Cursor, and any MCP-compatible client
|
|
24
|
+
- **BYOK** — bring your own API keys, never stored or logged
|
|
25
|
+
- **Open source** — Apache-2.0, fully self-hostable
|
|
26
|
+
|
|
27
|
+
## Supported Models
|
|
28
|
+
|
|
29
|
+
Set environment variables for the models you want to use:
|
|
30
|
+
|
|
31
|
+
| Model | Env Variable | Free Tier Available |
|
|
32
|
+
|---------|---------------------|---------------------|
|
|
33
|
+
| Gemini | `GEMINI_API_KEY` | ✅ Unlimited |
|
|
34
|
+
| Grok | `GROK_API_KEY` | ✅ $25 credits |
|
|
35
|
+
| DeepSeek| `DEEPSEEK_API_KEY` | ✅ Unlimited |
|
|
36
|
+
| Qwen | `DASHSCOPE_API_KEY` | ✅ 1M tokens/month |
|
|
37
|
+
| Claude | `ANTHROPIC_API_KEY` | — |
|
|
38
|
+
| ChatGPT | `OPENAI_API_KEY` | — |
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
Apache-2.0 © Baichuan
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "baiduan-router"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Route prompts across multiple leading AI models through a single MCP server"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
dependencies = ["mcp>=1.0.0", "httpx>=0.27.0"]
|
|
12
|
+
keywords = ["mcp", "model-routing", "cross-model", "llm", "ai-router"]
|
|
13
|
+
authors = [{ name = "Baichuan" }]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
baiduan-router = "baiduan_router.server:run"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/baiduan_router"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Baichuan Router - Multi-model MCP routing server
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Baichuan Dispatcher - SQLite user & usage store.
|
|
2
|
+
|
|
3
|
+
Zero external deps (Python stdlib). Deploy-to-init.
|
|
4
|
+
Confidential credentials never stored.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
DB_PATH = os.environ.get("DISPATCHER_DB") or os.path.join(os.path.dirname(__file__), "dispatcher.db")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _conn():
|
|
15
|
+
c = sqlite3.connect(DB_PATH)
|
|
16
|
+
c.row_factory = sqlite3.Row
|
|
17
|
+
c.execute("PRAGMA journal_mode=WAL")
|
|
18
|
+
return c
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def init_db():
|
|
22
|
+
with _conn() as db:
|
|
23
|
+
db.executescript("""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
api_key_hash TEXT UNIQUE NOT NULL,
|
|
27
|
+
tier TEXT NOT NULL DEFAULT 'free'
|
|
28
|
+
CHECK(tier IN ('free','pro')),
|
|
29
|
+
daily_quota INTEGER NOT NULL DEFAULT 100,
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS usage_logs (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
35
|
+
model TEXT NOT NULL,
|
|
36
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
cost_cents INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_usage_user_ts
|
|
42
|
+
ON usage_logs(user_id, timestamp);
|
|
43
|
+
""")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _hash_key(raw: str) -> str:
|
|
47
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_user_by_key(api_key: str) -> dict | None:
|
|
51
|
+
h = _hash_key(api_key)
|
|
52
|
+
with _conn() as db:
|
|
53
|
+
row = db.execute("SELECT * FROM users WHERE api_key_hash=?", (h,)).fetchone()
|
|
54
|
+
return dict(row) if row else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_daily_usage(user_id: int, date: str) -> int:
|
|
58
|
+
with _conn() as db:
|
|
59
|
+
row = db.execute(
|
|
60
|
+
"SELECT COUNT(*) AS cnt FROM usage_logs WHERE user_id=? AND date(timestamp)=?",
|
|
61
|
+
(user_id, date),
|
|
62
|
+
).fetchone()
|
|
63
|
+
return row["cnt"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def log_usage(user_id: int, model: str, prompt_tokens: int, completion_tokens: int, cost_cents: int):
|
|
67
|
+
with _conn() as db:
|
|
68
|
+
db.execute(
|
|
69
|
+
"INSERT INTO usage_logs (user_id, model, prompt_tokens, completion_tokens, cost_cents) VALUES (?,?,?,?,?)",
|
|
70
|
+
(user_id, model, prompt_tokens, completion_tokens, cost_cents),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Auto-init on import (zero ops)
|
|
75
|
+
init_db()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Baichuan Dispatcher - model cost mapping.
|
|
2
|
+
|
|
3
|
+
Unit: US cents per million tokens (approximate).
|
|
4
|
+
Free/ultra-cheap models: cost=0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# (input_cents_per_1M, output_cents_per_1M)
|
|
8
|
+
PRICES = {
|
|
9
|
+
"chatgpt": (1500, 6000), # ~$15/$60
|
|
10
|
+
"claude": (1500, 7500), # ~$15/$75
|
|
11
|
+
"grok": (0, 0), # free key
|
|
12
|
+
"gemini": (0, 0), # free key
|
|
13
|
+
"deepseek": (14, 110), # ~$0.14/$1.10 (ultra-cheap, rounded to 0)
|
|
14
|
+
"qwen": (0, 0), # DashScope free quota
|
|
15
|
+
"silicon": (0, 0), # SiliconFlow free quota
|
|
16
|
+
"groq": (0, 0), # free
|
|
17
|
+
"llama": (0, 0), # local
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> int:
|
|
22
|
+
"""Return cost_cents (integer cents). Free models return 0."""
|
|
23
|
+
inp, out = PRICES.get(model, (0, 0))
|
|
24
|
+
if inp == 0 and out == 0:
|
|
25
|
+
return 0
|
|
26
|
+
cost = (input_tokens / 1_000_000) * inp + (output_tokens / 1_000_000) * out
|
|
27
|
+
return max(0, round(cost))
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Baichuan Dispatcher.
|
|
2
|
+
|
|
3
|
+
Multi-model routing MCP server.
|
|
4
|
+
Credentials via env vars, never in code or logs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import date as _today
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from .db import get_user_by_key, log_usage, get_daily_usage
|
|
17
|
+
from .pricing import calculate_cost
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP("Baichuan Dispatcher", host="127.0.0.1", port=47832)
|
|
20
|
+
|
|
21
|
+
SUPPORTED_MODELS = (
|
|
22
|
+
"chatgpt", "grok", "claude", "gemini", "llama",
|
|
23
|
+
# Free / open-source backends (Chinese ecosystem)
|
|
24
|
+
"deepseek", # DeepSeek - unlimited free tier, excellent coding
|
|
25
|
+
"qwen", # Alibaba DashScope - Qwen models, free tier 1M tokens
|
|
26
|
+
"silicon", # SiliconFlow - open-source model cloud inference
|
|
27
|
+
"groq", # Groq - free fast inference, hosts Llama etc.
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
PRO_MODELS = {"chatgpt", "claude"}
|
|
31
|
+
FREE_MODELS = set(SUPPORTED_MODELS) - PRO_MODELS
|
|
32
|
+
DAILY_QUOTA = {"free": 100, "pro": 1000}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _reply(model: str, status: str, text=None, error=None, usage=None):
|
|
36
|
+
return {
|
|
37
|
+
"model": model,
|
|
38
|
+
"status": status,
|
|
39
|
+
"text": text,
|
|
40
|
+
"usage": usage or {"input_tokens": 0, "output_tokens": 0},
|
|
41
|
+
"task_id": str(uuid.uuid4()),
|
|
42
|
+
"error": error,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _secret(name: str) -> str:
|
|
47
|
+
value = (os.environ.get(name) or "").strip()
|
|
48
|
+
if not value:
|
|
49
|
+
raise RuntimeError(f"missing credential: {name}")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _post_json(url: str, headers: dict, payload: dict):
|
|
54
|
+
with httpx.Client(timeout=180) as client:
|
|
55
|
+
resp = client.post(url, headers=headers, json=payload)
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
return resp.json()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _call_openai_compat(default_model, default_base, prompt, system, max_tokens, temperature, key_env, base_env, key_optional=False):
|
|
61
|
+
if key_optional:
|
|
62
|
+
key = (os.environ.get(key_env) or "").strip() or "local"
|
|
63
|
+
else:
|
|
64
|
+
key = _secret(key_env)
|
|
65
|
+
base = (os.environ.get(base_env) or default_base).rstrip("/")
|
|
66
|
+
payload = {
|
|
67
|
+
"model": os.environ.get(key_env.replace("_API_KEY", "") + "_MODEL") or default_model,
|
|
68
|
+
"messages": [],
|
|
69
|
+
}
|
|
70
|
+
if system:
|
|
71
|
+
payload["messages"].append({"role": "system", "content": system})
|
|
72
|
+
payload["messages"].append({"role": "user", "content": prompt})
|
|
73
|
+
if max_tokens:
|
|
74
|
+
payload["max_tokens"] = max_tokens
|
|
75
|
+
if temperature is not None:
|
|
76
|
+
payload["temperature"] = temperature
|
|
77
|
+
data = _post_json(f"{base}/chat/completions", {"Authorization": f"Bearer {key}"}, payload)
|
|
78
|
+
text = data["choices"][0]["message"]["content"]
|
|
79
|
+
usage = {
|
|
80
|
+
"input_tokens": data.get("usage", {}).get("prompt_tokens", 0),
|
|
81
|
+
"output_tokens": data.get("usage", {}).get("completion_tokens", 0),
|
|
82
|
+
}
|
|
83
|
+
return text, usage
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _call_claude(prompt, system, max_tokens, temperature):
|
|
87
|
+
key = _secret("ANTHROPIC_API_KEY")
|
|
88
|
+
base = (os.environ.get("ANTHROPIC_BASE_URL") or "https://api.anthropic.com").rstrip("/")
|
|
89
|
+
model = os.environ.get("CLAUDE_MODEL") or "claude-opus-5"
|
|
90
|
+
payload = {
|
|
91
|
+
"model": model,
|
|
92
|
+
"max_tokens": max_tokens or 4096,
|
|
93
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
94
|
+
}
|
|
95
|
+
if system:
|
|
96
|
+
payload["system"] = system
|
|
97
|
+
if temperature is not None:
|
|
98
|
+
payload["temperature"] = temperature
|
|
99
|
+
headers = {"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
|
|
100
|
+
data = _post_json(f"{base}/v1/messages", headers, payload)
|
|
101
|
+
text = "".join(b.get("text", "") for b in data.get("content", []) if b.get("type") == "text")
|
|
102
|
+
usage = {
|
|
103
|
+
"input_tokens": data.get("usage", {}).get("input_tokens", 0),
|
|
104
|
+
"output_tokens": data.get("usage", {}).get("output_tokens", 0),
|
|
105
|
+
}
|
|
106
|
+
return text, usage
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _call_gemini(prompt, system, max_tokens, temperature):
|
|
110
|
+
key = _secret("GEMINI_API_KEY")
|
|
111
|
+
model = os.environ.get("GEMINI_MODEL") or "gemini-2.5-pro"
|
|
112
|
+
base = (os.environ.get("GEMINI_BASE_URL") or "https://generativelanguage.googleapis.com").rstrip("/")
|
|
113
|
+
url = f"{base}/v1beta/models/{model}:generateContent?key={key}"
|
|
114
|
+
contents = []
|
|
115
|
+
if system:
|
|
116
|
+
contents.append({"role": "user", "parts": [{"text": system}]})
|
|
117
|
+
contents.append({"role": "user", "parts": [{"text": prompt}]})
|
|
118
|
+
payload = {"contents": contents}
|
|
119
|
+
gen = {}
|
|
120
|
+
if max_tokens:
|
|
121
|
+
gen["maxOutputTokens"] = max_tokens
|
|
122
|
+
if temperature is not None:
|
|
123
|
+
gen["temperature"] = temperature
|
|
124
|
+
if gen:
|
|
125
|
+
payload["generationConfig"] = gen
|
|
126
|
+
data = _post_json(url, {"content-type": "application/json"}, payload)
|
|
127
|
+
try:
|
|
128
|
+
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
129
|
+
except (KeyError, IndexError):
|
|
130
|
+
text = ""
|
|
131
|
+
usage = {
|
|
132
|
+
"input_tokens": data.get("usageMetadata", {}).get("promptTokenCount", 0),
|
|
133
|
+
"output_tokens": data.get("usageMetadata", {}).get("candidatesTokenCount", 0),
|
|
134
|
+
}
|
|
135
|
+
return text, usage
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _route(model, prompt, system, max_tokens, temperature):
|
|
139
|
+
if model == "chatgpt":
|
|
140
|
+
return _call_openai_compat("gpt-6-astra", "https://api.openai.com/v1", prompt, system, max_tokens, temperature, "OPENAI_API_KEY", "OPENAI_BASE_URL")
|
|
141
|
+
if model == "grok":
|
|
142
|
+
return _call_openai_compat("grok-4.6", "https://api.x.ai/v1", prompt, system, max_tokens, temperature, "GROK_API_KEY", "GROK_BASE_URL")
|
|
143
|
+
if model == "llama":
|
|
144
|
+
# Override default model via LLAMA_MODEL env var, e.g. export LLAMA_MODEL=qwen2.5:7b
|
|
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)
|
|
146
|
+
if model == "claude":
|
|
147
|
+
return _call_claude(prompt, system, max_tokens, temperature)
|
|
148
|
+
if model == "gemini":
|
|
149
|
+
return _call_gemini(prompt, system, max_tokens, temperature)
|
|
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")
|
|
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")
|
|
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")
|
|
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")
|
|
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
|
+
) -> dict:
|
|
171
|
+
"""Route prompt to a model, return unified response.
|
|
172
|
+
|
|
173
|
+
model: chatgpt|grok|claude|gemini|llama|deepseek|qwen|silicon|groq
|
|
174
|
+
External users must pass api_key; internal calls bypass auth (unlimited).
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if model not in SUPPORTED_MODELS:
|
|
178
|
+
return _reply(model, "failed", error=f"unsupported model: {model}")
|
|
179
|
+
# ---- Auth & permissions (external users only; no api_key = bypass) ----
|
|
180
|
+
user_id = 0 # 0 = internal (bypass billing/limits)
|
|
181
|
+
if api_key:
|
|
182
|
+
user = get_user_by_key(api_key)
|
|
183
|
+
if not user:
|
|
184
|
+
return _reply(model, "failed", error="invalid api_key")
|
|
185
|
+
# Paywall: PRO models only for pro tier users
|
|
186
|
+
if model in PRO_MODELS and user["tier"] != "pro":
|
|
187
|
+
return _reply(model, "failed", error="pro tier required for this model")
|
|
188
|
+
# Daily quota check
|
|
189
|
+
today = _today().isoformat()
|
|
190
|
+
cnt = get_daily_usage(user["id"], today)
|
|
191
|
+
if cnt >= user["daily_quota"]:
|
|
192
|
+
return _reply(model, "failed", error="daily quota exhausted")
|
|
193
|
+
user_id = user["id"]
|
|
194
|
+
# ---- Route ----
|
|
195
|
+
text, usage = _route(model, prompt, system or None, max_tokens or None, temperature)
|
|
196
|
+
# ---- Log usage (external users only) ----
|
|
197
|
+
if user_id:
|
|
198
|
+
cost = calculate_cost(model, usage["input_tokens"], usage["output_tokens"])
|
|
199
|
+
log_usage(user_id, model, usage["input_tokens"], usage["output_tokens"], cost)
|
|
200
|
+
return _reply(model, "completed", text=text, usage=usage)
|
|
201
|
+
except Exception as exc: # noqa: BLE001
|
|
202
|
+
return _reply(model, "failed", error=str(exc))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@mcp.tool()
|
|
206
|
+
def list_models() -> dict:
|
|
207
|
+
"""List available models with tier (free/pro)."""
|
|
208
|
+
models = []
|
|
209
|
+
for m in SUPPORTED_MODELS:
|
|
210
|
+
models.append({"model": m, "tier": "pro" if m in PRO_MODELS else "free"})
|
|
211
|
+
return {"models": models}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@mcp.tool()
|
|
215
|
+
def health_check() -> dict:
|
|
216
|
+
"""System health check: backend connectivity, DB status, process info."""
|
|
217
|
+
import platform, time
|
|
218
|
+
start = time.time()
|
|
219
|
+
result = {
|
|
220
|
+
"status": "ok",
|
|
221
|
+
"host": platform.node(),
|
|
222
|
+
"python": platform.python_version(),
|
|
223
|
+
"uptime_seconds": time.time() - start, # approximate
|
|
224
|
+
"backends": {},
|
|
225
|
+
"db": "ok",
|
|
226
|
+
}
|
|
227
|
+
# Test DB
|
|
228
|
+
try:
|
|
229
|
+
from .db import _conn
|
|
230
|
+
c = _conn()
|
|
231
|
+
c.execute("SELECT COUNT(*) FROM users")
|
|
232
|
+
result["db_users"] = c.fetchone()[0]
|
|
233
|
+
c.execute("SELECT COUNT(*) FROM usage_logs")
|
|
234
|
+
result["db_logs"] = c.fetchone()[0]
|
|
235
|
+
c.close()
|
|
236
|
+
except Exception as e:
|
|
237
|
+
result["db"] = str(e)
|
|
238
|
+
# Test backends (quick connectivity)
|
|
239
|
+
for model in SUPPORTED_MODELS:
|
|
240
|
+
try:
|
|
241
|
+
result["backends"][model] = {"model": model, "tier": "pro" if model in PRO_MODELS else "free", "env_set": bool(os.environ.get(f"{model.upper()}_API_KEY"))}
|
|
242
|
+
except Exception:
|
|
243
|
+
result["backends"][model] = {"model": model, "error": "check failed"}
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def run():
|
|
249
|
+
mcp.run(transport="sse")
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
run()
|