Skip to content

Commit c5b83af

Browse files
committed
fix: address review — TTFT metric, chunk interface, nil checks
- Move TTFT metric recording after if/else so it applies to both buffered and streaming paths (comment 3+4) - Change ResponseChunkProcessor interface: takes string (framework converts once) + InferenceResponse (for header mutation) (comment 5) - Add nil profile check and empty processor check in HandleResponseChunk for bodiless requests like GET /v1/models (comment 6) - Extract runResponseChunkProcessors and buildStreamedChunkResponse following the same patterns as the body processing path Signed-off-by: Noy Itzikowitz <nitzikow@redhat.com>
1 parent 541110e commit c5b83af

3 files changed

Lines changed: 38 additions & 16 deletions

File tree

pkg/framework/interface/requesthandling/plugins.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ type ResponseProcessor interface {
5252
}
5353

5454
// ResponseChunkProcessor processes individual response body chunks as they
55-
// stream through without buffering. The framework calls ProcessResponseChunk
56-
// for each chunk; the returned bytes are forwarded to the client.
55+
// stream through without buffering. The framework converts the raw chunk bytes
56+
// to a string once and passes it to all chunk processors. Plugins receive the
57+
// InferenceResponse to allow header mutation.
5758
type ResponseChunkProcessor interface {
5859
plugin.Plugin
59-
ProcessResponseChunk(ctx context.Context, cycleState *plugin.CycleState, chunk []byte, isFinal bool) ([]byte, error)
60+
ProcessResponseChunk(ctx context.Context, cycleState *plugin.CycleState, response *InferenceResponse, chunk string, isFinal bool) error
6061
}
6162

6263
type PostProcessor interface {

pkg/handlers/response.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,45 @@ func (s *Server) generateEmptyResponseBodyResponse(responseBodyBytes []byte) []*
133133

134134
// HandleResponseChunk runs ResponseChunkProcessors on a single response body chunk
135135
// and wraps the result in the ext_proc streaming response format.
136-
func (s *Server) HandleResponseChunk(ctx context.Context, reqCtx *RequestContext, chunk []byte, endOfStream bool) ([]*eppb.ProcessingResponse, error) {
136+
func (s *Server) HandleResponseChunk(ctx context.Context, reqCtx *RequestContext, chunkBytes []byte, endOfStream bool) ([]*eppb.ProcessingResponse, error) {
137+
// Bodiless requests (e.g., GET /v1/models) may not have a profile set.
138+
if reqCtx.Profile == nil || len(reqCtx.Profile.ResponseChunkProcessors) == 0 {
139+
return s.buildStreamedChunkResponse(chunkBytes, endOfStream), nil
140+
}
141+
142+
logger := log.FromContext(ctx).V(logutil.DEFAULT)
143+
144+
chunk := string(chunkBytes)
145+
146+
if err := s.runResponseChunkProcessors(ctx, reqCtx.CycleState, reqCtx.Response, chunk, endOfStream, reqCtx.Profile.ResponseChunkProcessors); err != nil {
147+
logger.Error(err, "Failed to run response chunk processors")
148+
return nil, err
149+
}
150+
151+
return s.buildStreamedChunkResponse(chunkBytes, endOfStream), nil
152+
}
153+
154+
// runResponseChunkProcessors executes chunk processors in the order they were registered.
155+
func (s *Server) runResponseChunkProcessors(ctx context.Context, cycleState *plugin.CycleState, response *requesthandling.InferenceResponse, chunk string, isFinal bool, processors []requesthandling.ResponseChunkProcessor) error {
137156
logger := log.FromContext(ctx).V(logutil.DEFAULT)
138157
verboseLogger := logger.V(logutil.VERBOSE)
139158

140-
for _, cp := range reqCtx.Profile.ResponseChunkProcessors {
159+
for _, cp := range processors {
141160
if verboseLogger.Enabled() {
142161
verboseLogger.Info("Executing response chunk plugin", "plugin", cp.TypedName())
143162
}
144-
var err error
145163
before := time.Now()
146-
chunk, err = cp.ProcessResponseChunk(ctx, reqCtx.CycleState, chunk, endOfStream)
164+
err := cp.ProcessResponseChunk(ctx, cycleState, response, chunk, isFinal)
147165
metrics.RecordPluginProcessingLatency(responsePluginExtensionPoint, cp.TypedName().Type, cp.TypedName().Name, time.Since(before))
148166
if err != nil {
149-
logger.Error(err, "Failed to execute response chunk plugin", "plugin", cp.TypedName())
150-
return nil, err
167+
return err
151168
}
152169
}
170+
return nil
171+
}
153172

173+
// buildStreamedChunkResponse wraps a chunk in the ext_proc streaming response format.
174+
func (s *Server) buildStreamedChunkResponse(chunk []byte, endOfStream bool) []*eppb.ProcessingResponse {
154175
responses := []*eppb.ProcessingResponse{
155176
{
156177
Response: &eppb.ProcessingResponse_ResponseBody{
@@ -179,7 +200,7 @@ func (s *Server) HandleResponseChunk(ctx context.Context, reqCtx *RequestContext
179200
responses = append([]*eppb.ProcessingResponse{headerResp}, responses...)
180201
}
181202

182-
return responses, nil
203+
return responses
183204
}
184205

185206
// HandleResponseTrailers handles response trailers.

pkg/handlers/server.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,18 +182,18 @@ func (s *Server) Process(srv extProcPb.ExternalProcessor_ProcessServer) error {
182182
if !v.ResponseBody.EndOfStream {
183183
continue
184184
}
185-
reqCtx.ResponseCompleteTimestamp = time.Now()
186-
model, _ := reqCtx.Request.Body["model"].(string)
187-
metrics.RecordRequestTTFT(model, reqCtx.ResponseFirstChunkTimestamp.Sub(reqCtx.RequestReceivedTimestamp))
188185
responses, err = s.HandleResponseBody(ctx, reqCtx, responseBody)
189186
loggerVerbose.Info("processing response body complete")
190187
} else {
191-
if v.ResponseBody.EndOfStream {
192-
reqCtx.ResponseCompleteTimestamp = time.Now()
193-
}
194188
responses, err = s.HandleResponseChunk(ctx, reqCtx, v.ResponseBody.Body, v.ResponseBody.EndOfStream)
195189
loggerVerbose.Info("response chunk processing complete")
196190
}
191+
192+
if v.ResponseBody.EndOfStream {
193+
reqCtx.ResponseCompleteTimestamp = time.Now()
194+
model, _ := reqCtx.Request.Body["model"].(string)
195+
metrics.RecordRequestTTFT(model, reqCtx.ResponseFirstChunkTimestamp.Sub(reqCtx.RequestReceivedTimestamp))
196+
}
197197
case *extProcPb.ProcessingRequest_ResponseTrailers:
198198
responses, err = s.HandleResponseTrailers(v.ResponseTrailers)
199199
default:

0 commit comments

Comments
 (0)