TestProbeChatNoReasoningEffortIsRaceFree pins the atomic.Bool guard on Client.noReasoningEffort. The startup probe and the first chat can run on the same *Client concurrently (probe from Init, chat when the user submits early): both read the flag via postChat, and a 400 fallback writes it. A plain b
(t *testing.T)
| 695 | // value, OpenAI's non reasoning model: drops the field, retries once, and |
| 696 | // stays sticky for the Client's life so later turns don't burn a 400 each. |
| 697 | func TestChatFallsBackWhenReasoningRejected(t *testing.T) { |
| 698 | for name, body := range map[string]string{ |
| 699 | "ollama": `{"error":"\"test-model:latest\" does not support thinking"}`, |
| 700 | "vllm": `{"error":{"message":"Unexpected reasoning effort medium. Supported types are xhigh (default), high, and low.","type":"BadRequestError","param":null,"code":400}}`, |
| 701 | "openai": `{"error":{"message":"Unsupported parameter: 'reasoning.effort' is not supported with this model.","type":"invalid_request_error","param":"reasoning.effort","code":"unsupported_parameter"}}`, |
| 702 | } { |
| 703 | t.Run(name, func(t *testing.T) { |
| 704 | var bodies []string |
| 705 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 706 | b, _ := io.ReadAll(r.Body) |
| 707 | bodies = append(bodies, string(b)) |
| 708 | if strings.Contains(string(b), `"reasoning"`) { |
| 709 | w.WriteHeader(400) |
| 710 | fmt.Fprintln(w, body) |
| 711 | return |
| 712 | } |
| 713 | sseOK(w, []string{textDelta("ok"), completed(1, 0)}) |
| 714 | })) |
| 715 | defer srv.Close() |
| 716 | |
| 717 | c := New(srv.URL, "test-model", "") |
| 718 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 719 | if e.Kind == EventError { |
| 720 | t.Fatalf("first turn must succeed via fallback, got error: %v", e.Err) |
| 721 | } |
| 722 | } |
| 723 | if len(bodies) != 2 || !strings.Contains(bodies[0], `"reasoning"`) || strings.Contains(bodies[1], `"reasoning"`) { |
| 724 | t.Fatalf("first turn should send reasoning, then retry without it: %v", bodies) |
| 725 | } |
| 726 | bodies = nil |
| 727 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 728 | if e.Kind == EventError { |
| 729 | t.Fatalf("second turn must not error: %v", e.Err) |
| 730 | } |
| 731 | } |
| 732 | if len(bodies) != 1 || strings.Contains(bodies[0], `"reasoning"`) { |