Skip to content

Commit add7644

Browse files
authored
fix(coordinator): force non-streaming prefill for chat completions (#2002)
* fix(coordinator): force non-streaming prefill for chat completions Prefill cloned the client's request body verbatim, including stream=true. The backend then replied with an SSE stream, which prefill's single-object JSON decode couldn't parse. Prefill only needs kv_transfer_params from a max_tokens=1 call, so it must not stream regardless of what the client requested; decode still honors the client's stream value. Fixes #2000 Signed-off-by: roytman <roytman@il.ibm.com> * fix copilot comments Signed-off-by: roytman <roytman@il.ibm.com> * fix copilot comments Signed-off-by: roytman <roytman@il.ibm.com> --------- Signed-off-by: roytman <roytman@il.ibm.com>
1 parent f762cfe commit add7644

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

pkg/coordinator/steps/prefill.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ func (s *PrefillStep) buildPrefillBody(ctx context.Context, reqCtx *pipeline.Req
137137
switch format {
138138
case gateway.FormatChatCompletions:
139139
body := maps.Clone(reqCtx.Body)
140+
body["stream"] = false
141+
delete(body, "stream_options")
140142
tokens := map[string]any{
141143
"token_ids": reqCtx.TokenIDs,
142144
}

pkg/coordinator/steps/prefill_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,57 @@ func TestPrefillStep_ChatCompletionsFormat(t *testing.T) {
367367
}
368368
}
369369

370+
func TestPrefillStep_ChatCompletionsFormat_ForcesNonStreaming(t *testing.T) {
371+
var prefillBody map[string]any
372+
373+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
374+
if r.URL.Path != gateway.PathChatCompletions {
375+
t.Fatalf("unexpected path: %s", r.URL.Path)
376+
}
377+
if r.Header.Get(gateway.EPPPhaseHeader) != gateway.PhasePrefill {
378+
t.Fatalf("expected EPP-Phase: prefill, got %q", r.Header.Get(gateway.EPPPhaseHeader))
379+
}
380+
body, _ := io.ReadAll(r.Body)
381+
_ = json.Unmarshal(body, &prefillBody)
382+
_ = json.NewEncoder(w).Encode(map[string]any{
383+
"kv_transfer_params": map[string]any{"block_id": "block-3"},
384+
})
385+
}))
386+
defer server.Close()
387+
388+
gwClient := gateway.New(config.GatewayConfig{Address: server.URL})
389+
step, err := NewPrefillStep(gwClient, map[string]any{})
390+
if err != nil {
391+
t.Fatal(err)
392+
}
393+
394+
reqCtx := &pipeline.RequestContext{
395+
RequestID: "req-chat-stream",
396+
OriginalPath: gateway.PathChatCompletions,
397+
Model: "test-model",
398+
Body: map[string]any{
399+
"model": "test-model",
400+
"stream": true,
401+
"stream_options": map[string]any{"include_usage": true},
402+
"messages": []any{
403+
map[string]any{"role": "user", "content": "hello"},
404+
},
405+
},
406+
KVTransferParams: make(map[string]any),
407+
}
408+
409+
if err := step.Execute(context.Background(), reqCtx); err != nil {
410+
t.Fatalf("unexpected error: %v", err)
411+
}
412+
413+
if prefillBody["stream"] != false {
414+
t.Fatalf("expected prefill request to force stream=false, got %v", prefillBody["stream"])
415+
}
416+
if _, ok := prefillBody["stream_options"]; ok {
417+
t.Fatalf("expected stream_options to be stripped from prefill request, got %v", prefillBody["stream_options"])
418+
}
419+
}
420+
370421
// TestSharedStorage_OmitsECTransferParams_InPrefillBody verifies that the
371422
// ec-shared-storage EC connector never emits an ec_transfer_params field on
372423
// the prefill body, in every prefill wire format (chat-completions,

0 commit comments

Comments
 (0)