diff --git a/e2e/agentnetwork/custom_pricing_test.go b/e2e/agentnetwork/custom_pricing_test.go index 2fe093602..b3ca5028f 100644 --- a/e2e/agentnetwork/custom_pricing_test.go +++ b/e2e/agentnetwork/custom_pricing_test.go @@ -446,10 +446,10 @@ func TestPriceChangeUpdatesRecordedCost(t *testing.T) { } row, ok := lookupAccessLogBySession(repriceCtx, lastSession, repriceIngestWindow) if !ok { - // No row for this request. The provider update rebuilds the proxy's - // middleware chain, and a request served mid-rebuild can complete - // without a resolved provider — 200 to the caller, nothing to - // attribute, so no row is ever written for it. Fire another one. + // No row for this request. The proxy now publishes a rebuilt chain + // before the route that reaches it, so a request can no longer be + // served unattributed mid-update; this retry covers the ingest + // window alone. Fire another one under a fresh session. t.Logf("no access-log row for session %q within %s; retrying under a fresh session", lastSession, repriceIngestWindow) continue } diff --git a/proxy/server.go b/proxy/server.go index bd70b7e70..2a62db421 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -2074,9 +2074,15 @@ func (s *Server) updateMapping(ctx context.Context, mapping *proto.ProxyMapping) return fmt.Errorf("auth setup for domain %s: %w", mapping.GetDomain(), err) } m := s.protoToMapping(ctx, mapping) - s.proxy.AddMapping(m) - s.meter.AddMapping(m) + // The chain is published before the route that leads to it. A request + // arriving at a target whose chain has not been rebuilt yet is served + // straight through, so a provider update that added the route first left a + // window in which an inference could complete unrouted and unmetered. + // Rebuilding first inverts that: the worst a request in the window meets is + // the new chain in front of the previous target, which is still counted. s.rebuildMiddlewareChains(svcID, m) + s.meter.AddMapping(m) + s.proxy.AddMapping(m) return nil }