Add virtual model support for multi-key expansion

Virtual models generated from multi-key expansion are now marked and
filtered during config persistence. Virtual models display with a badge
in the UI and cannot be set as default.
This commit is contained in:
uiyzzi
2026-03-24 23:56:45 +08:00
parent 9fb01bc7f8
commit be6bf9f6c6
9 changed files with 214 additions and 4 deletions
+9 -1
View File
@@ -42,6 +42,7 @@ type modelResponse struct {
// Meta
Configured bool `json:"configured"`
IsDefault bool `json:"is_default"`
IsVirtual bool `json:"is_virtual"`
}
// handleListModels returns all model_list entries with masked API keys.
@@ -86,6 +87,7 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
ExtraBody: m.ExtraBody,
Configured: configured[i],
IsDefault: m.ModelName == defaultModel,
IsVirtual: m.IsVirtual(),
})
}
@@ -288,11 +290,13 @@ func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request)
return
}
// Verify the model_name exists in model_list
// Verify the model_name exists in model_list and is not a virtual model
found := false
isVirtual := false
for _, m := range cfg.ModelList {
if m.ModelName == req.ModelName {
found = true
isVirtual = m.IsVirtual()
break
}
}
@@ -300,6 +304,10 @@ func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request)
http.Error(w, fmt.Sprintf("Model %q not found in model_list", req.ModelName), http.StatusNotFound)
return
}
if isVirtual {
http.Error(w, fmt.Sprintf("Cannot set virtual model %q as default", req.ModelName), http.StatusBadRequest)
return
}
cfg.Agents.Defaults.ModelName = req.ModelName
+40
View File
@@ -356,6 +356,46 @@ func TestHandleAddModel_PersistsAPIKey(t *testing.T) {
}
}
// TestHandleSetDefaultModel_RejectsNonexistentModel tests that setting a non-existent
// model as default returns 404. This covers the case where virtual models (which are
// filtered by SaveConfig) cannot be set as default.
func TestHandleSetDefaultModel_RejectsNonexistentModel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
// First save a valid config with a primary model
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.ModelList = []*config.ModelConfig{
{ModelName: "gpt-4", Model: "openai/gpt-4o"},
}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
// Try to set a non-existent model (like a virtual model name) as default
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/models/default", bytes.NewBufferString(`{
"model_name": "gpt-4__key_1"
}`))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(rec, req)
// Should return 404 because the virtual model doesn't exist in the persisted config
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusNotFound, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "not found") {
t.Fatalf("error message should mention 'not found', got: %s", rec.Body.String())
}
}
func TestMaskAPIKey(t *testing.T) {
tests := []struct {
name string
+1
View File
@@ -21,6 +21,7 @@ export interface ModelInfo {
// Meta
configured: boolean
is_default: boolean
is_virtual: boolean
}
interface ModelsListResponse {
@@ -28,7 +28,7 @@ export function ModelCard({
}: ModelCardProps) {
const { t } = useTranslation()
const isOAuth = model.auth_method === "oauth"
const canSetDefault = model.configured && !model.is_default
const canSetDefault = model.configured && !model.is_default && !model.is_virtual
return (
<div
@@ -64,6 +64,11 @@ export function ModelCard({
{t("models.badge.default")}
</span>
)}
{model.is_virtual && (
<span className="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[10px] leading-none font-medium">
{t("models.badge.virtual")}
</span>
)}
</div>
<div className="flex shrink-0 items-center gap-0.5">
+2 -1
View File
@@ -154,7 +154,8 @@
"unconfigured": "Not configured"
},
"badge": {
"default": "Default"
"default": "Default",
"virtual": "Virtual"
},
"action": {
"edit": "Edit API key",
+2 -1
View File
@@ -154,7 +154,8 @@
"unconfigured": "未配置"
},
"badge": {
"default": "默认"
"default": "默认",
"virtual": "虚拟"
},
"action": {
"edit": "编辑 API Key",