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 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 637 | w.WriteHeader(http.StatusNotFound) |
| 638 | fmt.Fprint(w, "{\"error\":{\"message\":\"The model `nope` does not exist.\",\"code\":404}}") |
| 639 | })) |
| 640 | defer srv.Close() |
| 641 | err := New(srv.URL, "nope", "").Probe(context.Background()) |
| 642 | if err == nil { |
| 643 | t.Fatal("404 must fail the probe") |
| 644 | } |
| 645 | for _, want := range []string{"404", "The model `nope` does not exist.", "Responses API", "API base URL"} { |
| 646 | if !strings.Contains(err.Error(), want) { |
| 647 | t.Fatalf("404 error should mention %q: %v", want, err) |
| 648 | } |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | func TestLiteLLM404NamesTheBridge(t *testing.T) { |
| 653 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 654 | w.WriteHeader(http.StatusNotFound) |
| 655 | fmt.Fprint(w, `{"error":{"message":"litellm.NotFoundError: OpenAIException - 404: Not Found"}}`) |
| 656 | })) |
| 657 | defer srv.Close() |
| 658 | err := New(srv.URL, "local-model", "").Probe(context.Background()) |
| 659 | if err == nil { |
| 660 | t.Fatal("404 must fail the probe") |
| 661 | } |
| 662 | for _, want := range []string{"404", "litellm.NotFoundError", "upstream URL and model", "use_chat_completions_api: true"} { |
| 663 | if !strings.Contains(err.Error(), want) { |
| 664 | t.Errorf("missing %q in %v", want, err) |
| 665 | } |
| 666 | } |
| 667 | if strings.Contains(err.Error(), "Ollama 0.13.3") { |
| 668 | t.Errorf("proxy error must not prescribe a local server upgrade: %v", err) |
| 669 | } |
| 670 | } |
| 671 | |
| 672 | // TestChatStructuredErrorFallsBackToMessage: with only `error.message`, surface |
| 673 | // that, not the raw JSON envelope. |
| 674 | func TestChatStructuredErrorFallsBackToMessage(t *testing.T) { |
| 675 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 676 | w.Header().Set("Content-Type", "application/json") |
| 677 | w.WriteHeader(http.StatusServiceUnavailable) |
| 678 | fmt.Fprint(w, `{"error":{"message":"upstream unavailable","type":"upstream_unavailable","upstream_status":503}}`) |
| 679 | })) |
| 680 | defer srv.Close() |
| 681 | c := New(srv.URL, "m", "") |
| 682 | c.RetryBackoff = nil |
| 683 | evs := collect(c.Chat(context.Background(), nil, nil)) |
| 684 | if len(evs) != 1 || evs[0].Kind != EventError { |
| 685 | t.Fatalf("want single error event, got %+v", evs) |
| 686 | } |
| 687 | msg := evs[0].Err.Error() |
| 688 | if !strings.Contains(msg, "503") || !strings.Contains(msg, "upstream unavailable") || strings.Contains(msg, `{"error"`) { |
| 689 | t.Fatalf("error should carry status and message, never the raw envelope: %v", msg) |
| 690 | } |
| 691 | } |
| 692 | |
| 693 | // TestChatFallsBackWhenReasoningRejected: each dialect's refusal of the |