Skip to content

Commit 5bcade2

Browse files
committed
Release v0.12.0: Gemini authentication precedence and Live session API key support
- Update default Gemini auth resolution to prioritize application configuration (config :gemini_ex, api_key: ...) over the GEMINI_API_KEY environment variable. - Modify Gemini.Client.HTTP to implicitly select Gemini auth when an api_key is passed, even if the :auth option is omitted. - Enable Live session API key propagation by updating Gemini.Live.Session to pass per-session credentials through to WebSocket connections. - Ensure WebSocket auth resolution favors connection-scoped keys before falling back to global configuration. - Enhance documentation for the updated precedence order and session-scoped override paths. - Add regression tests for config precedence, implicit HTTP auth selection, and proper propagation of Live session API keys.
1 parent 5dfbccc commit 5bcade2

13 files changed

Lines changed: 250 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.12.0] - 2026-04-02
11+
12+
### Changed
13+
- **Gemini API key precedence**: default Gemini auth now prefers application configuration over `GEMINI_API_KEY`, making `config :gemini_ex, api_key: ...` the explicit default when both are present
14+
- **Published auth docs**: README, the Live API guide, and the authentication guide now document the new precedence order and the session-scoped Live override path
15+
16+
### Fixed
17+
- **Implicit Gemini override selection**: `Gemini.Client.HTTP` now treats a request-scoped `api_key:` as sufficient to select Gemini auth even when `auth:` is omitted
18+
- **Live session API key overrides**: `Gemini.Live.Session.start_link/1` now propagates `api_key:` through to `Gemini.Client.WebSocket`, so per-session Gemini credentials do not depend on global config
19+
- **WebSocket auth resolution**: Gemini Live connections now prefer a connection-scoped API key before falling back to global configuration, with redacted logging preserved
20+
21+
### Tests
22+
- Added focused regression coverage for config precedence, implicit HTTP auth selection, and Live session/WebSocket API key propagation
23+
1024
## [0.11.1] - 2026-03-27
1125

1226
### Added
@@ -1671,8 +1685,6 @@ similarity = ContentEmbedding.cosine_similarity(normalized, other_normalized)
16711685
- Added `convert_thinking_config_to_api/1` to properly convert field names to camelCase
16721686
- `GenerationConfig.ThinkingConfig` is now a typed struct (not plain map)
16731687

1674-
## [Unreleased]
1675-
16761688
## [0.2.1] - 2025-08-08
16771689

16781690
### Added
@@ -2056,7 +2068,8 @@ config :gemini_ex,
20562068
- Minimal latency overhead
20572069
- Concurrent request processing
20582070

2059-
[Unreleased]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.11.1...HEAD
2071+
[Unreleased]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.12.0...HEAD
2072+
[0.12.0]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.11.1...v0.12.0
20602073
[0.11.1]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.11.0...v0.11.1
20612074
[0.11.0]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.10.0...v0.11.0
20622075
[0.10.0]: https://github.com/nshkrdotcom/gemini_ex/compare/v0.9.1...v0.10.0

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Add `gemini` to your list of dependencies in `mix.exs`:
5959
```elixir
6060
def deps do
6161
[
62-
{:gemini_ex, "~> 0.11.1"}
62+
{:gemini_ex, "~> 0.12.0"}
6363
]
6464
end
6565
```
@@ -83,6 +83,8 @@ Or set the environment variable:
8383
export GEMINI_API_KEY="your_api_key_here"
8484
```
8585

86+
For default Gemini auth resolution, `config :gemini_ex, api_key: ...` now takes precedence over `GEMINI_API_KEY`. Narrower overrides still win: pass `api_key:` directly on a request or on `Gemini.Live.Session.start_link/1` for session-scoped credentials.
87+
8688
### Simple Content Generation
8789

8890
```elixir
@@ -1377,15 +1379,25 @@ The examples follow a consistent pattern:
13771379

13781380
### Gemini API Key (Recommended for Development)
13791381

1380-
```elixir
1381-
# Environment variable (recommended)
1382+
Default Gemini key precedence is:
1383+
- Per-request or per-session `api_key:`
1384+
- Application config: `config :gemini_ex, api_key: ...`
1385+
- Environment variable: `GEMINI_API_KEY`
1386+
1387+
```bash
13821388
export GEMINI_API_KEY="your_api_key"
1389+
```
13831390

1384-
# Application config
1391+
```elixir
13851392
config :gemini_ex, api_key: "your_api_key"
13861393

