refactor(addons): pick engine job backends by gallery tag

The finetune.start and quantize.start operations defaulted to the
backend names "trl" and "llama-cpp-quantization", and the backend
lister had one method per engine family. Both were special cases that
a new engine could not use.

The lister now answers for a gallery tag. A start call that names no
backend gets the first installed backend of the tag in gallery order,
and a named backend must carry the tag. With no engine installed the
call fails with a 400 that says so. The shipped gallery order keeps
trl and llama-cpp-quantization as the defaults, and a test pins it.

The change is additive for add-ons: no manifest field or operation
changes.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 14:39:13 +00:00
1 parent 6eb187dab0
commit 474e3eefbe
6 files changed
+147 -29

No files matched your search

+26
View File
@@ -1663,3 +1663,29 @@ var _ = Describe("installed development backend preference root guards", func()
Expect(found).To(HaveKey("cpu-example-engine-development"))
})
})
var _ = Describe("the shipped backend index", func() {
// An add-on job that names no backend gets the first backend of its tag in
// gallery order, so the order of these entries is the default.
It("lists the default engine of each family first", func() {
raw, err := os.ReadFile("../../backend/index.yaml")
Expect(err).ToNot(HaveOccurred())
var all []GalleryBackend
Expect(yaml.Unmarshal(raw, &all)).To(Succeed())
first := func(tag string) string {
for _, b := range all {
for _, t := range b.Tags {
if t == tag {
if b.Alias != "" {
return b.Alias
}
return b.Name
}
}
}
return ""
}
Expect(first("fine-tuning")).To(Equal("trl"))
Expect(first("quantization")).To(Equal("llama-cpp-quantization"))
})
})
@@ -71,11 +71,17 @@ type EngineBackends struct {
var _ hostops.BackendLister = EngineBackends{}
func (e EngineBackends) FineTune() []string { return e.names("fine-tuning") }
// Backends lists the installed backends that carry tag, in gallery order.
func (e EngineBackends) Backends(tag string) []string {
infos := e.lookup(tag)
out := make([]string, 0, len(infos))
for _, i := range infos {
out = append(out, i.Name)
}
return out
}
func (e EngineBackends) Quantization() []string { return e.names("quantization") }
func (e EngineBackends) names(tag string) []string {
func (e EngineBackends) lookup(tag string) []engineBackendInfo {
// The interface has no request context, so the lookup gets its own bound.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
@@ -83,11 +89,7 @@ func (e EngineBackends) names(tag string) []string {
if err != nil {
return nil
}
out := make([]string, 0, len(infos))
for _, i := range infos {
out = append(out, i.Name)
}
return out
return infos
}
// FineTuneJobs adapts the fine-tuning service to the add-on host operations.
+32 -14
View File
@@ -42,11 +42,32 @@ type Quantizer interface {
List(userID string) []*schema.QuantizationJob
}
// BackendLister names the backends of each engine feature that the gallery
// offers. A start operation accepts only these.
// Gallery tags that name an engine family. A backend carries the tag in its
// gallery entry; the host reads the engines of a family from there, so a new
// engine needs a gallery entry and no code.
const (
TagFineTuning = "fine-tuning"
TagQuantization = "quantization"
)
// BackendLister names the installed backends that carry a gallery tag, in
// gallery order. A start operation accepts only these.
type BackendLister interface {
FineTune() []string
Quantization() []string
Backends(tag string) []string
}
// pickEngine answers the engine for a start call. A named engine is kept for
// requireEngine to check. Without a name the default is the first installed
// engine of the tag in gallery order, so the gallery picks the default by the
// order of its entries; "" means none is installed.
func pickEngine(l BackendLister, tag, named string) string {
if named != "" {
return named
}
if all := l.Backends(tag); len(all) > 0 {
return all[0]
}
return ""
}
// Checkpoint is what the operations need of a saved checkpoint. The path is
@@ -115,11 +136,8 @@ func fineTuneSpecs(b *Broker, ft FineTuner, backends BackendLister) []Spec {
if a.LearningRate < 0 || a.NumEpochs < 0 || a.BatchSize < 0 || a.MaxSteps < 0 || a.AdapterRank < 0 || a.AdapterAlpha < 0 {
return nil, fmt.Errorf("%w: numeric arguments must not be negative", ErrBadRequest)
}
backend := a.Backend
if backend == "" {
backend = "trl"
}
if err := requireEngine("fine-tuning", backend, backends.FineTune()); err != nil {
backend := pickEngine(backends, TagFineTuning, a.Backend)
if err := requireEngine("fine-tuning", backend, backends.Backends(TagFineTuning)); err != nil {
return nil, err
}
resp, err := ft.Start(ctx, userKey(c), schema.FineTuneJobRequest{
@@ -198,11 +216,8 @@ func quantizeSpecs(q Quantizer, backends BackendLister) []Spec {
if a.QuantizationType != "" && !quantType.MatchString(a.QuantizationType) {
return nil, fmt.Errorf("%w: quantizationType is not a quantization name", ErrBadRequest)
}
backend := a.Backend
if backend == "" {
backend = "llama-cpp-quantization"
}
if err := requireEngine("quantization", backend, backends.Quantization()); err != nil {
backend := pickEngine(backends, TagQuantization, a.Backend)
if err := requireEngine("quantization", backend, backends.Backends(TagQuantization)); err != nil {
return nil, err
}
resp, err := q.Start(ctx, userKey(c), schema.QuantizationJobRequest{
@@ -319,6 +334,9 @@ var engineName = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
// the engine, in a short fixed sentence that the add-on can pass on to the
// person, so a missing engine is not mistaken for a bad argument.
func requireEngine(kind, backend string, installed []string) error {
if backend == "" {
return fmt.Errorf("%w: no %s engine is installed", ErrBadRequest, kind)
}
if !engineName.MatchString(backend) {
return refusedField("backend")
}
@@ -74,8 +74,15 @@ func (q *fakeQ) List(user string) []*schema.QuantizationJob {
type backends struct{}
func (backends) FineTune() []string { return []string{"trl"} }
func (backends) Quantization() []string { return []string{"llama-cpp-quantization"} }
func (backends) Backends(tag string) []string {
switch tag {
case TagFineTuning:
return []string{"trl"}
case TagQuantization:
return []string{"llama-cpp-quantization"}
}
return nil
}
func call(spec Spec, mode, user, args string) (json.RawMessage, error) {
return spec.Handler(context.Background(), Call{AddonID: "fine-tuning", OperationID: "op1", Actor: contract.Actor{UserID: user, Mode: mode}, Arguments: json.RawMessage(args)})
@@ -87,6 +94,13 @@ func register(ft FineTuner, q Quantizer) map[string]Spec {
return b.specs
}
// registerWith is register over a given backend lister.
func registerWith(ft FineTuner, q Quantizer, l BackendLister) map[string]Spec {
b := New(NewInFlight(), func(string, string) bool { return true }, contract.DefaultLimits())
Expect(RegisterEngineJobs(b, ft, q, l)).To(Succeed())
return b.specs
}
// registerWithout is register with a host that has no engine installed.
func registerWithout(ft FineTuner, q Quantizer) map[string]Spec {
b := New(NewInFlight(), func(string, string) bool { return true }, contract.DefaultLimits())
@@ -96,8 +110,12 @@ func registerWithout(ft FineTuner, q Quantizer) map[string]Spec {
type noBackends struct{}
func (noBackends) FineTune() []string { return nil }
func (noBackends) Quantization() []string { return nil }
func (noBackends) Backends(string) []string { return nil }
// tagged is a lister over a fixed map of engines per tag.
type tagged struct{ engines map[string][]string }
func (t tagged) Backends(tag string) []string { return t.engines[tag] }
func specs(ft FineTuner) map[string]Spec {
return register(ft, nil)
@@ -162,7 +180,7 @@ var _ = Describe("Fine-tuning and quantization job operations", func() {
s := registerWithout(&fakeFT{}, nil)
_, err := call(s["finetune.start"], "named-user", "u1", `{"model":"org/base","datasetSource":"org/data"}`)
Expect(err).To(MatchError(ErrBadRequest))
Expect(err.Error()).To(ContainSubstring("fine-tuning engine trl is not installed"))
Expect(err.Error()).To(ContainSubstring("no fine-tuning engine is installed"))
_, err = call(s["finetune.start"], "named-user", "u1", `{"model":"org/base","datasetSource":"org/data","backend":"../x"}`)
Expect(err).To(MatchError(ErrBadRequest))
Expect(err.Error()).NotTo(ContainSubstring(".."))
@@ -261,7 +279,7 @@ var _ = Describe("Fine-tuning and quantization job operations", func() {
s := registerWithout(nil, &fakeQ{})
_, err := call(s["quantize.start"], "named-user", "u1", `{"model":"org/base"}`)
Expect(err).To(MatchError(ErrBadRequest))
Expect(err.Error()).To(ContainSubstring("quantization engine llama-cpp-quantization is not installed"))
Expect(err.Error()).To(ContainSubstring("no quantization engine is installed"))
})
It("start refuses a field that is not in the tool schema", func() {
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
package hostops
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// The engine of a job comes from the gallery tag, not from a name in the host.
var _ = Describe("engine selection by gallery tag", func() {
start := func(l BackendLister, ft *fakeFT, q *fakeQ, op, args string) error {
b := registerWith(ft, q, l)
_, err := call(b[op], "named-user", "u1", args)
return err
}
It("uses the first engine of the tag in gallery order when no backend is named", func() {
ft, q := &fakeFT{}, &fakeQ{}
l := tagged{engines: map[string][]string{TagFineTuning: {"beta", "alpha"}, TagQuantization: {"delta", "gamma"}}}
Expect(start(l, ft, q, "finetune.start", `{"model":"org/base","datasetSource":"org/data"}`)).To(Succeed())
Expect(start(l, ft, q, "quantize.start", `{"model":"org/base"}`)).To(Succeed())
Expect(ft.started[0].Backend).To(Equal("beta"))
Expect(q.started[0].Backend).To(Equal("delta"))
})
It("accepts a named engine that carries the tag, and refuses one that does not", func() {
ft := &fakeFT{}
l := tagged{engines: map[string][]string{TagFineTuning: {"alpha", "beta"}}}
Expect(start(l, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data","backend":"beta"}`)).To(Succeed())
Expect(ft.started[0].Backend).To(Equal("beta"))
err := start(l, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data","backend":"trl"}`)
Expect(err).To(MatchError(ErrBadRequest))
Expect(err.Error()).To(ContainSubstring("fine-tuning engine trl is not installed"))
})
It("refuses a start with no backend when none is installed", func() {
ft := &fakeFT{}
err := start(tagged{}, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data"}`)
Expect(err).To(MatchError(ErrBadRequest))
Expect(err.Error()).To(ContainSubstring("no fine-tuning engine is installed"))
Expect(ft.started).To(BeEmpty())
})
It("does not mix the families", func() {
l := tagged{engines: map[string][]string{TagFineTuning: {"alpha"}, TagQuantization: {"gamma"}}}
err := start(l, &fakeFT{}, &fakeQ{}, "quantize.start", `{"model":"org/base","backend":"alpha"}`)
Expect(err).To(MatchError(ErrBadRequest))
})
})
+5
View File
@@ -432,6 +432,11 @@ The generic binary selects no policy automatically.
Fine-tuning and Quantization include operation names passed through mutation and undo wrappers.
They do not request the host's unused progress operations.
The `finetune.start` and `quantize.start` operations take an optional `backend`.
The host accepts only an installed backend whose gallery entry carries the tag of the operation (`fine-tuning` or `quantization`).
When the call names no backend, the host uses the first installed backend of that tag in gallery order.
A new engine needs only a gallery entry with the tag. With no engine installed, the call fails with a `400` that says so.
Telegram's historical `connector.` deployment grant does not imply a hostclient call or manufacture a manifest request.
Sandbox's proxy uses the optional egress operations when configured connections need policy decisions.