Files
ModelRouter/internal/core/channels_qoder_adapter_test.go
JianFeeeee 688dce88d1 refactor(rename)!: llmsproxy → modelrouter(module / 二进制 / 路径 / env)
产品名早已是 ModelRouter,但仓库标识符仍沿用旧名。这次统一,读写两侧都做兼容,
已部署的机器不会因为顺序问题立刻失效。

- go module `llmsproxy` → `modelrouter`;全仓 import 与 `cmd/modelrouter` 同步。
- `Makefile BINARY=modelrouter`;发行包名、systemd unit、/usr/share 资源、
  `/usr/local/bin` 目标全部改为 modelrouter。
- 默认配置目录 `/etc/llmsproxy` → `/etc/modelrouter`。**读取侧回退**:插件与
  `storeDir` 仍会检查旧路径,所以"先换二进制、后改 unit"不会立即失效。
- 环境变量 `LLMSPROXY_*`(PLUGIN_DIR / XDISPLAY_BASE / ARCH / LIVE_ZEN /
  BUILD_DIR)改为 `MODELROUTER_*`;`LLMS_PROXY_MASTER_KEY` →
  `MODELROUTER_MASTER_KEY`。旧名在读取侧继续接受。
- 桌面端内嵌核心 `bin/llmsproxy` → `bin/modelrouter`,并保留旧文件名回退,
  避免升级到一半的 profile 被判成"core binary missing"。
- 新增 `packaging/migrate-to-modelrouter.sh`:**默认 dry-run**,复制而非移动、
  旧目录永不删除、支持 `--rollback`;随 release tar 分发并由 core-dist 校验。
- `deploy.sh` 增加改名保护:发现旧 `/etc/llmsproxy` 而新配置不存在时**中止**,
  否则会部署出一个"能启动但没有 key、没有源"的空实例。
- 临时目录前缀 `/tmp/llmsproxy-*` → `/tmp/modelrouter-*`;清理逻辑同时回收两代。

兼容别名(`__llmsproxy_*` 全局、`llmsproxy` SDK 别名、旧路径/旧 env)刻意保留,
它们是升级路径而非遗留;docs/plugins.md 已标注为废弃。

本提交未包含:`adapters/opencodezen.lua`、`adapter_opencodezen_test.go`、
`adapter_opencodezen_live_test.go`(OpenCode UA/免费池的既有改动,另批提交)。
2026-10-26 22:28:08 +08:00

89 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"strings"
"testing"
"modelrouter/internal/lua"
)
// qoderReason 调真实的 transform_error 钩子(不自己重算)。
func qoderReason(t *testing.T, vm *lua.VM, status int, body string) string {
t.Helper()
reason, ok, err := vm.TransformError("channels-qoder", status, body)
if err != nil {
t.Fatalf("transform_error 调用失败: %v", err)
}
if !ok {
t.Fatalf("channels-qoder 适配器未装载或未定义 transform_error(body=%s)", body)
}
return reason
}
// Qoder 的错误信封是**双层编码**的,且两个业务码语义**相反**:
//
// 10605 排队 → 暂时受阻,等一下就好
// 110 额度 → 当天耗尽,重试无用(参考实测:原先归成通用服务端错误 → 白重试 5 次)
//
// 通用 openai 适配器只能看到外层,产出 `10605: {"isQueued":true,…}` ——
// 用户看不懂是排队还是额度用完。故用**插件自装的 channels-qoder**,
// 厂商码值留在插件域(内核零厂商名)。
func TestQoderAdapterParsesQueueAndBilling(t *testing.T) {
c, _ := poolCore(t)
vm := c.VM()
// ① 排队:外层 code=10605 + 内层 JSON 字符串(参考实测的真实形状)
out := qoderReason(t, vm, 403,
`{"code":"10605","message":"{\"isQueued\":true,\"queueCount\":12,\"retryAfterSeconds\":30}"}`)
if !strings.Contains(out, "排队") {
t.Errorf("10605 必须说成排队,实得 %q", out)
}
// 等待时长与队列长度要从**内层**取到(只读顶层永远拿不到)
if !strings.Contains(out, "30") {
t.Errorf("应从内层取到 retryAfterSeconds=30,实得 %q", out)
}
if !strings.Contains(out, "12") {
t.Errorf("应从内层取到 queueCount=12,实得 %q", out)
}
// ② SSE 错误帧形态:**内层没有 code 字段**,只有排队标志。
// ⚠️ 参考第一版要求「必须命中 code 才算排队」,导致这一形态被判成非排队、
// SSE 通道的修复静默失效(探针显示 sleep=0)。这里必须能识别。
out2 := qoderReason(t, vm, 200,
`{"message":"{\"isQueued\":true,\"queueCount\":3,\"retryAfterSeconds\":8}"}`)
if !strings.Contains(out2, "排队") {
t.Errorf("内层无 code 也必须识别为排队(这是参考踩过的坑),实得 %q", out2)
}
if !strings.Contains(out2, "8") {
t.Errorf("应取到内层 retryAfterSeconds=8,实得 %q", out2)
}
// ③ 额度耗尽:必须与排队**分开说**,且明确"重试无用"
out3 := qoderReason(t, vm, 403, `{"code":"110","message":"Billing daily count exceeded"}`)
if !strings.Contains(out3, "额度") {
t.Errorf("110 必须说成额度耗尽,实得 %q", out3)
}
if strings.Contains(out3, "排队") {
t.Errorf("110 绝不能被说成排队(两者语义相反),实得 %q", out3)
}
if !strings.Contains(out3, "重试无用") {
t.Errorf("额度耗尽要明确告知重试无用,实得 %q", out3)
}
// ④ 泛词不能误伤:正文里讨论"配额/余额"不该被当成额度错误
//(关键词必须窄 —— 宽词会把模型正常输出误判成故障)
out4 := qoderReason(t, vm, 500,
`{"error":{"message":"the response discusses quota and balance concepts"}}`)
if strings.Contains(out4, "额度已用尽") {
t.Errorf("泛词 quota/balance 不得误伤(关键词必须窄),实得 %q", out4)
}
// ⑤ 与通用 openai 适配器对比:证明**通用器确实做不到**这件事
//(否则这个专用适配器就没有存在理由)
gen, okGen, _ := vm.TransformError("openai", 403,
`{"code":"10605","message":"{\"isQueued\":true,\"retryAfterSeconds\":30}"}`)
if okGen && strings.Contains(gen, "排队") {
t.Error("通用 openai 适配器不该能识别排队 —— 若能,则专用适配器无必要")
}
}