1387-
# Per-request override
13881394
Gemini.generate("Hello", api_key: "specific_key")
1395+
1396+
{:ok, session} =
1397+
Gemini.Live.Session.start_link(
1398+
model: Gemini.Live.Models.resolve(:text),
1399+
api_key: "session_specific_key"
1400+
)
13891401
```
13901402

13911403
### Vertex AI (Recommended for Production)

guides/AUTHENTICATION_SYSTEM.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,16 @@ graph TD
103103

104104
* **Purpose**: To detect and load the *default* authentication credentials for the application.
105105
* **Mechanism**: The `Gemini.Config.auth_config/0` function establishes a strict priority order for finding credentials:
106-
1. **Environment Variables**: Checks for `GEMINI_API_KEY`, `VERTEX_SERVICE_ACCOUNT`/`VERTEX_JSON_FILE`, and ADC vars such as `GOOGLE_APPLICATION_CREDENTIALS_JSON` and `GOOGLE_APPLICATION_CREDENTIALS`. This is the highest priority.
107-
2. **Application Config**: If no environment variables are found, it checks for `:gemini, :auth` or `:gemini, :api_key` in the Elixir application environment (e.g., `config/runtime.exs`).
106+
1. **Application Config**: Checks `:gemini, :auth`, `:gemini_ex, :auth`, and Gemini API keys configured via `:gemini` or `:gemini_ex`.
107+
2. **Environment Variables**: Falls back to `GEMINI_API_KEY`, `VERTEX_SERVICE_ACCOUNT`/`VERTEX_JSON_FILE`, and ADC vars such as `GOOGLE_APPLICATION_CREDENTIALS_JSON` and `GOOGLE_APPLICATION_CREDENTIALS`.
108+
3. **Per-request / per-session overrides**: Higher layers can still override the resolved defaults by passing `:api_key`, `:project_id`, or other auth opts directly on a request or Live session.
108109
* **Code Example (`gemini/config.ex`):**
109110
```elixir
110111
def auth_config do
111-
cond do
112-
gemini_api_key() ->
113-
%{type: :gemini, credentials: %{api_key: gemini_api_key()}}
114-
# ... other checks for Vertex, etc.
115-
true ->
116-
Application.get_env(:gemini, :auth) # ...
117-
end
112+
auth_config_from_app() ||
113+
gemini_auth_config_from_env() ||
114+
vertex_access_token_config_from_env() ||
115+
vertex_service_account_config_from_env()
118116
end
119117
```
120118

@@ -239,6 +237,8 @@ The library will automatically detect and use these variables.
239237

240238
Configure the library in your project's `config/runtime.exs` file. This is the canonical approach for managing secrets within an Elixir application.
241239
240+
When both are present for Gemini auth, application config now takes precedence over `GEMINI_API_KEY`.
241+
242242
* **For Gemini API Key:**
243243
```elixir
244244
# config/runtime.exs
@@ -275,15 +275,25 @@ When using `Gemini.Live.Session`, authentication is handled automatically using
275275
```elixir
276276
alias Gemini.Live.{Models, Session}
277277
278-
# Uses environment variable or application config
278+
# Uses application config or environment variables
279279
{:ok, session} = Session.start_link(
280280
model: Models.resolve(:text),
281281
auth: :gemini, # or :vertex_ai
282282
generation_config: %{response_modalities: ["TEXT"]}
283283
)
284284
```
285285
286-
The WebSocket connection URL includes the API key as a query parameter (which is automatically redacted in logs for security).
286+
You can also scope a Gemini API key to a single Live session:
287+
288+
```elixir
289+
{:ok, session} = Gemini.Live.Session.start_link(
290+
model: Gemini.Live.Models.resolve(:text),
291+
api_key: "session-specific-key",
292+
generation_config: %{response_modalities: ["TEXT"]}
293+
)
294+
```
295+
296+
The WebSocket connection URL includes the API key as a query parameter when Gemini auth is used, and that query parameter is automatically redacted in logs for security.
287297
288298
#### Client-Side Authentication with Ephemeral Tokens
289299

