Packages
nous
0.16.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/providers/gemini.ex
defmodule Nous.Providers.Gemini do
@moduledoc """
Google Gemini provider implementation.
Supports Gemini models via the Google AI Generative Language API.
## Usage
# Basic usage
{:ok, response} = Nous.Providers.Gemini.chat(%{
model: "gemini-2.0-flash-exp",
contents: [%{"role" => "user", "parts" => [%{"text" => "Hello"}]}]
})
# With system instruction
{:ok, response} = Nous.Providers.Gemini.chat(%{
model: "gemini-2.0-flash-exp",
systemInstruction: %{"parts" => [%{"text" => "You are helpful."}]},
contents: [%{"role" => "user", "parts" => [%{"text" => "Hello"}]}],
generationConfig: %{"temperature" => 0.7, "maxOutputTokens" => 1024}
})
# Streaming
{:ok, stream} = Nous.Providers.Gemini.chat_stream(params)
Enum.each(stream, fn event -> IO.inspect(event) end)
## Configuration
# In config.exs
config :nous, :gemini,
api_key: "AIza...",
base_url: "https://generativelanguage.googleapis.com/v1beta" # optional
## Note on Authentication
Unlike OpenAI/Anthropic which use headers, Gemini uses query parameter auth:
`?key=API_KEY`
## Thinking (Gemini 2.5/3.x)
For models that support extended thinking, pass `:thinking_config` in
`default_settings`. Both the Elixir shape (snake_case atoms) and the native
Vertex shape (camelCase strings) are accepted:
# Elixir shape (recommended)
Nous.new("gemini:gemini-2.5-pro",
default_settings: %{
thinking_config: %{thinking_budget: 1024, include_thoughts: true}
}
)
# Native Vertex shape
Nous.new("gemini:gemini-2.5-pro",
default_settings: %{
thinking_config: %{"thinkingBudget" => 1024, "includeThoughts" => true}
}
)
When `include_thoughts: true`, thought summaries arrive as
`Message.reasoning_content`. For tool-using thinking models, Vertex emits a
`thoughtSignature` on each tool call; Nous preserves it in
`tool_call["metadata"]["thought_signature"]` and echoes it back on the next
turn automatically — required for multi-turn thinking + tool loops to keep
working.
See https://ai.google.dev/gemini-api/docs/thinking for the Gemini-side docs
and https://cloud.google.com/vertex-ai/generative-ai/docs/thinking for the
Vertex equivalent.
"""
use Nous.Provider,
id: :gemini,
default_base_url: "https://generativelanguage.googleapis.com/v1beta",
default_env_key: "GOOGLE_AI_API_KEY"
alias Nous.Providers.HTTP
# Single source of truth for the provider's HTTP receive timeout. The actual
# timeout used at request time is `model.receive_timeout` (set via
# `Model.parse(..., receive_timeout: ms)`), which flows through
# `build_provider_opts/1` as the `:timeout` option. This constant is only the
# safety-net default when `chat/2` or `chat_stream/2` is called directly
# without a model-level value.
@default_timeout 120_000
# Override to convert from generic format to Gemini's format
defp build_request_params(model, messages, settings) do
merged_settings = Map.merge(model.default_settings, settings)
# Convert messages to Gemini format: {system_prompt, contents}
{system_prompt, contents} = Nous.Messages.to_provider_format(messages, :gemini)
params = %{"model" => model.model, "contents" => contents}
params =
if system_prompt do
Map.put(params, "systemInstruction", %{"parts" => [%{"text" => system_prompt}]})
else
params
end
# Map generic settings to Gemini's generationConfig
generation_config =
%{}
|> maybe_put("temperature", merged_settings[:temperature])
|> maybe_put("maxOutputTokens", merged_settings[:max_tokens])
|> maybe_put("topP", merged_settings[:top_p])
|> maybe_put("topK", merged_settings[:top_k])
|> maybe_put("seed", merged_settings[:seed])
|> maybe_put("candidateCount", merged_settings[:candidate_count])
|> maybe_put("presencePenalty", merged_settings[:presence_penalty])
|> maybe_put("frequencyPenalty", merged_settings[:frequency_penalty])
|> maybe_put("responseModalities", merged_settings[:response_modalities])
|> maybe_put("stopSequences", merged_settings[:stop_sequences] || merged_settings[:stop])
|> maybe_put(
"thinkingConfig",
Nous.Messages.Gemini.normalize_thinking_config(merged_settings[:thinking_config])
)
|> Map.merge(Nous.Messages.Gemini.json_config_for_settings(merged_settings))
# Merge any explicit generationConfig from settings
generation_config =
Map.merge(generation_config, merged_settings[:generationConfig] || %{})
params =
if map_size(generation_config) > 0 do
Map.put(params, "generationConfig", generation_config)
else
params
end
params =
params
|> maybe_put(
"tools",
Nous.Messages.Gemini.build_tools(
merged_settings[:tools] || [],
merged_settings[:native_tools]
)
)
|> maybe_put(
"safetySettings",
Nous.Messages.Gemini.normalize_safety_settings(merged_settings[:safety_settings])
)
|> maybe_put("toolConfig", resolve_tool_config(merged_settings))
|> maybe_put("cachedContent", merged_settings[:cached_content])
maybe_merge_extra_body(params, merged_settings[:extra_body])
end
defp resolve_tool_config(settings) do
settings[:tool_config] ||
Nous.Messages.Gemini.normalize_tool_choice(settings[:tool_choice])
end
@impl Nous.Provider
def chat(params, opts \\ []) do
model = Map.get(params, "model") || Map.get(params, :model) || "gemini-2.0-flash-exp"
api_key = api_key(opts)
url = build_url(base_url(opts), model, api_key, :generate)
headers = build_headers()
timeout = Keyword.get(opts, :timeout, @default_timeout)
# Remove model from params (it's in the URL)
body = params |> Map.delete("model") |> Map.delete(:model)
HTTP.post(url, body, headers, timeout: timeout)
end
@impl Nous.Provider
def chat_stream(params, opts \\ []) do
model = Map.get(params, "model") || Map.get(params, :model) || "gemini-2.0-flash-exp"
api_key = api_key(opts)
url = build_url(base_url(opts), model, api_key, :stream)
headers = build_headers()
timeout = Keyword.get(opts, :timeout, @default_timeout)
finch_name = Keyword.get(opts, :finch_name, Nous.Finch)
# Remove model from params (it's in the URL)
body = params |> Map.delete("model") |> Map.delete(:model)
HTTP.stream(url, body, headers,
timeout: timeout,
finch_name: finch_name,
stream_parser: Nous.Providers.HTTP.JSONArrayParser
)
end
# Build URL with model and API key in query params
defp build_url(base_url, model, api_key, :generate) do
"#{base_url}/models/#{model}:generateContent?key=#{api_key}"
end
defp build_url(base_url, model, api_key, :stream) do
"#{base_url}/models/#{model}:streamGenerateContent?key=#{api_key}"
end
# Auth header is omitted because the API key lives in the URL query string.
defp build_headers, do: HTTP.json_headers()
end