Jido AI Keyring
View SourceOverview
The Jido AI Keyring helps manage LLM API Keys. It provides a centralized system for managing environment variables, API keys, and configuration settings across different environments and execution contexts with a hierarchical, context-aware approach.
Key Features
- Hierarchical Configuration: Values are loaded with explicit precedence rules
- Process-Specific Overrides: Isolate configuration changes to specific processes with per-PID session values
- Runtime Configuration: Change settings without application restarts
- Default Fallbacks: Specify fallback values for missing configurations
- Consistent API Provider Integration: Seamless integration with various AI provider configurations
- Automatic Startup: Started automatically by
Jido.AI.Application
Conceptual Model
The Keyring implements a hierarchical lookup system with the following precedence (highest to lowest):
- Session Values: Process-specific overrides (stored in ETS)
- Environment Variables: System-wide environment settings (via Dotenvy)
- Application Environment: Configuration in your application
- Default Values: Fallbacks for missing configurations
Session Values → Environment Variables → Application Environment → Default Values
(highest) (lowest)
Basic Usage
Installation
The Keyring is automatically started by the Jido.AI.Application
module, so you don't need to add it to your application's supervision tree. The relevant implementation is:
# In Jido.AI.Application
defmodule Jido.AI.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Start the Keyring GenServer
Jido.AI.Keyring
]
opts = [strategy: :one_for_one, name: Jido.AI.Supervisor]
Supervisor.start_link(children, opts)
end
end
Retrieving Configuration Values
To retrieve values from the Keyring:
# Basic usage with default value
api_key = Jido.AI.Keyring.get(:openai_api_key, "default_key")
# Without a default (returns nil if not found)
model_name = Jido.AI.Keyring.get(:model_name)
# High-level API convenience function
api_key = Jido.AI.get(:anthropic_api_key)
Setting Session Values
Session values provide process-specific configuration overrides. Each process can have its own set of configuration values, which take precedence over environment values:
# Override a value for the current process only
Jido.AI.Keyring.set_session_value(:openai_api_key, "test_key_for_this_process")
# Using high-level API
Jido.AI.set_session_value(:anthropic_api_key, "my_session_key")
# Set a value for a specific process (not just the current one)
other_pid = spawn(fn -> receive do :ok -> :ok end end)
Jido.AI.Keyring.set_session_value(:openai_api_key, "process_specific_key", other_pid)
# Later in the same process
api_key = Jido.AI.Keyring.get(:openai_api_key) # Returns "test_key_for_this_process"
Clearing Session Values
Remove process-specific overrides when no longer needed:
# Clear a specific session value for the current process
Jido.AI.Keyring.clear_session_value(:openai_api_key)
# Clear a specific session value for another process
Jido.AI.Keyring.clear_session_value(:openai_api_key, other_pid)
# Clear all session values for the current process
Jido.AI.Keyring.clear_all_session_values()
# Clear all session values for another process
Jido.AI.Keyring.clear_all_session_values(other_pid)
# Using high-level API (always operates on current process)
Jido.AI.clear_session_value(:anthropic_api_key)
Jido.AI.clear_all_session_values()
Configuration Sources
Environment Variables with Dotenvy
The Keyring uses the Dotenvy library under the hood to load environment variables from multiple sources. It automatically loads variables from several locations in a specific order:
./envs/.env # Base environment file
./envs/.{environment}.env # Environment-specific (dev/test/prod)
./envs/.{environment}.overrides.env # Local overrides (not committed to source control)
System environment variables # OS-level environment variables
Dotenvy handles type conversion, with all values loaded as strings by default. For more complex types, use the application environment configuration.
Environment variables are converted to atoms by:
- Converting to lowercase
- Replacing non-alphanumeric characters with underscores
Example:
OPENAI_API_KEY=sk-123456789 → :openai_api_key
Here's how the implementation loads environment variables:
defp load_from_env do
env_dir_prefix = Path.expand("./envs/")
Dotenvy.source!([
Path.join(File.cwd!(), ".env"),
Path.absname(".env", env_dir_prefix),
Path.absname(".#{Mix.env()}.env", env_dir_prefix),
Path.absname(".#{Mix.env()}.overrides.env", env_dir_prefix),
System.get_env()
])
|> Enum.reduce(%{}, fn {key, value}, acc ->
atom_key = env_var_to_atom(key)
Map.put(acc, atom_key, value)
end)
end
Application Environment
Configure values in your config/config.exs
or environment-specific config files:
# In config/config.exs
config :jido_ai, :keyring, %{
openai_api_key: System.get_env("OPENAI_API_KEY"),
model_name: "gpt-4",
temperature: 0.7
}
Integration with AI Providers
The Keyring is designed to seamlessly work with various AI providers in the Jido ecosystem. It's a critical component for the Model
module and provider adapters:
defmodule MyAI do
alias Jido.AI.Model
def generate_response(prompt) do
# The API key is automatically retrieved from Keyring
{:ok, model} = Model.from({:anthropic, [model: "claude-3-opus"]})
# Use the model with automatic API key management
ChatClient.generate(model, prompt)
end
end
Provider Keys
The Keyring looks for provider-specific keys in the following formats:
- OpenAI:
:openai_api_key
orOPENAI_API_KEY
- Anthropic:
:anthropic_api_key
orANTHROPIC_API_KEY
- OpenRouter:
:openrouter_api_key
orOPENROUTER_API_KEY
- Cloudflare:
:cloudflare_api_key
orCLOUDFLARE_API_KEY
- Google:
:google_api_key
orGOOGLE_API_KEY
Advanced Usage
Process Isolation for Testing
Session values are isolated to the calling process, making them ideal for testing. The Keyring's implementation uses ETS tables to store process-specific values, indexed by the process PID:
defmodule MyTest do
use ExUnit.Case
alias Jido.AI.Keyring
setup do
# Override configuration for this test only
Keyring.set_session_value(:anthropic_api_key, "test_key")
Keyring.set_session_value(:model, "test-model-name")
on_exit(fn ->
# Clean up after the test
Keyring.clear_all_session_values()
end)
:ok
end
test "my feature with mocked API key" do
# Test code will use the session values
assert MyModule.process() == :expected_result
end
end
Under the hood, the session storage works by inserting values into an ETS table with the process PID as part of the key:
def set_session_value(server \\ @default_name, key, value, pid \\ self()) when is_atom(key) do
registry = GenServer.call(server, :get_registry)
:ets.insert(registry, {{pid, key}, value})
:ok
end
Named Instances
For more complex applications, you can run multiple Keyring instances:
# Start a custom Keyring instance
{:ok, _pid} = Jido.AI.Keyring.start_link(name: :custom_keyring, registry: :custom_registry)
# Use the custom instance
api_key = Jido.AI.Keyring.get(:custom_keyring, :openai_api_key)
Value Validation
Check if a configuration value is set and non-empty:
api_key = Jido.AI.Keyring.get(:openai_api_key)
if Jido.AI.Keyring.has_value?(api_key) do
# Proceed with API request
else
# Handle missing configuration
{:error, "OpenAI API key not configured"}
end
The Keyring provides a helper function to check if values are valid:
@spec has_value?(term()) :: boolean()
def has_value?(nil), do: false
def has_value?(""), do: false
def has_value?(value) when is_binary(value), do: true
def has_value?(_), do: false
Debugging Tips
List all available configuration keys:
Jido.AI.Keyring.list()
# => [:openai_api_key, :anthropic_api_key, :model_name, ...]
Compare environment and session values:
# Get the environment value directly
env_value = Jido.AI.Keyring.get_env_value(:openai_api_key)
# Get the session value
session_value = Jido.AI.Keyring.get_session_value(:openai_api_key)
# Get the effective value (session overrides environment)
effective_value = Jido.AI.Keyring.get(:openai_api_key)
Implementation Details
ETS Table for Session Storage
The Keyring uses ETS (Erlang Term Storage) for session values, providing:
- Efficient key-value lookups
- Process-specific storage
- Automatic cleanup when processes terminate
Loading from Environment Files
The implementation leverages Dotenvy for sophisticated environment file loading:
env_dir_prefix = Path.expand("./envs/")
Dotenvy.source!([
Path.join(File.cwd!(), ".env"),
Path.absname(".env", env_dir_prefix),
Path.absname(".#{Mix.env()}.env", env_dir_prefix),
Path.absname(".#{Mix.env()}.overrides.env", env_dir_prefix),
System.get_env()
])
Conversion of Environment Variables
Environment variables are automatically converted to atom keys for consistent access:
defp env_var_to_atom(env_var) do
env_var
|> String.downcase()
|> String.replace(~r/[^a-z0-9_]/, "_")
|> String.to_atom()
end
Common Questions
How does the Keyring handle environment variable types?
By default, all values are loaded as strings. For more complex types, use the application environment to define typed values:
# In config/config.exs
config :jido_ai, :keyring, %{
max_tokens: 2048, # integer
temperature: 0.7, # float
use_cache: true # boolean
}
Can I modify environment values at runtime?
The Keyring primarily focuses on reading environment values, but you can simulate updates using session values for the current process.
What happens if a process terminates?
Session values are automatically cleaned up when a process terminates, preventing memory leaks.
How does the Keyring handle multiple environments?
The Keyring loads environment-specific files based on the current Mix environment:
Path.absname(".#{Mix.env()}.env", env_dir_prefix)
This allows for different configurations in development, test, and production environments.
Common Questions
How does the Keyring handle environment variable types?
The Keyring uses Dotenvy under the hood, which loads values as strings by default. For typed values, use the application environment configuration:
# In config/config.exs
config :jido_ai, :keyring, %{
max_tokens: 2048, # integer
temperature: 0.7, # float
use_cache: true # boolean
}
What happens if a process terminates?
Session values are automatically cleaned up when a process terminates, as they're stored in an ETS table with the process PID as part of the key. This prevents memory leaks.
Can I get values for other processes?
Yes, the Keyring supports specifying a PID when getting session values:
# Get a value for another process
other_pid = spawn(fn -> receive do :ok -> :ok end end)
value = Jido.AI.Keyring.get_session_value(:my_key, other_pid)
Is the Keyring thread-safe?
Yes, the Keyring uses ETS for session storage and GenServer for environment values, both of which are thread-safe in Elixir's concurrent environment.