Route templates: a route named x-* serves any request route x-<something>

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-25 13:38:24 -07:00
parent 4863e53eb8
commit 4e1dd03d07
10 changed files with 344 additions and 9 deletions
+5 -5
View File
@@ -144,13 +144,13 @@ func (p *Handler) route(r *http.Request) (route, rest string, code int, msg stri
if hdr != "" {
rest := r.URL.Path
// A path that also carries a (different) route name is a client mistake: the header is the
// operator's intent, but the path disagrees.
// operator's intent, but the path disagrees. Compare concrete names.
if rname, _, ok := SplitRoute(rest); ok {
if _, known := p.cfg.Routes[rname]; known && rname != hdr {
if _, _, rok := p.cfg.Route(rname); rok && rname != hdr {
return "", "", http.StatusBadRequest, "conflicting route"
}
}
if _, known := p.cfg.Routes[hdr]; !known {
if _, _, ok := p.cfg.Route(hdr); !ok {
return "", "", http.StatusNotFound, "unknown route"
}
if !allowedPath(rest) {
@@ -162,7 +162,7 @@ func (p *Handler) route(r *http.Request) (route, rest string, code int, msg stri
if !ok {
return "", "", http.StatusBadRequest, "missing route"
}
if _, known := p.cfg.Routes[route]; !known {
if _, _, ok := p.cfg.Route(route); !ok {
return "", "", http.StatusNotFound, "unknown route"
}
if !allowedPath(rest) {
@@ -207,7 +207,7 @@ func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.writeError(w, code, msg)
return
}
routeCfg := p.cfg.Routes[route]
routeCfg, _, _ := p.cfg.Route(route)
model, body, err := peekModel(r)
if err != nil {
+85
View File
@@ -0,0 +1,85 @@
package proxy_test
import (
"net/http"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/proxy"
"git.wntrmute.dev/kyle/crossbar/internal/store"
)
const templateHosts = `
listen = "127.0.0.1:1"
[hosts.alpha]
base_url = %q
models = { "shared" = { parallel = 4 } }
[routes."opencode-*"]
hosts = ["alpha"]
default_model = "shared"
[routes.opencode-fixed]
hosts = ["alpha"]
default_model = "shared"
`
// One OpenCode instance per route, without listing every instance in the config: a route named
// "opencode-*" serves any request route "opencode-<something>". Leases and accounting are keyed
// by the concrete route name, so two instances never share a lease and each gets its own usage
// row. The literal template name is never a request route.
func TestRouteTemplateServesConcreteRoutes(t *testing.T) {
alpha := newUpstream(t, "alpha")
r := newRig(t, templateHosts, alpha)
resp := r.post("/opencode-projecta-4242/v1/chat/completions", conversation(1, 1))
drain(resp)
if resp.StatusCode != 200 || resp.Header.Get(proxy.HostHeader) != "alpha" || resp.Header.Get(proxy.LeaseHeader) != "new" {
t.Fatalf("first turn on a templated route: %d %q %q, want 200 alpha new", resp.StatusCode, resp.Header.Get(proxy.HostHeader), resp.Header.Get(proxy.LeaseHeader))
}
resp = r.post("/opencode-projecta-4242/v1/chat/completions", conversation(1, 2))
drain(resp)
if resp.Header.Get(proxy.LeaseHeader) != "reused" {
t.Errorf("second turn should reuse the lease, got %q", resp.Header.Get(proxy.LeaseHeader))
}
// A second instance with the same conversation shape is a different route: its own lease.
resp = r.post("/opencode-projectb-7/v1/chat/completions", conversation(1, 1))
drain(resp)
if resp.StatusCode != 200 || resp.Header.Get(proxy.LeaseHeader) != "new" {
t.Errorf("another instance must get its own lease: %d %q", resp.StatusCode, resp.Header.Get(proxy.LeaseHeader))
}
// The header form resolves templates too.
resp = r.post("/v1/chat/completions", conversation(2, 1), proxy.RouteHeader, "opencode-projectc-1")
drain(resp)
if resp.StatusCode != 200 {
t.Errorf("X-Crossbar-Route with a templated name: %d, want 200", resp.StatusCode)
}
// An exact route still works and is not shadowed by the template.
resp = r.post("/opencode-fixed/v1/chat/completions", conversation(3, 1))
drain(resp)
if resp.StatusCode != 200 {
t.Errorf("exact route: %d, want 200", resp.StatusCode)
}
for _, path := range []string{"/opencode-*/v1/models", "/opencode-/v1/models", "/opencode/v1/models", "/opencodex/v1/models"} {
req, _ := http.NewRequest(http.MethodGet, r.front.URL+path, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
drain(resp)
if resp.StatusCode != 404 {
t.Errorf("%s: %d, want 404 unknown route", path, resp.StatusCode)
}
}
rows, _ := r.store.Usage(time.Time{}, store.ByRoute)
keys := map[string]int64{}
for _, row := range rows {
keys[row.Key] = row.Requests
}
if keys["opencode-projecta-4242"] != 2 || keys["opencode-projectb-7"] != 1 || keys["opencode-projectc-1"] != 1 || keys["opencode-fixed"] != 1 {
t.Errorf("usage by route = %v, want rows per concrete route", keys)
}
if _, present := keys["opencode-*"]; present {
t.Errorf("the template name must never be an accounting key: %v", keys)
}
}