Skip to content

Commit 4d39d1b

Browse files
authored
feat(llm-gateway): forward priority (#277)
* feat(llm-api-gateway): forward resolved x-priority to Stargate NVCF API now resolves a caller priority at auth time and returns it on AuthLlmInvokeResponse. Carry that value through the gateway and forward it to Stargate as the X-Priority header so Stargate can apply per-priority routing and queue estimation. - Add optional uint32 priority to the AuthLlmInvokeResponse proto and regenerate the generated Go. - Carry the nullable priority on InvocationAuthResponse and copy it onto RequestContext in the invocation auth middleware. - Emit X-Priority on all three LLM paths (chat/completions, responses, and the embeddings/generic proxy) only when a value is resolved. An explicit 0 (highest priority) is forwarded; an unset value omits the header so Stargate applies its own default, matching how the gateway already omits x-routing-method and x-cache-affinity-key. The clone-based proxy path still forwards a client-supplied X-Priority when the gateway resolves none. Rejecting client-supplied X-Priority is handled separately; that change should land with or before this one. Signed-off-by: along <along@nvidia.com> * test(llm-api-gateway): pin priority unset and override semantics Close two review gaps, both mutation-verified: a client mapping that turns an unset proto priority into 0 and a proxy that unconditionally emits X-Priority 0 previously passed the whole suite. - Assert AuthorizeInvocation maps an unset proto priority to nil, and add a round-trip case for explicit 0 and the uint32 max as set values. - Assert Proxy omits X-Priority when no priority is resolved, and that a resolved value replaces a client-supplied header from the clone with exactly one value rather than appending. Signed-off-by: along <along@nvidia.com> * feat(llm-api-gateway): strip client-supplied x-priority on proxied paths Signed-off-by: along <along@nvidia.com> * test(llm-api-gateway): cover priority propagation on all LLM entries Address review feedback: the middleware priority tests exercised only /v1/chat/completions. The auth middleware is registered globally and the routing key comes from the request body model field, so propagation is path-independent; make the resolved and unset cases table-driven over /v1/chat/completions, /v1/responses, and /v1/embeddings to pin that for every entry that forwards X-Priority. Signed-off-by: along <along@nvidia.com> --------- Signed-off-by: along <along@nvidia.com>
1 parent 44c5fac commit 4d39d1b

13 files changed

Lines changed: 446 additions & 7 deletions

File tree

‎src/invocation-plane-services/llm-api-gateway/api/auth_middleware.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func applyInvocationAuth(
133133
reqCtx.RoutingKey = authRoutingKey
134134
}
135135
reqCtx.ModelSpecs = authResponse.ModelSpecs
136+
reqCtx.Priority = authResponse.Priority
136137

137138
return nil
138139
}

‎src/invocation-plane-services/llm-api-gateway/api/auth_middleware_test.go‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,125 @@ func TestNVCFAuthMiddlewareEnrichesRequestContext(t *testing.T) {
110110
}
111111
}
112112

