fix(googleai): keep the partial response when a stream callback fails

GenerateContent discarded the accumulated response whenever the inner
layer returned an error, so a caller that stops a stream from its
StreamingFunc got (nil, err) and could not read the text produced before
the abort. The streaming path already returns the pair, and the
structured-output path below returns it too — this aligns the exported
entry point with both.

PAGI-125

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-08-19 13:24:35 +07:00
parent da7016e399
commit 14bed4b917
2 changed files with 39 additions and 1 deletions
+1 -1
View File
@@ -192,7 +192,7 @@ func (g *GoogleAI) GenerateContent(
response, err = g.generateFromMessages(ctx, opts.GetModel(), messages, config, &opts)
}
if err != nil {
return nil, err
return response, err
}
// When structured output was requested, validate each normal-final candidate
+38
View File
@@ -1,15 +1,20 @@
package googleai
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/vxcontrol/langchaingo/llms"
"github.com/vxcontrol/langchaingo/llms/streaming"
"google.golang.org/genai"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNew(t *testing.T) {
@@ -920,3 +925,36 @@ func TestValidateGoogleStructuredOutput(t *testing.T) {
}
})
}
type stubStreamTransport struct{ body string }
func (t stubStreamTransport) RoundTrip(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader(t.body)),
}, nil
}
func TestGenerateContentKeepsPartialResponseWhenStreamingFuncFails(t *testing.T) {
t.Parallel()
chunk := `{"candidates":[{"content":{"parts":[{"text":"partial answer"}],` +
`"role":"model"},"finishReason":"STOP","index":0}]}`
llm, err := New(t.Context(),
WithAPIKey("test-api-key"),
WithRest(),
WithHTTPClient(&http.Client{Transport: stubStreamTransport{body: "data: " + chunk + "\n\n"}}),
)
require.NoError(t, err)
errAbort := errors.New("caller stopped the stream")
resp, err := llm.GenerateContent(t.Context(),
[]llms.MessageContent{llms.TextParts(llms.ChatMessageTypeHuman, "hi")},
llms.WithStreamingFunc(func(context.Context, streaming.Chunk) error { return errAbort }),
)
require.ErrorIs(t, err, errAbort)
require.NotNil(t, resp)
require.Equal(t, "partial answer", resp.Choices[0].Content)
}