diff --git a/guides/google.md b/guides/google.md index 8883cdbf..d32278d1 100644 --- a/guides/google.md +++ b/guides/google.md @@ -27,6 +27,12 @@ Passed via `:provider_options` keyword: - **Example**: `provider_options: [google_grounding: %{enable: true}]` - **Cost tracking**: Usage tracked in `response.usage.tool_usage.web_search` with `unit: "query"` +### `google_url_context` +- **Type**: `boolean` | `map` +- **Purpose**: Enable URL context grounding - allows model to fetch and use content from specific URLs +- **Example**: `provider_options: [google_url_context: true]` +- **Note**: Requires v1beta (default) + ### `google_thinking_budget` - **Type**: Non-negative integer - **Purpose**: Control thinking tokens for Gemini 2.5 models diff --git a/lib/req_llm/providers/google.ex b/lib/req_llm/providers/google.ex index 28ef4868..4a0acc00 100644 --- a/lib/req_llm/providers/google.ex +++ b/lib/req_llm/providers/google.ex @@ -105,6 +105,11 @@ defmodule ReqLLM.Providers.Google do doc: "Enable Google Search grounding - allows model to search the web. Set to %{enable: true} for modern models, or %{dynamic_retrieval: %{mode: \"MODE_DYNAMIC\", dynamic_threshold: 0.7}} for Gemini 1.5 legacy support. Requires v1beta (default)." ], + google_url_context: [ + type: {:or, [:boolean, :map]}, + doc: + "Enable URL context grounding - allows model to fetch and use content from specific URLs. Pass `true` or a map with options. Requires v1beta (default)." + ], dimensions: [ type: :pos_integer, doc: @@ -896,28 +901,30 @@ defmodule ReqLLM.Providers.Google do tool_config = build_google_tool_config(request.options[:tool_choice]) + grounding_tools = build_grounding_tools(request.options[:google_grounding]) + url_context_tools = build_url_context_tools(request.options[:google_url_context]) + builtin_tools = grounding_tools ++ url_context_tools + tools_data = case request.options[:tools] do tools when is_list(tools) and tools != [] -> - grounding_tools = build_grounding_tools(request.options[:google_grounding]) - user_tools = [ %{functionDeclarations: Enum.map(tools, &ReqLLM.Tool.to_schema(&1, :google))} ] - all_tools = grounding_tools ++ user_tools + all_tools = builtin_tools ++ user_tools %{tools: all_tools} |> maybe_put(:toolConfig, tool_config) _ -> - case build_grounding_tools(request.options[:google_grounding]) do + case builtin_tools do [] -> %{} |> maybe_put(:toolConfig, tool_config) - grounding_tools -> - %{tools: grounding_tools} + tools -> + %{tools: tools} |> maybe_put(:toolConfig, tool_config) end end @@ -1341,6 +1348,10 @@ defmodule ReqLLM.Providers.Google do defp build_grounding_tools(_), do: [] + defp build_url_context_tools(true), do: [%{url_context: %{}}] + defp build_url_context_tools(%{} = opts), do: [%{url_context: opts}] + defp build_url_context_tools(_), do: [] + defp extract_grounding_metadata(%{"candidates" => [candidate | _]}) do case candidate do %{"groundingMetadata" => metadata} when is_map(metadata) -> diff --git a/test/providers/google_test.exs b/test/providers/google_test.exs index ecafc8fc..22ebd657 100644 --- a/test/providers/google_test.exs +++ b/test/providers/google_test.exs @@ -305,6 +305,115 @@ defmodule ReqLLM.Providers.GoogleTest do end end + describe "google_url_context option" do + test "encode_body includes url_context tool when boolean true" do + {:ok, model} = ReqLLM.model("google:gemini-2.0-flash") + context = context_fixture() + + mock_request = %Req.Request{ + options: [ + context: context, + model: model.model, + stream: false, + google_url_context: true + ] + } + + updated_request = Google.encode_body(mock_request) + decoded = Jason.decode!(updated_request.body) + + assert is_list(decoded["tools"]) + assert length(decoded["tools"]) == 1 + [tool] = decoded["tools"] + assert Map.has_key?(tool, "url_context") + assert tool["url_context"] == %{} + end + + test "encode_body includes url_context tool with map options" do + {:ok, model} = ReqLLM.model("google:gemini-2.0-flash") + context = context_fixture() + + mock_request = %Req.Request{ + options: [ + context: context, + model: model.model, + stream: false, + google_url_context: %{some_option: "value"} + ] + } + + updated_request = Google.encode_body(mock_request) + decoded = Jason.decode!(updated_request.body) + + assert is_list(decoded["tools"]) + assert length(decoded["tools"]) == 1 + [tool] = decoded["tools"] + assert Map.has_key?(tool, "url_context") + assert tool["url_context"] == %{"some_option" => "value"} + end + + test "encode_body combines url_context with user tools" do + {:ok, model} = ReqLLM.model("google:gemini-2.0-flash") + context = context_fixture() + + tool = + ReqLLM.Tool.new!( + name: "test_tool", + description: "A test tool", + parameter_schema: [ + name: [type: :string, required: true, doc: "A name parameter"] + ], + callback: fn _ -> {:ok, "result"} end + ) + + mock_request = %Req.Request{ + options: [ + context: context, + model: model.model, + stream: false, + tools: [tool], + google_url_context: true, + operation: :chat + ] + } + + updated_request = Google.encode_body(mock_request) + decoded = Jason.decode!(updated_request.body) + + assert is_list(decoded["tools"]) + assert length(decoded["tools"]) == 2 + + tool_types = Enum.map(decoded["tools"], fn t -> Map.keys(t) end) |> List.flatten() + assert "url_context" in tool_types + assert "functionDeclarations" in tool_types + end + + test "encode_body combines url_context with grounding" do + {:ok, model} = ReqLLM.model("google:gemini-2.0-flash") + context = context_fixture() + + mock_request = %Req.Request{ + options: [ + context: context, + model: model.model, + stream: false, + google_grounding: %{enable: true}, + google_url_context: true + ] + } + + updated_request = Google.encode_body(mock_request) + decoded = Jason.decode!(updated_request.body) + + assert is_list(decoded["tools"]) + assert length(decoded["tools"]) == 2 + + tool_types = Enum.map(decoded["tools"], fn t -> Map.keys(t) end) |> List.flatten() + assert "url_context" in tool_types + assert "google_search" in tool_types + end + end + describe "response decoding" do test "decode_response handles non-streaming responses" do # Create a mock Google response