From 2afbfd513e20c5d992749e67bfe67852c1dcb2e5 Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Wed, 13 May 2026 10:58:06 +0200 Subject: [PATCH 1/5] Fix googleai malformed function call --- test/chat_models/chat_google_ai_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/chat_models/chat_google_ai_test.exs b/test/chat_models/chat_google_ai_test.exs index bbbcc12c..92dc58f5 100644 --- a/test/chat_models/chat_google_ai_test.exs +++ b/test/chat_models/chat_google_ai_test.exs @@ -2022,14 +2022,14 @@ defmodule ChatModels.ChatGoogleAITest do ) delta1 = %LangChain.MessageDelta{ - content: %LangChain.Message.ContentPart{type: :text, content: "Part 1"}, + content: [%LangChain.Message.ContentPart{type: :text, content: "Part 1"}], index: 0, role: :assistant, status: :incomplete } delta2 = %LangChain.MessageDelta{ - content: %LangChain.Message.ContentPart{type: :text, content: "Part 2"}, + content: [%LangChain.Message.ContentPart{type: :text, content: "Part 2"}], index: 0, role: :assistant, status: :incomplete From 002ab208264fae610804760bc9e3c44fd857268b Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Wed, 13 May 2026 12:52:13 +0200 Subject: [PATCH 2/5] Use correct shape for MessageDelta.content Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/chat_models/chat_google_ai_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/chat_models/chat_google_ai_test.exs b/test/chat_models/chat_google_ai_test.exs index 92dc58f5..bbbcc12c 100644 --- a/test/chat_models/chat_google_ai_test.exs +++ b/test/chat_models/chat_google_ai_test.exs @@ -2022,14 +2022,14 @@ defmodule ChatModels.ChatGoogleAITest do ) delta1 = %LangChain.MessageDelta{ - content: [%LangChain.Message.ContentPart{type: :text, content: "Part 1"}], + content: %LangChain.Message.ContentPart{type: :text, content: "Part 1"}, index: 0, role: :assistant, status: :incomplete } delta2 = %LangChain.MessageDelta{ - content: [%LangChain.Message.ContentPart{type: :text, content: "Part 2"}], + content: %LangChain.Message.ContentPart{type: :text, content: "Part 2"}, index: 0, role: :assistant, status: :incomplete From ba4d12f0fdca5d86ca1b7ff4d12f23cb4f6af8f8 Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Tue, 19 May 2026 11:00:50 +0200 Subject: [PATCH 3/5] fix: surface empty Gemini streaming body as LangChainError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a streaming request to Gemini returns 200 OK with zero delta chunks (e.g. immediate finish without content, or all chunks filtered by the SSE decoder), `Req.Response.body` stays as the binary default `""` instead of the list of `%MessageDelta{}` the downstream code expects. `Utils.handle_stream_fn/3` only converts `""` to `[]` on the first `{:data, ...}` callback — if no callback fires, the conversion is skipped. The body then reaches `List.flatten(data)` and crashes with `FunctionClauseError` on `:lists.flatten("")`, killing the upstream Task. Symptom in production: ** (FunctionClauseError) no function clause matching in :lists.flatten/1 :lists.flatten("") at lists.erl:1145 LangChain.ChatModels.ChatGoogleAI.do_api_request/3 at line 634 Add an `is_list(data)` guard to the existing 200 OK clause and a sibling clause that returns `{:error, %LangChainError{type: "empty_stream"}}` for the zero-chunk case. The upstream chain already knows how to surface `%LangChainError{}` as a clean status change instead of crashing. Regression test asserts the structured error is returned instead of the FunctionClauseError. --- lib/chat_models/chat_google_ai.ex | 17 ++++++++++++++++- test/chat_models/chat_google_ai_test.exs | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/chat_models/chat_google_ai.ex b/lib/chat_models/chat_google_ai.ex index e4250370..5e8d499b 100644 --- a/lib/chat_models/chat_google_ai.ex +++ b/lib/chat_models/chat_google_ai.ex @@ -619,7 +619,7 @@ defmodule LangChain.ChatModels.ChatGoogleAI do ) ) |> case do - {:ok, %Req.Response{status: 200, body: data} = response} -> + {:ok, %Req.Response{status: 200, body: data} = response} when is_list(data) -> Callbacks.fire(google_ai.callbacks, :on_llm_response_headers, [response.headers]) flattened = List.flatten(data) @@ -640,6 +640,21 @@ defmodule LangChain.ChatModels.ChatGoogleAI do |> Enum.reverse() end + {: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 Gemini 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 Gemini (no delta chunks received)", + original: response + )} + {:ok, %Req.Response{body: {:error, %LangChainError{} = error}}} -> {:error, error} diff --git a/test/chat_models/chat_google_ai_test.exs b/test/chat_models/chat_google_ai_test.exs index bbbcc12c..8737ca35 100644 --- a/test/chat_models/chat_google_ai_test.exs +++ b/test/chat_models/chat_google_ai_test.exs @@ -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) + + 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 From 1495bec2230479d04129538f42e5fa9a0d94b1a0 Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Tue, 9 Jun 2026 08:41:47 +0200 Subject: [PATCH 4/5] Improve match clause order when handling request to google apis --- lib/chat_models/chat_google_ai.ex | 6 +++--- test/chat_models/chat_google_ai_test.exs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/chat_models/chat_google_ai.ex b/lib/chat_models/chat_google_ai.ex index 5e8d499b..92669869 100644 --- a/lib/chat_models/chat_google_ai.ex +++ b/lib/chat_models/chat_google_ai.ex @@ -619,6 +619,9 @@ defmodule LangChain.ChatModels.ChatGoogleAI do ) ) |> case do + {: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]) @@ -655,9 +658,6 @@ defmodule LangChain.ChatModels.ChatGoogleAI do original: response )} - {:ok, %Req.Response{body: {:error, %LangChainError{} = error}}} -> - {:error, error} - {:ok, %Req.Response{status: status} = response} when status != 200 -> # Try to extract error from the buffered error data case Utils.extract_stream_error(response) do diff --git a/test/chat_models/chat_google_ai_test.exs b/test/chat_models/chat_google_ai_test.exs index 8737ca35..e83680c8 100644 --- a/test/chat_models/chat_google_ai_test.exs +++ b/test/chat_models/chat_google_ai_test.exs @@ -2072,7 +2072,7 @@ defmodule ChatModels.ChatGoogleAITest do # 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: ""}} + {:ok, %Req.Response{status: 200, headers: [], body: ""}} end) model = ChatGoogleAI.new!(%{stream: true, model: "gemini-2.5-flash"}) From ae50ac25c46f833deef05e5502cb3a25e137b7bc Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Tue, 9 Jun 2026 08:50:08 +0200 Subject: [PATCH 5/5] Interpolate model name in empty stream response message --- lib/chat_models/chat_google_ai.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/chat_models/chat_google_ai.ex b/lib/chat_models/chat_google_ai.ex index 92669869..0088dbe1 100644 --- a/lib/chat_models/chat_google_ai.ex +++ b/lib/chat_models/chat_google_ai.ex @@ -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), @@ -646,7 +646,7 @@ defmodule LangChain.ChatModels.ChatGoogleAI do {: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 Gemini returns 200 with no streamed + # `{: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 @@ -654,7 +654,7 @@ defmodule LangChain.ChatModels.ChatGoogleAI do {:error, LangChainError.exception( type: "empty_stream", - message: "Empty streaming response from Gemini (no delta chunks received)", + message: "Empty streaming response from #{model} (no delta chunks received)", original: response )}