llmgw ctl model adapters lists a model's adapters and adds or removes
them through POST /api/v1/models/{name}/adapters, which reads each new
file first and refuses one that is not an adapter for the model's
architecture, as the mirror page's card does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
72 lines
2.2 KiB
Go
72 lines
2.2 KiB
Go
package adminapi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
|
|
)
|
|
|
|
// ggufStart is the start of a GGUF file with these string keys.
|
|
func ggufStart(kv ...string) []byte {
|
|
var b bytes.Buffer
|
|
w := func(v any) { _ = binary.Write(&b, binary.LittleEndian, v) }
|
|
str := func(s string) { w(uint64(len(s))); b.WriteString(s) }
|
|
b.WriteString("GGUF")
|
|
w(uint32(3))
|
|
w(uint64(0))
|
|
w(uint64(len(kv) / 2))
|
|
for i := 0; i < len(kv); i += 2 {
|
|
str(kv[i])
|
|
w(uint32(8)) // string
|
|
str(kv[i+1])
|
|
}
|
|
b.Write(make([]byte, 4096))
|
|
return b.Bytes()
|
|
}
|
|
|
|
func TestOnlyAnAdapterForTheModelsArchitectureIsAttached(t *testing.T) {
|
|
files := map[string][]byte{
|
|
"/base.gguf": ggufStart("general.architecture", "qwen2"),
|
|
"/model.gguf": ggufStart("general.architecture", "qwen2", "general.type", "model"),
|
|
"/llama.gguf": ggufStart("general.architecture", "llama", "general.type", "adapter"),
|
|
"/lora.gguf": ggufStart("general.architecture", "qwen2", "general.type", "adapter"),
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
_, _ = w.Write(files[r.URL.Path])
|
|
}))
|
|
defer srv.Close()
|
|
|
|
models := []catalog.Model{{
|
|
Name: "coder", Source: srv.URL + "/base.gguf",
|
|
Scaling: catalog.Scaling{Min: 1, Max: 1},
|
|
Profiles: []catalog.Profile{{Name: "cpu", Engine: "llamacpp", MinMemoryGB: 8}},
|
|
}}
|
|
for _, tc := range []struct {
|
|
file, want string
|
|
code int
|
|
}{
|
|
{"/model.gguf", "not a LoRA adapter", http.StatusBadRequest},
|
|
{"/llama.gguf", "for llama models", http.StatusBadRequest},
|
|
{"/lora.gguf", "", http.StatusOK},
|
|
} {
|
|
a, _ := setup(t, models)
|
|
fm := &fakeModels{}
|
|
a.SetModels(fm)
|
|
mux := http.NewServeMux()
|
|
a.Register(mux)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/models/coder/adapters",
|
|
strings.NewReader(`{"adapters":[{"name":"fix","source":"`+srv.URL+tc.file+`"}]}`))
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != tc.code || !strings.Contains(rec.Body.String(), tc.want) {
|
|
t.Errorf("%s: %d %s; want %d mentioning %q", tc.file, rec.Code, rec.Body.String(), tc.code, tc.want)
|
|
}
|
|
}
|
|
}
|