113+
// The auth middleware is registered globally, so priority propagation is
114+
// path-independent; exercise every LLM entry that forwards X-Priority.
115+
var priorityMiddlewarePaths = []struct {
116+
path string
117+
body string
118+
}{
119+
{
120+
path: "/v1/chat/completions",
121+
body: `{"model":"fn-chat/company-name/model-name","messages":[{"role":"user","content":"hello"}]}`,
122+
},
123+
{
124+
path: "/v1/responses",
125+
body: `{"model":"fn-chat/company-name/model-name","input":"hello"}`,
126+
},
127+
{
128+
path: "/v1/embeddings",
129+
body: `{"model":"fn-chat/company-name/model-name","input":"hello"}`,
130+
},
131+
}
132+
133+
func TestNVCFAuthMiddlewarePropagatesResolvedPriority(t *testing.T) {
134+
t.Parallel()
135+
136+
for _, tc := range priorityMiddlewarePaths {
137+
t.Run(tc.path, func(t *testing.T) {
138+
t.Parallel()
139+
140+
wantPriority := uint32(3)
141+
authClient := &stubInvocationAuthClient{
142+
authResponse: &nvcf.InvocationAuthResponse{
143+
RoutingKey: "fn-chat",
144+
ClientAuthID: "subject-123",
145+
AuthContext: map[string]string{"ncaId": "nca-456"},
146+
RateLimitKey: "nca-456",
147+
Priority: &wantPriority,
148+
},
149+
}
150+
151+
cfg := config.Default()
152+
153+
e := echo.New()
154+
e.Use(NewContextMiddleware(cfg))
155+
e.Use(NewNVCFAuthMiddleware(authClient))
156+
e.POST(tc.path, func(ec echo.Context) error {
157+
gc := ec.(*GatewayContext)
158+
reqCtx := gc.RequestContext()
159+
if reqCtx == nil {
160+
t.Fatal("request context was not set")
161+
}
162+
if reqCtx.Priority == nil {
163+
t.Fatal("priority was not propagated to request context")
164+
}
165+
if *reqCtx.Priority != 3 {
166+
t.Fatalf("priority = %d, want 3", *reqCtx.Priority)
167+
}
168+
return gc.NoContent(http.StatusNoContent)
169+
})
170+
171+
req := httptest.NewRequest(http.MethodPost, tc.path, strings.NewReader(tc.body))
172+
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
173+
req.Header.Set(echo.HeaderAuthorization, "Bearer sk-live")
174+
rec := httptest.NewRecorder()
175+
176+
e.ServeHTTP(rec, req)
177+
178+
if rec.Code != http.StatusNoContent {
179+
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusNoContent, rec.Body.String())
180+
}
181+
})
182+
}
183+
}
184+
185+
func TestNVCFAuthMiddlewareLeavesPriorityUnsetWhenAbsent(t *testing.T) {
186+
t.Parallel()
187+
188+
for _, tc := range priorityMiddlewarePaths {
189+
t.Run(tc.path, func(t *testing.T) {
190+
t.Parallel()
191+
192+
authClient := &stubInvocationAuthClient{
193+
authResponse: &nvcf.InvocationAuthResponse{
194+
RoutingKey: "fn-chat",
195+
ClientAuthID: "subject-123",
196+
AuthContext: map[string]string{"ncaId": "nca-456"},
197+
RateLimitKey: "nca-456",
198+
},
199+
}
200+
201+
cfg := config.Default()
202+
203+
e := echo.New()
204+
e.Use(NewContextMiddleware(cfg))
205+
e.Use(NewNVCFAuthMiddleware(authClient))
206+
e.POST(tc.path, func(ec echo.Context) error {
207+
gc := ec.(*GatewayContext)
208+
reqCtx := gc.RequestContext()
209+
if reqCtx == nil {
210+
t.Fatal("request context was not set")
211+
}
212+
if reqCtx.Priority != nil {
213+
t.Fatalf("priority = %d, want unset", *reqCtx.Priority)
214+
}
215+
return gc.NoContent(http.StatusNoContent)
216+
})
217+
218+
req := httptest.NewRequest(http.MethodPost, tc.path, strings.NewReader(tc.body))
219+
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
220+
req.Header.Set(echo.HeaderAuthorization, "Bearer sk-live")
221+
rec := httptest.NewRecorder()
222+
223+
e.ServeHTTP(rec, req)
224+
225+
if rec.Code != http.StatusNoContent {
226+
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusNoContent, rec.Body.String())
227+
}
228+
})
229+
}
230+
}
231+
113232
func TestNVCFAuthMiddlewareRejectsMissingBearerToken(t *testing.T) {
114233
t.Parallel()
115234

‎src/invocation-plane-services/llm-api-gateway/api/responses_handler.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const (
5353
headerResponsesMethod = "X-Routing-Method"
5454
headerResponsesModel = "X-Model"
5555
headerResponsesAffinity = "X-Cache-Affinity-Key"
56+
headerResponsesPriority = "X-Priority"
5657
)
5758

5859
func (h *ResponsesHandlers) RegisterRoutes(group *echo.Group) {
@@ -293,6 +294,12 @@ func setResponsesProxyContextHeaders(headers http.Header, reqCtx *requestctx.Req
293294
if reqCtx.CacheAffinityKey != "" {
294295
headers.Set(headerResponsesAffinity, reqCtx.CacheAffinityKey)
295296
}
297+
// X-Priority is gateway-owned: strip any client-supplied value from the
298+
// cloned inbound headers, then set it only when a priority resolved.
299+
headers.Del(headerResponsesPriority)
300+
if reqCtx.Priority != nil {
301+
headers.Set(headerResponsesPriority, strconv.FormatUint(uint64(*reqCtx.Priority), 10))
302+
}
296303
}
297304

298305
func (h *ResponsesHandlers) relayNativeResponsesStream(

‎src/invocation-plane-services/llm-api-gateway/api/responses_handler_test.go‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,46 @@ func TestSetResponsesProxyContextHeadersSetsTargetRegionCompatibilityHeaders(t *
167167
}
168168
}
169169

170+
func TestSetResponsesProxyContextHeadersForwardsResolvedPriority(t *testing.T) {
171+
t.Parallel()
172+
173+
priority := uint32(3)
174+
headers := http.Header{}
175+
setResponsesProxyContextHeaders(headers, &requestctx.RequestContext{
176+
Priority: &priority,
177+
})
178+
179+
if got := headers.Get(headerResponsesPriority); got != "3" {
180+
t.Fatalf("%s = %q, want 3", headerResponsesPriority, got)
181+
}
182+
}
183+
184+
func TestSetResponsesProxyContextHeadersOmitsUnsetPriority(t *testing.T) {
185+
t.Parallel()
186+
187+
headers := http.Header{}
188+
setResponsesProxyContextHeaders(headers, &requestctx.RequestContext{})
189+
190+
if got := headers.Get(headerResponsesPriority); got != "" {
191+
t.Fatalf("%s = %q, want empty", headerResponsesPriority, got)
192+
}
193+
}
194+
195+
func TestSetResponsesProxyContextHeadersStripsClientSuppliedPriorityWhenUnset(t *testing.T) {
196+
t.Parallel()
197+
198+
// The responses path clones inbound client headers before applying
199+
// context headers; a client-supplied X-Priority must be stripped when no
200+
// priority resolved.
201+
headers := http.Header{}
202+
headers.Set(headerResponsesPriority, "9")
203+
setResponsesProxyContextHeaders(headers, &requestctx.RequestContext{})
204+
205+
if got := headers.Get(headerResponsesPriority); got != "" {
206+
t.Fatalf("%s = %q, want empty", headerResponsesPriority, got)
207+
}
208+
}
209+
170210
func TestCreateResponseReusesReturnedSessionIDForAffinity(t *testing.T) {
171211
t.Parallel()
172212

‎src/invocation-plane-services/llm-api-gateway/nvcf/client.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,18 @@ func (c *GRPCClient) AuthorizeInvocation(
262262
AuthContext: authContext,
263263
RateLimitKey: deriveRateLimitKey(authContext),
264264
ModelSpecs: modelSpecsFromProto(resp.GetModelSpecs()),
265+
Priority: priorityFromProto(resp),
265266
}, nil
266267
}
267268

269+
func priorityFromProto(resp *llmgatewaypb.AuthLlmInvokeResponse) *uint32 {
270+
if resp == nil || resp.Priority == nil {
271+
return nil
272+
}
273+
value := resp.GetPriority()
274+
return &value
275+
}
276+
268277
func modelSpecsFromProto(specs map[string]*llmgatewaypb.AuthLlmInvokeResponse_ModelSpec) map[string]ModelSpec {
269278
if specs == nil {
270279
return nil

‎src/invocation-plane-services/llm-api-gateway/nvcf/client_test.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,61 @@ func TestGRPCClientAuthorizeInvocation(t *testing.T) {
110110
if len(spec.URIs) != 1 || spec.URIs[0] != "https://example.com/model" {
111111
t.Fatalf("uris = %#v, want [https://example.com/model]", spec.URIs)
112112
}
113+
if authResponse.Priority != nil {
114+
t.Fatalf("priority = %d, want unset", *authResponse.Priority)
115+
}
116+
}
117+
118+
func TestGRPCClientAuthorizeInvocationMapsResolvedPriority(t *testing.T) {
119+
t.Parallel()
120+
121+
// Explicit 0 (highest priority) and the uint32 max (lowest) must both
122+
// round-trip as set values, distinct from an unset priority.
123+
for _, priority := range []uint32{0, 4294967295} {
124+
invocationService := &stubInvocationService{
125+
t: t,
126+
priority: uint32Ptr(priority),
127+
}
128+
129+
listener := bufconn.Listen(1024 * 1024)
130+
server := grpc.NewServer()
131+
llmgatewaypb.RegisterLlmGatewayServer(server, invocationService)
132+
133+
go func() {
134+
_ = server.Serve(listener)
135+
}()
136+
t.Cleanup(func() {
137+
server.Stop()
138+
_ = listener.Close()
139+
})
140+
141+
conn, err := grpc.NewClient(
142+
"passthrough:///bufnet",
143+
grpc.WithTransportCredentials(insecure.NewCredentials()),
144+
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
145+
return listener.Dial()
146+
}),
147+
)
148+
if err != nil {
149+
t.Fatalf("create client conn: %v", err)
150+
}
151+
t.Cleanup(func() {
152+
_ = conn.Close()
153+
})
154+
155+
client := NewClientWithConn(conn, func() string { return "service-token" }, time.Second)
156+
157+
authResponse, err := client.AuthorizeInvocation(context.Background(), "client-token", "fn-123")
158+
if err != nil {
159+
t.Fatalf("authorize invocation: %v", err)
160+
}
161+
if authResponse.Priority == nil {
162+
t.Fatalf("priority = nil, want %d", priority)
163+
}
164+
if *authResponse.Priority != priority {
165+
t.Fatalf("priority = %d, want %d", *authResponse.Priority, priority)
166+
}
167+
}
113168
}
114169

115170
func TestGRPCClientAuthorizeInvocationDoesNotFallbackRateLimitKey(t *testing.T) {
@@ -306,6 +361,7 @@ type stubInvocationService struct {
306361
clientAuthID string
307362
clientNCAID string
308363
clientProjectID string
364+
priority *uint32
309365
expectedAuthHeader string
310366
traceparent string
311367
}
@@ -346,6 +402,7 @@ func (s *stubInvocationService) AuthLlmInvocation(
346402
resp := &llmgatewaypb.AuthLlmInvokeResponse{
347403
RoutingKey: "fn-123",
348404
ClientAuthSubject: clientAuthID,
405+
Priority: s.priority,
349406
ModelSpecs: map[string]*llmgatewaypb.AuthLlmInvokeResponse_ModelSpec{
350407
"gateway-model": {
351408
Uris: []string{"https://example.com/model"},
@@ -385,3 +442,7 @@ func incomingMetadataValue(ctx context.Context, key string) string {
385442
func stringPtr(value string) *string {
386443
return &value
387444
}
445+
446+
func uint32Ptr(value uint32) *uint32 {
447+
return &value
448+
}

‎src/invocation-plane-services/llm-api-gateway/nvcf/pb/llm_gateway.pb.go‎

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/invocation-plane-services/llm-api-gateway/nvcf/pb/llm_gateway.proto‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ message AuthLlmInvokeResponse {
3535
string clientAuthSubject = 2;
3636
map<string, string> authContext = 3;
3737
map<string, ModelSpec> modelSpecs = 4;
38+
// Resolved caller priority; lower value = higher priority, 0 is highest.
39+
// Unset when no priority config applies; absent is not 0, check hasPriority().
40+
optional uint32 priority = 5;
3841

3942
message ModelSpec {
4043
reserved 3;

‎src/invocation-plane-services/llm-api-gateway/nvcf/pb/llm_gateway_grpc.pb.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)