diff --git a/providers/anthropic/anthropic.go b/providers/anthropic/anthropic.go index 2760b0d71b244b86f46da5d6fce60c094b8a1128..27dd22698532749c9d35ca86e1a9b507dd1eba1f 100644 --- a/providers/anthropic/anthropic.go +++ b/providers/anthropic/anthropic.go @@ -1171,6 +1171,9 @@ func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantas if err != nil { return nil, toProviderErr(err) } + if response == nil { + return nil, &fantasy.Error{Title: "no response", Message: "provider returned nil response"} + } var content []fantasy.Content for _, block := range response.Content { diff --git a/providers/openai/language_model.go b/providers/openai/language_model.go index 4515dabbd9c89e1ab19279b470acd71d5812209d..2907008de13b0ff05150a18aadf01b54d99213ed 100644 --- a/providers/openai/language_model.go +++ b/providers/openai/language_model.go @@ -251,6 +251,9 @@ func (o languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantas if err != nil { return nil, toProviderErr(err) } + if response == nil { + return nil, &fantasy.Error{Title: "no response", Message: "provider returned nil response"} + } if len(response.Choices) == 0 { return nil, &fantasy.Error{Title: "no response", Message: "no response generated"} diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index 254ae635412917fe6c5f9b46db4ec0d9eb6ccabd..2230deae43e26c63435aa0f1235933a661e4b5db 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -2010,6 +2010,31 @@ func TestDoGenerate(t *testing.T) { require.Equal(t, "ServiceTier", result.Warnings[0].Setting) require.Contains(t, result.Warnings[0].Details, "priority processing is only available") }) + + t.Run("should return error instead of panic on empty response body", func(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // Write empty body — some OpenAI-compatible endpoints may do this + // under edge conditions, causing the SDK to return (nil, nil). + })) + defer server.Close() + + provider, err := New( + WithAPIKey("test-api-key"), + WithBaseURL(server.URL), + ) + require.NoError(t, err) + model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo") + + require.NotPanics(t, func() { + _, _ = model.Generate(context.Background(), fantasy.Call{ + Prompt: testPrompt, + }) + }) + }) } type streamingMockServer struct { diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 06da0aa6e3d869fffe618bf0cd61c30ea5fa35e8..35c88ed7e55a3df649e25e41ebefcbf67c38aafa 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -789,6 +789,9 @@ func (o responsesLanguageModel) Generate(ctx context.Context, call fantasy.Call) if err != nil { return nil, toProviderErr(err) } + if response == nil { + return nil, &fantasy.Error{Title: "no response", Message: "provider returned nil response"} + } if response.Error.Message != "" { return nil, &fantasy.Error{