TestChatMidStreamErrorFrameSurfacesAsError: OpenAI-compatible backends (and OpenRouter-style proxies like the hosted endpoint) report a post-200 provider failure as a final `data: {"error":{...}}` frame followed by connection close with no [DONE]. That frame must surface as EventError; left undecode
(t *testing.T)
| 364 | // undecoded it parses to zero choices, the close reads as clean EOF, and a |
| 365 | // mid-sentence-truncated turn finalizes as a confident EventDone success. |
| 366 | func TestChatMidStreamErrorFrameSurfacesAsError(t *testing.T) { |
| 367 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 368 | w.Header().Set("Content-Type", "text/event-stream") |
| 369 | fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"partial answer\"}}]}\n\n") |
| 370 | fmt.Fprint(w, "data: {\"error\":{\"code\":502,\"message\":\"Provider returned error\"}}\n\n") |
| 371 | // connection closes without [DONE] |
| 372 | })) |
| 373 | defer srv.Close() |
| 374 | |
| 375 | events := collect(New(srv.URL, "m", "").Chat(context.Background(), |
| 376 | []chmctx.Message{{Role: chmctx.RoleUser, Content: "hi"}}, nil)) |
| 377 | |
| 378 | last := events[len(events)-1] |
| 379 | if last.Kind != EventError { |
| 380 | t.Fatalf("stream must end in EventError, got kind %v (events: %+v)", last.Kind, events) |
| 381 | } |
| 382 | if !strings.Contains(last.Err.Error(), "Provider returned error") { |
| 383 | t.Fatalf("error must carry the server's message, got %v", last.Err) |
| 384 | } |
| 385 | for _, e := range events { |
| 386 | if e.Kind == EventDone { |
| 387 | t.Fatal("a stream that died mid-generation must not emit EventDone") |
| 388 | } |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | // TestChatReadsUsageTokens: tokens come from `usage.completion_tokens` (and |
| 393 | // prompt_tokens rides along for the debug-log calibration), not content |