TestChatFallsBackWhenReasoningEffortRejected: newer OpenAI models reject tools + reasoning_effort on /v1/chat/completions with a 400. postChat must drop the field, retry once, and stay sticky for the Client's life, else every turn burns a 400.
(t *testing.T)
| 636 | t.Fatalf("reasoning must not leak into final message content: %+v", done.Final) |
| 637 | } |
| 638 | if done.Tokens != 3 { |
| 639 | t.Fatalf("want 3 tokens, got %d", done.Tokens) |
| 640 | } |
| 641 | } |
| 642 | |
| 643 | // TestChatFallsBackWhenReasoningEffortRejected: newer OpenAI models reject tools + |
| 644 | // reasoning_effort on /v1/chat/completions with a 400. postChat must drop the |
| 645 | // field, retry once, and stay sticky for the Client's life, else every turn |
| 646 | // burns a 400. |
| 647 | func TestChatFallsBackWhenReasoningEffortRejected(t *testing.T) { |
| 648 | var bodies []string |
| 649 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 650 | b, _ := io.ReadAll(r.Body) |
| 651 | bodies = append(bodies, string(b)) |
| 652 | if strings.Contains(string(b), `"reasoning_effort"`) { |
| 653 | w.WriteHeader(400) |
| 654 | fmt.Fprintln(w, `{`) |
| 655 | fmt.Fprintln(w, ` "error": {`) |
| 656 | fmt.Fprintln(w, ` "message": "Function tools with reasoning_effort are not supported for this model in /v1/chat/completions. Please use /v1/responses instead.",`) |
| 657 | fmt.Fprintln(w, ` "param": "reasoning_effort"`) |
| 658 | fmt.Fprintln(w, ` }`) |
| 659 | fmt.Fprintln(w, `}`) |
| 660 | return |
| 661 | } |
| 662 | sseOK(w, []string{ |
| 663 | `{"choices":[{"delta":{"content":"ok"}}],"usage":{"completion_tokens":1}}`, |
| 664 | }) |
| 665 | })) |
| 666 | defer srv.Close() |
| 667 | |
| 668 | c := New(srv.URL, "cloud-model", "") |
| 669 | |
| 670 | // First turn: 400 → fallback → success. |
| 671 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 672 | if e.Kind == EventError { |
| 673 | t.Fatalf("first turn must succeed via fallback, got error: %v", e.Err) |
| 674 | } |
| 675 | } |
| 676 | if len(bodies) != 2 { |
| 677 | t.Fatalf("first turn should send initial + retry (2 requests), got %d", len(bodies)) |
| 678 | } |
| 679 | if !strings.Contains(bodies[0], `"reasoning_effort"`) { |
| 680 | t.Fatalf("first attempt should send reasoning_effort: %s", bodies[0]) |
| 681 | } |
| 682 | if strings.Contains(bodies[1], `"reasoning_effort"`) { |
| 683 | t.Fatalf("retry must drop reasoning_effort: %s", bodies[1]) |
| 684 | } |
| 685 | |
| 686 | // Second turn on the same Client: flag is sticky, no 400, no retry. |
| 687 | bodies = nil |
| 688 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 689 | if e.Kind == EventError { |
| 690 | t.Fatalf("second turn must not error: %v", e.Err) |
| 691 | } |
| 692 | } |
| 693 | if len(bodies) != 1 { |