-
-
Notifications
You must be signed in to change notification settings - Fork 11
Agent Config Spec
This document specifies how API keys and base URLs are resolved in the dartantic_ai package, including the precedence hierarchy and interaction between different configuration methods.
The Agent configuration system follows a clear architectural principle: Agents are configured with providers, and providers manage their own API keys and base URLs. This separation of concerns ensures that:
- Agents focus on orchestration and tool execution
- Providers handle authentication and endpoint configuration
- Models inherit configuration from their providers
Agents can be created with either:
- A provider name string (e.g.,
'openai'or'openai:gpt-4') - A provider instance (e.g., a custom
OpenAIProviderwith specific configuration)
API keys are resolved at the provider level, not the agent level. The resolution order is:
-
Provider Instance apiKey Property - API keys set directly on provider instances take highest precedence
-
Agent.environment Map - Environment variables set via
Agent.environment -
System Environment Variable - System environment variables accessed via
Platform.environment -
No API Key
- Some providers (like Ollama) don't require API keys
- If a required API key is not found, an exception is thrown
flowchart TD
A[Agent Creation] --> B{Using provider name?}
B -->|Yes| C[Look up provider by name<br/>e.g. Agent('openai')]
B -->|No| D[Use existing provider instance<br/>e.g. Agent.forProvider(provider)]
C --> E[Provider constructor uses tryGetEnv()]
D --> F[Provider already constructed]
E --> G[Provider.createChatModel/<br/>Provider.createEmbeddingsModel]
F --> G
G --> H{Provider.apiKey exists?}
H -->|Yes| I[Use provider.apiKey]
H -->|No| J{Provider has apiKeyName?}
J -->|Yes| K[Model validates API key]
J -->|No| L[No API key needed]
K --> M{Agent.environment[apiKeyName]?}
M -->|Yes| N[Use it]
M -->|No| O{Platform.environment[apiKeyName]?}
O -->|Yes| P[Use it]
O -->|No| Q[Throw if required]
I --> R[Create model]
L --> R
N --> R
P --> R
style A fill:#f9f,stroke:#333,stroke-width:2px
style R fill:#9f9,stroke:#333,stroke-width:2px
style Q fill:#f99,stroke:#333,stroke-width:2px
note right of E: Provider constructors use tryGetEnv()<br/>to allow lazy initialization without throwing
note right of K: Model creation validates API key<br/>and throws if required but missing
Base URLs are resolved at the provider level, following the same principle as API keys:
- Provider Constructor Parameter - Base URLs set directly on provider instances
- Provider's Default Base URL - Each provider defines its own default base URL
- Model's Default Base URL - Each model class defines its own default base URL
flowchart TD
A[Provider.createChatModel/<br/>Provider.createEmbeddingsModel] --> B{Provider.baseUrl?}
B -->|Yes| C[Pass to model]
B -->|No| D[Pass null to model]
C --> E[Model Constructor]
D --> E
E --> F[Model uses baseUrl<br/>or knows its own default]
style A fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#9f9,stroke:#333,stroke-width:2px
Each provider has its own configuration that defines:
-
name: The canonical provider name (e.g., 'openai', 'anthropic') -
aliases: Alternative names for the provider (e.g., 'claude' for Anthropic) -
displayName: Human-readable name for UI display -
defaultModelNames: Map of default models by ModelKind (chat, embeddings) -
baseUrl: The API endpoint (nullable - uses model's default if not set) -
apiKeyName: The environment variable name for the API key (nullable - some providers like Ollama don't need API keys) -
caps: Set of capabilities (chat, embeddings, vision, etc.)
Providers can be configured with custom API keys and base URLs by creating custom instances with overrides.
Local providers like Ollama don't require API keys and can be configured with custom base URLs for remote servers.
Custom providers should extend the Provider base class and implement the required factory methods for creating models.
When creating a model through a provider:
flowchart TD
A[provider.createChatModel/<br/>createEmbeddingsModel] --> B[Provider resolves API key]
B --> C{provider.apiKeyName exists?}
C -->|Yes| D[apiKey = provider.apiKey ??<br/>tryGetEnv(provider.apiKeyName)]
C -->|No| E[apiKey = provider.apiKey]
D --> F[Pass to Model Constructor]
E --> F
F --> G[apiKey: resolved API key<br/>may still be null]
F --> H[baseUrl: provider.baseUrl<br/>may be null]
G --> I[Model Constructor]
H --> I
I --> J[Model may do additional resolution]
J --> K[Uses provided apiKey if not null]
J --> L[Otherwise calls getEnv(apiKeyName) as fallback]
style A fill:#f9f,stroke:#333,stroke-width:2px
style I fill:#bbf,stroke:#333,stroke-width:2px
Note: The createChatModel and createEmbeddingsModel methods do not accept apiKey or baseUrl
parameters. These are set at the provider level through the constructor. Provider constructors use
tryGetEnv() to allow lazy initialization, and API key validation happens at model creation time.
Providers can be discovered by:
-
Name:
Providers.get('openai') -
Alias:
Providers.get('claude')→ resolves to Anthropic -
Capabilities:
Providers.allWith({ProviderCaps.chatVision}) -
All Providers:
Providers.all
Each provider defines its own environment variable for API keys:
| Provider | apiKeyName | Example |
|---|---|---|
| OpenAI | OPENAI_API_KEY |
sk-... |
| Anthropic | ANTHROPIC_API_KEY |
sk-ant-... |
GEMINI_API_KEY |
... |
|
| Mistral | MISTRAL_API_KEY |
... |
| Cohere | COHERE_API_KEY |
... |
| OpenRouter | OPENROUTER_API_KEY |
sk-or-... |
| Together | TOGETHER_API_KEY |
... |
| Ollama | null |
No API key needed |
When using a custom provider instance with an apiKey or baseUrl set, it overrides all other sources:
// Provider's apiKey takes precedence over Agent.environment
Agent.environment['OPENAI_API_KEY'] = 'sk-env-key';
final provider = OpenAIProvider(
apiKey: 'sk-provider-key',
// ... other params
);
final model = provider.createChatModel(); // Uses 'sk-provider-key'Agent.environment takes precedence over system environment variables when
looked up via tryGetEnv:
// System env: OPENAI_API_KEY=sk-system-key
Agent.environment['OPENAI_API_KEY'] = 'sk-agent-env-key';
final agent = Agent('openai'); // Uses 'sk-agent-env-key'However, provider instance apiKey takes precedence over both:
// System env: OPENAI_API_KEY=sk-system-key
Agent.environment['OPENAI_API_KEY'] = 'sk-agent-env-key';
final provider = OpenAIProvider(
apiKey: 'sk-provider-key',
// ... other params
);
// Uses 'sk-provider-key', ignoring both environment sourcesEach provider may have different apiKeyName values:
// OpenAI looks for OPENAI_API_KEY
// Anthropic looks for ANTHROPIC_API_KEY
// Mistral looks for MISTRAL_API_KEYEmpty strings in provider configuration are treated as "not provided":
final provider = OpenAIProvider(
apiKey: '', // Will fall back to environment lookup
// ... other params
);Null values are treated the same as missing parameters:
final provider = OpenAIProvider(
apiKey: null, // Same as not providing apiKey
// ... other params
);- Only
Agent.environmentis available (no system environment) - Must use
Agent.environmentor direct parameters
- Both
Agent.environmentand system environment available - System environment accessed via
Platform.environment
// Throws when trying to create a model, not when creating the agent
final agent = Agent('openai'); // OK - provider created lazily
final result = await agent.send('Hello'); // Throws: OPENAI_API_KEY is required- No validation at configuration time
- Errors occur during API calls
Providers must implement both chat and embeddings model creation:
Must accept configuration and resolve API key from environment if needed:
Agent.environment['OPENAI_API_KEY'] = 'sk-env-456';
final agent = Agent('openai:gpt-4');
// Uses: apiKey='sk-env-456', baseUrl='https://api.openai.com/v1'// Create a custom provider with specific configuration
final provider = OpenAIProvider(
name: 'openai',
displayName: 'OpenAI',
defaultModelNames: {
ModelKind.chat: 'gpt-4o',
ModelKind.embeddings: 'text-embedding-3-small',
},
apiKeyName: 'OPENAI_API_KEY',
apiKey: 'sk-custom-key',
baseUrl: Uri.parse('https://proxy.company.com/v1'),
caps: Providers.openai.caps,
);
final agent = Agent.forProvider(provider);
// Uses: apiKey='sk-custom-key', baseUrl='https://proxy.company.com/v1'// Provider instance with custom baseUrl, apiKey from environment
Agent.environment['OPENAI_API_KEY'] = 'sk-env-789';
final provider = OpenAIProvider(
name: 'openai',
displayName: 'OpenAI',
defaultModelNames: {
ModelKind.chat: 'gpt-4o',
ModelKind.embeddings: 'text-embedding-3-small',
},
apiKeyName: 'OPENAI_API_KEY',
baseUrl: Uri.parse('https://custom.api.com'),
caps: Providers.openai.caps,
);
final agent = Agent.forProvider(provider);
// Uses: apiKey='sk-env-789' (from environment), baseUrl='https://custom.api.com'// Create a custom provider instance with overrides
final provider = OpenAIProvider(
name: 'openai',
displayName: 'OpenAI',
defaultModelNames: {
ModelKind.chat: 'gpt-4o',
ModelKind.embeddings: 'text-embedding-3-small',
},
apiKeyName: 'OPENAI_API_KEY',
apiKey: 'sk-provider-key', // Override API key
baseUrl: Uri.parse('https://provider.api.com'), // Override base URL
caps: Providers.openai.caps,
);
final chatModel = provider.createChatModel();
// Uses: apiKey='sk-provider-key', baseUrl='https://provider.api.com'// Create provider with custom API key and base URL
final provider = OpenAIProvider(
name: 'openai',
displayName: 'OpenAI',
defaultModelNames: {
ModelKind.chat: 'gpt-4o',
ModelKind.embeddings: 'text-embedding-3-small',
},
apiKeyName: 'OPENAI_API_KEY',
apiKey: 'sk-custom-key',
baseUrl: Uri.parse('https://custom.api.com'),
caps: Providers.openai.caps,
);
// List models will use the provider's apiKey and baseUrl
await for (final model in provider.listModels()) {
print('${model.id} supports ${model.kinds}');
}Tests must verify:
- Each level of the precedence hierarchy
- Interaction between different configuration methods
- Cross-platform behavior differences
- Error cases for missing required configuration
- Empty string and null handling
- Provider-specific apiKeyName resolution
- Agent creation with provider names vs provider instances
For the architectural separation of concerns between Agent, Provider, and Model layers, see the Separation of Concerns section in the Unified Provider Architecture specification.
- For production use: Create custom provider instances with explicit configuration
- For development: Use environment variables with static provider instances
- For testing: Use Agent.environment to avoid system environment dependencies
- For multi-tenant: Create separate provider instances per tenant with different API keys