86 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|