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