TestChatDoesNotFallBackOnUnrelatedThinking: a 400 that is NOT about reasoning but happens to contain both "not support" and the word "thinking" must not trip the reasoning_effort fallback. Otherwise the bare-"thinking" match burns a wasted retry and latches reasoning off for the Client's whole life
(t *testing.T)
| 788 | if strings.Contains(bodies[0], `"reasoning_effort"`) { |
| 789 | t.Fatalf("second turn must not resend reasoning_effort: %s", bodies[0]) |
| 790 | } |
| 791 | } |
| 792 | |
| 793 | // TestChatFallsBackWhenEffortValueUnsupported: models that define their own |
| 794 | // effort scale 400 on a value outside it. Qwen3.8 (vLLM) ships xhigh/medium/low |
| 795 | // and has no `high`, the exact value Chat sends, so every turn died on a 400 |
| 796 | // while the two older rejection shapes sailed past the matcher. Body is the |
| 797 | // server's verbatim response. Same remedy: drop the field, retry once, stay |
| 798 | // sticky — the server then applies its own default (xhigh here). |
| 799 | func TestChatFallsBackWhenEffortValueUnsupported(t *testing.T) { |
| 800 | var bodies []string |
| 801 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 802 | b, _ := io.ReadAll(r.Body) |
| 803 | bodies = append(bodies, string(b)) |
| 804 | if strings.Contains(string(b), `"reasoning_effort"`) { |
| 805 | w.WriteHeader(400) |
| 806 | fmt.Fprintln(w, `{"error":{"message":"Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low.","type":"BadRequestError","param":null,"code":400}}`) |
| 807 | return |
| 808 | } |
| 809 | sseOK(w, []string{ |
| 810 | `{"choices":[{"delta":{"content":"ok"}}],"usage":{"completion_tokens":1}}`, |
| 811 | }) |
| 812 | })) |
| 813 | defer srv.Close() |
| 814 | |
| 815 | c := New(srv.URL, "Qwen3.8-27B", "") |