mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-05 07:02:29 +00:00
此前 AUTO 链是全局单值(cfg.Auto + Core.AutoChain()),所有用户共用一条链。
管理员无法为某个用户单独指定调度链。
按 per-key 覆盖 + 全局兜底实现:
- config.GWKey 增加 Auto 字段与 HasOwnAuto()。未配置即继承全局链,
存量部署零改动,新密钥天然继承全局链。
- Core 把 buildAutoChain 的归一化逻辑抽成 chainForRules,全局链、
生图链、per-key 链共用同一套编译,避免两处漂移。
- Core 增加 keyAutoChains 缓存 + AutoChainFor(key)。请求路径读缓存不
加锁,与全局链查询一致。缓存整表原子替换,不会看到半成品。
- 请求侧 chat.go 改用 AutoChainFor(reqKey)。冷却仍在 Provider 上按
model+source 共享:两条链指向同一个 slot 时共用冷却,与今天单链行为
相同,也避免为 per-key 维度重构冷却而改变现有可观测语义。
- API:GET/PUT/DELETE /api/keys/{key}/auto(admin),GET
/api/keys/me/auto(任意角色,只能读自己的)。空 PUT 与 DELETE 等价于
"恢复继承",无法持久化一条会 503 的空链。写入时回报解析出的槽位数,
让管理员当场看到模型名写错,而不是等用户下次请求 503。
- 源变更时一并重编译 per-key 链,加源后无需重启即可生效。
判据 18 条,5 个变异全部被抓住:AutoChainFor 忽略 key、清空后不重建
缓存、源变更不重建、空 PUT 落盘成空链、me/auto 误要求 admin。
UI 判据做变异时发现漏放:只查函数定义存在,删掉按钮后仍通过。已改为
断言 keyCanvasHtml 内的调用点。
端到端实测(真实 HTTP + 两个 mock 上游):admin 与 bob 初始同为 m-fast,
给 bob 配 m-cheap 后两者分流,重启后仍分流,DELETE 后 bob 回到 m-fast。
Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
78 lines
2.6 KiB
Go
78 lines
2.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Per-key AUTO chain UI contract. The feature spans three layers (API handler,
|
|
// config field, WebUI) and the WebUI part is plain inline JS, so a rename or a
|
|
// dropped button fails silently at runtime: the admin simply finds no way to
|
|
// configure a key's chain and has no error to chase.
|
|
//
|
|
// These checks pin the wiring that is easy to break by refactor.
|
|
|
|
func TestUIPerKeyAutoWiring(t *testing.T) {
|
|
src := uiSource(t)
|
|
|
|
// The admin key list must offer the editor, otherwise the whole feature is
|
|
// unreachable from the UI. Assert the *call site* inside keyCanvasHtml,
|
|
// not just that the functions exist: removing the button leaves every
|
|
// definition intact, and that is exactly the regression to catch.
|
|
kcStart := strings.Index(src, "function keyCanvasHtml")
|
|
if kcStart < 0 {
|
|
t.Fatal("keyCanvasHtml is gone — cannot check the AUTO chain button")
|
|
}
|
|
kcEnd := strings.Index(src[kcStart:], "\n }")
|
|
if kcEnd < 0 {
|
|
t.Fatal("could not delimit keyCanvasHtml")
|
|
}
|
|
kcBody := src[kcStart : kcStart+kcEnd]
|
|
if !strings.Contains(kcBody, "openKeyAutoModal(") {
|
|
t.Fatal("per-key AUTO chain button is missing from the key card header")
|
|
}
|
|
for _, fn := range []string{
|
|
"function openKeyAutoModal",
|
|
"function keyAutoRow",
|
|
"function keyAutoAddRow",
|
|
"function keyAutoClear",
|
|
"function keyAutoSave",
|
|
} {
|
|
if !strings.Contains(src, fn) {
|
|
t.Fatalf("UI is missing %s — the editor cannot function", fn)
|
|
}
|
|
}
|
|
|
|
// The editor must talk to the endpoint the gateway actually serves.
|
|
if !strings.Contains(src, "/auto\"") {
|
|
t.Fatal("UI never calls the /api/keys/{key}/auto endpoint")
|
|
}
|
|
}
|
|
|
|
// The editor's empty-row path sends {"auto":[]} and relies on the server
|
|
// treating that as "clear the override". If the UI instead sent a sentinel,
|
|
// a future server change would silently persist a broken empty chain.
|
|
func TestUIPerKeyAutoSendsEmptyListToClear(t *testing.T) {
|
|
src := uiSource(t)
|
|
if !strings.Contains(src, "JSON.stringify({ auto: rules })") {
|
|
t.Fatal("keyAutoSave must PUT the collected rule list verbatim")
|
|
}
|
|
if !strings.Contains(src, "keyAutoClear") {
|
|
t.Fatal("no way to clear a key's own chain from the UI")
|
|
}
|
|
}
|
|
|
|
// Both languages need the strings, otherwise a non-Chinese admin sees raw
|
|
// i18n keys in the modal.
|
|
func TestUIPerKeyAutoHasBothLanguages(t *testing.T) {
|
|
src := uiSource(t)
|
|
for _, k := range []string{
|
|
"kAutoChainTitle", "kAutoChainHint", "kAutoChainInherit",
|
|
"kAutoChainAdd", "kAutoChainSave", "kAutoChainEmpty",
|
|
} {
|
|
n := strings.Count(src, k+`: "`)
|
|
if n < 2 {
|
|
t.Fatalf("i18n key %s defined %d time(s), want both zh and en", k, n)
|
|
}
|
|
}
|
|
} |