mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
fix(gateway): AUTO 被密钥模型范围拦下时报错误导 + 编辑器给出提示
一个密钥的模型范围不含 AUTO 时,它用不了 AUTO —— 范围过滤发生在链之前。 但这两条路径都不说真话: 1. 请求侧:AUTO 走的是通用分支,返回 model_not_found "model \"AUTO\" is not configured",读起来像 AUTO 没配置。非 AUTO 模型早就有 model_not_allowed 的专门提示,AUTO 漏了。补上,并说明修法(把 AUTO 加进该密钥的 models, 或去掉范围限制)。 2. 编辑器侧:per-key AUTO 链编辑器可以正常配链、保存也成功,看起来一切正常, 但该密钥的每个请求都会 403。管理员无从得知。现在弹窗顶部在检测到冲突时 显示警告,并列出当前范围。 刻意不做的事:不自动把 AUTO 加进该密钥的模型范围。那等于悄悄授予运营 没要求的访问权,比一个显眼的警告更糟。 判据 1 条,除确认提示出现外还断言该函数体内没有 PUT/POST/fetch/api —— 它只报告,不得写回。变异(去掉 AUTO 判断)判红。 CDP 实测四种场景:范围含 AUTO → 无提示;范围不含 → 警告并列出范围; 空范围(不受限)→ 无提示;范围含 AUTO → 无提示。 Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
@ -452,6 +452,19 @@ func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A key whose model scope does not include AUTO cannot use AUTO at all,
|
||||||
|
// even when it has its own AUTO chain configured. That combination is
|
||||||
|
// easy to set up by accident and produced a misleading error: the request
|
||||||
|
// fell through to the generic "model %q is not configured" below, which
|
||||||
|
// claims AUTO does not exist — it does, this key just may not use it. Name
|
||||||
|
// the actual reason so the admin can fix the scope.
|
||||||
|
if isAuto(model) {
|
||||||
|
if allow := g.allowedModels(r.Context()); allow != nil && !g.hasScopeModel(allow, model) {
|
||||||
|
writeError(w, http.StatusForbidden, "model_not_allowed",
|
||||||
|
fmt.Sprintf("model %q is not in this key's model scope, so it cannot use AUTO; add %q to the key's models or remove the scope restriction", model, model))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
cands, effective := g.resolveCands(r.Context(), &req)
|
cands, effective := g.resolveCands(r.Context(), &req)
|
||||||
if len(cands) == 0 {
|
if len(cands) == 0 {
|
||||||
writeError(w, http.StatusNotFound, "model_not_found", fmt.Sprintf("model %q is not configured", model))
|
writeError(w, http.StatusNotFound, "model_not_found", fmt.Sprintf("model %q is not configured", model))
|
||||||
|
|||||||
@ -517,7 +517,20 @@
|
|||||||
#toast{left:12px;right:12px;bottom:12px;text-align:center}
|
#toast{left:12px;right:12px;bottom:12px;text-align:center}
|
||||||
th,td{padding:8px 10px}
|
th,td{padding:8px 10px}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* Shown when a key's model scope blocks AUTO: the chain editor can look
|
||||||
|
fine while every request 403s, so the conflict must be visible here. */
|
||||||
|
.warn-box {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(224, 108, 51, 0.45);
|
||||||
|
background: rgba(224, 108, 51, 0.1);
|
||||||
|
color: #b45309;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="bgfx" aria-hidden="true">
|
<div id="bgfx" aria-hidden="true">
|
||||||
@ -835,6 +848,7 @@
|
|||||||
kAutoChainSeeded: "已复制全局 AUTO 链作为编辑起点,保存后该密钥将使用这条独立链",
|
kAutoChainSeeded: "已复制全局 AUTO 链作为编辑起点,保存后该密钥将使用这条独立链",
|
||||||
kAutoChainNewOwn: "该密钥当前跟随全局链,保存后改用这里配置的独立链",
|
kAutoChainNewOwn: "该密钥当前跟随全局链,保存后改用这里配置的独立链",
|
||||||
kAutoChainWillInherit: "保存后该密钥将恢复跟随全局 AUTO 链",
|
kAutoChainWillInherit: "保存后该密钥将恢复跟随全局 AUTO 链",
|
||||||
|
kAutoChainScopeWarn: "警告:该密钥的模型范围不包含 AUTO,配了独立 AUTO 链也用不了。当前范围:",
|
||||||
kAutoChainBad: "已保存,但槽位无法解析(模型与源的组合不存在),AUTO 请求会失败",
|
kAutoChainBad: "已保存,但槽位无法解析(模型与源的组合不存在),AUTO 请求会失败",
|
||||||
kName: "名称",
|
kName: "名称",
|
||||||
kRole: "角色",
|
kRole: "角色",
|
||||||
@ -1109,6 +1123,7 @@
|
|||||||
kAutoChainSeeded: "Copied the global AUTO chain as a starting point — saving gives this key its own chain",
|
kAutoChainSeeded: "Copied the global AUTO chain as a starting point — saving gives this key its own chain",
|
||||||
kAutoChainNewOwn: "This key currently follows the global chain; saving switches it to the chain configured here",
|
kAutoChainNewOwn: "This key currently follows the global chain; saving switches it to the chain configured here",
|
||||||
kAutoChainWillInherit: "Saving makes this key follow the global AUTO chain again",
|
kAutoChainWillInherit: "Saving makes this key follow the global AUTO chain again",
|
||||||
|
kAutoChainScopeWarn: "Warning: this key\u2019s model scope does not include AUTO, so a per-key AUTO chain will still be rejected. Current scope: ",
|
||||||
kAutoChainBad: "Saved, but the slot does not resolve (no source serves that model); AUTO requests will fail",
|
kAutoChainBad: "Saved, but the slot does not resolve (no source serves that model); AUTO requests will fail",
|
||||||
kName: "Name",
|
kName: "Name",
|
||||||
kRole: "Role",
|
kRole: "Role",
|
||||||
@ -4811,8 +4826,11 @@ function afterChainEdit() {
|
|||||||
async function loadKeys() {
|
async function loadKeys() {
|
||||||
const el = $("#k-list");
|
const el = $("#k-list");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const j = await api("/api/keys");
|
const j = await api("/api/keys");
|
||||||
const ks = j.keys || [];
|
const ks = j.keys || [];
|
||||||
|
// Cached so the per-key AUTO editor can read a key's model scope
|
||||||
|
// without a second request (keyAutoScopeWarning).
|
||||||
|
window._keyRows = ks;
|
||||||
el.innerHTML = ks.length
|
el.innerHTML = ks.length
|
||||||
? ks.map((k) => keyCanvasHtml(k)).join("")
|
? ks.map((k) => keyCanvasHtml(k)).join("")
|
||||||
: `<div class="muted">${t("kEmpty")}</div>`;
|
: `<div class="muted">${t("kEmpty")}</div>`;
|
||||||
@ -5268,6 +5286,7 @@ function afterChainEdit() {
|
|||||||
wrap.innerHTML = `<div class="card" style="width:900px;max-width:100%">
|
wrap.innerHTML = `<div class="card" style="width:900px;max-width:100%">
|
||||||
<h2>${esc(t("kAutoChainTitle"))} \u00b7 ${esc(name || key)}</h2>
|
<h2>${esc(t("kAutoChainTitle"))} \u00b7 ${esc(name || key)}</h2>
|
||||||
<div class="muted" style="margin-bottom:10px">${esc(t("kAutoChainHint"))}</div>
|
<div class="muted" style="margin-bottom:10px">${esc(t("kAutoChainHint"))}</div>
|
||||||
|
<div id="ka-scope-warn"></div>
|
||||||
<div id="ka-canvas-host"></div>
|
<div id="ka-canvas-host"></div>
|
||||||
<p><button class="ghost small" onclick="scrAddModal()">+ ${esc(t("sortAdd"))}</button>
|
<p><button class="ghost small" onclick="scrAddModal()">+ ${esc(t("sortAdd"))}</button>
|
||||||
<button class="ghost small" onclick="keyAutoInherit()">${esc(t("kAutoChainInherit"))}</button>
|
<button class="ghost small" onclick="keyAutoInherit()">${esc(t("kAutoChainInherit"))}</button>
|
||||||
@ -5309,6 +5328,7 @@ try {
|
|||||||
renderSortEditor($("#ka-canvas-host"), { canvasId: "key-auto-canvas" });
|
renderSortEditor($("#ka-canvas-host"), { canvasId: "key-auto-canvas" });
|
||||||
keyAutoState.seeded = seededFromGlobal;
|
keyAutoState.seeded = seededFromGlobal;
|
||||||
keyAutoInheritNotice();
|
keyAutoInheritNotice();
|
||||||
|
keyAutoScopeWarning(key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e));
|
toast(String(e));
|
||||||
}
|
}
|
||||||
@ -5364,6 +5384,31 @@ try {
|
|||||||
paintSort();
|
paintSort();
|
||||||
keyAutoInheritNotice();
|
keyAutoInheritNotice();
|
||||||
}
|
}
|
||||||
|
// keyAutoScopeWarning tells the admin when the key's model scope blocks
|
||||||
|
// AUTO outright. Configuring a per-key AUTO chain for such a key looks
|
||||||
|
// like it works, yet every request 403s — the scope filter runs before
|
||||||
|
// the chain is consulted. We deliberately do NOT widen the scope
|
||||||
|
// automatically: that would silently grant access nobody asked for.
|
||||||
|
function keyAutoScopeWarning(key) {
|
||||||
|
const host = $("#ka-scope-warn");
|
||||||
|
if (!host) return;
|
||||||
|
const rec = (window._keyRows || []).find((k) => k.key === key);
|
||||||
|
const models = (rec && rec.models) || [];
|
||||||
|
if (!models.length) {
|
||||||
|
host.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasAuto = models.some(
|
||||||
|
(m) => (m.model || "").toUpperCase() === "AUTO",
|
||||||
|
);
|
||||||
|
host.innerHTML = hasAuto
|
||||||
|
? ""
|
||||||
|
: '<div class="warn-box">' +
|
||||||
|
esc(t("kAutoChainScopeWarn")) +
|
||||||
|
" " +
|
||||||
|
esc(models.map((m) => m.model).join(", ")) +
|
||||||
|
"</div>";
|
||||||
|
}
|
||||||
function keyAutoClose() {
|
function keyAutoClose() {
|
||||||
const m = document.getElementById("key-auto-modal");
|
const m = document.getElementById("key-auto-modal");
|
||||||
if (m) m.remove();
|
if (m) m.remove();
|
||||||
|
|||||||
@ -329,3 +329,36 @@ func TestUIKeyAutoNoticeDistinguishesSavedFromCopied(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A key whose model scope excludes AUTO cannot use AUTO at all — the scope
|
||||||
|
// filter runs before the chain — yet the editor happily lets an admin
|
||||||
|
// configure a per-key chain. That combination is easy to create by accident
|
||||||
|
// and produces a 403 with no visible cause, so the dialog must say so.
|
||||||
|
//
|
||||||
|
// We deliberately do NOT widen the scope automatically: silently granting a
|
||||||
|
// model the operator did not ask for is worse than a loud warning.
|
||||||
|
func TestUIKeyAutoWarnsOnScopeConflict(t *testing.T) {
|
||||||
|
src := uiSource(t)
|
||||||
|
if !strings.Contains(src, "function keyAutoScopeWarning(") {
|
||||||
|
t.Fatal("no scope-conflict check in the per-key editor")
|
||||||
|
}
|
||||||
|
body := readFuncBody(t, src, "keyAutoScopeWarning")
|
||||||
|
if !strings.Contains(body, "AUTO") {
|
||||||
|
t.Fatal("keyAutoScopeWarning must test whether the scope lists AUTO")
|
||||||
|
}
|
||||||
|
// It must not write anything back to the key: reporting only.
|
||||||
|
for _, forbidden := range []string{"PUT", "POST", "fetch(", "api("} {
|
||||||
|
if strings.Contains(body, forbidden) {
|
||||||
|
t.Fatalf("keyAutoScopeWarning calls %s — it must only report the "+
|
||||||
|
"conflict, never widen the scope itself", forbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Empty scope means unrestricted, so no warning is correct there.
|
||||||
|
if !strings.Contains(body, "if (!models.length)") {
|
||||||
|
t.Fatal("an unrestricted key (no models) must not be warned")
|
||||||
|
}
|
||||||
|
// Both languages.
|
||||||
|
if n := strings.Count(src, "kAutoChainScopeWarn"); n < 2 {
|
||||||
|
t.Fatalf("i18n key kAutoChainScopeWarn appears %d time(s), want zh+en", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user