Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions internal/serviceoffercontroller/hostoffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,72 @@ func TestBuildOfferBundles(t *testing.T) {
}
}

// TestBuildOfferBundles_InferenceOfferAgreesWithOpenAPI pins that a
// hostname-bound inference offer (no custom Spec.Routes — synthesized
// root catch-all) advertises the same paid path on both discovery
// surfaces: openapi.json paths and /.well-known/x402 resources.
// Before the openAPIRelPathForOfferRoute fix, x402 collapsed "/*" to
// the bare origin while openapi hard-coded /v1/chat/completions.
func TestBuildOfferBundles_InferenceOfferAgreesWithOpenAPI(t *testing.T) {
profile := schemas.StorefrontProfile{DisplayName: "Acme"}
offer := &monetizeapi.ServiceOffer{
ObjectMeta: metav1.ObjectMeta{Name: "chat", Namespace: "llm"},
Spec: monetizeapi.ServiceOfferSpec{
Type: "inference",
Hostname: "chat.v1337.example",
Upstream: monetizeapi.ServiceOfferUpstream{Service: "gateway", Namespace: "llm", Port: 8080},
Payment: monetizeapi.ServiceOfferPayment{
Network: "base-sepolia",
PayTo: "0x1111111111111111111111111111111111111111",
Price: monetizeapi.ServiceOfferPriceTable{PerRequest: "0.1"},
},
// Spec.Routes intentionally empty: EffectiveRoutes synthesizes /*.
},
Status: monetizeapi.ServiceOfferStatus{
Conditions: []monetizeapi.Condition{
{Type: "ModelReady", Status: "True"},
{Type: "UpstreamHealthy", Status: "True"},
{Type: "PaymentGateReady", Status: "True"},
{Type: "RoutePublished", Status: "True"},
},
},
}

bundles := buildOfferBundles([]*monetizeapi.ServiceOffer{offer}, profile, noUpstreamOpenAPI)
byPath := map[string]string{}
for _, f := range bundles {
byPath[f.Path] = f.Content
}

var doc map[string]any
if err := json.Unmarshal([]byte(byPath["offers/llm/chat/openapi.json"]), &doc); err != nil {
t.Fatalf("openapi bundle: %v", err)
}
paths := doc["paths"].(map[string]any)
if len(paths) != 1 {
t.Fatalf("paths = %v, want exactly one key", mapKeys(paths))
}
if _, ok := paths["/v1/chat/completions"]; !ok {
t.Fatalf("paths = %v, want /v1/chat/completions", mapKeys(paths))
}

var wk struct {
Resources []struct {
Resource string `json:"resource"`
} `json:"resources"`
}
if err := json.Unmarshal([]byte(byPath["offers/llm/chat/x402.json"]), &wk); err != nil {
t.Fatalf("x402 bundle: %v", err)
}
if len(wk.Resources) != 1 {
t.Fatalf("len(resources) = %d, want 1", len(wk.Resources))
}
want := "https://chat.v1337.example/v1/chat/completions"
if wk.Resources[0].Resource != want {
t.Errorf("resources[0].resource = %q, want %q (must agree with openapi.json path)", wk.Resources[0].Resource, want)
}
}

// TestBuildOfferBundles_BrandingOverride pins the per-origin identity merge:
// spec.branding fields override the storefront profile on the dedicated
// origin's surfaces, empty fields inherit.
Expand Down
16 changes: 15 additions & 1 deletion internal/serviceoffercontroller/offerbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,20 @@ func rerootAuthInfo(item map[string]any) map[string]any {
return out
}

// openAPIRelPathForOfferRoute resolves the OpenAPI-relative path a single
// route maps to, matching openAPIPathsForOffer's per-type path selection so
// /.well-known/x402 and openapi.json agree on the same paid operation path.
// Inference/agent offers always expose the fixed OpenAI-compatible endpoint
// regardless of the declared route table (openAPIPathsForOffer ignores
// spec.routes for these types); every other type maps the route's own path
// through openAPIRelPathForRoute exactly as the OpenAPI builder does.
func openAPIRelPathForOfferRoute(offer *monetizeapi.ServiceOffer, routePath string) string {
if offer.IsInference() || offer.IsAgent() {
return "/v1/chat/completions"
}
return openAPIRelPathForRoute(routePath)
}

// buildOfferWellKnownX402 renders the /.well-known/x402 discovery document:
// one resource entry per paid route, each carrying the signable payment
// requirements (mirrors the 402 accepts[] fields so a crawler can price the
Expand Down Expand Up @@ -228,7 +242,7 @@ func buildOfferWellKnownX402(offer *monetizeapi.ServiceOffer) string {
desc = offerDescription(offer, "x402 payment-gated service.")
}
resources = append(resources, map[string]any{
"resource": origin + joinOpenAPIPath("/", openAPIRelPathForRoute(rt.Path)),
"resource": origin + joinOpenAPIPath("/", openAPIRelPathForOfferRoute(offer, rt.Path)),
"type": "http",
"method": method,
"description": desc,
Expand Down
Loading