Files
kyle 4e1dd03d07 Route templates: a route named x-* serves any request route x-<something>
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-25 13:38:24 -07:00

86 lines
3.2 KiB
Go

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)
}
}