guides/live_api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ model = Models.resolve(:text)
286286
# Session is now ready for messages
287287
```
288288

289+
For Gemini sessions, you can also pass `api_key:` directly to `Session.start_link/1`. When `api_key:` is present and `auth:` is omitted, the session uses Gemini auth for that connection only.
290+
291+
```elixir
292+
{:ok, session} = Gemini.Live.Session.start_link(
293+
model: Gemini.Live.Models.resolve(:text),
294+
api_key: "session-specific-key",
295+
generation_config: %{response_modalities: ["TEXT"]}
296+
)
297+
```
298+
289299
### Full Configuration Options
290300

291301
```elixir
@@ -297,6 +307,7 @@ alias Gemini.Live.Models
297307

298308
# Authentication
299309
auth: :gemini, # or :vertex_ai
310+
api_key: "session-specific-key", # optional per-session Gemini override when using Gemini auth
300311
project_id: "your-project", # required for :vertex_ai
301312
location: "us-central1", # optional, default: "us-central1"
302313
api_version: "v1alpha",

lib/gemini/client/http.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ defmodule Gemini.Client.HTTP do
2424
alias Gemini.RateLimiter
2525
alias Gemini.Telemetry
2626

27+
@vertex_auth_override_keys [
28+
:project_id,
29+
:location,
30+
:access_token,
31+
:service_account_key,
32+
:service_account,
33+
:service_account_data,
34+
:quota_project_id
35+
]
36+
2737
@doc """
2838
Make a GET request using the configured authentication.
2939
"""
@@ -142,7 +152,7 @@ defmodule Gemini.Client.HTTP do
142152

143153
@spec resolve_auth_config(keyword()) :: Config.auth_config() | nil
144154
defp resolve_auth_config(opts) when is_list(opts) do
145-
case normalize_auth_strategy(Keyword.get(opts, :auth)) do
155+
case normalize_auth_strategy(Keyword.get(opts, :auth)) || infer_auth_strategy(opts) do
146156
nil ->
147157
Config.auth_config()
148158

@@ -160,6 +170,14 @@ defmodule Gemini.Client.HTTP do
160170
defp normalize_auth_strategy(:vertex_ai), do: :vertex_ai
161171
defp normalize_auth_strategy(_), do: nil
162172

173+
defp infer_auth_strategy(opts) do
174+
cond do
175+
Keyword.has_key?(opts, :api_key) -> :gemini
176+
Enum.any?(@vertex_auth_override_keys, &Keyword.has_key?(opts, &1)) -> :vertex_ai
177+
true -> nil
178+
end
179+
end
180+
163181
defp apply_auth_overrides(credentials, :gemini, opts) do
164182
credentials
165183
|> maybe_put_cred(:api_key, Keyword.get(opts, :api_key))

