diff --git a/internal/gateway/chat.go b/internal/gateway/chat.go
index 2e3af12..5a70914 100644
--- a/internal/gateway/chat.go
+++ b/internal/gateway/chat.go
@@ -452,6 +452,19 @@ func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
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)
if len(cands) == 0 {
writeError(w, http.StatusNotFound, "model_not_found", fmt.Sprintf("model %q is not configured", model))
diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html
index 42328f1..f35a998 100644
--- a/internal/gateway/ui/index.html
+++ b/internal/gateway/ui/index.html
@@ -517,7 +517,20 @@
#toast{left:12px;right:12px;bottom:12px;text-align:center}
th,td{padding:8px 10px}
}
-
+
+ /* 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;
+ }
+
@@ -835,6 +848,7 @@
kAutoChainSeeded: "已复制全局 AUTO 链作为编辑起点,保存后该密钥将使用这条独立链",
kAutoChainNewOwn: "该密钥当前跟随全局链,保存后改用这里配置的独立链",
kAutoChainWillInherit: "保存后该密钥将恢复跟随全局 AUTO 链",
+ kAutoChainScopeWarn: "警告:该密钥的模型范围不包含 AUTO,配了独立 AUTO 链也用不了。当前范围:",
kAutoChainBad: "已保存,但槽位无法解析(模型与源的组合不存在),AUTO 请求会失败",
kName: "名称",
kRole: "角色",
@@ -1109,6 +1123,7 @@
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",
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",
kName: "Name",
kRole: "Role",
@@ -4811,8 +4826,11 @@ function afterChainEdit() {
async function loadKeys() {
const el = $("#k-list");
if (!el) return;
- const j = await api("/api/keys");
+const j = await api("/api/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
? ks.map((k) => keyCanvasHtml(k)).join("")
: `
";
+ }
function keyAutoClose() {
const m = document.getElementById("key-auto-modal");
if (m) m.remove();
diff --git a/internal/gateway/ui_key_auto_test.go b/internal/gateway/ui_key_auto_test.go
index 7ada43d..c83c70e 100644
--- a/internal/gateway/ui_key_auto_test.go
+++ b/internal/gateway/ui_key_auto_test.go
@@ -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)
+ }
+}