@wangmingfa/model-gate 0.0.1-beta.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wangmingfa
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.
package/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # model-gate
2
+
3
+ LLM API 中转网关:在一个配置文件里配置多家运营商的模型(DeepSeek、Kimi、通义、智谱……),对外暴露**一个统一的 OpenAI 兼容接口**,供你机器上的多个 coding agent 使用。
4
+
5
+ **解决什么问题**:你有很多 coding agent(Claude Code、Cursor、各种开源 agent),每个都要单独配置各家厂商的 base_url、api_key、模型名。有了 model-gate,agent 只认一个地址 + 一个 key + 一组**模型别名**;换上游、换模型、加厂商都只改网关的配置文件,热加载即生效,agent 侧零改动。
6
+
7
+ ---
8
+
9
+ ## 特性
10
+
11
+ - **统一 OpenAI 兼容入口**:`POST /v1/chat/completions`(含流式 SSE)+ `GET /v1/models`
12
+ - **模型别名**:agent 只认识别名(如 `fast`、`reason`),不感知背后的真实模型;响应与流式 chunk 中的 model 字段统一改写为别名
13
+ - **有序 failover**:一个别名可绑多个 `provider:model`(数组,顺序即 failover 顺序);管理界面里每个别名当前从下拉选一个目标(数组格式仍允许配置多个作为 failover 备选)
14
+ - **多下游密钥**:多个 agent 各用一个 key,为按 agent 记账/排查打基础
15
+ - **配置热加载**:改 `config.json` 立即生效,无需重启
16
+ - **请求日志**:控制台每请求一行摘要 + `access.log`(JSONL)逐请求记录 token 用量,可开关
17
+ - **密钥支持环境变量**:`api_key` 可以写 `${ENV_VAR}` 引用环境变量
18
+ - **Web 配置界面**:`/admin` 下的 SPA(Vite+Vue3+Naive UI),可视化编辑提供商(provider)/别名/密钥/默认模型,保存即校验并热加载;本机回环免登录,非本机访问需 `admin_password` 密码登录;`api_key` 以密码框遮挡显示
19
+
20
+ ## 快速开始
21
+
22
+ 前置:安装 [bun](https://bun.sh/)(运行时依赖,版本 >= 1.0.0):
23
+
24
+ ```bash
25
+ curl -fsSL https://bun.sh/install | bash
26
+ ```
27
+
28
+ ```bash
29
+ # 1. 安装依赖(会同时安装 admin 子项目依赖)
30
+ bun install
31
+
32
+ # 2. 复制示例配置并编辑(填你的各家 key)
33
+ cp config.example.json config.json
34
+ vim config.json
35
+
36
+ # 3. 开发模式(api 热重载 + Vite 前端热更新,开箱即用)
37
+ bun run dev
38
+
39
+ # 3'. 生产构建:内联前端资源并打包成单文件二进制 model-gate.js
40
+ bun run build
41
+ bun run model-gate.js # 运行构建产物(等同于 node 跑这个 bun 二进制)
42
+
43
+ # 指定配置文件(开发/生产均可):
44
+ bun run dev -- --config /path/to/config.json
45
+ # 或设环境变量 MODEL_GATE_CONFIG=/path/to/config.json
46
+ ```
47
+
48
+ ## Web 配置界面(/admin)
49
+
50
+ 不用手改 JSON,浏览器里可视化编辑配置:
51
+
52
+ ```bash
53
+ bun run build # 构建并内联前端资源(Vite 产物 admin/dist/ → src/admin-assets.generated.ts → 打包进二进制)
54
+ bun run model-gate.js # 启动网关(或开发模式 bun run dev)
55
+ ```
56
+
57
+ 打开浏览器:
58
+
59
+ - **本机访问**:`http://127.0.0.1:8787/admin`(免登录直接进入)
60
+ - **局域网/远程访问**:`host` 配成 `0.0.0.0` 时,启动日志会打印所有 IPv4 入口(如 `http://192.168.1.100:8787/admin`),其他机器用该地址访问并密码登录
61
+
62
+ - **保存 = 校验通过后原子写回 config.json 并热加载生效**,config.json 始终是唯一真相源
63
+ - 可编辑:providers(base_url/api_key/模型列表 + 每个 provider 的"测试连接"按钮)、aliases(别名 → 有序 `provider:model`,顺序即 failover 顺序)、keys(下游密钥)、默认模型
64
+ - port / host / timeout 等启动参数只读展示(修改需编辑 config.json 后重启)
65
+ - **访问控制**:本机回环(127.0.0.1/::1)免登录直接进入;**非本机访问需密码登录**——在 config.json 顶层配置 `admin_password`(支持 `${ENV_VAR}` 插值,留空 = 未配置);未配置时登录页会提示去实际配置文件设置。会话为内存态(24 小时过期,重启失效),登录页提供登出;连续 5 次密码错误锁定 60 秒
66
+ - **安全**:`api_key` 返回真实值,前端以密码框(type=password)遮挡显示,明文不外露;编辑时留空 = 保持原值(不清空原密钥),填新值即覆盖;`admin_password` 不进界面编辑范围,只在配置文件改
67
+ - 开发模式:`bun run dev` 会并行起 Vite dev server(端口 5173,代理 `/admin/api` 到网关)和网关 api,前端改动热更新
68
+
69
+ 启动后服务监听在配置的 `host:port`(默认 `http://127.0.0.1:8787`)。
70
+
71
+ ---
72
+
73
+ ## 配置文件说明(config.json)
74
+
75
+ 所有字段如下,标 ⭐ 的为必填:
76
+
77
+ | 字段 | 类型 | 默认值 | 含义 |
78
+ |---|---|---|---|
79
+ | `port` ⭐ | number | `8787` | 监听端口(1-65535) |
80
+ | `host` | string | `"127.0.0.1"` | 监听地址;本机自用默认即可,远程访问改 `"0.0.0.0"` |
81
+ | `default_model` | string | 第一个别名 | agent 请求未指定 `model` 时使用的别名 |
82
+ | `timeout_seconds` | number | `60` | 非流式请求的整体超时;流式请求的"空闲超时"(见下文"超时") |
83
+ | `access_log` | boolean | `true` | 是否写 `access.log`(JSONL) |
84
+ | `keys` ⭐ | string[] | — | 下游鉴权密钥列表,agent 必须携带其中之一(非空) |
85
+ | `providers` ⭐ | object | — | 各上游厂商配置(见下表) |
86
+ | `aliases` ⭐ | object | — | 别名 → 有序的 `provider:model` 列表(顺序即 failover 顺序) |
87
+
88
+ `providers` 中每个 provider 的字段:
89
+
90
+ | 字段 | 类型 | 含义 |
91
+ |---|---|---|
92
+ | `base_url` ⭐ | string | 厂商的 OpenAI 兼容端点,如 `https://api.deepseek.com/v1`(尾部斜杠自动去掉) |
93
+ | `api_key` ⭐ | string | 该厂商的密钥;以 `${VAR}` 开头时从环境变量读取(见"环境变量插值") |
94
+ | `models` ⭐ | string[] | 该 provider 可用的模型 id 列表(非空) |
95
+
96
+ **启动时校验**:key 非空、base_url 是 http(s) URL、每个别名项必须是 `provider:model` 且引用存在的 provider 和其 models 列表中的模型、`default_model` 必须是已定义别名。任何一项不合法都会报错退出。
97
+
98
+ ### 完整示例
99
+
100
+ ```json
101
+ {
102
+ "port": 8787,
103
+ "host": "127.0.0.1",
104
+ "default_model": "fast",
105
+ "timeout_seconds": 60,
106
+ "access_log": true,
107
+
108
+ "keys": ["sk-local-claude", "sk-local-cursor"],
109
+
110
+ "providers": {
111
+ "deepseek": {
112
+ "base_url": "https://api.deepseek.com/v1",
113
+ "api_key": "${DEEPSEEK_API_KEY}",
114
+ "models": ["deepseek-chat", "deepseek-reasoner"]
115
+ },
116
+ "kimi": {
117
+ "base_url": "https://api.moonshot.cn/v1",
118
+ "api_key": "sk-xxxxxxxxxxxxxxxx",
119
+ "models": ["moonshot-v1-8k", "moonshot-v1-32k"]
120
+ },
121
+ "tongyi": {
122
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
123
+ "api_key": "sk-xxxxxxxxxxxxxxxx",
124
+ "models": ["qwen-plus", "qwen-max"]
125
+ }
126
+ },
127
+
128
+ "aliases": {
129
+ "fast": ["deepseek:deepseek-chat", "kimi:moonshot-v1-8k"],
130
+ "reason": ["deepseek:deepseek-reasoner", "tongyi:qwen-max"],
131
+ "long": ["kimi:moonshot-v1-32k"]
132
+ }
133
+ }
134
+ ```
135
+
136
+ - `fast` 别名:先试 DeepSeek 的 `deepseek-chat`,失败自动切 Kimi 的 `moonshot-v1-8k`(failover 顺序)
137
+ - `reason` 别名:DeepSeek 推理模型,挂了切通义 qwen-max
138
+ - `long` 别名:给长上下文场景用
139
+
140
+ ### 环境变量插值
141
+
142
+ `api_key` 的值以 `${VAR}` 开头时,启动时从环境变量读取;环境变量不存在则启动报错。否则按字面量使用:
143
+
144
+ ```json
145
+ { "providers": { "deepseek": { "api_key": "${DEEPSEEK_API_KEY}" } } }
146
+ ```
147
+
148
+ ```bash
149
+ export DEEPSEEK_API_KEY=sk-xxxx
150
+ bun run dev # 或 bun run model-gate.js(生产构建后)
151
+ ```
152
+
153
+ ### 配置热加载
154
+
155
+ 修改 `config.json` 后**最多 1 秒自动生效**(轮询文件 mtime):新增/删除别名、换 key、改默认模型、改 provider 都不需要重启服务。校验失败时**保留旧配置**并打印错误,不影响服务运行。
156
+
157
+ ---
158
+
159
+ ## API 说明
160
+
161
+ ### 鉴权
162
+
163
+ 所有 `/v1/*` 请求必须携带配置文件 `keys` 列表中的某个 key:
164
+
165
+ ```
166
+ Authorization: Bearer sk-local-claude
167
+ ```
168
+
169
+ key 不合法返回 `401`。`/health` 不鉴权。
170
+
171
+ ### `GET /health`
172
+
173
+ ```json
174
+ { "status": "ok" }
175
+ ```
176
+
177
+ ### `GET /v1/models`
178
+
179
+ 返回可用别名列表(agent 通常用它发现模型):
180
+
181
+ ```json
182
+ { "object": "list", "data": [ { "id": "fast", "object": "model", "created": 1787100000, "owned_by": "model-gate" } ] }
183
+ ```
184
+
185
+ ### `POST /v1/chat/completions`
186
+
187
+ 与 OpenAI 官方接口完全兼容:
188
+
189
+ - **请求**:除 `model` 外所有参数(`messages`、`temperature`、`max_tokens`、`tools`、`stream`、`top_p` 等)原样透传
190
+ - **model 字段**:填别名;不填则用 `default_model`;填了不存在的别名返回 `400`(错误信息里列出可用别名)
191
+ - **响应**:上游响应原样透传,仅 `model` 字段改写为别名;`usage`(token 用量)原样透传
192
+ - **流式**:`"stream": true` 时返回 SSE(`text/event-stream`),每个 chunk 的 `model` 也改写为别名,`data: [DONE]` 正常结束
193
+
194
+ 请求示例:
195
+
196
+ ```bash
197
+ curl http://127.0.0.1:8787/v1/chat/completions \
198
+ -H "Authorization: Bearer sk-local-claude" \
199
+ -H "Content-Type: application/json" \
200
+ -d '{
201
+ "model": "fast",
202
+ "messages": [{"role": "user", "content": "你好"}],
203
+ "stream": true
204
+ }'
205
+ ```
206
+
207
+ ### 错误格式
208
+
209
+ 统一的 OpenAI 风格错误体:
210
+
211
+ ```json
212
+ { "error": { "message": "……", "type": "……", "code": "……" } }
213
+ ```
214
+
215
+ | 场景 | 状态码 | code |
216
+ |---|---|---|
217
+ | key 缺失/错误 | 401 | `invalid_api_key` |
218
+ | 请求体非法 JSON | 400 | `invalid_json` |
219
+ | 未知模型别名 | 400 | `model_not_found` |
220
+ | 所有 provider 失败 | 502(最后一个失败是 4xx 则沿用其状态码) | `upstream_failed` |
221
+ | 未实现的端点(如 `/v1/embeddings`) | 501 | `not_implemented` |
222
+
223
+ ### failover 行为
224
+
225
+ - 按 `aliases` 中 `provider:model` 的**顺序**逐个尝试;网络错误、超时、任何非 2xx 状态都切下一个
226
+ - 4xx 也参与切换:不同厂商上下文窗口不同(如 64k vs 128k),A 家报 context 超长会自动落到窗口更大的 B 家
227
+ - 全部失败时:错误消息聚合列出每个目标及失败原因(`provider:model: 原因`)
228
+ - **网关不自动重试**同一目标(避免重复计费),重试决策交给 agent
229
+ - **流式**:只有"未拿到上游 200 之前"的失败才切换;一旦开始推流即提交,流中途不切换(断了由 agent 重试)
230
+
231
+ ### 超时
232
+
233
+ - 非流式请求:`timeout_seconds`(默认 60s)整体超时
234
+ - 流式请求:拿到响应头后进入"空闲超时"——超过 `timeout_seconds` 没有新数据就断开下游(防止上游挂死)
235
+
236
+ ---
237
+
238
+ ## 日志
239
+
240
+ ### 控制台
241
+
242
+ 每个请求一行摘要:
243
+
244
+ ```
245
+ [2026-08-19T14:51:18.376Z] POST /v1/chat/completions 200 438ms key=sk-local-claude alias=fast model=deepseek:deepseek-chat tokens=523 stream
246
+ ```
247
+
248
+ ### access.log(JSONL,可开关)
249
+
250
+ 每条请求一行 JSON,字段:
251
+
252
+ | 字段 | 含义 |
253
+ |---|---|
254
+ | `ts` | 时间(ISO 8601) |
255
+ | `method` / `path` | 请求方法 / 路径 |
256
+ | `status` / `ms` | 状态码 / 耗时(毫秒) |
257
+ | `key` | 下游密钥(按 agent 审计的依据) |
258
+ | `alias` | 请求的别名 |
259
+ | `realModel` | 实际命中的 `provider:model` |
260
+ | `stream` | 是否流式 |
261
+ | `promptTokens` / `completionTokens` / `totalTokens` | token 用量(流式取自最后一个携带 usage 的 chunk;上游没返回则缺省) |
262
+
263
+ ```json
264
+ {"ts":"2026-08-19T14:51:18.376Z","method":"POST","path":"/v1/chat/completions","status":200,"ms":438,"key":"sk-local-claude","alias":"fast","realModel":"deepseek:deepseek-chat","totalTokens":523}
265
+ ```
266
+
267
+ ---
268
+
269
+ ## agent 接入示例
270
+
271
+ 所有 agent 都指向同一个 base_url 和各自的 key,`model` 填别名即可。
272
+
273
+ ### Claude Code
274
+
275
+ ```bash
276
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
277
+ export ANTHROPIC_AUTH_TOKEN=sk-local-claude
278
+ ```
279
+
280
+ (Claude Code 会把 `ANTHROPIC_BASE_URL` 当 OpenAI 兼容端点用,`ANTHROPIC_AUTH_TOKEN` 会以 `Authorization: Bearer` 发送;配合 `/v1/models` 返回的别名选择模型。)
281
+
282
+ ### Cursor / 各种带自定义 OpenAI API 配置的 IDE
283
+
284
+ 在设置里把 API Base 填 `http://127.0.0.1:8787/v1`,API Key 填 `sk-local-cursor`,模型名填别名如 `fast`。
285
+
286
+ ### OpenAI SDK
287
+
288
+ Python:
289
+
290
+ ```python
291
+ from openai import OpenAI
292
+
293
+ client = OpenAI(
294
+ base_url="http://127.0.0.1:8787/v1",
295
+ api_key="sk-local-claude",
296
+ )
297
+ resp = client.chat.completions.create(
298
+ model="fast", # 别名
299
+ messages=[{"role": "user", "content": "你好"}],
300
+ stream=True,
301
+ )
302
+ for chunk in resp:
303
+ print(chunk.choices[0].delta.content or "", end="")
304
+ ```
305
+
306
+ Node.js:
307
+
308
+ ```js
309
+ import OpenAI from "openai";
310
+
311
+ const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1", apiKey: "sk-local-claude" });
312
+ const stream = await client.chat.completions.create({
313
+ model: "reason",
314
+ messages: [{ role: "user", content: "写一段快速排序" }],
315
+ stream: true,
316
+ });
317
+ for await (const chunk of stream) {
318
+ process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
319
+ }
320
+ ```
321
+
322
+ ### 任意 OpenAI 兼容客户端
323
+
324
+ 只要支持自定义 base_url + api_key + model 的 OpenAI 兼容工具,都填:
325
+
326
+ ```
327
+ base_url: http://127.0.0.1:8787/v1
328
+ api_key: <config.json 里 keys 中的任意一个>
329
+ model: <config.json 里 aliases 的任意键>
330
+ ```
331
+
332
+ ---
333
+
334
+ ## 开发
335
+
336
+ ```bash
337
+ bun test # 单元测试(config 校验 / failover / SSE 改写 / 鉴权路由;默认忽略 admin/ 前端测试)
338
+ bun run typecheck # 见下
339
+ ```
340
+
341
+ 没有真实上游 key 时,可用仓库自带的本地 mock 做端到端自测:
342
+
343
+ ```bash
344
+ bun scripts/mock-upstream.ts # 起一个 OpenAI 兼容 mock 上游(端口 9999,需 config.json 指向它)
345
+ bun scripts/smoke.ts # 端到端冒烟:health/models/鉴权/chat 转发/流式/failover/热加载
346
+ ```
347
+
348
+ 目录结构:
349
+
350
+ ```
351
+ ├── config.example.json # 配置示例(复制为 config.json 使用)
352
+ ├── src/
353
+ │ ├── index.ts # 入口:加载配置、热加载、Bun.serve
354
+ │ ├── app.ts # Hono 路由:鉴权、日志中间件、/v1/* 端点
355
+ │ ├── admin.ts # /admin 管理后端 API(配置读写、测试连接等)
356
+ │ ├── config.ts # 配置类型、校验、${ENV} 插值
357
+ │ ├── providers.ts # 上游调用:转发、SSE 改写、failover、超时
358
+ │ ├── logger.ts # 控制台摘要 + access.log
359
+ │ └── *.test.ts # 测试
360
+ ├── admin/ # Vue3 + Naive UI 管理前端(Vite 构建)
361
+ ├── docs/adr/ # 架构决策记录
362
+ └── CONTEXT.md # 领域词汇表
363
+ ```
364
+
365
+ 类型检查(可选):
366
+
367
+ ```bash
368
+ bun run typecheck
369
+ ```
370
+
371
+ ## 限制与路线图
372
+
373
+ - 上游仅支持 OpenAI 兼容端点(DeepSeek、Kimi、通义、智谱、SiliconFlow 等均兼容);Anthropic / Gemini 原生协议待加适配层
374
+ - 未实现 `/v1/embeddings`、`/v1/completions` 等端点(返回 501)
375
+ - 无按 key 限流(本机自用足够;架构上已留身份维度)
376
+ - 无成本估算(access.log 已有 token 用量,需要时可直接算)
@@ -0,0 +1,36 @@
1
+ {
2
+ "port": 8787,
3
+ "host": "127.0.0.1",
4
+ "default_model": "fast",
5
+ "timeout_seconds": 60,
6
+ "access_log": true,
7
+ "keys": [
8
+ {
9
+ "name": "Claude",
10
+ "key": "sk-local-claude",
11
+ "created_at": "2026-01-01T00:00:00.000Z"
12
+ },
13
+ {
14
+ "name": "Cursor",
15
+ "key": "sk-local-cursor",
16
+ "created_at": "2026-01-02T00:00:00.000Z"
17
+ }
18
+ ],
19
+ "admin_password": "",
20
+ "providers": {
21
+ "deepseek": {
22
+ "base_url": "https://api.deepseek.com/v1",
23
+ "api_key": "${DEEPSEEK_API_KEY}",
24
+ "models": ["deepseek-chat", "deepseek-reasoner"]
25
+ },
26
+ "kimi": {
27
+ "base_url": "https://api.moonshot.cn/v1",
28
+ "api_key": "sk-your-kimi-key-here",
29
+ "models": ["moonshot-v1-8k", "moonshot-v1-32k"]
30
+ }
31
+ },
32
+ "aliases": {
33
+ "fast": ["deepseek:deepseek-chat", "kimi:moonshot-v1-8k"],
34
+ "reason": ["deepseek:deepseek-reasoner"]
35
+ }
36
+ }