lib/gemini/client/websocket.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ defmodule Gemini.Client.WebSocket do
7070
auth_strategy: auth_strategy() | nil,
7171
status: connection_status(),
7272
model: String.t() | nil,
73+
api_key: String.t() | nil,
7374
project_id: String.t() | nil,
7475
location: String.t() | nil,
7576
api_version: String.t(),
@@ -98,6 +99,7 @@ defmodule Gemini.Client.WebSocket do
9899
:stream_ref,
99100
:auth_strategy,
100101
:model,
102+
:api_key,
101103
:project_id,
102104
:location,
103105
status: :connecting,
@@ -187,6 +189,7 @@ defmodule Gemini.Client.WebSocket do
187189
def connect(auth_strategy, opts \\ []) do
188190
start_time = System.monotonic_time()
189191
model = Keyword.fetch!(opts, :model)
192+
api_key = Keyword.get(opts, :api_key)
190193
project_id = Keyword.get(opts, :project_id)
191194
location = Keyword.get(opts, :location, "us-central1")
192195
api_version = Keyword.get(opts, :api_version, "v1beta")
@@ -201,6 +204,7 @@ defmodule Gemini.Client.WebSocket do
201204
conn = %__MODULE__{
202205
auth_strategy: auth_strategy,
203206
model: model,
207+
api_key: api_key,
204208
project_id: project_id,
205209
location: location,
206210
api_version: api_version,
@@ -592,6 +596,11 @@ defmodule Gemini.Client.WebSocket do
592596
end
593597

594598
@spec get_auth_params(t()) :: {:ok, map()} | {:error, term()}
599+
defp get_auth_params(%__MODULE__{auth_strategy: :gemini, api_key: api_key})
600+
when is_binary(api_key) and api_key != "" do
601+
{:ok, %{api_key: api_key}}
602+
end
603+
595604
defp get_auth_params(%__MODULE__{auth_strategy: :gemini}) do
596605
case Config.api_key() do
597606
nil -> {:error, :no_api_key}

lib/gemini/config.ex

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ defmodule Gemini.Config do
229229
:gemini ->
230230
%{
231231
auth_type: :gemini,
232-
api_key: gemini_api_key() || Application.get_env(:gemini_ex, :api_key),
232+
api_key: api_key(),
233233
model: default_model()
234234
}
235235

@@ -254,11 +254,11 @@ defmodule Gemini.Config do
254254
end
255255

256256
@doc """
257-
Detect authentication type based on environment variables.
257+
Detect authentication type based on configured Gemini or Vertex credentials.
258258
"""
259259
def detect_auth_type do
260260
cond do
261-
gemini_api_key() -> :gemini
261+
api_key() -> :gemini
262262
vertex_project_id() && vertex_project_id() != "" -> :vertex
263263
# default
264264
true -> :gemini
@@ -291,24 +291,23 @@ defmodule Gemini.Config do
291291
292292
Returns a map with the authentication type and credentials.
293293
Priority order:
294-
1. Environment variables
295-
2. Application configuration
296-
3. Default to Gemini with API key
294+
1. Application configuration
295+
2. Environment variables
297296
"""
298297
@spec auth_config() :: auth_config() | nil
299298
def auth_config do
300-
gemini_auth_config_from_env() ||
299+
auth_config_from_app() ||
300+
gemini_auth_config_from_env() ||
301301
vertex_access_token_config_from_env() ||
302-
vertex_service_account_config_from_env() ||
303-
auth_config_from_app()
302+
vertex_service_account_config_from_env()
304303
end
305304

306305
@doc """
307-
Get the API key from environment or application config.
306+
Get the API key from application config or environment.
308307
(Legacy function for backward compatibility)
309308
"""
310309
def api_key do
311-
gemini_api_key() || Application.get_env(:gemini_ex, :api_key)
310+
Application.get_env(:gemini_ex, :api_key) || gemini_api_key()
312311
end
313312

314313
defp gemini_auth_config_from_env do

lib/gemini/live/session.ex

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,15 @@ defmodule Gemini.Live.Session do
298298
@impl true
299299
def init(opts) do
300300
model = Keyword.fetch!(opts, :model)
301-
auth = Keyword.get(opts, :auth, detect_auth_strategy())
301+
auth = Keyword.get(opts, :auth, detect_auth_strategy(opts))
302302

303303
state = %{
304304
websocket: nil,
305305
status: :disconnected,
306306
config: %{
307307
model: model,
308308
auth: auth,
309+
api_key: Keyword.get(opts, :api_key),
309310
project_id: Keyword.get(opts, :project_id),
310311
location: Keyword.get(opts, :location, "us-central1"),
311312
generation_config: Keyword.get(opts, :generation_config),
@@ -476,6 +477,7 @@ defmodule Gemini.Live.Session do
476477
defp do_connect(state) do
477478
base_opts = [
478479
model: state.config.model,
480+
api_key: state.config.api_key,
479481
project_id: state.config.project_id,
480482
location: state.config.location
481483
]
@@ -941,9 +943,10 @@ defmodule Gemini.Live.Session do
941943
@spec default_callback(term()) :: :ok
942944
defp default_callback(_), do: :ok
943945

944-
@spec detect_auth_strategy() :: :gemini | :vertex_ai
945-
defp detect_auth_strategy do
946+
@spec detect_auth_strategy(keyword()) :: :gemini | :vertex_ai
947+
defp detect_auth_strategy(opts) do
946948
cond do
949+
Keyword.get(opts, :api_key) -> :gemini
947950
Config.api_key() -> :gemini
948951
Config.get_auth_config(:vertex_ai)[:project_id] -> :vertex_ai
949952
true -> :gemini

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Gemini.MixProject do
22
use Mix.Project
33

4-
@version "0.11.1"
4+
@version "0.12.0"
55
@source_url "https://github.com/nshkrdotcom/gemini_ex"
66

77
def project do

0 commit comments

Comments
 (0)