mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
Accumulated work from the branch, carried across the rebase onto origin/studio and merged with what landed there in the meantime. Storage: a workspace can name where its state lives, and the server tracks the one relocation it may be running (internal/storage, config/storage_roots, serve/storage, the Storage panel). Composer intake: what a drop or a long paste becomes is one pure function — payload in, action out — so "what happens if I drop a folder" is a test rather than a branch of an onDrop handler. It sits on the drop router that landed upstream: that one answers which pane a drop hit, this one answers what it means and says so before the pointer is released. A paste long enough to bury the composer becomes a chip beside it instead. Session: the checkpoint agreement and the lineage/torn-log recovery cases, plus the conflict diagnostic learning where two transcripts parted and which roles stood there — a position and a role tell a local reshape from a foreign write without recording any content. Model refs: the default-model write checks the live catalog, so an extension model the runtime knows can be persisted as the default.
212 lines
6.3 KiB
Go
212 lines
6.3 KiB
Go
package serve
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"reasonix/internal/config"
|
|
"reasonix/internal/control"
|
|
)
|
|
|
|
// One host reached under both protocols, holding a mixed vision/text model set,
|
|
// a per-model price and a context window — every capability the picker reads.
|
|
const mixedVendorConfig = `default_model = "mixed/text-only"
|
|
|
|
[[providers]]
|
|
name = "mixed"
|
|
kind = "openai"
|
|
base_url = "https://gateway.invalid/v1"
|
|
models = ["text-only", "vision-pro"]
|
|
default = "text-only"
|
|
vision_models = ["vision-pro"]
|
|
context_window = 131072
|
|
api_key_env = "MIXED_API_KEY"
|
|
|
|
[providers.prices.vision-pro]
|
|
input = 2.0
|
|
output = 8.0
|
|
currency = "CNY"
|
|
|
|
[[providers]]
|
|
name = "mixed-anthropic"
|
|
kind = "anthropic"
|
|
base_url = "https://gateway.invalid/anthropic"
|
|
models = ["text-only"]
|
|
default = "text-only"
|
|
api_key_env = "MIXED_API_KEY"
|
|
`
|
|
|
|
func modelsByRef(t *testing.T, cfgBody string) map[string]modelEntry {
|
|
t.Helper()
|
|
t.Setenv("REASONIX_HOME", t.TempDir())
|
|
t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
|
|
if _, err := config.SetCredential("MIXED_API_KEY", "sk-test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := config.UserConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(cfgBody), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bc := NewBroadcaster()
|
|
ctrl := control.New(control.Options{
|
|
Sink: bc, Label: "text-only", ModelRef: "mixed/text-only", SessionDir: t.TempDir(),
|
|
})
|
|
srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/models")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("GET /models = %d", resp.StatusCode)
|
|
}
|
|
var body struct {
|
|
Models []modelEntry `json:"models"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := make(map[string]modelEntry, len(body.Models))
|
|
for _, m := range body.Models {
|
|
out[m.Ref] = m
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestModelsReportVisionPerModelNotPerProvider(t *testing.T) {
|
|
got := modelsByRef(t, mixedVendorConfig)
|
|
|
|
if e, ok := got["mixed/text-only"]; !ok || e.Vision {
|
|
t.Fatalf("mixed/text-only vision = %v (present %v), want text-only", e.Vision, ok)
|
|
}
|
|
if e, ok := got["mixed/vision-pro"]; !ok || !e.Vision {
|
|
t.Fatalf("mixed/vision-pro vision = %v (present %v), want image input", e.Vision, ok)
|
|
}
|
|
}
|
|
|
|
// The old picker offered six effort levels to every model. A model whose
|
|
// endpoint exposes no effort vocabulary has to say so by staying empty, or the
|
|
// list is back to advertising levels the provider will reject.
|
|
func TestModelsOfferOnlyTheEffortLevelsTheEndpointHas(t *testing.T) {
|
|
got := modelsByRef(t, mixedVendorConfig)
|
|
|
|
plain, ok := got["mixed/text-only"]
|
|
if !ok {
|
|
t.Fatal("mixed/text-only missing from the catalog")
|
|
}
|
|
if len(plain.Efforts) != 0 {
|
|
t.Fatalf("a plain OpenAI-compatible endpoint offered efforts %v", plain.Efforts)
|
|
}
|
|
|
|
anth, ok := got["mixed-anthropic/text-only"]
|
|
if !ok {
|
|
t.Fatal("mixed-anthropic/text-only missing from the catalog")
|
|
}
|
|
if len(anth.Efforts) == 0 || anth.Efforts[0] != "auto" {
|
|
t.Fatalf("anthropic efforts = %v, want a list starting at auto", anth.Efforts)
|
|
}
|
|
}
|
|
|
|
// Two config entries on one host are one service with two routes. Without a
|
|
// shared vendor the picker lists the same model twice with nothing to choose
|
|
// between the rows.
|
|
func TestModelsShareAVendorAcrossProtocolRoutes(t *testing.T) {
|
|
got := modelsByRef(t, mixedVendorConfig)
|
|
|
|
openai := got["mixed/text-only"]
|
|
anth := got["mixed-anthropic/text-only"]
|
|
if openai.Vendor != "gateway.invalid" || anth.Vendor != openai.Vendor {
|
|
t.Fatalf("vendors = %q and %q, want both gateway.invalid", openai.Vendor, anth.Vendor)
|
|
}
|
|
if openai.Kind == anth.Kind {
|
|
t.Fatalf("both routes reported kind %q; the fold would have nothing to offer", openai.Kind)
|
|
}
|
|
}
|
|
|
|
func TestModelsCarryPriceAndWindowWhereDeclared(t *testing.T) {
|
|
got := modelsByRef(t, mixedVendorConfig)
|
|
|
|
priced, ok := got["mixed/vision-pro"]
|
|
if !ok {
|
|
t.Fatal("mixed/vision-pro missing from the catalog")
|
|
}
|
|
if priced.Price == nil {
|
|
t.Fatal("the per-model price did not reach the catalog")
|
|
}
|
|
if priced.Price.Input != 2 || priced.Price.Output != 8 || priced.Price.Currency != "CNY" {
|
|
t.Fatalf("price = %+v", priced.Price)
|
|
}
|
|
if priced.ContextWindow != 131072 {
|
|
t.Fatalf("context window = %d, want the provider-wide 131072", priced.ContextWindow)
|
|
}
|
|
|
|
// Nothing declares a price for this one, and a picker must not invent one.
|
|
if e := got["mixed/text-only"]; e.Price != nil {
|
|
t.Fatalf("an undeclared price surfaced as %+v", e.Price)
|
|
}
|
|
}
|
|
|
|
// The switch rebuilds the running controller; nothing about that reaches disk.
|
|
// Without persistence the next launch boots from default_model and lands back
|
|
// on the previous choice, which reads as the picker not having worked.
|
|
func TestSwitchingModelSurvivesARestart(t *testing.T) {
|
|
t.Setenv("REASONIX_HOME", t.TempDir())
|
|
t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
|
|
if _, err := config.SetCredential("MIXED_API_KEY", "sk-test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := config.UserConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(mixedVendorConfig), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
persistDefaultModel("mixed-anthropic/text-only", nil)
|
|
|
|
// A fresh load is what a restart sees.
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.DefaultModel != "mixed-anthropic/text-only" {
|
|
t.Fatalf("default_model = %q after switching, want the model that was chosen", cfg.DefaultModel)
|
|
}
|
|
}
|
|
|
|
// A ref nothing resolves must not overwrite a working default.
|
|
func TestPersistingAnUnknownModelLeavesTheDefaultAlone(t *testing.T) {
|
|
t.Setenv("REASONIX_HOME", t.TempDir())
|
|
t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
|
|
if _, err := config.SetCredential("MIXED_API_KEY", "sk-test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := config.UserConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(mixedVendorConfig), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
persistDefaultModel("nobody/nothing", nil)
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.DefaultModel != "mixed/text-only" {
|
|
t.Fatalf("default_model = %q, want the original left intact", cfg.DefaultModel)
|
|
}
|
|
}
|