Skip to content

Commit bc57abf

Browse files
feat: add google_url_context provider option (#392)
Adds support for URL context grounding in Google provider, allowing responses to be grounded with content from specific URLs. Closes #226 Amp-Thread-ID: https://ampcode.com/threads/T-019c117b-7aa8-72ba-9556-23dc1118034f Co-authored-by: Amp <amp@ampcode.com>
1 parent 37b7100 commit bc57abf

3 files changed

Lines changed: 132 additions & 6 deletions

File tree

guides/google.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ Passed via `:provider_options` keyword:
2727
- **Example**: `provider_options: [google_grounding: %{enable: true}]`
2828
- **Cost tracking**: Usage tracked in `response.usage.tool_usage.web_search` with `unit: "query"`
2929

30+
### `google_url_context`
31+
- **Type**: `boolean` | `map`
32+
- **Purpose**: Enable URL context grounding - allows model to fetch and use content from specific URLs
33+
- **Example**: `provider_options: [google_url_context: true]`
34+
- **Note**: Requires v1beta (default)
35+
3036
### `google_thinking_budget`
3137
- **Type**: Non-negative integer
3238
- **Purpose**: Control thinking tokens for Gemini 2.5 models

lib/req_llm/providers/google.ex

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ defmodule ReqLLM.Providers.Google do
105105
doc:
106106
"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)."
107107
],
108+
google_url_context: [
109+
type: {:or, [:boolean, :map]},
110+
doc:
111+
"Enable URL context grounding - allows model to fetch and use content from specific URLs. Pass `true` or a map with options. Requires v1beta (default)."
112+
],
108113
dimensions: [
109114
type: :pos_integer,
110115
doc:
@@ -896,28 +901,30 @@ defmodule ReqLLM.Providers.Google do
896901

897902
tool_config = build_google_tool_config(request.options[:tool_choice])
898903

904+
grounding_tools = build_grounding_tools(request.options[:google_grounding])
905+
url_context_tools = build_url_context_tools(request.options[:google_url_context])
906+
builtin_tools = grounding_tools ++ url_context_tools
907+
899908
tools_data =
900909
case request.options[:tools] do
901910
tools when is_list(tools) and tools != [] ->
902-
grounding_tools = build_grounding_tools(request.options[:google_grounding])
903-
904911
user_tools = [
905912
%{functionDeclarations: Enum.map(tools, &ReqLLM.Tool.to_schema(&1, :google))}
906913
]
907914

908-
all_tools = grounding_tools ++ user_tools
915+
all_tools = builtin_tools ++ user_tools
909916

910917
%{tools: all_tools}
911918
|> maybe_put(:toolConfig, tool_config)
912919

913920
_ ->
914-
case build_grounding_tools(request.options[:google_grounding]) do
921+
case builtin_tools do
915922
[] ->
916923
%{}
917924
|> maybe_put(:toolConfig, tool_config)
918925

919-
grounding_tools ->
920-
%{tools: grounding_tools}
926+
tools ->
927+
%{tools: tools}
921928
|> maybe_put(:toolConfig, tool_config)
922929
end
923930
end
@@ -1341,6 +1348,10 @@ defmodule ReqLLM.Providers.Google do
13411348

13421349
defp build_grounding_tools(_), do: []
13431350

1351+
defp build_url_context_tools(true), do: [%{url_context: %{}}]
1352+
defp build_url_context_tools(%{} = opts), do: [%{url_context: opts}]
1353+
defp build_url_context_tools(_), do: []
1354+
13441355
defp extract_grounding_metadata(%{"candidates" => [candidate | _]}) do
13451356
case candidate do
13461357
%{"groundingMetadata" => metadata} when is_map(metadata) ->

test/providers/google_test.exs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,115 @@ defmodule ReqLLM.Providers.GoogleTest do
305305
end
306306
end
307307

308+
describe "google_url_context option" do
309+
test "encode_body includes url_context tool when boolean true" do
310+
{:ok, model} = ReqLLM.model("google:gemini-2.0-flash")
311+
context = context_fixture()
312+
313+
mock_request = %Req.Request{
314+
options: [
315+
context: context,
316+
model: model.model,
317+
stream: false,
318+
google_url_context: true
319+
]
320+
}
321+
322+
updated_request = Google.encode_body(mock_request)
323+
decoded = Jason.decode!(updated_request.body)
324+
325+
assert is_list(decoded["tools"])
326+
assert length(decoded["tools"]) == 1
327+
[tool] = decoded["tools"]
328+
assert Map.has_key?(tool, "url_context")
329+
assert tool["url_context"] == %{}
330+
end
331+
332+
test "encode_body includes url_context tool with map options" do
333+
{:ok, model} = ReqLLM.model("google:gemini-2.0-flash")
334+
context = context_fixture()
335+
336+
mock_request = %Req.Request{
337+
options: [
338+
context: context,
339+
model: model.model,
340+
stream: false,
341+
google_url_context: %{some_option: "value"}
342+
]
343+
}
344+
345+
updated_request = Google.encode_body(mock_request)
346+
decoded = Jason.decode!(updated_request.body)
347+
348+
assert is_list(decoded["tools"])
349+
assert length(decoded["tools"]) == 1
350+
[tool] = decoded["tools"]
351+
assert Map.has_key?(tool, "url_context")
352+
assert tool["url_context"] == %{"some_option" => "value"}
353+
end
354+
355+
test "encode_body combines url_context with user tools" do
356+
{:ok, model} = ReqLLM.model("google:gemini-2.0-flash")
357+
context = context_fixture()
358+
359+
tool =
360+
ReqLLM.Tool.new!(
361+
name: "test_tool",
362+
description: "A test tool",
363+
parameter_schema: [
364+
name: [type: :string, required: true, doc: "A name parameter"]
365+
],
366+
callback: fn _ -> {:ok, "result"} end
367+
)
368+
369+
mock_request = %Req.Request{
370+
options: [
371+
context: context,
372+
model: model.model,
373+
stream: false,
374+
tools: [tool],
375+
google_url_context: true,
376+
operation: :chat
377+
]
378+
}
379+
380+
updated_request = Google.encode_body(mock_request)
381+
decoded = Jason.decode!(updated_request.body)
382+
383+
assert is_list(decoded["tools"])
384+
assert length(decoded["tools"]) == 2
385+
386+
tool_types = Enum.map(decoded["tools"], fn t -> Map.keys(t) end) |> List.flatten()
387+
assert "url_context" in tool_types
388+
assert "functionDeclarations" in tool_types
389+
end
390+
391+
test "encode_body combines url_context with grounding" do
392+
{:ok, model} = ReqLLM.model("google:gemini-2.0-flash")
393+
context = context_fixture()
394+
395+
mock_request = %Req.Request{
396+
options: [
397+
context: context,
398+
model: model.model,
399+
stream: false,
400+
google_grounding: %{enable: true},
401+
google_url_context: true
402+
]
403+
}
404+
405+
updated_request = Google.encode_body(mock_request)
406+
decoded = Jason.decode!(updated_request.body)
407+
408+
assert is_list(decoded["tools"])
409+
assert length(decoded["tools"]) == 2
410+
411+
tool_types = Enum.map(decoded["tools"], fn t -> Map.keys(t) end) |> List.flatten()
412+
assert "url_context" in tool_types
413+
assert "google_search" in tool_types
414+
end
415+
end
416+
308417
describe "response decoding" do
309418
test "decode_response handles non-streaming responses" do
310419
# Create a mock Google response

0 commit comments

Comments
 (0)