Files
llmgw/internal/router/adapterowner_test.go
T
Ilker ManapandClaude Opus 5.5 85025dfb03 Trained adapters published, private to their owners like the knowledge bases they come from
An adapter now has an owner (org, team, project, user or key): only callers
there see it, and to anyone else it is a model that does not exist, absent
from /v1/models too. A training job's owner defaults to the one collection
its pairs come from, must be able to read every collection they come from,
and pairs from a knowledge base are never made public. ctl train publish
keeps the GGUF or PEFT files on the mirror, which accepts uploads now, and
sets the owned adapter after the usual architecture check.

The knowledge bases' cache_salt follows the collections searched rather
than the organization, so two teams of one organization no longer share
cached passages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 21:30:50 +02:00

62 lines
2.2 KiB
Go

package router
import (
"crypto/sha256"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/auth"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
func TestAnOwnedAdapterIsSeenOnlyInItsPartOfTheTree(t *testing.T) {
e := newFakeEngine(t, "qwen2.5-14b-instruct")
rt, owner := pricedRouter(t, e.srv.URL, "tone")
ctx := t.Context()
key, _ := rt.store.KeyByID(ctx, "key-1")
team, _ := rt.store.CreateTeam(ctx, mustOrgOf(t, rt), "other-team")
other, _ := rt.store.CreateProject(ctx, team, "other")
otherSecret := store.KeyPrefix + strings.Repeat("b", 43)
sum := sha256.Sum256([]byte(otherSecret))
if err := rt.store.CreateKey(ctx, store.Key{ID: "key-2", Hash: sum[:], Prefix: "bbbb", Name: "o", ProjectID: other, CreatedAt: time.Now()}); err != nil {
t.Fatal(err)
}
rt.storeModels([]catalog.Model{{Name: "qwen2.5-14b-instruct", Aliases: []string{"q14"},
Adapters: []catalog.Adapter{{Name: "tone", Source: "/t.gguf", Owner: "project:" + key.ProjectID}}}})
h := rt.Handler()
ask := func(secret string) int {
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions",
strings.NewReader(`{"model":"tone","messages":[{"role":"user","content":"hi"}]}`))
if secret != "" {
req.Header.Set("Authorization", "Bearer "+secret)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code
}
if code := ask(owner); code != http.StatusOK {
t.Errorf("its own project: %d", code)
}
if code := ask(otherSecret); code != http.StatusNotFound {
t.Errorf("another project: %d", code)
}
list := func(secret string) string {
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
req.Header.Set("Authorization", "Bearer "+secret)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Body.String()
}
if !strings.Contains(list(owner), `"tone"`) || strings.Contains(list(otherSecret), `"tone"`) {
t.Errorf("listed: own %s\nother %s", list(owner), list(otherSecret))
}
// No identity, no owned adapter.
if rt.adapterHidden(auth.Principal{}, false, "tone") != true {
t.Error("an owned adapter was shown to a caller with no identity")
}
}