Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions lib/chat_models/chat_google_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ defmodule LangChain.ChatModels.ChatGoogleAI do
end
end

def do_api_request(%ChatGoogleAI{stream: true} = google_ai, messages, tools) do
def do_api_request(%ChatGoogleAI{stream: true, model: model} = google_ai, messages, tools) do
Req.new(
url: build_url(google_ai),
json: for_api(google_ai, messages, tools),
Expand All @@ -619,7 +619,10 @@ defmodule LangChain.ChatModels.ChatGoogleAI do
)
)
|> case do
{:ok, %Req.Response{status: 200, body: data} = response} ->
{:ok, %Req.Response{body: {:error, %LangChainError{} = error}}} ->
{:error, error}

{:ok, %Req.Response{status: 200, body: data} = response} when is_list(data) ->
Callbacks.fire(google_ai.callbacks, :on_llm_response_headers, [response.headers])
Comment thread
nelsonkopliku marked this conversation as resolved.

flattened = List.flatten(data)
Comment thread
nelsonkopliku marked this conversation as resolved.
Expand All @@ -640,8 +643,20 @@ defmodule LangChain.ChatModels.ChatGoogleAI do
|> Enum.reverse()
end

{:ok, %Req.Response{body: {:error, %LangChainError{} = error}}} ->
{:error, error}
{:ok, %Req.Response{status: 200} = response} ->
# Stream ended with zero delta chunks — `Utils.handle_stream_fn/3`
# converts the default binary body `""` to `[]` only on the first
# `{:data, ...}` callback. If LLM returns 200 with no streamed
# chunks (e.g. immediate finish without content, or all chunks
# filtered by the SSE decoder), the body stays `""` and crashes
# `List.flatten/1`. Surface as a structured error so the chain can
# propagate it cleanly instead of taking down the Task.
{:error,
LangChainError.exception(
type: "empty_stream",
message: "Empty streaming response from #{model} (no delta chunks received)",
original: response
)}
Comment thread
nelsonkopliku marked this conversation as resolved.
Comment thread
nelsonkopliku marked this conversation as resolved.
Comment thread
nelsonkopliku marked this conversation as resolved.

{:ok, %Req.Response{status: status} = response} when status != 200 ->
# Try to extract error from the buffered error data
Expand Down
20 changes: 20 additions & 0 deletions test/chat_models/chat_google_ai_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2063,5 +2063,25 @@ defmodule ChatModels.ChatGoogleAITest do
assert error.message == "Unexpected response 1"
assert error.original["finishReason"] == "MALFORMED_FUNCTION_CALL"
end

test "streaming 200 with empty body returns empty_stream error instead of crashing" do
# `Req.Response.body` starts as the binary `""` and is converted to `[]`
# by `Utils.handle_stream_fn/3` only on the first `{:data, ...}` callback.
# If Gemini returns 200 with zero streamed chunks (e.g. immediate finish
# without content, or all chunks filtered by the SSE decoder), the body
# stays as the binary `""` and `List.flatten/1` crashes with a
# FunctionClauseError. This test guards that path.
expect(Req, :post, fn _req, _opts ->
{:ok, %Req.Response{status: 200, headers: [], body: ""}}
end)
Comment thread
nelsonkopliku marked this conversation as resolved.

model = ChatGoogleAI.new!(%{stream: true, model: "gemini-2.5-flash"})

assert {:error, %LangChainError{} = error} =
ChatGoogleAI.call(model, [Message.new_user!("Hello")])

assert error.type == "empty_stream"
assert error.message =~ "Empty streaming response"
end
end
end