mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
产品名早已是 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/免费池的既有改动,另批提交)。
132 lines
4.4 KiB
Go
132 lines
4.4 KiB
Go
package provider
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"modelrouter/internal/config"
|
||
"modelrouter/internal/types"
|
||
)
|
||
|
||
// 上游重试是**内核的通用机制**,判据纯数据(status + body 子串),
|
||
// 目的是让"账号级限流、等一会儿就好"这类拒绝不冒到客户端。
|
||
//
|
||
// 真实事故(2026-10-20):CodeArts 账户并发会话上限 3 个,连续第 3~4 个请求
|
||
// 回 400 TM.00001041,最后一个请求结束后约 44~60 秒才释放;流式更糟 ——
|
||
// 上游用 HTTP 200 + error 帧表达拒绝,内核只能报 `empty stream`。
|
||
// 参考实现的处理是每 10s 重试一次 chat 直到放行,本测试钉住同一语义。
|
||
func TestChatRetriesDeclaredUpstreamRejection(t *testing.T) {
|
||
var hits int32
|
||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := atomic.AddInt32(&hits, 1)
|
||
if n <= 2 {
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
fmt.Fprint(w, `{"error_code":"TM.00001041","error_msg":"并发会话数已达上限(3个)"}`)
|
||
return
|
||
}
|
||
fmt.Fprint(w, `{"choices":[{"message":{"content":"hi"},"finish_reason":"stop"}]}`)
|
||
}))
|
||
defer up.Close()
|
||
|
||
s := src("codearts-like", up.URL, "openai", "m1")
|
||
s.Retry = &config.RetryPolicy{
|
||
OnStatus: []int{400},
|
||
OnBodyContains: []string{"TM.00001041"},
|
||
Delay: "1ms",
|
||
MaxAttempts: 4,
|
||
}
|
||
p := newTestProvider(t, s)
|
||
resp, err := p.Chat(context.Background(), &types.ChatRequest{
|
||
Model: "m1",
|
||
Messages: []types.ChatMessage{{Role: "user", Content: types.StringContent("hi")}},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("命中重试策略后应成功,实得错误: %v", err)
|
||
}
|
||
if resp.Content != "hi" {
|
||
t.Fatalf("content = %q", resp.Content)
|
||
}
|
||
if got := atomic.LoadInt32(&hits); got != 3 {
|
||
t.Fatalf("应恰好尝试 3 次(2 次失败 + 1 次成功),实得 %d", got)
|
||
}
|
||
}
|
||
|
||
// 同一状态码但 body 不匹配 ⇒ **不许重试**(400 也常用于真正的请求错误,
|
||
// 无差别重试只会把错误延迟 3 分钟才暴露给用户)。
|
||
func TestChatDoesNotRetryUnmatchedBody(t *testing.T) {
|
||
var hits int32
|
||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
atomic.AddInt32(&hits, 1)
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
fmt.Fprint(w, `{"error_code":"TM.00001005","error_msg":"无正常可用套餐"}`)
|
||
}))
|
||
defer up.Close()
|
||
|
||
s := src("codearts-like", up.URL, "openai", "m1")
|
||
s.Retry = &config.RetryPolicy{
|
||
OnStatus: []int{400},
|
||
OnBodyContains: []string{"TM.00001041"},
|
||
Delay: "1ms",
|
||
MaxAttempts: 5,
|
||
}
|
||
p := newTestProvider(t, s)
|
||
_, err := p.Chat(context.Background(), &types.ChatRequest{
|
||
Model: "m1",
|
||
Messages: []types.ChatMessage{{Role: "user", Content: types.StringContent("hi")}},
|
||
})
|
||
if err == nil {
|
||
t.Fatal("不可重试的 body 必须失败")
|
||
}
|
||
if got := atomic.LoadInt32(&hits); got != 1 {
|
||
t.Fatalf("body 不匹配时只能尝试 1 次,实得 %d", got)
|
||
}
|
||
}
|
||
|
||
// 流式:只重试**握手**。命中策略 → 重试直到 200 才开始派发;一旦开始流式
|
||
// 就不再重试(重复输出比报错更糟)。
|
||
func TestChatStreamRetriesHandshakeOnly(t *testing.T) {
|
||
var hits int32
|
||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := atomic.AddInt32(&hits, 1)
|
||
if n == 1 {
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
fmt.Fprint(w, `{"error_code":"TM.00001041","error_msg":"并发会话数已达上限(3个)"}`)
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "text/event-stream")
|
||
fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"hi\"},\"finish_reason\":\"stop\"}]}\n\n")
|
||
fmt.Fprint(w, "data: [DONE]\n\n")
|
||
}))
|
||
defer up.Close()
|
||
|
||
s := src("codearts-like", up.URL, "openai", "m1")
|
||
s.Retry = &config.RetryPolicy{
|
||
OnStatus: []int{400},
|
||
OnBodyContains: []string{"TM.00001041"},
|
||
Delay: "1ms",
|
||
MaxAttempts: 4,
|
||
}
|
||
p := newTestProvider(t, s)
|
||
ch, err := p.ChatStream(context.Background(), &types.ChatRequest{
|
||
Model: "m1",
|
||
Messages: []types.ChatMessage{{Role: "user", Content: types.StringContent("hi")}},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("握手重试后应成功: %v", err)
|
||
}
|
||
var content string
|
||
for ck := range ch {
|
||
content += ck.Content
|
||
}
|
||
if content != "hi" {
|
||
t.Fatalf("content = %q", content)
|
||
}
|
||
if got := atomic.LoadInt32(&hits); got != 2 {
|
||
t.Fatalf("应握手重试 1 次(共 2 次请求),实得 %d", got)
|
||
}
|
||
}
|