mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06a74375d8 | |||
| de9080e06c | |||
| 80e79f8dc7 | |||
| b6bbff19e9 | |||
| d5bbb0eca5 | |||
| 9cb6254166 | |||
| 4514270e21 | |||
| ef95bc45d0 |
497
HANDOFF_2026-10-19.md
Normal file
497
HANDOFF_2026-10-19.md
Normal file
@ -0,0 +1,497 @@
|
||||
# ModelRouter / llmsproxy 交接文档
|
||||
|
||||
更新时间:2026-10-19
|
||||
工作目录:`/home/program/llmsproxy`
|
||||
当前分支:`release/v2.0.x`
|
||||
|
||||
## 1. 当前 Git 状态
|
||||
|
||||
最近提交:
|
||||
|
||||
- `c9f3453 fix(codearts): 识别 InferHub 未注册模型`
|
||||
- `12bfd53 fix: 收口渠道源迁移与测试页合并`
|
||||
- `25970a2 refactor(scheduler)!: 移除 priority 废弃字段 + 一通道一源 + discover 复用凭据`
|
||||
|
||||
`c9f3453` 已推送到 `origin/release/v2.0.x`。
|
||||
|
||||
注意:当前工作树在最后一轮添加 provider 回归测试时出现了一个**未完成且可能无法编译的测试编辑**。最后一次工具报错:
|
||||
|
||||
```text
|
||||
internal/provider/provider_test.go:99: missing ',' in argument list
|
||||
```
|
||||
|
||||
下一位 agent 接手后的第一步必须是:
|
||||
|
||||
```bash
|
||||
cd /home/program/llmsproxy
|
||||
git status --short
|
||||
gofmt -w internal/provider/provider_test.go 2>&1 || true
|
||||
go test -tags luajit ./internal/provider/ 2>&1
|
||||
```
|
||||
|
||||
如果测试文件处于半编辑状态,先检查 `TestProviderChatStreamNormalizesStopAfterToolCall`,修正 Go 字符串转义或删除该未完成测试后再继续。不要直接部署当前工作树。
|
||||
|
||||
## 2. 生产服务状态
|
||||
|
||||
生产服务:`llmsproxy.service`
|
||||
|
||||
- 当前服务 active
|
||||
- 监听:`0.0.0.0:8081`
|
||||
- 二进制已通过 `./deploy.sh` 原子替换并重启
|
||||
- 最近一次部署健康检查通过
|
||||
- 当前生产插件 SHA256 前缀曾核对为:`1bf26cbb3abcb107`
|
||||
- 当前生产二进制在最近部署后已更新
|
||||
|
||||
不要把密钥写入交接记录。生产 admin key 保存在本机运维环境中,不应复制到聊天、文档或提交。
|
||||
|
||||
## 3. Channel 源当前状态
|
||||
|
||||
生产源已经从旧的“一账号一源”收口成“一渠道一源”:
|
||||
|
||||
```text
|
||||
channels-trae
|
||||
channels-minimax
|
||||
channels-lobsterai
|
||||
channels-buddy
|
||||
channels-codearts
|
||||
channels-zcode
|
||||
```
|
||||
|
||||
旧形态源已经清理:
|
||||
|
||||
```text
|
||||
channels-<provider>-<account_id>
|
||||
```
|
||||
|
||||
此前旧源和新源并存,造成同一渠道同一模型出现多份,只能靠账号 hex 区分。
|
||||
|
||||
根因已修复:内核 `list_sources` 返回裸数组,但旧插件代码按 `{sources = [...]}` 信封读取,导致迁移清理循环永远为空。当前 `channels.lua` 已兼容:
|
||||
|
||||
- 裸数组
|
||||
- `{sources = [...]}`
|
||||
- `{json = {sources = [...]}}`
|
||||
|
||||
相关代码:
|
||||
|
||||
```text
|
||||
internal/lua/plugins/channels.lua: syncProviderSource
|
||||
internal/core/channels_legacy_source_cleanup_test.go
|
||||
```
|
||||
|
||||
当前生产源列表已实测只有 6 个新形态源。
|
||||
|
||||
## 4. CodeArts 当前状态
|
||||
|
||||
### 已确认可用的部分
|
||||
|
||||
CodeArts 使用:
|
||||
|
||||
```text
|
||||
adapter = codearts
|
||||
base_url = https://snap-access.cn-north-4.myhuaweicloud.com
|
||||
```
|
||||
|
||||
CodeArts 的 chat 请求需要完整 AK/SK/security token 凭据,由插件适配器签名,不是裸 Bearer token。
|
||||
|
||||
真实流式工具调用测试已经成功:
|
||||
|
||||
请求形状:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "channels-codearts:openpangu-2.0-flash",
|
||||
"messages": [{"role": "user", "content": "Use the weather tool for Beijing."}],
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}],
|
||||
"tool_choice": "auto",
|
||||
"stream": true,
|
||||
"max_tokens": 64
|
||||
}
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- HTTP 200
|
||||
- `Content-Type: text/event-stream`
|
||||
- 返回结构化 `tool_calls`
|
||||
- 函数名 `get_weather`
|
||||
- 参数最终为 `{"city":"Beijing"}`
|
||||
- `finish_reason = tool_calls`
|
||||
|
||||
因此 CodeArts 的流式工具调用链路当前是通的。
|
||||
|
||||
### CodeArts 模型刷新
|
||||
|
||||
生产调用:
|
||||
|
||||
```http
|
||||
POST /api/plugins/http/channels/model/refresh
|
||||
{"platform":"codearts"}
|
||||
```
|
||||
|
||||
实测返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"results": [
|
||||
{"platform": "codearts", "ok": true, "count": 11}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CodeArts 模型目录来自两个签名端点并合并:
|
||||
|
||||
1. `opengw.developer.huaweicloud.com/api/v1/gateway/config`
|
||||
- `result.models`
|
||||
2. `snap-access.cn-north-4.myhuaweicloud.com/v1/model/builtin`
|
||||
- `builtinModels`
|
||||
|
||||
相关解析:
|
||||
|
||||
```text
|
||||
internal/lua/plugins/channels.lua: parseRemoteModels
|
||||
internal/lua/plugins/channels.lua: refreshRemoteModels
|
||||
internal/core/channels_codearts_catalog_test.go
|
||||
```
|
||||
|
||||
### 已确认失效并禁用的 CodeArts 模型
|
||||
|
||||
下面 4 个模型曾收到明确的上游 404:
|
||||
|
||||
```text
|
||||
CodeArts-Pangu
|
||||
deepseek-v4-flash-0731
|
||||
deepseek-v4-pro-0813
|
||||
glm-5.3-flash
|
||||
```
|
||||
|
||||
生产上已通过 `channels/model/set_disabled` 禁用。它们仍可能在 `model/list` 中出现,但 `disabled=true`,不会作为可用模型路由。
|
||||
|
||||
新增的死模型判据:
|
||||
|
||||
```lua
|
||||
model is not registered
|
||||
```
|
||||
|
||||
对应提交:`c9f3453`。
|
||||
|
||||
注意:当前插件的 dead-model 机制只在收到真实请求失败后记录模型;刷新目录不会自动根据“远端目录没有该模型”删除模型,这是刻意设计,避免误删远端目录不完整的平台模型。
|
||||
|
||||
CodeArts 当前仍有一些模型返回 429/TPM 限流,例如:
|
||||
|
||||
```text
|
||||
deepseek-v4-flash
|
||||
deepseek-v4.1-flash
|
||||
```
|
||||
|
||||
429 不是模型失效,不能禁用。
|
||||
|
||||
## 5. 其他 Channel 实测结果
|
||||
|
||||
### Trae
|
||||
|
||||
真实流式 + tools 请求:
|
||||
|
||||
```text
|
||||
channels-trae:DeepSeek-V4-Flash
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- HTTP 200
|
||||
- SSE 正常返回
|
||||
- 结构化 tool call 返回
|
||||
- 但上游最后的结束原因可能是 `stop`,即使前面已经出现 tool-call fragments
|
||||
|
||||
这会让 Agent 把工具调用误判成普通文本完成。
|
||||
|
||||
当前准备中的修复在统一 provider 流式层:
|
||||
|
||||
```text
|
||||
internal/provider/provider.go
|
||||
```
|
||||
|
||||
逻辑是:只要本次流出现真实 tool-call 片段,后续 `FinishReason == "stop"` 就归一成 `tool_calls`。
|
||||
|
||||
另外,Trae 的非流式接口偶尔返回 SSE。当前 `trae.lua` 对非流式 SSE 只取最后一条 data,不能完整合并多条工具调用/推理片段。这是后续仍需处理的协议问题,但不能用非流式手工请求结果推断 Pi 的真实请求模式。
|
||||
|
||||
### Buddy
|
||||
|
||||
真实流式 + tools 请求:
|
||||
|
||||
```text
|
||||
channels-buddy:glm-5.3-flash
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- HTTP 200
|
||||
- SSE 正常返回
|
||||
- tool-call 参数分片可见
|
||||
- 最终有 `finish_reason = tool_calls`
|
||||
- 但某些上游 chunk 的 `tool_calls` 可能是空对象 `{}`
|
||||
|
||||
空对象不是合法的 OpenAI tool-call 数组,部分 Agent 客户端会拒绝。
|
||||
|
||||
当前准备中的修复在:
|
||||
|
||||
```text
|
||||
internal/lua/adapters/buddy.lua
|
||||
```
|
||||
|
||||
只在 `type(delta.tool_calls) == "table" and #delta.tool_calls > 0` 时输出 `tool_calls`,避免 Lua 空表被编码为 `{}`。
|
||||
|
||||
Buddy 的非流式错误:
|
||||
|
||||
```text
|
||||
11101: Non-stream chat request is currently not supported
|
||||
```
|
||||
|
||||
之前出现的该错误来自手工 `curl` 测试中显式设置 `stream:false`,不能归因到 Pi。真实 `stream:true + tools` 已经证明 Buddy 流式工具调用可以工作。
|
||||
|
||||
### MiniMax
|
||||
|
||||
生产日志反复出现:
|
||||
|
||||
```text
|
||||
adapter "anthropic": status=401 body={"error":"invalid access token"}
|
||||
provider channels-minimax: api error 401
|
||||
```
|
||||
|
||||
这是凭据失效问题,不是 tools 形状问题。
|
||||
|
||||
当前源:
|
||||
|
||||
```text
|
||||
channels-minimax
|
||||
adapter = anthropic
|
||||
endpoint = /mavis/api/v1/llm/v1/messages
|
||||
```
|
||||
|
||||
需要用户重新登录/提供有效凭据。不要擅自刷新一次性 refresh token,之前已经发生过 refresh token 被消耗后无法再次使用的事故。
|
||||
|
||||
## 6. 当前未提交的代码改动
|
||||
|
||||
最后一轮已经改动但尚未完成提交:
|
||||
|
||||
```text
|
||||
internal/provider/provider.go
|
||||
internal/lua/adapters/trae.lua
|
||||
internal/lua/adapters/buddy.lua
|
||||
internal/provider/provider_test.go # 可能有半编辑、需先修复
|
||||
```
|
||||
|
||||
已完成的意图:
|
||||
|
||||
1. provider 流式层记录 `sawToolCalls`
|
||||
2. 看到真实 tool-call 后,把后续 `stop` 归一为 `tool_calls`
|
||||
3. Trae 不输出空工具调用表
|
||||
4. Buddy 不输出空工具调用表
|
||||
5. provider 回归测试模拟“tool fragment + stop”场景
|
||||
|
||||
但 provider 测试最后一次编辑工具报错:
|
||||
|
||||
```text
|
||||
internal/provider/provider_test.go:99: missing ',' in argument list
|
||||
```
|
||||
|
||||
因此必须先修测试文件,再提交。
|
||||
|
||||
## 7. 推荐接手顺序
|
||||
|
||||
### 第一步:修复工作树
|
||||
|
||||
```bash
|
||||
cd /home/program/llmsproxy
|
||||
git status --short
|
||||
sed -n '1,150p' internal/provider/provider_test.go
|
||||
gofmt -w internal/provider/provider_test.go 2>&1 || true
|
||||
go test -tags luajit ./internal/provider/ 2>&1
|
||||
```
|
||||
|
||||
若测试函数字符串转义难以修复,先删除半成品测试,再重新用 raw string 写测试。
|
||||
|
||||
### 第二步:跑定向验证
|
||||
|
||||
```bash
|
||||
luac -p internal/lua/adapters/trae.lua
|
||||
luac -p internal/lua/adapters/buddy.lua
|
||||
go test -tags luajit ./internal/provider/
|
||||
go test -tags luajit ./internal/lua/ -run 'Adapter|Buddy|Trae|Stream'
|
||||
```
|
||||
|
||||
### 第三步:构建并部署临时验证版
|
||||
|
||||
修复确认后:
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
部署前确认:
|
||||
|
||||
```bash
|
||||
sha256sum internal/lua/plugins/channels.lua
|
||||
sha256sum /etc/llmsproxy/plugins/channels.lua
|
||||
systemctl is-active llmsproxy
|
||||
```
|
||||
|
||||
### 第四步:真实流式回归
|
||||
|
||||
不要用 `stream:false` 测试流式问题。使用:
|
||||
|
||||
```bash
|
||||
KEY='<从本机安全环境读取,不写入文档>'
|
||||
TOOL='{"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}'
|
||||
|
||||
curl --no-buffer --max-time 90 \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"channels-codearts:openpangu-2.0-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Use the weather tool for Beijing.\"}],\"tools\":[$TOOL],\"tool_choice\":\"auto\",\"stream\":true,\"max_tokens\":64}" \
|
||||
http://127.0.0.1:8081/v1/chat/completions
|
||||
```
|
||||
|
||||
分别将 model 替换为:
|
||||
|
||||
```text
|
||||
channels-codearts:openpangu-2.0-flash
|
||||
channels-trae:DeepSeek-V4-Flash
|
||||
channels-buddy:glm-5.3-flash
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- HTTP 200
|
||||
- `Content-Type: text/event-stream`
|
||||
- 不能出现 `tool_calls: {}`
|
||||
- 有 tool-call fragments 时最终必须是 `finish_reason: tool_calls`
|
||||
- CodeArts 当前应继续返回结构化工具调用
|
||||
|
||||
### 第五步:CodeArts 刷新验收
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"platform":"codearts"}' \
|
||||
http://127.0.0.1:8081/api/plugins/http/channels/model/refresh
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```json
|
||||
{"ok":true,"results":[{"platform":"codearts","ok":true,"count":11}]}
|
||||
```
|
||||
|
||||
再检查:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"platform":"codearts"}' \
|
||||
http://127.0.0.1:8081/api/plugins/http/channels/model/list
|
||||
```
|
||||
|
||||
预期:远端目录 11 个 + 兜底补位,已确认 404 的 4 个仍保持 `disabled=true`。
|
||||
|
||||
## 8. 发布/安装包状态
|
||||
|
||||
目前没有完成“全部安装包”发布闭环。
|
||||
|
||||
已经发生的事情:
|
||||
|
||||
- 核心包构建成功,但脚本实际使用了 `git describe`,产物版本类似:
|
||||
`2.0.40~4-gc9f3453`
|
||||
- Linux GUI 包构建成功,版本为 `1.9.7`
|
||||
- Windows 主机 Wine 构建失败,生成了一个 264KB 的坏 `ModelRouter Setup 1.9.7.exe`,绝对不能发布
|
||||
- Docker Windows 构建也失败在拉取 Docker Hub 基础镜像 `golang:1.25` 网络超时
|
||||
- apt 无法找到 `wine32:i386`
|
||||
- 旧的 1.9.5/1.9.6 Windows 包是历史产物,不能作为当前版本
|
||||
|
||||
不要把旧 Windows exe 当作新包发布。
|
||||
|
||||
仓库已有 Docker 方案:
|
||||
|
||||
```text
|
||||
cmd/gui/scripts/dist-win-docker.sh
|
||||
cmd/gui/docker/win-builder/Dockerfile
|
||||
```
|
||||
|
||||
但当前 Docker Hub 网络不可用。若要继续做 Windows 包:
|
||||
|
||||
1. 在可访问 Docker Hub 的构建机/CI 执行 Docker 方案;或
|
||||
2. 提供包含 Wine32/Wine64 的完整构建环境;或
|
||||
3. 暂时只发布 Linux/core 包,并明确说明 Windows 包未完成。
|
||||
|
||||
版本脚本注意:
|
||||
|
||||
- 核心包脚本使用位置参数:
|
||||
|
||||
```bash
|
||||
./packaging/core-dist.sh 2.0.41
|
||||
```
|
||||
|
||||
不是 `VERSION=2.0.41 ./packaging/core-dist.sh`。
|
||||
- GUI 版本来自 `cmd/gui/package.json`,环境变量 `VERSION` 不会自动修改 Electron 产品版本。
|
||||
- 清理旧产物后再打包,并在全部包完成后统一计算 `SHA256SUMS`。
|
||||
|
||||
## 9. 参考插件状态
|
||||
|
||||
用户要求按:
|
||||
|
||||
```text
|
||||
https://github.com/Ebony-Vinyl/dsh-our-free-model
|
||||
```
|
||||
|
||||
逐厂商对照参考插件。
|
||||
|
||||
当前本地路径:
|
||||
|
||||
```text
|
||||
/tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model
|
||||
```
|
||||
|
||||
但该目录当前不是有效可读源码副本:
|
||||
|
||||
- `vendor/channel-pack/src` 下面只有空目录;
|
||||
- `.git` 没有有效 `HEAD`/refs;
|
||||
- 无法从本地 Git 对象恢复源码;
|
||||
- 本地也没有 `buddy.ts`、`trae.ts`、`codearts.ts` 等文件。
|
||||
|
||||
因此不能声称当前适配器是逐行照参考插件移植的。若要继续按用户要求完成,必须先重新 clone/恢复该 GitHub 仓库源码,确认以下文件实际存在后再对照:
|
||||
|
||||
```text
|
||||
vendor/channel-pack/src/trae.ts
|
||||
vendor/channel-pack/src/buddy.ts
|
||||
vendor/channel-pack/src/codearts.ts
|
||||
vendor/channel-pack/src/*adapter*
|
||||
vendor/channel-pack/src/*model*
|
||||
```
|
||||
|
||||
当前仓库代码只能作为临时协议修复基础,不能代替参考插件源码。
|
||||
|
||||
## 10. 关键结论
|
||||
|
||||
- Pi 是否发送流式请求,历史日志没有保存入站 `stream/tools` 字段,不能靠旧日志判断。
|
||||
- 本次真实 `stream:true + tools` 测试证明 ModelRouter 流式路径能工作:
|
||||
- CodeArts:通过;
|
||||
- Trae:通过但结束原因需归一;
|
||||
- Buddy:通过但需过滤空 `{}`;
|
||||
- MiniMax:凭据 401,独立问题。
|
||||
- 不要再用 `stream:false` 的手工请求推断 Pi 的问题。
|
||||
- CodeArts 刷新接口当前真实返回 `ok=true,count=11`,刷新链路不是当前阻塞点。
|
||||
- 当前最紧急的代码状态是修复 `provider_test.go` 的半编辑,然后验证未提交的统一流式修复。
|
||||
- 未经测试、未重新构建、未重新部署,不要继续改生产账号或刷新一次性凭据。
|
||||
14
cmd/gui/package-lock.json
generated
14
cmd/gui/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "modelrouter-gui",
|
||||
"version": "2.0.49",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "modelrouter-gui",
|
||||
"version": "2.0.49",
|
||||
"version": "3.0.1",
|
||||
"license": "Proprietary",
|
||||
"devDependencies": {
|
||||
"electron": "^33.0.0",
|
||||
@ -1245,7 +1245,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
@ -2288,7 +2288,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
@ -2532,7 +2532,7 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
@ -2715,7 +2715,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
@ -3492,7 +3492,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "modelrouter-gui",
|
||||
"version": "2.0.49",
|
||||
"version": "3.0.1",
|
||||
"description": "ModelRouter Desktop — embedded ModelRouter core with tray",
|
||||
"author": "ModelRouter",
|
||||
"main": "main.js",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot Windows release build for the GUI — ENTIRELY inside docker.
|
||||
#
|
||||
# 1) cross-compile the embedded core in docker (llmsproxy.exe + lua51.dll)
|
||||
# 1) cross-compile the embedded core in docker (modelrouter.exe + lua51.dll)
|
||||
# 2) electron-builder NSIS packaging ALSO inside docker (wine for the
|
||||
# uninstaller step lives in the image — see Dockerfile). This is the fix
|
||||
# for the 1.3.0/1.4.0 broken installers: the host had amd64-only wine
|
||||
@ -37,13 +37,15 @@ docker run --rm \
|
||||
-v "$ROOT":/workspace \
|
||||
-v "$CACHE_VOL":/root/.cache \
|
||||
-e GOPROXY="$PROXY" \
|
||||
-e ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" \
|
||||
-e ELECTRON_BUILDER_BINARIES_MIRROR="https://npmmirror.com/mirrors/electron-builder-binaries/" \
|
||||
-w /workspace \
|
||||
"$IMAGE" bash -c '
|
||||
set -euo pipefail
|
||||
cp /opt/luajit2/src/lua51.dll /workspace/cmd/gui/bin/lua51.dll
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
|
||||
go build -buildvcs=false -tags luajit -trimpath \
|
||||
-o /workspace/cmd/gui/bin/llmsproxy.exe ./cmd/llmsproxy
|
||||
go build -buildvcs=false -tags luajit -trimpath -ldflags "-s -w" \
|
||||
-o /workspace/cmd/gui/bin/modelrouter.exe ./cmd/modelrouter
|
||||
echo " -> core built"
|
||||
# wine needs one prefix initialization so its wow64 runtime (syswow64/ntdll)
|
||||
# exists — the exact thing that was missing on the broken host. Use a shared
|
||||
|
||||
@ -17,7 +17,10 @@ function buildLinux() {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
execFileSync(
|
||||
"go",
|
||||
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"],
|
||||
// -s -w: same stripping as packaging/core-dist.sh. Without it the bundled
|
||||
// core carries debug info (~14.4 MB vs ~10.3 MB) and every installer pays
|
||||
// for it, for a binary the user never debugs.
|
||||
["build", "-tags", "luajit", "-trimpath", "-ldflags", "-s -w", "-o", out, "./cmd/modelrouter"],
|
||||
{ cwd: projectRoot, stdio: "inherit" },
|
||||
);
|
||||
return out;
|
||||
@ -43,7 +46,7 @@ function buildWin() {
|
||||
}
|
||||
execFileSync(
|
||||
"go",
|
||||
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"],
|
||||
["build", "-tags", "luajit", "-trimpath", "-ldflags", "-s -w", "-o", out, "./cmd/modelrouter"],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
|
||||
17
deploy.sh
17
deploy.sh
@ -115,6 +115,23 @@ precheck() {
|
||||
如需全新部署,先把旧目录移开:mv /etc/llmsproxy /etc/llmsproxy.bak"
|
||||
fi
|
||||
|
||||
# unit 与二进制路径必须一致。包安装把二进制放 /usr/bin,源码部署放
|
||||
# /usr/local/bin;unit 里写错一行,systemctl restart 只会给 203/EXEC,
|
||||
# 看起来像"部署成功但服务起不来"。此处提前拦住。
|
||||
local unit="/etc/systemd/system/modelrouter.service"
|
||||
if [[ -f "$unit" ]]; then
|
||||
local unit_exe
|
||||
unit_exe=$(awk -F'=' '/^ExecStart=/{print $2; exit}' "$unit" | awk '{print $1}')
|
||||
if [[ -n "$unit_exe" && "$unit_exe" != "$TARGET_BIN" && ! -x "$unit_exe" ]]; then
|
||||
fail "unit 的 ExecStart 指向 $unit_exe,但该文件不存在。
|
||||
包安装(deb/rpm)用 /usr/bin/modelrouter;源码部署(本脚本)用 $TARGET_BIN。
|
||||
请把 $unit 的 ExecStart 改成 $TARGET_BIN,或改用包安装。"
|
||||
fi
|
||||
if [[ -n "$unit_exe" && "$unit_exe" != "$TARGET_BIN" ]]; then
|
||||
warn "unit ExecStart=$unit_exe,而本脚本部署到 $TARGET_BIN(两者都存在,确认哪个该生效)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_CONFIG_SRC" ]]; then
|
||||
[[ -f "$NEW_CONFIG_SRC" ]] || fail "待部署配置不存在: $NEW_CONFIG_SRC"
|
||||
# 同一文件比对:--config 指向线上文件本身时没有"投放"动作可做,
|
||||
|
||||
@ -16,9 +16,14 @@ sudo ./deploy.sh --config other.yaml # 投放并部署另一份配置
|
||||
|
||||
部署目标(改名后):
|
||||
|
||||
| 项 | 路径 |
|
||||
> 两种布局并存,区别只在于二进制位置:**包安装**(deb/rpm)放 `/usr/bin`,
|
||||
> unit 自带;**源码部署**(`deploy.sh`)放 `/usr/local/bin`,需要让 unit 的
|
||||
> `ExecStart` 指向它。`deploy.sh` 会校验这一点,写错时直接中止而不是留下
|
||||
> 一个 203/EXEC 的服务。
|
||||
|
||||
| 项 | 路径(源码部署 / 包安装) |
|
||||
|---|---|
|
||||
| 二进制 | `/usr/local/bin/modelrouter` |
|
||||
| 二进制 | `/usr/local/bin/modelrouter` / `/usr/bin/modelrouter` |
|
||||
| 配置 | `/etc/modelrouter/config.yaml` |
|
||||
| 适配器 | `/etc/modelrouter/adapters/` |
|
||||
| 插件(可选) | `/etc/modelrouter/plugins/`(需在配置里设 `plugin_dir`) |
|
||||
|
||||
@ -14,10 +14,10 @@ import (
|
||||
)
|
||||
|
||||
// 真上游 e2e:用适配器产出的头 + body 直打 opencode /inference/v1。
|
||||
// 需要真实凭据(从生产配置解密),故用 LLMSPROXY_LIVE_ZEN=1 显式开启。
|
||||
// 需要真实凭据(从生产配置解密),故用 MODELROUTER_LIVE_ZEN=1 显式开启。
|
||||
func TestOpencodeZenLiveE2E(t *testing.T) {
|
||||
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启真上游 e2e")
|
||||
if os.Getenv("MODELROUTER_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 MODELROUTER_LIVE_ZEN=1 开启真上游 e2e")
|
||||
}
|
||||
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
|
||||
if err != nil {
|
||||
@ -30,8 +30,8 @@ func TestOpencodeZenLiveE2E(t *testing.T) {
|
||||
}
|
||||
|
||||
// 取生产凭据(解密)
|
||||
out, err := exec.Command("/usr/local/bin/llmsproxy", "-show-secrets",
|
||||
"-config", "/etc/llmsproxy/config.yaml").Output()
|
||||
out, err := exec.Command(gatewayBinary(), "-show-secrets",
|
||||
"-config", "/etc/modelrouter/config.yaml").Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -131,8 +131,8 @@ func TestOpencodeZenLiveE2E(t *testing.T) {
|
||||
// 经适配器完整转换链:上游 SSE → transform_stream_chunk → 统一格式。
|
||||
// 上一条判据直接读原始 SSE,只证明"上游放行";这条证明"适配器能解析"。
|
||||
func TestOpencodeZenLiveStreamThroughAdapter(t *testing.T) {
|
||||
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启")
|
||||
if os.Getenv("MODELROUTER_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 MODELROUTER_LIVE_ZEN=1 开启")
|
||||
}
|
||||
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
|
||||
if err != nil {
|
||||
@ -143,8 +143,8 @@ func TestOpencodeZenLiveStreamThroughAdapter(t *testing.T) {
|
||||
if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, _ := exec.Command("/usr/local/bin/llmsproxy", "-show-secrets",
|
||||
"-config", "/etc/llmsproxy/config.yaml").Output()
|
||||
out, _ := exec.Command(gatewayBinary(), "-show-secrets",
|
||||
"-config", "/etc/modelrouter/config.yaml").Output()
|
||||
apiKey := ""
|
||||
for _, l := range strings.Split(string(out), "\n") {
|
||||
if strings.HasPrefix(l, "source localzen") {
|
||||
@ -211,3 +211,17 @@ func TestOpencodeZenLiveStreamThroughAdapter(t *testing.T) {
|
||||
}
|
||||
t.Logf("✓ 完整链:%d 个 chunk,内容=%q", n, strings.TrimSpace(joined))
|
||||
}
|
||||
|
||||
// gatewayBinary 定位本机网关二进制。改名后默认是 /usr/local/bin/modelrouter;
|
||||
// 仍兼容改名前的 /usr/local/bin/llmsproxy,避免测试脚本要跟着运维机器改。
|
||||
func gatewayBinary() string {
|
||||
if v := os.Getenv("MODELROUTER_BIN"); v != "" {
|
||||
return v
|
||||
}
|
||||
for _, p := range []string{"/usr/local/bin/modelrouter", "/usr/local/bin/llmsproxy"} {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return "/usr/local/bin/modelrouter"
|
||||
}
|
||||
|
||||
@ -37,8 +37,13 @@ func TestOpencodeZenAdapterE2E(t *testing.T) {
|
||||
if len(h["x-opencode-request"]) != 4+12+14 || !strings.HasPrefix(h["x-opencode-request"], "msg_") {
|
||||
t.Fatalf("request 形状: %q", h["x-opencode-request"])
|
||||
}
|
||||
if !strings.Contains(h["User-Agent"], "opencode/1.19.0") {
|
||||
t.Fatalf("UA 应为 1.19.0: %q", h["User-Agent"])
|
||||
// ★ UA 必须与**抓包的官方版本**一致:上游按客户端指纹判定免费层资格。
|
||||
// 实测(2026-10-24,同一凭据同一模型):
|
||||
// CLI 1.18.29(发 4 个头) → 429 FreeUsageLimitError
|
||||
// CLI 1.19.4 (发 5 个头) → 200,任务真实完成
|
||||
// ⇒ UA 里声称的版本与发头集合必须同步升级,不能只改其一。
|
||||
if !strings.Contains(h["User-Agent"], "opencode/1.19.4") {
|
||||
t.Fatalf("UA 应为 1.19.4: %q", h["User-Agent"])
|
||||
}
|
||||
t.Logf("session=%s request=%s", s1, h["x-opencode-request"])
|
||||
// 公式校验:段 = (K - bucket_ms*4096) & MASK,其中 bucket 已对齐到整分钟
|
||||
|
||||
@ -59,7 +59,7 @@ adapter.hidden = true
|
||||
-- ⚠️ 参考实现 dsh-our-free-model 的门禁桩(bash + read,但无真实 properties)
|
||||
-- 恰好卡在第 3 条上 —— 即它「伪装需跟进上游更新」的真实原因。
|
||||
adapter.headers = {
|
||||
["User-Agent"] = "opencode/1.19.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14",
|
||||
["User-Agent"] = "opencode/1.19.4 ai-sdk/provider-utils/4.0.23 runtime/bun/1.4.4",
|
||||
}
|
||||
|
||||
-- rand_id:按官方的 48-bit 公式生成 12 位 hex 段 + 14 位 base62 随机段。
|
||||
@ -176,17 +176,39 @@ end
|
||||
function adapter.build_headers(meta)
|
||||
local ms = now_ms(meta)
|
||||
local bucket = session_bucket_ms(meta)
|
||||
-- 免费层要求两个头都在且值相同(对齐官方 CLI;缺任一个则 403)。
|
||||
-- 会话亲和键(可能为空)混入 tail:同一分钟桶内不同会话得到不同会号,
|
||||
-- 同一会话跨分钟仍共享指纹 ⇒ 前缀缓存按会话命中而非按分钟命中。
|
||||
-- ★ 本函数的头集合以**官方 CLI 抓包**为准(opencode 1.19.4,2026-10-24
|
||||
-- 通过本地抓包代理取得明文请求)。官方实际发这五个 x-opencode-* 头:
|
||||
--
|
||||
-- x-opencode-client: cli
|
||||
-- x-opencode-project: global
|
||||
-- x-opencode-request: msg_<12hex><14base62>
|
||||
-- x-opencode-session: ses_<12hex><14base62>
|
||||
-- x-opencode-session-id: ses_<12hex><14base62> ← 与 session 同值
|
||||
--
|
||||
-- ⚠⚠ 这里有一个**版本相关的陷阱**,用实测记录,因为它反反复复:
|
||||
-- 同一台机器、同一个 `Bearer public` 凭据、同一个模型:
|
||||
--
|
||||
-- CLI 1.18.29(只发 4 个头,无 session-id) → "Free usage limit reached"
|
||||
-- CLI 1.19.4 (发 5 个头,含 session-id) → 200,任务真实完成
|
||||
--
|
||||
-- 我一度根据 1.18.29 的抓包删掉了 `x-opencode-session-id`,并把注释
|
||||
-- 改写成「官方根本不发这个头」。**那是错的** —— 旧版客户端当时的报错
|
||||
-- 本来就是额度不足,不是头缺失;而新版上游确实要这个头。删掉它会让
|
||||
-- 1.19.x 的指纹不匹配,重新落回 429。
|
||||
--
|
||||
-- 结论:头集合必须与 **UA 里声称的版本**一致。声称 1.19.4 就必须发 5 个头。
|
||||
-- 将来升级 UA 时,必须**重新抓包**该版本的发头集合,不能沿用旧结论。
|
||||
--
|
||||
-- 会话亲和键(可空)混入 tail:同一分钟桶内不同会话得到不同会号,
|
||||
-- 同一会话跨分钟仍共享指纹 ⇒ 前缀缓存按会话命中而非按分钟命中。
|
||||
local sid = rand_id("ses_", bucket, "session", session_affinity_key(meta))
|
||||
return {
|
||||
["User-Agent"] = adapter.headers["User-Agent"],
|
||||
["x-opencode-client"] = "cli",
|
||||
-- project 固定:同网关实例共享一个工作区身份(非门禁必需,但官方也发)
|
||||
-- project 固定:同网关实例共享一个工作区身份(官方也发同样值)
|
||||
["x-opencode-project"] = "global",
|
||||
["x-opencode-session"] = sid,
|
||||
-- ★ 两个都要:上游对免费池同时校验 x-opencode-session 与这个头。
|
||||
-- ★ 1.19.4 实测**必须**并发这一份(与 session 同值)。见上面的版本陷阱。
|
||||
["x-opencode-session-id"] = sid,
|
||||
-- request id 每请求唯一(不参与缓存键),用当前时间而非桶,避免同桶内重复
|
||||
["x-opencode-request"] = rand_id("msg_", ms, "request"),
|
||||
|
||||
@ -26,9 +26,9 @@ import (
|
||||
)
|
||||
|
||||
// proxyFor picks the transport proxy for one source. An explicit per-source
|
||||
// proxy_url wins (e.g. clash on 127.0.0.1:7890 for a geo-blocked upstream);
|
||||
// otherwise fall back to the process-wide env proxy, which is direct by
|
||||
// default.
|
||||
// proxy_url wins (e.g. a local HTTP proxy on 127.0.0.1:7890 for an upstream
|
||||
// that is only reachable through it); otherwise fall back to the
|
||||
// process-wide env proxy, which is direct by default.
|
||||
func proxyFor(cfg config.Source) func(*http.Request) (*url.URL, error) {
|
||||
if cfg.ProxyURL != "" {
|
||||
proxyURL, err := url.Parse(cfg.ProxyURL)
|
||||
@ -403,9 +403,10 @@ func New(cfg config.Source, vm *lua.VM) *Provider {
|
||||
// Shared transport: ResponseHeaderTimeout bounds how long we wait for the
|
||||
// first response byte (applies to both paths); the stream client has no
|
||||
// client-level Timeout so the SSE body can run past the header timeout.
|
||||
// A per-source ProxyURL (e.g. clash on 127.0.0.1:7890) overrides the
|
||||
// process-wide env proxy for upstreams that are geo/IP-blocked; sources
|
||||
// without one keep http.ProxyFromEnvironment (direct by default).
|
||||
// A per-source ProxyURL (e.g. a local HTTP proxy on 127.0.0.1:7890)
|
||||
// overrides the process-wide env proxy for upstreams that need an explicit
|
||||
// egress path; sources without one keep http.ProxyFromEnvironment (direct
|
||||
// by default).
|
||||
tr := &http.Transport{
|
||||
Proxy: proxyFor(cfg),
|
||||
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
|
||||
@ -20,19 +20,122 @@ BUILD_BIN="$ROOT/cmd/build/modelrouter"
|
||||
|
||||
log() { printf '\033[1;34m[core-dist]\033[0m %s\n' "$*"; }
|
||||
|
||||
# nfpm is installed via `go install` -> lives in GOPATH/bin, which may not be
|
||||
# on the caller's PATH (non-interactive shells, cron). Resolve it explicitly.
|
||||
# nfpm is the primary packager, but it is a `go install` artifact and the build
|
||||
# host may be offline (no GOPROXY) — a release must not depend on being able to
|
||||
# fetch it. Fall back to dpkg-deb / rpmbuild, which are present wherever the
|
||||
# packages are actually verified. Both paths produce the same file layout.
|
||||
NFPM_BIN="$(command -v nfpm || true)"
|
||||
[ -z "$NFPM_BIN" ] && [ -n "$(go env GOPATH)" ] && {
|
||||
[ -x "$(go env GOPATH)/bin/nfpm" ] && NFPM_BIN="$(go env GOPATH)/bin/nfpm"
|
||||
}
|
||||
[ -n "$NFPM_BIN" ] || {
|
||||
echo "[core-dist] nfpm missing. Install: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.2"
|
||||
exit 1
|
||||
}
|
||||
if [ -n "$NFPM_BIN" ]; then
|
||||
log "packager: nfpm ($NFPM_BIN)"
|
||||
else
|
||||
command -v dpkg-deb >/dev/null || { echo "[core-dist] neither nfpm nor dpkg-deb available"; exit 1; }
|
||||
command -v rpmbuild >/dev/null || { echo "[core-dist] neither nfpm nor rpmbuild available"; exit 1; }
|
||||
log "packager: nfpm missing -> dpkg-deb + rpmbuild fallback"
|
||||
fi
|
||||
|
||||
mkdir -p "$DIST"
|
||||
|
||||
# ---- fallback packagers -------------------------------------------------
|
||||
# Same file layout as packaging/nfpm.yaml. Kept in this file (not a separate
|
||||
# script) so the two paths cannot drift silently: both are exercised by the
|
||||
# archive-content gate at the end.
|
||||
package_deb_fallback() {
|
||||
local root="$ROOT/cmd/build/pkgroot-deb"
|
||||
rm -rf "$root"
|
||||
mkdir -p "$root/DEBIAN" "$root/usr/bin" "$root/etc/systemd/system" \
|
||||
"$root/usr/share/modelrouter/adapters" "$root/usr/share/doc/modelrouter"
|
||||
install -m 0755 "$BUILD_BIN" "$root/usr/bin/modelrouter"
|
||||
install -m 0644 "$ROOT/packaging/modelrouter.service" "$root/etc/systemd/system/modelrouter.service"
|
||||
install -m 0644 "$ROOT/packaging/config.example.yaml" "$root/usr/share/modelrouter/config.example.yaml"
|
||||
install -m 0644 "$ROOT"/internal/lua/adapters/*.lua "$root/usr/share/modelrouter/adapters/"
|
||||
install -m 0755 "$ROOT/packaging/install-browser-stack.sh" "$root/usr/share/modelrouter/install-browser-stack.sh"
|
||||
install -m 0644 "$ROOT/README.md" "$root/usr/share/doc/modelrouter/README.md"
|
||||
install -m 0755 "$ROOT/packaging/postinst.sh" "$root/DEBIAN/postinst"
|
||||
install -m 0755 "$ROOT/packaging/prerm.sh" "$root/DEBIAN/prerm"
|
||||
local inst_size
|
||||
inst_size=$(du -sk "$root" | awk '{print $1}')
|
||||
cat > "$root/DEBIAN/control" <<EOF
|
||||
Package: modelrouter
|
||||
Version: $VERSION
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Installed-Size: $inst_size
|
||||
Maintainer: JianFeeeee <dev@modelrouter.local>
|
||||
Recommends: ca-certificates
|
||||
Homepage: https://gitcode.com/JianFeeeee/ModelRouter
|
||||
Description: Unified OpenAI-compatible multi-source LLM gateway.
|
||||
Routes requests across multiple upstream LLM providers with Lua adapters,
|
||||
model scheduling, quota windows, an elastic Lua worker pool, and a Web UI.
|
||||
EOF
|
||||
dpkg-deb --root-owner-group -Zgzip --build "$root" \
|
||||
"$DIST/modelrouter_${VERSION}_amd64.deb" >/dev/null
|
||||
log " built $(basename "$DIST/modelrouter_${VERSION}_amd64.deb")"
|
||||
}
|
||||
|
||||
package_rpm_fallback() {
|
||||
local top="$ROOT/cmd/build/rpmbuild"
|
||||
rm -rf "$top"
|
||||
mkdir -p "$top"/{BUILD,RPMS,SOURCES,SPECS,BUILDROOT}
|
||||
local spec="$top/SPECS/modelrouter.spec"
|
||||
{
|
||||
cat <<EOF
|
||||
Name: modelrouter
|
||||
Version: $VERSION
|
||||
Release: 1
|
||||
Summary: Unified OpenAI-compatible multi-source LLM gateway
|
||||
License: MIT
|
||||
URL: https://gitcode.com/JianFeeeee/ModelRouter
|
||||
BuildArch: x86_64
|
||||
Requires: ca-certificates
|
||||
AutoReqProv: no
|
||||
%description
|
||||
Unified OpenAI-compatible multi-source LLM gateway. Routes requests across
|
||||
multiple upstream LLM providers with Lua adapters, model scheduling, quota
|
||||
windows, an elastic Lua worker pool, and a Web UI.
|
||||
%install
|
||||
mkdir -p %{buildroot}/usr/bin %{buildroot}/etc/systemd/system \\
|
||||
%{buildroot}/usr/share/modelrouter/adapters \\
|
||||
%{buildroot}/usr/share/doc/modelrouter %{buildroot}/etc/modelrouter
|
||||
install -m 0755 $BUILD_BIN %{buildroot}/usr/bin/modelrouter
|
||||
install -m 0644 $ROOT/packaging/modelrouter.service %{buildroot}/etc/systemd/system/modelrouter.service
|
||||
install -m 0644 $ROOT/packaging/config.example.yaml %{buildroot}/usr/share/modelrouter/config.example.yaml
|
||||
install -m 0644 $ROOT/internal/lua/adapters/*.lua %{buildroot}/usr/share/modelrouter/adapters/
|
||||
install -m 0755 $ROOT/packaging/install-browser-stack.sh %{buildroot}/usr/share/modelrouter/install-browser-stack.sh
|
||||
install -m 0644 $ROOT/README.md %{buildroot}/usr/share/doc/modelrouter/README.md
|
||||
%post
|
||||
EOF
|
||||
sed '1{/^#!/d}' "$ROOT/packaging/postinst.sh"
|
||||
# Script files may lack a trailing newline. Without this, `%preun` is glued
|
||||
# onto the last line of %post and the whole %files section is swallowed —
|
||||
# rpmbuild then fails with "Installed (but unpackaged) file(s) found".
|
||||
echo
|
||||
echo '%preun'
|
||||
sed '1{/^#!/d}' "$ROOT/packaging/prerm.sh"
|
||||
echo
|
||||
cat <<'EOF'
|
||||
%files
|
||||
/usr/bin/modelrouter
|
||||
/etc/systemd/system/modelrouter.service
|
||||
/usr/share/modelrouter
|
||||
/usr/share/doc/modelrouter/README.md
|
||||
%dir /etc/modelrouter
|
||||
EOF
|
||||
} > "$spec"
|
||||
rpmbuild --define "_topdir $top" --define "_binary_payload w9.gzdio" -bb "$spec" \
|
||||
>/dev/null 2>&1 || {
|
||||
echo "[core-dist] FATAL: rpmbuild failed; spec kept at $spec"; exit 1
|
||||
}
|
||||
local built
|
||||
built=$(find "$top/RPMS" -name 'modelrouter-*.rpm' | head -1)
|
||||
[ -n "$built" ] || { echo "[core-dist] FATAL: rpmbuild produced no rpm"; exit 1; }
|
||||
cp "$built" "$DIST/modelrouter-${VERSION}.x86_64.rpm"
|
||||
log " built $(basename "$DIST/modelrouter-${VERSION}.x86_64.rpm")"
|
||||
}
|
||||
|
||||
# 1) build the core binary (luajit tag, -s -w like deploy.sh)
|
||||
log "building core binary (luajit, stripped)..."
|
||||
CGO_ENABLED=1 go build -tags luajit -trimpath -ldflags="-s -w" -o "$BUILD_BIN" ./cmd/modelrouter
|
||||
@ -58,10 +161,15 @@ log " ok: $(stat -c%s "$BUILD_BIN") bytes, symbols present"
|
||||
# stamp the version into a throwaway copy of the config; -t places the
|
||||
# generated package in the dist dir.
|
||||
log "packaging deb + rpm (v$VERSION)..."
|
||||
NFPM_CFG="$(mktemp)" && sed "s/__VERSION__/$VERSION/g" "$ROOT/packaging/nfpm.yaml" > "$NFPM_CFG"
|
||||
"$NFPM_BIN" package --config "$NFPM_CFG" --packager deb -t "$DIST"
|
||||
"$NFPM_BIN" package --config "$NFPM_CFG" --packager rpm -t "$DIST"
|
||||
rm -f "$NFPM_CFG"
|
||||
if [ -n "$NFPM_BIN" ]; then
|
||||
NFPM_CFG="$(mktemp)" && sed "s/__VERSION__/$VERSION/g" "$ROOT/packaging/nfpm.yaml" > "$NFPM_CFG"
|
||||
"$NFPM_BIN" package --config "$NFPM_CFG" --packager deb -t "$DIST"
|
||||
"$NFPM_BIN" package --config "$NFPM_CFG" --packager rpm -t "$DIST"
|
||||
rm -f "$NFPM_CFG"
|
||||
else
|
||||
package_deb_fallback
|
||||
package_rpm_fallback
|
||||
fi
|
||||
|
||||
# 4) tar.gz
|
||||
log "packaging tar.gz..."
|
||||
|
||||
@ -15,7 +15,10 @@ Type=simple
|
||||
# the extra GC cycles are free.
|
||||
Environment=GOGC=50
|
||||
Environment=MALLOC_ARENA_MAX=2
|
||||
ExecStart=/usr/local/bin/modelrouter -config /etc/modelrouter/config.yaml
|
||||
# Package installs put the binary in /usr/bin (FHS). A source deployment via
|
||||
# deploy.sh uses /usr/local/bin — that script verifies the unit below points at
|
||||
# a binary that actually exists, so the two layouts cannot silently disagree.
|
||||
ExecStart=/usr/bin/modelrouter -config /etc/modelrouter/config.yaml
|
||||
WorkingDirectory=/etc/modelrouter
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
Reference in New Issue
Block a user