openai-router 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Astral Software Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: openai-router
3
+ Version: 0.1.0
4
+ Summary: openai-router
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.txt
8
+ Requires-Dist: fastapi>=0.120.3
9
+ Requires-Dist: gradio>=5.49.1
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: sqlmodel>=0.0.27
13
+ Requires-Dist: uvicorn>=0.38.0
14
+ Dynamic: license-file
15
+
16
+
17
+ <!-- markdownlint-disable MD033 -->
18
+ <h1 align="center">
19
+ 🚀 OpenAI Router
20
+ </h1>
21
+
22
+ <p align="center">
23
+ <b>轻量级、持久化、零配置的 OpenAI API 统一网关</b><br>
24
+ 一键聚合 vLLM、SGLang、lmdeploy、Ollama…
25
+ </p>
26
+
27
+ <p align="center">
28
+ <a href="#"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square"></a>
29
+ <a href="https://fastapi.tiangolo.com"><img src="https://img.shields.io/badge/FastAPI-v0.115+-teal?style=flat-square"></a>
30
+ <a href="https://gradio.app"><img src="https://img.shields.io/badge/Gradio-v5+-orange?style=flat-square"></a>
31
+ <a href="#"><img src="https://img.shields.io/badge/SQLite-内置存储-lightgrey?style=flat-square"></a>
32
+ </p>
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+ | Feature | Description |
38
+ |---------|-------------|
39
+ | 🌍 统一入口 | `/chat/completions`、`/embeddings`、`/images/generations`… 全部转发 |
40
+ | 🧩 多后端 | vLLM、SGLang、lmdeploy、Ollama… 任意组合 |
41
+ | 💾 持久化 | SQLite + SQLModel 零配置存储路由 |
42
+ | ⚡ 实时流 | SSE & Chunked Transfer 全双工支持 |
43
+ | 🎨 Web UI | Gradio 即用的管理面板 |
44
+ | 🔍 兼容 OpenAI | SDK / LangChain / AutoGen 等无需改动一行代码 |
45
+
46
+ ---
47
+
48
+ ### 📦 Quick Start
49
+ Step-1:安装
50
+ ```bash
51
+ uv sync
52
+ ```
53
+
54
+
55
+ Step-2:启动
56
+ ```bash
57
+ python -m openai_router.main --host localhost --port 8000
58
+ ```
59
+ 浏览器自动打开
60
+ 📍 UI:`http://localhost:8000`
61
+ 📍 API:`http://localhost:8000/v1`
62
+
63
+ Step-3:添加后端
64
+ 在 Web UI 「添加 / 更新」填入:
65
+ - 模型名:`gpt-4`
66
+ - 后端 URL:`http://localhost:8080/v1`
67
+
68
+ <img src="static/ui.png" width="800">
69
+
70
+ ---
71
+
72
+ ## 🔧 API Usage
73
+ ### **像官方 OpenAI SDK一样调用**
74
+ ```python
75
+ from openai import OpenAI
76
+ client = OpenAI(
77
+ base_url="http://localhost:8000/v1",
78
+ api_key="sk-dummy"
79
+ )
80
+ resp = client.chat.completions.create(
81
+ model="gpt-4",
82
+ messages=[{"role":"user","content":"hello"}],
83
+ stream=True
84
+ )
85
+ for chunk in resp:
86
+ print(chunk.choices[0].delta.content or "", end="")
87
+ ```
88
+
89
+ cURL
90
+ ```bash
91
+ curl http://localhost:8000/v1/chat/completions \
92
+ -H "Content-Type: application/json" \
93
+ -d '{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"stream":true}'
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 🗂️ Endpoints
99
+ | Method | Path | Description |
100
+ |--------|------|-------------|
101
+ | `GET` | `/` | Gradio Admin UI |
102
+ | `GET` | `/docs` | OpenAPI Swagger |
103
+ | `GET` | `/v1/models` | List available models |
104
+ `POST` | `/v1/responses` | Responses API |
105
+ | `POST` | `/v1/chat/completions` | Chat completion |
106
+ | `POST` | `/v1/embeddings` | Text embeddings |
107
+ | `POST` | `/v1/images/generations` | DALL·E style |
108
+ | `POST` | `/v1/audio/transcriptions` | Whisper |
109
+ | … | … | All OpenAI endpoints supported |
110
+
111
+ ---
112
+
113
+ ## ⚙️ Configuration
114
+ CLI Options
115
+ ```bash
116
+ python -m openai_router.main --help
117
+ ```
118
+ | Flag | Default | Description |
119
+ |------|---------|-------------|
120
+ | `--host` | `localhost` | Bind address |
121
+ | `--port` | `8000` | Bind port |
122
+
123
+
124
+ ---
125
+
126
+ ## 🏗️ Architecture
127
+ <img src="static/arch.png" width="800">
@@ -0,0 +1,112 @@
1
+
2
+ <!-- markdownlint-disable MD033 -->
3
+ <h1 align="center">
4
+ 🚀 OpenAI Router
5
+ </h1>
6
+
7
+ <p align="center">
8
+ <b>轻量级、持久化、零配置的 OpenAI API 统一网关</b><br>
9
+ 一键聚合 vLLM、SGLang、lmdeploy、Ollama…
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="#"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square"></a>
14
+ <a href="https://fastapi.tiangolo.com"><img src="https://img.shields.io/badge/FastAPI-v0.115+-teal?style=flat-square"></a>
15
+ <a href="https://gradio.app"><img src="https://img.shields.io/badge/Gradio-v5+-orange?style=flat-square"></a>
16
+ <a href="#"><img src="https://img.shields.io/badge/SQLite-内置存储-lightgrey?style=flat-square"></a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ ## ✨ Features
22
+ | Feature | Description |
23
+ |---------|-------------|
24
+ | 🌍 统一入口 | `/chat/completions`、`/embeddings`、`/images/generations`… 全部转发 |
25
+ | 🧩 多后端 | vLLM、SGLang、lmdeploy、Ollama… 任意组合 |
26
+ | 💾 持久化 | SQLite + SQLModel 零配置存储路由 |
27
+ | ⚡ 实时流 | SSE & Chunked Transfer 全双工支持 |
28
+ | 🎨 Web UI | Gradio 即用的管理面板 |
29
+ | 🔍 兼容 OpenAI | SDK / LangChain / AutoGen 等无需改动一行代码 |
30
+
31
+ ---
32
+
33
+ ### 📦 Quick Start
34
+ Step-1:安装
35
+ ```bash
36
+ uv sync
37
+ ```
38
+
39
+
40
+ Step-2:启动
41
+ ```bash
42
+ python -m openai_router.main --host localhost --port 8000
43
+ ```
44
+ 浏览器自动打开
45
+ 📍 UI:`http://localhost:8000`
46
+ 📍 API:`http://localhost:8000/v1`
47
+
48
+ Step-3:添加后端
49
+ 在 Web UI 「添加 / 更新」填入:
50
+ - 模型名:`gpt-4`
51
+ - 后端 URL:`http://localhost:8080/v1`
52
+
53
+ <img src="static/ui.png" width="800">
54
+
55
+ ---
56
+
57
+ ## 🔧 API Usage
58
+ ### **像官方 OpenAI SDK一样调用**
59
+ ```python
60
+ from openai import OpenAI
61
+ client = OpenAI(
62
+ base_url="http://localhost:8000/v1",
63
+ api_key="sk-dummy"
64
+ )
65
+ resp = client.chat.completions.create(
66
+ model="gpt-4",
67
+ messages=[{"role":"user","content":"hello"}],
68
+ stream=True
69
+ )
70
+ for chunk in resp:
71
+ print(chunk.choices[0].delta.content or "", end="")
72
+ ```
73
+
74
+ cURL
75
+ ```bash
76
+ curl http://localhost:8000/v1/chat/completions \
77
+ -H "Content-Type: application/json" \
78
+ -d '{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"stream":true}'
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 🗂️ Endpoints
84
+ | Method | Path | Description |
85
+ |--------|------|-------------|
86
+ | `GET` | `/` | Gradio Admin UI |
87
+ | `GET` | `/docs` | OpenAPI Swagger |
88
+ | `GET` | `/v1/models` | List available models |
89
+ `POST` | `/v1/responses` | Responses API |
90
+ | `POST` | `/v1/chat/completions` | Chat completion |
91
+ | `POST` | `/v1/embeddings` | Text embeddings |
92
+ | `POST` | `/v1/images/generations` | DALL·E style |
93
+ | `POST` | `/v1/audio/transcriptions` | Whisper |
94
+ | … | … | All OpenAI endpoints supported |
95
+
96
+ ---
97
+
98
+ ## ⚙️ Configuration
99
+ CLI Options
100
+ ```bash
101
+ python -m openai_router.main --help
102
+ ```
103
+ | Flag | Default | Description |
104
+ |------|---------|-------------|
105
+ | `--host` | `localhost` | Bind address |
106
+ | `--port` | `8000` | Bind port |
107
+
108
+
109
+ ---
110
+
111
+ ## 🏗️ Architecture
112
+ <img src="static/arch.png" width="800">
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "openai-router"
3
+ version = "0.1.0"
4
+ description = "openai-router"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "fastapi>=0.120.3",
9
+ "gradio>=5.49.1",
10
+ "httpx>=0.28.1",
11
+ "loguru>=0.7.3",
12
+ "sqlmodel>=0.0.27",
13
+ "uvicorn>=0.38.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ openai_router = "openai_router.main:cli_app"
18
+
19
+ [[tool.uv.index]]
20
+ url = "https://pypi.org/simple/"
21
+ default = true
22
+
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [build-system]
28
+ requires = ["setuptools", "wheel"]
29
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,474 @@
1
+ from typing import Annotated
2
+ from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi.responses import Response, StreamingResponse
4
+ from starlette.concurrency import run_in_threadpool
5
+ import httpx
6
+ from loguru import logger
7
+ from contextlib import asynccontextmanager
8
+ import gradio as gr
9
+ import pandas as pd
10
+ from datetime import datetime, timezone
11
+ import typer
12
+ import uvicorn
13
+ import webbrowser
14
+ import time
15
+
16
+ # --- 导入 SQLModel 和同步组件 ---
17
+ # 切换到同步 Session 和 Engine
18
+ from sqlmodel import Field, SQLModel, create_engine, Session, select
19
+ from sqlalchemy.engine import Engine # 导入同步 Engine
20
+
21
+ # --- 数据库配置 ---
22
+ SQLITE_DB_FILE = "routes.db"
23
+ # 使用同步 SQLite URL
24
+ SQLITE_URL = f"sqlite:///{SQLITE_DB_FILE}"
25
+
26
+ # --- 全局变量 ---
27
+ client: httpx.AsyncClient = None
28
+ # 同步 Engine
29
+ engine: Engine = None
30
+
31
+
32
+ # --- SQLModel 数据模型 ---
33
+ class ModelRoute(SQLModel, table=True):
34
+ """
35
+ 一个 SQLModel 模型,用于存储模型名称到后端 URL 的路由。
36
+ 'model_name' 是主键,确保了唯一性。
37
+ """
38
+
39
+ model_name: str = Field(primary_key=True, index=True)
40
+ model_url: str
41
+ created: datetime = Field(
42
+ default_factory=lambda: datetime.now(timezone.utc), nullable=False
43
+ )
44
+
45
+
46
+ # --- 同步数据库辅助函数 (将在线程池中执行) ---
47
+
48
+
49
+ def create_db_and_tables_sync():
50
+ """同步创建数据库和表"""
51
+ SQLModel.metadata.create_all(engine)
52
+
53
+
54
+ def get_all_model_names_sync():
55
+ """同步地从数据库中查询所有可用的模型名称"""
56
+ with Session(engine) as session:
57
+ statement = select(ModelRoute.model_name)
58
+ results = session.exec(statement)
59
+ available_models = results.all()
60
+ return available_models
61
+
62
+
63
+ def get_all_routes_sync():
64
+ """同步地从数据库中查询所有可用的 ModelRoute 记录"""
65
+ with Session(engine) as session:
66
+ # 查询 ModelRoute 的所有记录
67
+ statement = select(ModelRoute)
68
+ results = session.exec(statement)
69
+ # 返回 ModelRoute 对象的列表
70
+ all_routes = results.all()
71
+ return all_routes
72
+
73
+
74
+ def get_routing_info_sync(model: str):
75
+ """同步地从数据库中查询模型和所有可用模型"""
76
+ with Session(engine) as session:
77
+ # 1. 获取特定模型
78
+ db_route = session.get(ModelRoute, model)
79
+
80
+ # 2. 获取所有可用模型
81
+ available_models = get_all_model_names_sync()
82
+ server = db_route.model_url if db_route else None
83
+
84
+ return server, available_models
85
+
86
+
87
+ # --- FastAPI 生命周期 ---
88
+
89
+
90
+ @asynccontextmanager
91
+ async def lifespan(app: FastAPI):
92
+ global client, engine
93
+
94
+ # --- 启动时的逻辑 ---
95
+
96
+ # 1. 初始化数据库引擎 (同步)
97
+ engine = create_engine(SQLITE_URL, echo=False)
98
+
99
+ # 2. 创建数据库和表 (使用 run_in_threadpool 执行同步代码)
100
+ await run_in_threadpool(create_db_and_tables_sync)
101
+
102
+ # 3. **移除环境变量同步逻辑**
103
+
104
+ # 4. 初始化 httpx 客户端
105
+ timeout = httpx.Timeout(10.0, connect=60.0, read=None, write=60.0)
106
+ client = httpx.AsyncClient(timeout=timeout)
107
+
108
+ yield # 应用运行期间
109
+
110
+ # --- 关闭时的逻辑 ---
111
+
112
+ if client:
113
+ await client.aclose()
114
+ logger.info("HTTPX client closed.")
115
+
116
+ if engine:
117
+ engine.dispose()
118
+ logger.info("Database engine disposed.")
119
+
120
+
121
+ # --- FastAPI 核心应用 ---
122
+
123
+ # 使用 lifespan 创建 FastAPI 实例
124
+ app = FastAPI(lifespan=lifespan)
125
+
126
+
127
+ async def _get_routing_info(request: Request):
128
+ """
129
+ 异步辅助函数:解析请求体,并在线程池中执行同步数据库查询。
130
+ """
131
+ try:
132
+ json_body = await request.json()
133
+ except Exception as e:
134
+ logger.error(f"Failed to parse request body: {e}")
135
+ raise HTTPException(status_code=400, detail="Invalid JSON body")
136
+
137
+ model = json_body.get("model")
138
+ if model is None:
139
+ raise HTTPException(
140
+ status_code=400, detail="'model' field is required in request body"
141
+ )
142
+
143
+ # 在线程池中执行同步查询
144
+ server, available_models = await run_in_threadpool(get_routing_info_sync, model)
145
+
146
+ if server is None:
147
+ raise HTTPException(
148
+ status_code=400,
149
+ detail=f"Invalid model: {model}. Available models: {available_models}",
150
+ )
151
+
152
+ backend_url = server + request.url.path
153
+ logger.info(f"Routing to backend_url: {backend_url} for model {model}")
154
+
155
+ return backend_url, json_body
156
+
157
+
158
+ async def _stream_proxy(backend_url: str, request: Request, json_body: dict):
159
+ """
160
+ 这是一个异步生成器,用于代理流式响应。
161
+ """
162
+ headers = {
163
+ h: v
164
+ for h, v in request.headers.items()
165
+ if h.lower() not in ["host", "content-length"]
166
+ }
167
+
168
+ try:
169
+ async with client.stream(
170
+ request.method,
171
+ backend_url,
172
+ params=request.query_params,
173
+ json=json_body,
174
+ headers=headers,
175
+ ) as response:
176
+ if response.status_code >= 400:
177
+ error_content = await response.aread()
178
+ logger.warning(
179
+ f"Backend error: {response.status_code} - {error_content.decode()}"
180
+ )
181
+ raise HTTPException(
182
+ status_code=response.status_code, detail=error_content.decode()
183
+ )
184
+
185
+ async for chunk in response.aiter_bytes():
186
+ yield chunk
187
+
188
+ except httpx.ConnectError as e:
189
+ logger.error(f"Connection error to backend {backend_url}: {e}")
190
+ raise HTTPException(status_code=503, detail="Backend service unavailable")
191
+ except Exception as e:
192
+ logger.error(f"An error occurred during streaming proxy: {e}")
193
+ logger.error("Streaming interrupted due to an error.")
194
+
195
+
196
+ async def _non_stream_proxy(backend_url: str, request: Request, json_body: dict):
197
+ """
198
+ 处理非流式请求的代理逻辑。
199
+ """
200
+ headers = {
201
+ h: v
202
+ for h, v in request.headers.items()
203
+ if h.lower() not in ["host", "content-length"]
204
+ }
205
+
206
+ try:
207
+ response = await client.post(
208
+ backend_url, params=request.query_params, json=json_body, headers=headers
209
+ )
210
+ return Response(
211
+ content=response.content,
212
+ media_type=response.headers.get("Content-Type"),
213
+ status_code=response.status_code,
214
+ )
215
+
216
+ except httpx.ConnectError as e:
217
+ logger.error(f"Connection error to backend {backend_url}: {e}")
218
+ raise HTTPException(status_code=503, detail="Backend service unavailable")
219
+ except httpx.ReadTimeout as e:
220
+ logger.error(f"Read timeout from backend {backend_url}: {e}")
221
+ raise HTTPException(status_code=504, detail="Backend request timed out")
222
+ except Exception as e:
223
+ logger.error(f"An error occurred during non-streaming proxy: {e}")
224
+ raise HTTPException(status_code=500, detail=f"Internal proxy error: {e}")
225
+
226
+
227
+ @app.get("/v1/models", summary="List available models")
228
+ async def list_models():
229
+ """
230
+ OpenAI 兼容接口: 列出所有可用的模型。
231
+ """
232
+ try:
233
+ all_routes: list[ModelRoute] = await run_in_threadpool(get_all_routes_sync)
234
+
235
+ models_data = []
236
+ for route in all_routes:
237
+ created_timestamp = int(route.created.timestamp())
238
+
239
+ models_data.append(
240
+ {
241
+ "id": route.model_name,
242
+ "object": "model",
243
+ # 3. 使用数据库中的 created 时间戳
244
+ "created": created_timestamp,
245
+ "owned_by": "openai_router",
246
+ "permission": [],
247
+ }
248
+ )
249
+
250
+ response_data = {"object": "list", "data": models_data}
251
+
252
+ logger.info(f"Returning {len(all_routes)} available models for /v1/models.")
253
+
254
+ return response_data
255
+
256
+ except Exception as e:
257
+ logger.error(f"Failed to list models: {e}")
258
+ raise HTTPException(
259
+ status_code=500, detail=f"Internal server error when retrieving models: {e}"
260
+ )
261
+
262
+
263
+ @app.post("/v1/responses", summary="/v1/responses ")
264
+ @app.post("/v1/completions", summary="/v1/completions")
265
+ @app.post("/v1/chat/completions", summary="/v1/chat/completions")
266
+ @app.post("/v1/embeddings", summary="/v1/embeddings")
267
+ @app.post("/v1/moderations", summary="/v1/moderations")
268
+ @app.post("/v1/images/generations", summary="/v1/images/generations")
269
+ @app.post("/v1/images/edits", summary="/v1/images/edits")
270
+ @app.post("/v1/images/variations", summary="/v1/images/variations")
271
+ @app.post("/v1/audio/transcriptions", summary="/v1/audio/transcriptions")
272
+ @app.post("/v1/audio/speech", summary="/v1/audio/speech")
273
+ @app.post("/v1/rerank", summary="/v1/rerank")
274
+ async def router(request: Request):
275
+ backend_url, json_body = await _get_routing_info(request)
276
+ if json_body.get("stream", False):
277
+ return StreamingResponse(
278
+ _stream_proxy(backend_url, request, json_body),
279
+ media_type="text/event-stream",
280
+ )
281
+ else:
282
+ return await _non_stream_proxy(backend_url, request, json_body)
283
+
284
+
285
+ # --- Gradio 管理界面逻辑 (同步数据库操作) ---
286
+
287
+
288
+ def get_current_routes_sync():
289
+ """同步获取当前路由表"""
290
+ with Session(engine) as session:
291
+ statement = select(ModelRoute)
292
+ routes_db = session.exec(statement)
293
+ routes = [[route.model_name, route.model_url] for route in routes_db.all()]
294
+ return routes
295
+
296
+
297
+ def add_or_update_route_sync(model_name: str, model_url: str):
298
+ """同步添加或更新一个路由到数据库"""
299
+ status_message = ""
300
+ with Session(engine) as session:
301
+ db_route = session.get(ModelRoute, model_name)
302
+
303
+ if db_route:
304
+ db_route.model_url = model_url
305
+ status_message = f"路由 '{model_name}' 已更新。"
306
+ else:
307
+ db_route = ModelRoute(model_name=model_name, model_url=model_url)
308
+ status_message = f"路由 '{model_name}' 已添加。"
309
+
310
+ session.add(db_route)
311
+ session.commit()
312
+
313
+ logger.info(f"[Admin] {status_message}")
314
+ return status_message
315
+
316
+
317
+ def delete_route_sync(model_name: str):
318
+ """同步从数据库删除一个路由"""
319
+ status_message = ""
320
+ with Session(engine) as session:
321
+ db_route = session.get(ModelRoute, model_name)
322
+
323
+ if db_route:
324
+ session.delete(db_route)
325
+ session.commit()
326
+ status_message = f"路由 '{model_name}' 已删除。"
327
+ logger.info(f"[Admin] Route deleted: {model_name}")
328
+ else:
329
+ status_message = f"错误: 未找到路由 '{model_name}'。"
330
+
331
+ return status_message
332
+
333
+
334
+ # Gradio 异步事件处理函数 (使用 run_in_threadpool 调用同步函数)
335
+
336
+
337
+ async def get_current_routes():
338
+ """异步调用同步函数获取当前路由表"""
339
+ return await run_in_threadpool(get_current_routes_sync)
340
+
341
+
342
+ async def add_or_update_route(model_name: str, model_url: str):
343
+ """异步调用同步函数添加或更新路由"""
344
+ if not model_name or not model_url:
345
+ return "模型名称和 URL 不能为空", await get_current_routes()
346
+
347
+ status_message = await run_in_threadpool(
348
+ add_or_update_route_sync, model_name, model_url
349
+ )
350
+ return status_message, await get_current_routes()
351
+
352
+
353
+ async def delete_route(model_name: str):
354
+ """异步调用同步函数删除路由"""
355
+ if not model_name:
356
+ return "要删除的模型名称不能为空", await get_current_routes()
357
+
358
+ status_message = await run_in_threadpool(delete_route_sync, model_name)
359
+ return status_message, await get_current_routes()
360
+
361
+
362
+ def on_select_route(routes_data: pd.DataFrame, evt: gr.SelectData):
363
+ """
364
+ Gradio: 当用户点击表格中的一行时,填充输入框。
365
+ """
366
+ if evt.index is None:
367
+ return "", ""
368
+
369
+ selected_row = routes_data.iloc[evt.index[0]]
370
+ model_name = selected_row.iloc[0]
371
+ model_url = selected_row.iloc[1]
372
+
373
+ return model_name, model_url
374
+
375
+
376
+ def create_admin_ui():
377
+ """创建 Gradio Blocks 界面"""
378
+ with gr.Blocks(
379
+ title="模型路由管理器", css="footer {display: none !important}"
380
+ ) as admin_ui:
381
+ gr.Markdown(
382
+ "<h1 style='text-align:center;'>模型路由管理器</h1>", elem_id="title"
383
+ )
384
+ gr.Markdown(
385
+ """**将不同端口、不同服务的`openAI`的接口通过统一的url进行路由!兼容 `vLLM`、`SGLang`、`lmdeoply`、`Ollama`等。**\n
386
+ **注意:** 所有路由配置都持久化到 `routes.db` 数据库中。您需要手动添加初始路由。"""
387
+ )
388
+
389
+ with gr.Row():
390
+ refresh_button = gr.Button("刷新路由列表")
391
+
392
+ with gr.Row():
393
+ with gr.Column(scale=2):
394
+ routes_datagrid = gr.DataFrame(
395
+ headers=["模型名称 (Model Name)", "后端 URL (Backend URL)"],
396
+ label="当前路由表",
397
+ row_count=(1, "fixed"),
398
+ col_count=(2, "fixed"),
399
+ interactive=False,
400
+ )
401
+ with gr.Column(scale=1):
402
+ gr.Markdown("### 管理路由")
403
+ status_output = gr.Textbox(
404
+ label="操作状态",
405
+ interactive=False,
406
+ value="这里用于显示上一次的操作状态",
407
+ )
408
+ model_name_input = gr.Textbox(label="模型名称", value="gpt4")
409
+ model_url_input = gr.Textbox(
410
+ label="后端 URL", value="http://localhost:8082"
411
+ )
412
+ with gr.Row():
413
+ add_update_button = gr.Button("添加 / 更新")
414
+ delete_button = gr.Button(
415
+ "删除",
416
+ variant="stop",
417
+ )
418
+
419
+ # --- 绑定 Gradio 事件 ---
420
+ admin_ui.load(get_current_routes, outputs=routes_datagrid)
421
+ refresh_button.click(get_current_routes, outputs=routes_datagrid)
422
+
423
+ add_update_button.click(
424
+ add_or_update_route,
425
+ inputs=[model_name_input, model_url_input],
426
+ outputs=[status_output, routes_datagrid],
427
+ )
428
+
429
+ delete_button.click(
430
+ delete_route,
431
+ inputs=[model_name_input],
432
+ outputs=[status_output, routes_datagrid],
433
+ )
434
+
435
+ routes_datagrid.select(
436
+ on_select_route,
437
+ inputs=[routes_datagrid],
438
+ outputs=[model_name_input, model_url_input],
439
+ )
440
+
441
+ return admin_ui
442
+
443
+
444
+ # --- 挂载 Gradio 应用 ---
445
+ admin_interface = create_admin_ui()
446
+ app = gr.mount_gradio_app(app, admin_interface, path="/")
447
+
448
+ cli_app = typer.Typer()
449
+
450
+
451
+ @cli_app.command()
452
+ def main(
453
+ host: Annotated[
454
+ str, typer.Option(help="指定监听的主机地址", show_default=True)
455
+ ] = "localhost",
456
+ port: Annotated[
457
+ int, typer.Option(help="指定监听的主机端口", show_default=True)
458
+ ] = 8000,
459
+ ):
460
+ base_url = f"http://{host}:{port}"
461
+ logger.info(f"UI 界面: http://{host}:{port}")
462
+ logger.info(f"openAI API 文档: http://{host}:{port}/docs")
463
+ time.sleep(1)
464
+ try:
465
+ if "0.0.0.0" in base_url:
466
+ base_url = f"http://localhost:{port}"
467
+ webbrowser.open_new_tab(base_url)
468
+ except Exception as e:
469
+ logger.warning(f"无法自动打开浏览器: {e}")
470
+ uvicorn.run(app, host=host, port=port)
471
+
472
+
473
+ if __name__ == "__main__":
474
+ cli_app()
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: openai-router
3
+ Version: 0.1.0
4
+ Summary: openai-router
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.txt
8
+ Requires-Dist: fastapi>=0.120.3
9
+ Requires-Dist: gradio>=5.49.1
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: sqlmodel>=0.0.27
13
+ Requires-Dist: uvicorn>=0.38.0
14
+ Dynamic: license-file
15
+
16
+
17
+ <!-- markdownlint-disable MD033 -->
18
+ <h1 align="center">
19
+ 🚀 OpenAI Router
20
+ </h1>
21
+
22
+ <p align="center">
23
+ <b>轻量级、持久化、零配置的 OpenAI API 统一网关</b><br>
24
+ 一键聚合 vLLM、SGLang、lmdeploy、Ollama…
25
+ </p>
26
+
27
+ <p align="center">
28
+ <a href="#"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square"></a>
29
+ <a href="https://fastapi.tiangolo.com"><img src="https://img.shields.io/badge/FastAPI-v0.115+-teal?style=flat-square"></a>
30
+ <a href="https://gradio.app"><img src="https://img.shields.io/badge/Gradio-v5+-orange?style=flat-square"></a>
31
+ <a href="#"><img src="https://img.shields.io/badge/SQLite-内置存储-lightgrey?style=flat-square"></a>
32
+ </p>
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+ | Feature | Description |
38
+ |---------|-------------|
39
+ | 🌍 统一入口 | `/chat/completions`、`/embeddings`、`/images/generations`… 全部转发 |
40
+ | 🧩 多后端 | vLLM、SGLang、lmdeploy、Ollama… 任意组合 |
41
+ | 💾 持久化 | SQLite + SQLModel 零配置存储路由 |
42
+ | ⚡ 实时流 | SSE & Chunked Transfer 全双工支持 |
43
+ | 🎨 Web UI | Gradio 即用的管理面板 |
44
+ | 🔍 兼容 OpenAI | SDK / LangChain / AutoGen 等无需改动一行代码 |
45
+
46
+ ---
47
+
48
+ ### 📦 Quick Start
49
+ Step-1:安装
50
+ ```bash
51
+ uv sync
52
+ ```
53
+
54
+
55
+ Step-2:启动
56
+ ```bash
57
+ python -m openai_router.main --host localhost --port 8000
58
+ ```
59
+ 浏览器自动打开
60
+ 📍 UI:`http://localhost:8000`
61
+ 📍 API:`http://localhost:8000/v1`
62
+
63
+ Step-3:添加后端
64
+ 在 Web UI 「添加 / 更新」填入:
65
+ - 模型名:`gpt-4`
66
+ - 后端 URL:`http://localhost:8080/v1`
67
+
68
+ <img src="static/ui.png" width="800">
69
+
70
+ ---
71
+
72
+ ## 🔧 API Usage
73
+ ### **像官方 OpenAI SDK一样调用**
74
+ ```python
75
+ from openai import OpenAI
76
+ client = OpenAI(
77
+ base_url="http://localhost:8000/v1",
78
+ api_key="sk-dummy"
79
+ )
80
+ resp = client.chat.completions.create(
81
+ model="gpt-4",
82
+ messages=[{"role":"user","content":"hello"}],
83
+ stream=True
84
+ )
85
+ for chunk in resp:
86
+ print(chunk.choices[0].delta.content or "", end="")
87
+ ```
88
+
89
+ cURL
90
+ ```bash
91
+ curl http://localhost:8000/v1/chat/completions \
92
+ -H "Content-Type: application/json" \
93
+ -d '{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"stream":true}'
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 🗂️ Endpoints
99
+ | Method | Path | Description |
100
+ |--------|------|-------------|
101
+ | `GET` | `/` | Gradio Admin UI |
102
+ | `GET` | `/docs` | OpenAPI Swagger |
103
+ | `GET` | `/v1/models` | List available models |
104
+ `POST` | `/v1/responses` | Responses API |
105
+ | `POST` | `/v1/chat/completions` | Chat completion |
106
+ | `POST` | `/v1/embeddings` | Text embeddings |
107
+ | `POST` | `/v1/images/generations` | DALL·E style |
108
+ | `POST` | `/v1/audio/transcriptions` | Whisper |
109
+ | … | … | All OpenAI endpoints supported |
110
+
111
+ ---
112
+
113
+ ## ⚙️ Configuration
114
+ CLI Options
115
+ ```bash
116
+ python -m openai_router.main --help
117
+ ```
118
+ | Flag | Default | Description |
119
+ |------|---------|-------------|
120
+ | `--host` | `localhost` | Bind address |
121
+ | `--port` | `8000` | Bind port |
122
+
123
+
124
+ ---
125
+
126
+ ## 🏗️ Architecture
127
+ <img src="static/arch.png" width="800">
@@ -0,0 +1,11 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ src/openai_router/__init__.py
5
+ src/openai_router/main.py
6
+ src/openai_router.egg-info/PKG-INFO
7
+ src/openai_router.egg-info/SOURCES.txt
8
+ src/openai_router.egg-info/dependency_links.txt
9
+ src/openai_router.egg-info/entry_points.txt
10
+ src/openai_router.egg-info/requires.txt
11
+ src/openai_router.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openai_router = openai_router.main:cli_app
@@ -0,0 +1,6 @@
1
+ fastapi>=0.120.3
2
+ gradio>=5.49.1
3
+ httpx>=0.28.1
4
+ loguru>=0.7.3
5
+ sqlmodel>=0.0.27
6
+ uvicorn>=0.38.0
@@ -0,0 +1 @@
1
+ openai_router