mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
test(studio): guard alias identity and persistence boundaries
This commit is contained in:
1 parent
f9e3f3a5e5
commit
62ddce6804
5 files changed
+196
No files matched your search
@@ -0,0 +1,42 @@
|
||||
---
|
||||
owner: @SivanCola
|
||||
backup: @esengine
|
||||
status: active
|
||||
reviewed: 2026-10-12
|
||||
---
|
||||
|
||||
# Model display aliases
|
||||
|
||||
In Studio's model service settings, each selected model has an optional display
|
||||
alias beneath its full model ID. Save the connection to apply it. Leave the alias
|
||||
empty to show the model ID again. Revert discards unsaved changes with the rest of
|
||||
the connection form.
|
||||
|
||||
The chat model picker and its active-model button show the alias. The picker keeps
|
||||
the full model ID below it and searches both names. Duplicate aliases are allowed:
|
||||
the provider and raw model ID still identify the selection.
|
||||
|
||||
Aliases are scoped to one provider entry and one exact model ID, including case
|
||||
and any slash in the model ID. They do not change provider requests, defaults,
|
||||
role assignments, saved conversation references, or model availability. Editing
|
||||
only aliases does not rebuild or interrupt a running conversation.
|
||||
|
||||
The user configuration stores them in the provider's `model_aliases` map:
|
||||
|
||||
```toml
|
||||
[[providers]]
|
||||
name = "local"
|
||||
kind = "openai"
|
||||
base_url = "http://localhost:11434/v1"
|
||||
models = ["vendor/long-model-id"]
|
||||
model_aliases = { "vendor/long-model-id" = "Short name" }
|
||||
```
|
||||
|
||||
Aliases use the display-name limit of 64 Unicode characters and cannot contain
|
||||
control characters. Unselecting a model retains its stored alias for later use;
|
||||
clearing a selected model's alias removes it. Clients that omit `modelAliases`
|
||||
when editing a provider preserve existing aliases.
|
||||
|
||||
Resolver-backed remote panes use their resolver's catalog. This machine's local
|
||||
aliases are never overlaid onto that catalog. Removing fetched models is a
|
||||
separate part of [#11774](https://github.com/esengine/DeepSeek-Reasonix/issues/11774).
|
||||
@@ -93,6 +93,7 @@ Checked against `studio` at `d779d89bc` on 2026-10-08.
|
||||
|
||||
| Feature | Status | Studio location |
|
||||
| --- | --- | --- |
|
||||
| Per-model display aliases | Have | Model service settings and chat picker; [configuration and scope](MODEL_DISPLAY_ALIASES.md); #11774 |
|
||||
| Icon rail with three display modes | Have | #12377, #12428; sidebar footer entries appear only when icon navigation is hidden (#12478) |
|
||||
| Usage, feedback, account and settings footer / 用量、反馈、账号与设置底部入口 | Have differently | `ui/Nav.tsx` when navigation is visible; `ui/Sidebar.tsx` otherwise, including the narrow-window drawer. 导航可见时使用图标栏入口,隐藏时保留侧栏入口 (#12478) |
|
||||
| Completed-unread marker on sessions | Have | #12218 |
|
||||
@@ -0,0 +1,56 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/netclient"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/contract/provider"
|
||||
)
|
||||
|
||||
func TestModelAliasDoesNotChangeBuildIdentityOrWireModel(t *testing.T) {
|
||||
gotModel := make(chan string, 2)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
gotModel <- body.Model
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n"))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
cfg := &config.Config{Providers: []config.ProviderEntry{{Name: "relay", Kind: "openai", BaseURL: upstream.URL, Models: []string{"vendor/long-v1"}}}}
|
||||
entry, _ := cfg.ResolveModel("relay/vendor/long-v1")
|
||||
before := ResolveProviderBuildIdentity(entry, netclient.ProxySpec{}, nil)
|
||||
for _, aliases := range []map[string]string{nil, {"vendor/long-v1": "Short"}} {
|
||||
if err := cfg.Providers[0].SetModelAliases(aliases); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entry, _ = cfg.ResolveModel("relay/vendor/long-v1")
|
||||
if got := ResolveProviderBuildIdentity(entry, netclient.ProxySpec{}, nil); got != before {
|
||||
t.Fatal("display alias changed provider build identity")
|
||||
}
|
||||
p, err := NewProviderWithProxy(entry, netclient.ProxySpec{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stream, err := p.Stream(t.Context(), provider.Request{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for chunk := range stream {
|
||||
if chunk.Err != nil {
|
||||
t.Fatal(chunk.Err)
|
||||
}
|
||||
}
|
||||
if got := <-gotModel; got != "vendor/long-v1" {
|
||||
t.Fatalf("wire model = %q", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestModelAliasFailedSaveDoesNotPublishAndCanRetry(t *testing.T) {
|
||||
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
|
||||
t.Skip("requires enforced directory write permissions")
|
||||
}
|
||||
srv := newRichProviderServer(t)
|
||||
path := config.UserConfigPath()
|
||||
before, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.Chmod(dir, 0o500); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chmod(dir, 0o700) })
|
||||
body := `{"name":"rich","models":["alpha","beta"],"default":"alpha","vision":[],"modelAliases":{"alpha":"Short"}}`
|
||||
resp := postProvider(t, srv.URL, "/providers/edit", body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("failed save = %d", resp.StatusCode)
|
||||
}
|
||||
after, err := os.ReadFile(path)
|
||||
if err != nil || string(before) != string(after) {
|
||||
t.Fatalf("failed save changed config: %v", err)
|
||||
}
|
||||
var got struct {
|
||||
Models []modelEntry `json:"models"`
|
||||
}
|
||||
getJSON(t, srv.URL+"/models", &got)
|
||||
for _, m := range got.Models {
|
||||
if m.ModelAlias != "" {
|
||||
t.Fatalf("failed alias was published: %+v", m)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(dir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp = postProvider(t, srv.URL, "/providers/edit", body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("retry = %d", resp.StatusCode)
|
||||
}
|
||||
p, _ := config.LoadForEdit(path).Provider("rich")
|
||||
if p.ModelAlias("alpha") != "Short" {
|
||||
t.Fatal("successful retry did not persist alias")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"reasonix/internal/contract/config"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestModelAliasesNeverOverlayRemoteCatalog(t *testing.T) {
|
||||
writeServeModelConfig(t)
|
||||
closeSharedCatalogsOnCleanup(t)
|
||||
cfg := config.LoadForEdit(config.UserConfigPath())
|
||||
cfg.Providers = append(cfg.Providers, config.ProviderEntry{Name: "home", Kind: "openai", BaseURL: "https://local.invalid", Models: []string{"chat-a", "chat-b"}, ModelAliases: map[string]string{"chat-a": "Local alias", "chat-b": "Local alias"}})
|
||||
if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := NewHub(HubOptions{ProviderResolver: homeResolver()})
|
||||
defer h.Shutdown()
|
||||
rt, err := h.Open(t.Context(), OpenRequest{Root: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := httptest.NewServer(operatorHandler(h))
|
||||
defer srv.Close()
|
||||
var got struct {
|
||||
Current string `json:"current"`
|
||||
Models []modelEntry `json:"models"`
|
||||
}
|
||||
getJSON(t, srv.URL+"/rt/"+rt.ID+"/models", &got)
|
||||
if got.Current != "home/chat-b" || len(got.Models) != 2 {
|
||||
t.Fatalf("remote catalog changed: %+v", got)
|
||||
}
|
||||
for _, m := range got.Models {
|
||||
if m.ModelAlias != "" {
|
||||
t.Fatalf("local alias leaked into remote catalog: %+v", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user