diff --git a/agent-schema.json b/agent-schema.json
index ce8598912..ed7fba093 100644
--- a/agent-schema.json
+++ b/agent-schema.json
@@ -77,11 +77,21 @@
"definitions": {
"ProviderConfig": {
"type": "object",
- "description": "Configuration for a custom model provider. Can be used for custom gateways",
+ "description": "Configuration for a model provider. Defines reusable defaults that models can inherit by referencing the provider name. Supports any provider type (openai, anthropic, google, amazon-bedrock, etc.).",
"properties": {
+ "provider": {
+ "type": "string",
+ "description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, azure, xai, ollama, mistral, etc.).",
+ "examples": [
+ "openai",
+ "anthropic",
+ "google",
+ "amazon-bedrock"
+ ]
+ },
"api_type": {
"type": "string",
- "description": "The API schema type to use. Determines which API schema to use.",
+ "description": "The API schema type to use. Only applicable for OpenAI-compatible providers.",
"enum": [
"openai_chatcompletions",
"openai_responses"
@@ -94,7 +104,7 @@
},
"base_url": {
"type": "string",
- "description": "Base URL for the provider's API endpoint (required)",
+ "description": "Base URL for the provider's API endpoint. Required for OpenAI-compatible providers, optional for native providers.",
"format": "uri",
"examples": [
"https://router.example.com/v1"
@@ -102,15 +112,67 @@
},
"token_key": {
"type": "string",
- "description": "Environment variable name containing the API token. If not set, requests will be sent without authentication.",
+ "description": "Environment variable name containing the API token. If not set, requests will use the default token for the provider type.",
"examples": [
- "CUSTOM_PROVIDER_API_KEY"
+ "CUSTOM_PROVIDER_API_KEY",
+ "ANTHROPIC_API_KEY"
+ ]
+ },
+ "temperature": {
+ "type": "number",
+ "description": "Default sampling temperature for models using this provider.",
+ "minimum": 0,
+ "maximum": 2
+ },
+ "max_tokens": {
+ "type": "integer",
+ "description": "Default maximum number of tokens for models using this provider."
+ },
+ "top_p": {
+ "type": "number",
+ "description": "Default top-p (nucleus) sampling parameter.",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "frequency_penalty": {
+ "type": "number",
+ "description": "Default frequency penalty.",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "presence_penalty": {
+ "type": "number",
+ "description": "Default presence penalty.",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "parallel_tool_calls": {
+ "type": "boolean",
+ "description": "Whether to enable parallel tool calls by default."
+ },
+ "provider_opts": {
+ "type": "object",
+ "description": "Provider-specific options passed through to the underlying client.",
+ "additionalProperties": true
+ },
+ "track_usage": {
+ "type": "boolean",
+ "description": "Whether to track token usage by default."
+ },
+ "thinking_budget": {
+ "description": "Default reasoning effort/budget for models using this provider. Can be an integer token count or a string effort level.",
+ "oneOf": [
+ {
+ "type": "integer",
+ "description": "Token budget for reasoning"
+ },
+ {
+ "type": "string",
+ "description": "Effort level (e.g., \"low\", \"medium\", \"high\", \"none\", \"adaptive\")"
+ }
]
}
},
- "required": [
- "base_url"
- ],
"additionalProperties": false
},
"AgentConfig": {
@@ -359,7 +421,7 @@
"cooldown": {
"type": "string",
"description": "Duration to stick with a successful fallback model before retrying the primary. Only applies after a non-retryable error (e.g., 429 rate limit). Use Go duration format (e.g., '1m', '30s', '2m30s'). Default is '1m'.",
- "pattern": "^([0-9]+(ns|us|µs|ms|s|m|h))+$",
+ "pattern": "^([0-9]+(ns|us|\u00b5s|ms|s|m|h))+$",
"default": "1m",
"examples": [
"1m",
@@ -758,7 +820,7 @@
},
"instruction": {
"type": "string",
- "description": "Custom instruction for this MCP server's tools. By default, setting this field replaces the toolset's built-in instructions entirely. To enrich (rather than replace) the original instructions, include the placeholder {ORIGINAL_INSTRUCTIONS} in your text — it will be substituted with the toolset's built-in instructions at runtime. For example: '{ORIGINAL_INSTRUCTIONS}\nAlways prefer JSON output.' will prepend the original instructions and append your extra guidance."
+ "description": "Custom instruction for this MCP server's tools. By default, setting this field replaces the toolset's built-in instructions entirely. To enrich (rather than replace) the original instructions, include the placeholder {ORIGINAL_INSTRUCTIONS} in your text \u2014 it will be substituted with the toolset's built-in instructions at runtime. For example: '{ORIGINAL_INSTRUCTIONS}\nAlways prefer JSON output.' will prepend the original instructions and append your extra guidance."
},
"name": {
"type": "string",
@@ -874,7 +936,7 @@
},
"instruction": {
"type": "string",
- "description": "Custom instruction for this toolset. By default, setting this field replaces the toolset's built-in instructions entirely. To enrich (rather than replace) the original instructions, include the placeholder {ORIGINAL_INSTRUCTIONS} in your text — it will be substituted with the toolset's built-in instructions at runtime. For example: '{ORIGINAL_INSTRUCTIONS}\nAlways prefer JSON output.' will prepend the original instructions and append your extra guidance."
+ "description": "Custom instruction for this toolset. By default, setting this field replaces the toolset's built-in instructions entirely. To enrich (rather than replace) the original instructions, include the placeholder {ORIGINAL_INSTRUCTIONS} in your text \u2014 it will be substituted with the toolset's built-in instructions at runtime. For example: '{ORIGINAL_INSTRUCTIONS}\nAlways prefer JSON output.' will prepend the original instructions and append your extra guidance."
},
"toon": {
"type": "string",
diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml
index b5f99b49a..b48b0b14b 100644
--- a/docs/_data/nav.yml
+++ b/docs/_data/nav.yml
@@ -119,7 +119,7 @@
url: /providers/minimax/
- title: Local Models
url: /providers/local/
- - title: Custom Providers
+ - title: Provider Definitions
url: /providers/custom/
- section: Guides
diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md
index da3ffc2d5..b3bb1885f 100644
--- a/docs/configuration/models/index.md
+++ b/docs/configuration/models/index.md
@@ -189,3 +189,30 @@ models:
```
See [Local Models]({{ '/providers/local/' | relative_url }}) for more examples of custom endpoints.
+
+## Inheriting from Provider Definitions
+
+Models can reference a named provider to inherit shared defaults. Model-level settings always take precedence:
+
+```yaml
+providers:
+ my_anthropic:
+ provider: anthropic
+ token_key: MY_ANTHROPIC_KEY
+ max_tokens: 16384
+ thinking_budget: high
+ temperature: 0.5
+
+models:
+ claude:
+ provider: my_anthropic
+ model: claude-sonnet-4-5
+ # Inherits max_tokens, thinking_budget, temperature from provider
+
+ claude_fast:
+ provider: my_anthropic
+ model: claude-haiku-4-5
+ thinking_budget: low # Overrides provider default
+```
+
+See [Provider Definitions]({{ '/providers/custom/' | relative_url }}) for the full list of inheritable properties.
diff --git a/docs/configuration/overview/index.md b/docs/configuration/overview/index.md
index 3679956ea..afaaefdd6 100644
--- a/docs/configuration/overview/index.md
+++ b/docs/configuration/overview/index.md
@@ -46,12 +46,12 @@ rag:
- type: chunked-embeddings
model: openai/text-embedding-3-small
-# 6. Providers — optional custom provider definitions
+# 6. Providers — optional reusable provider definitions
providers:
my_provider:
- api_type: openai_chatcompletions
- base_url: https://api.example.com/v1
+ provider: anthropic # or openai (default), google, amazon-bedrock, etc.
token_key: MY_API_KEY
+ max_tokens: 16384
# 7. Permissions — agent-level tool permission rules (optional)
# For user-wide global permissions, see ~/.config/cagent/config.yaml
@@ -220,34 +220,52 @@ See [Agent Distribution]({{ '/concepts/distribution/' | relative_url }}) for pub
## Custom Providers Section
-Define reusable provider configurations for custom or self-hosted endpoints:
+Define reusable provider configurations with shared defaults. Providers can wrap any provider type — not just OpenAI-compatible endpoints:
```yaml
providers:
+ # OpenAI-compatible custom endpoint
azure:
api_type: openai_chatcompletions
base_url: https://my-resource.openai.azure.com/openai/deployments/gpt-4o
token_key: AZURE_OPENAI_API_KEY
- internal_llm:
- api_type: openai_chatcompletions
- base_url: https://llm.internal.company.com/v1
- token_key: INTERNAL_API_KEY
+ # Anthropic with shared model defaults
+ team_anthropic:
+ provider: anthropic
+ token_key: TEAM_ANTHROPIC_KEY
+ max_tokens: 32768
+ thinking_budget: high
models:
azure_gpt:
- provider: azure # References the custom provider
+ provider: azure
model: gpt-4o
+ claude:
+ provider: team_anthropic
+ model: claude-sonnet-4-5
+ # Inherits max_tokens, thinking_budget from provider
+
agents:
root:
- model: azure_gpt
+ model: claude
```
-| Field | Description |
-| ----------- | -------------------------------------------------------------------- |
-| `api_type` | API schema: `openai_chatcompletions` (default) or `openai_responses` |
-| `base_url` | Base URL for the API endpoint |
-| `token_key` | Environment variable name for the API token |
-
-See [Custom Providers]({{ '/providers/custom/' | relative_url }}) for more details.
+| Field | Description |
+| --------------------- | ---------------------------------------------------------------------------------------- |
+| `provider` | Underlying provider type: `openai` (default), `anthropic`, `google`, `amazon-bedrock`, etc. |
+| `api_type` | API schema: `openai_chatcompletions` (default) or `openai_responses`. OpenAI-only. |
+| `base_url` | Base URL for the API endpoint. Required for OpenAI-compatible providers. |
+| `token_key` | Environment variable name for the API token. |
+| `temperature` | Default sampling temperature. |
+| `max_tokens` | Default maximum response tokens. |
+| `thinking_budget` | Default reasoning effort/budget. |
+| `top_p` | Default top-p sampling parameter. |
+| `frequency_penalty` | Default frequency penalty. |
+| `presence_penalty` | Default presence penalty. |
+| `parallel_tool_calls` | Enable parallel tool calls by default. |
+| `track_usage` | Track token usage by default. |
+| `provider_opts` | Provider-specific options. |
+
+See [Provider Definitions]({{ '/providers/custom/' | relative_url }}) for more details.
diff --git a/docs/getting-started/introduction/index.md b/docs/getting-started/introduction/index.md
index 5fd73427c..1e41d1bae 100644
--- a/docs/getting-started/introduction/index.md
+++ b/docs/getting-started/introduction/index.md
@@ -30,7 +30,7 @@ their model, personality, tools, and how they collaborate — and docker-agent h
🧠
Multi-Model Support
-
OpenAI, Anthropic, Google Gemini, AWS Bedrock, Docker Model Runner, and custom OpenAI-compatible providers.
+
OpenAI, Anthropic, Google Gemini, AWS Bedrock, Docker Model Runner, and reusable provider definitions with shared defaults.
diff --git a/docs/providers/custom/index.md b/docs/providers/custom/index.md
index 8f0731465..900d75b62 100644
--- a/docs/providers/custom/index.md
+++ b/docs/providers/custom/index.md
@@ -1,43 +1,43 @@
---
-title: "Custom Providers"
-description: "Connect docker-agent to any OpenAI-compatible API endpoint — without modifying docker-agent's source code."
+title: "Provider Definitions"
+description: "Define reusable provider configurations with shared defaults for any provider type — OpenAI, Anthropic, Google, Bedrock, and more."
permalink: /providers/custom/
---
-# Custom Providers
+# Provider Definitions
-_Connect docker-agent to any OpenAI-compatible API endpoint — without modifying docker-agent's source code._
+_Define reusable provider configurations with shared defaults for any provider type — OpenAI, Anthropic, Google, Bedrock, and more._
## Overview
-The `providers` section in your agent YAML lets you define custom providers that work with any OpenAI-compatible API. This is useful for:
+The `providers` section in your agent YAML lets you define named provider configurations that models can reference. This is useful for:
-- Self-hosted models (vLLM, Ollama, LocalAI, etc.)
-- API proxies and routers (Requesty, LiteLLM, etc.)
-- Enterprise deployments with custom endpoints
-- Any service with an OpenAI-compatible chat completions API
+- **Grouping shared defaults** — Set temperature, max_tokens, thinking_budget once and share across models
+- **Custom endpoints** — Connect to self-hosted models, API proxies, or gateways
+- **Centralizing credentials** — Define token_key once for all models using a provider
+- **Any provider type** — Works with OpenAI, Anthropic, Google, Bedrock, and any OpenAI-compatible API
-
ℹ️ Works with any OpenAI-compatible API
+
ℹ️ Works with any provider
-
If a service supports the /v1/chat/completions endpoint, you can use it with docker-agent. No source code changes needed.
+
The providers section supports all provider types: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias. When the provider field is not set, it defaults to openai for backward compatibility.
## Configuration
+### OpenAI-compatible endpoint
+
```yaml
providers:
- my_provider:
- api_type: openai_chatcompletions # or openai_responses
+ my_gateway:
base_url: https://api.example.com/v1
- token_key: MY_API_KEY # env var name
+ token_key: MY_API_KEY
models:
my_model:
- provider: my_provider
+ provider: my_gateway
model: gpt-4o
- max_tokens: 32768
agents:
root:
@@ -45,26 +45,114 @@ agents:
instruction: You are a helpful assistant.
```
+### Anthropic with shared defaults
+
+```yaml
+providers:
+ my_anthropic:
+ provider: anthropic
+ token_key: MY_ANTHROPIC_KEY
+ max_tokens: 16384
+ thinking_budget: high
+
+models:
+ claude_smart:
+ provider: my_anthropic
+ model: claude-sonnet-4-5
+ # Inherits max_tokens: 16384, thinking_budget: high
+
+ claude_fast:
+ provider: my_anthropic
+ model: claude-haiku-4-5
+ thinking_budget: low # Overrides provider default
+
+agents:
+ root:
+ model: claude_smart
+ instruction: You are a helpful assistant.
+```
+
+### Google with shared temperature
+
+```yaml
+providers:
+ my_google:
+ provider: google
+ temperature: 0.3
+
+models:
+ gemini:
+ provider: my_google
+ model: gemini-2.5-flash
+ # Inherits temperature: 0.3
+
+agents:
+ root:
+ model: gemini
+ instruction: You are a helpful assistant.
+```
+
## Provider Properties
-| Property | Description | Default |
-| ----------- | ---------------------------------------------------------- | ------------------------ |
-| `api_type` | API schema: `openai_chatcompletions` or `openai_responses` | `openai_chatcompletions` |
-| `base_url` | Base URL for the API endpoint | — |
-| `token_key` | Name of the environment variable containing the API token | — |
+| Property | Type | Description | Default |
+| --------------------- | ---------- | ------------------------------------------------------------------------------------- | ------------------------ |
+| `provider` | string | Underlying provider type: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, etc. | `openai` |
+| `api_type` | string | API schema: `openai_chatcompletions` or `openai_responses`. Only for OpenAI-compatible providers. | `openai_chatcompletions` |
+| `base_url` | string | Base URL for the API endpoint. Required for OpenAI-compatible providers, optional for native providers. | — |
+| `token_key` | string | Environment variable name containing the API token. | — |
+| `temperature` | float | Default sampling temperature (0.0–2.0). | — |
+| `max_tokens` | int | Default maximum response tokens. | — |
+| `top_p` | float | Default nucleus sampling threshold (0.0–1.0). | — |
+| `frequency_penalty` | float | Default frequency penalty (-2.0–2.0). | — |
+| `presence_penalty` | float | Default presence penalty (-2.0–2.0). | — |
+| `parallel_tool_calls` | boolean | Whether to enable parallel tool calls by default. | — |
+| `track_usage` | boolean | Whether to track token usage by default. | — |
+| `thinking_budget` | string/int | Default reasoning effort/budget. | — |
+| `provider_opts` | object | Provider-specific options passed through to the client. | — |
+
+## Default Inheritance
+
+Models referencing a provider inherit all its defaults. Model-level settings always take precedence:
+
+```yaml
+providers:
+ my_anthropic:
+ provider: anthropic
+ token_key: MY_ANTHROPIC_KEY
+ max_tokens: 16384
+ temperature: 0.7
+ thinking_budget: high
+
+models:
+ # Inherits everything from provider
+ claude_default:
+ provider: my_anthropic
+ model: claude-sonnet-4-5
+
+ # Overrides temperature and thinking_budget, inherits the rest
+ claude_custom:
+ provider: my_anthropic
+ model: claude-sonnet-4-5
+ temperature: 0.2
+ thinking_budget: low
+```
## Shorthand Syntax
-Once a custom provider is defined, you can use the shorthand `provider/model` syntax:
+Once a provider is defined, you can use the shorthand `provider_name/model` syntax:
```yaml
agents:
root:
- model: my_provider/gpt-4o-mini # uses the provider's base_url and token
+ model: my_gateway/gpt-4o-mini # uses the provider's defaults
+ researcher:
+ model: my_anthropic/claude-sonnet-4-5 # uses anthropic provider defaults
```
## API Types
+Only applicable for OpenAI-compatible providers (when `provider` is `openai` or unset):
+
- **`openai_chatcompletions`** — Standard OpenAI Chat Completions API. Works with most OpenAI-compatible endpoints.
- **`openai_responses`** — OpenAI Responses API. For newer models that require the Responses API format.
@@ -75,7 +163,6 @@ agents:
```yaml
providers:
local_llm:
- api_type: openai_chatcompletions
base_url: http://localhost:8000/v1
agents:
@@ -88,7 +175,6 @@ agents:
```yaml
providers:
router:
- api_type: openai_chatcompletions
base_url: https://router.requesty.ai/v1
token_key: REQUESTY_API_KEY
@@ -109,11 +195,65 @@ models:
api_version: 2024-12-01-preview
```
+### Anthropic Team Setup
+
+```yaml
+providers:
+ team_anthropic:
+ provider: anthropic
+ token_key: TEAM_ANTHROPIC_KEY
+ max_tokens: 32768
+ thinking_budget: high
+ temperature: 0.5
+
+models:
+ architect:
+ provider: team_anthropic
+ model: claude-sonnet-4-5
+
+ reviewer:
+ provider: team_anthropic
+ model: claude-haiku-4-5
+ thinking_budget: low # faster reviews
+
+agents:
+ root:
+ model: architect
+ sub_agents: [code_reviewer]
+ code_reviewer:
+ model: reviewer
+```
+
+### Multi-Provider with Shared Defaults
+
+```yaml
+providers:
+ fast_openai:
+ base_url: https://api.openai.com/v1
+ token_key: OPENAI_API_KEY
+ temperature: 0.3
+ max_tokens: 8192
+
+ smart_anthropic:
+ provider: anthropic
+ token_key: ANTHROPIC_API_KEY
+ max_tokens: 64000
+ thinking_budget: high
+
+agents:
+ root:
+ model: smart_anthropic/claude-sonnet-4-5
+ sub_agents: [helper]
+ helper:
+ model: fast_openai/gpt-4o-mini
+```
+
## How It Works
-When you reference a custom provider:
+When you reference a provider:
-1. The provider's `base_url` is applied to the model (if not already set)
-2. The provider's `token_key` is applied to the model (if not already set)
-3. The provider's `api_type` is stored in `provider_opts.api_type`
-4. The model is used with the appropriate API client
+1. The provider's `provider` field determines which API client to use (defaults to `openai`)
+2. The provider's `base_url` and `token_key` are applied to the model (if not already set on the model)
+3. All model-level defaults (temperature, max_tokens, thinking_budget, etc.) are inherited (model settings take precedence)
+4. For OpenAI-compatible providers, the `api_type` is stored in `provider_opts.api_type`
+5. The model is used with the appropriate API client
diff --git a/docs/providers/overview/index.md b/docs/providers/overview/index.md
index a3f628a22..d1ad570bf 100644
--- a/docs/providers/overview/index.md
+++ b/docs/providers/overview/index.md
@@ -38,8 +38,8 @@ _docker-agent supports multiple AI model providers. Choose the right one for you
🔧
- Custom Providers
- Connect to any OpenAI-compatible API endpoint.
+ Provider Definitions
+ Define reusable provider configurations with shared defaults for any provider type.
diff --git a/examples/custom_provider.yaml b/examples/custom_provider.yaml
index f173c5535..faca5a42c 100644
--- a/examples/custom_provider.yaml
+++ b/examples/custom_provider.yaml
@@ -1,24 +1,39 @@
# Example: Custom Provider Configuration
#
-# This example demonstrates how to define and use custom providers in Cagent.
-# Custom providers allow you to connect to OpenAI-compatible APIs with reusable
-# configuration for base URLs, API tokens, and API schema types.
+# This example demonstrates how to define and use providers in docker-agent.
+# Providers allow you to group reusable configuration (base URLs, API tokens,
+# model defaults) that models can inherit by referencing the provider name.
+#
+# Providers support any provider type: openai, anthropic, google, amazon-bedrock, etc.
+# When the 'provider' field is not set, it defaults to 'openai' for backward compatibility.
-# Define custom providers with reusable configuration
+# Define providers with reusable configuration
providers:
- # Example: A custom OpenAI Chat Completions compatible API gateway
+ # Example: An OpenAI Chat Completions compatible API gateway (default behavior)
my_gateway:
api_type: openai_chatcompletions # Use the Chat Completions API schema
base_url: https://api.example.com/
token_key: API_KEY_ENV_VAR_NAME # Environment variable containing the API token
- # Example: A custom OpenAI Responses compatible API gateway
+ # Example: An OpenAI Responses compatible API gateway
responses_provider:
api_type: openai_responses
base_url: https://responses.example.com/
token_key: API_KEY_ENV_VAR_NAME
-# Define models that use the custom providers
+ # Example: Anthropic provider with shared defaults
+ my_anthropic:
+ provider: anthropic
+ token_key: MY_ANTHROPIC_KEY
+ max_tokens: 16384
+ thinking_budget: high
+
+ # Example: Google provider with shared defaults
+ my_google:
+ provider: google
+ temperature: 0.7
+
+# Define models that use the providers
models:
# Model using the custom gateway provider
gateway_gpt4o:
@@ -33,6 +48,17 @@ models:
model: gpt-5
max_tokens: 16000
+ # Model using the Anthropic provider - inherits max_tokens and thinking_budget
+ claude_model:
+ provider: my_anthropic
+ model: claude-sonnet-4-5
+
+ # Model overriding the provider's thinking_budget
+ claude_fast:
+ provider: my_anthropic
+ model: claude-haiku-4-5
+ thinking_budget: low
+
# Define agents that use the models
agents:
root:
@@ -48,3 +74,10 @@ agents:
description: Sub-agent for specialized tasks
instruction: |
You are a specialized assistant for specific tasks.
+
+ # Example using Anthropic provider with shorthand
+ researcher:
+ model: my_anthropic/claude-sonnet-4-5
+ description: Research assistant
+ instruction: |
+ You are a research assistant that provides thorough analysis.
diff --git a/pkg/config/config.go b/pkg/config/config.go
index ea5d15a22..0aa1707c9 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -185,17 +185,19 @@ func validateProviders(cfg *latest.Config) error {
return fmt.Errorf("provider '%s': %w", name, err)
}
- // Validate api_type
+ // Validate api_type if set
if !providerAPITypes[provCfg.APIType] {
return fmt.Errorf("provider '%s': invalid api_type '%s' (must be one of: openai_chatcompletions, openai_responses)", name, provCfg.APIType)
}
- // base_url is required for custom providers
- if provCfg.BaseURL == "" {
- return fmt.Errorf("provider '%s': base_url is required", name)
- }
- if _, err := url.Parse(provCfg.BaseURL); err != nil {
- return fmt.Errorf("provider '%s': invalid base_url '%s': %w", name, provCfg.BaseURL, err)
+ // base_url is required for OpenAI-compatible providers (the default)
+ // but optional for native providers like anthropic, google, amazon-bedrock
+ if provCfg.BaseURL != "" {
+ if _, err := url.Parse(provCfg.BaseURL); err != nil {
+ return fmt.Errorf("provider '%s': invalid base_url '%s': %w", name, provCfg.BaseURL, err)
+ }
+ } else if isOpenAICustomProvider(provCfg) {
+ return fmt.Errorf("provider '%s': base_url is required for OpenAI-compatible providers", name)
}
// token_key is optional - if not set, requests will be sent without bearer token
@@ -204,6 +206,18 @@ func validateProviders(cfg *latest.Config) error {
return nil
}
+// isOpenAICustomProvider returns true if the provider config describes an OpenAI-compatible
+// custom provider (i.e., Provider is empty or "openai", or api_type is explicitly set to an
+// OpenAI schema). These providers require a base_url because they don't have a built-in default.
+func isOpenAICustomProvider(cfg latest.ProviderConfig) bool {
+ // If api_type is explicitly set, it's an OpenAI-compatible provider
+ if cfg.APIType != "" {
+ return true
+ }
+ // If provider is empty (defaults to openai) or explicitly "openai"
+ return cfg.Provider == "" || cfg.Provider == "openai"
+}
+
// validateProviderName validates that a provider name is valid
func validateProviderName(name string) error {
trimmed := strings.TrimSpace(name)
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 38f5517c9..d80d80490 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -680,6 +680,46 @@ func TestProviders_Validation(t *testing.T) {
},
wantErr: "name cannot contain '/'",
},
+ {
+ name: "valid anthropic provider without base_url",
+ providers: map[string]latest.ProviderConfig{
+ "my_anthropic": {
+ Provider: "anthropic",
+ TokenKey: "MY_ANTHROPIC_KEY",
+ },
+ },
+ wantErr: "",
+ },
+ {
+ name: "valid google provider with defaults",
+ providers: map[string]latest.ProviderConfig{
+ "my_google": {
+ Provider: "google",
+ },
+ },
+ wantErr: "",
+ },
+ {
+ name: "openai provider without base_url requires it",
+ providers: map[string]latest.ProviderConfig{
+ "my_openai": {
+ Provider: "openai",
+ },
+ },
+ wantErr: "base_url is required",
+ },
+ {
+ name: "provider with model defaults",
+ providers: map[string]latest.ProviderConfig{
+ "my_anthropic": {
+ Provider: "anthropic",
+ TokenKey: "MY_KEY",
+ MaxTokens: new(int64),
+ Temperature: new(float64),
+ },
+ },
+ wantErr: "",
+ },
}
for _, tt := range tests {
diff --git a/pkg/config/gather.go b/pkg/config/gather.go
index a34119e2b..61eef3a70 100644
--- a/pkg/config/gather.go
+++ b/pkg/config/gather.go
@@ -97,6 +97,13 @@ func addEnvVarsForModelConfig(model *latest.ModelConfig, customProviders map[str
if provCfg, exists := customProviders[model.Provider]; exists {
if provCfg.TokenKey != "" {
requiredEnv[provCfg.TokenKey] = true
+ } else {
+ // Fall through to check the effective provider type
+ effective := provCfg.Provider
+ if effective == "" {
+ effective = "openai"
+ }
+ addEnvVarsForCoreProvider(effective, model, requiredEnv)
}
}
} else if alias, exists := provider.Aliases[model.Provider]; exists {
@@ -105,20 +112,24 @@ func addEnvVarsForModelConfig(model *latest.ModelConfig, customProviders map[str
requiredEnv[alias.TokenEnvVar] = true
}
} else {
- // Fallback to hardcoded mappings for core providers
- switch model.Provider {
- case "openai":
- requiredEnv["OPENAI_API_KEY"] = true
- case "anthropic":
- requiredEnv["ANTHROPIC_API_KEY"] = true
- case "google":
- if model.ProviderOpts["project"] == nil && model.ProviderOpts["location"] == nil {
- if os.Getenv("GOOGLE_GENAI_USE_VERTEXAI") != "" {
- requiredEnv["GOOGLE_CLOUD_PROJECT"] = true
- requiredEnv["GOOGLE_CLOUD_LOCATION"] = true
- } else if _, exist := os.LookupEnv("GEMINI_API_KEY"); !exist {
- requiredEnv["GOOGLE_API_KEY"] = true
- }
+ addEnvVarsForCoreProvider(model.Provider, model, requiredEnv)
+ }
+}
+
+// addEnvVarsForCoreProvider adds the required env vars for a core provider type.
+func addEnvVarsForCoreProvider(providerType string, model *latest.ModelConfig, requiredEnv map[string]bool) {
+ switch providerType {
+ case "openai":
+ requiredEnv["OPENAI_API_KEY"] = true
+ case "anthropic":
+ requiredEnv["ANTHROPIC_API_KEY"] = true
+ case "google":
+ if model.ProviderOpts["project"] == nil && model.ProviderOpts["location"] == nil {
+ if os.Getenv("GOOGLE_GENAI_USE_VERTEXAI") != "" {
+ requiredEnv["GOOGLE_CLOUD_PROJECT"] = true
+ requiredEnv["GOOGLE_CLOUD_LOCATION"] = true
+ } else if _, exist := os.LookupEnv("GEMINI_API_KEY"); !exist {
+ requiredEnv["GOOGLE_API_KEY"] = true
}
}
}
diff --git a/pkg/config/latest/types.go b/pkg/config/latest/types.go
index 54929d5f8..11fa3d6bc 100644
--- a/pkg/config/latest/types.go
+++ b/pkg/config/latest/types.go
@@ -215,17 +215,43 @@ func (c *Agents) Update(name string, update func(a *AgentConfig)) bool {
}
// ProviderConfig represents a reusable provider definition.
-// It allows users to define custom providers with default base URLs and token keys.
-// Models can reference these providers by name, inheriting the defaults.
+// It allows users to define providers with default settings that models can inherit.
+// Models referencing a provider by name will inherit any settings not explicitly overridden.
+//
+// The Provider field specifies the underlying provider type (e.g., "openai", "anthropic",
+// "google", "amazon-bedrock"). When not set, it defaults to "openai" for backward compatibility.
type ProviderConfig struct {
- // APIType specifies which API schema to use. Supported values:
- // - "openai_chatcompletions" (default): Use the OpenAI Chat Completions API
+ // Provider specifies the underlying provider type. Supported values include:
+ // "openai", "anthropic", "google", "amazon-bedrock", "dmr", and any built-in alias.
+ // Defaults to "openai" when not set, preserving backward compatibility.
+ Provider string `json:"provider,omitempty"`
+ // APIType specifies which API schema to use. Only applicable for OpenAI-compatible providers.
+ // Supported values:
+ // - "openai_chatcompletions" (default for openai): Use the OpenAI Chat Completions API
// - "openai_responses": Use the OpenAI Responses API
APIType string `json:"api_type,omitempty"`
// BaseURL is the base URL for the provider's API endpoint
- BaseURL string `json:"base_url"`
+ BaseURL string `json:"base_url,omitempty"`
// TokenKey is the environment variable name containing the API token
TokenKey string `json:"token_key,omitempty"`
+ // Temperature is the default sampling temperature for models using this provider
+ Temperature *float64 `json:"temperature,omitempty"`
+ // MaxTokens is the default maximum number of tokens for models using this provider
+ MaxTokens *int64 `json:"max_tokens,omitempty"`
+ // TopP is the default top-p sampling parameter
+ TopP *float64 `json:"top_p,omitempty"`
+ // FrequencyPenalty is the default frequency penalty
+ FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
+ // PresencePenalty is the default presence penalty
+ PresencePenalty *float64 `json:"presence_penalty,omitempty"`
+ // ParallelToolCalls controls whether parallel tool calls are enabled by default
+ ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
+ // ProviderOpts allows provider-specific options
+ ProviderOpts map[string]any `json:"provider_opts,omitempty"`
+ // TrackUsage controls whether token usage tracking is enabled by default
+ TrackUsage *bool `json:"track_usage,omitempty"`
+ // ThinkingBudget controls reasoning effort/budget for models using this provider
+ ThinkingBudget *ThinkingBudget `json:"thinking_budget,omitempty"`
}
// FallbackConfig represents fallback model configuration for an agent.
diff --git a/pkg/config/testdata/providers.yaml b/pkg/config/testdata/providers.yaml
index efa89c5f5..6fc8fd92a 100644
--- a/pkg/config/testdata/providers.yaml
+++ b/pkg/config/testdata/providers.yaml
@@ -9,6 +9,12 @@ providers:
base_url: https://responses.example.com/v1
token_key: RESPONSES_API_KEY
+ anthropic_provider:
+ provider: anthropic
+ token_key: MY_ANTHROPIC_KEY
+ max_tokens: 16384
+ thinking_budget: high
+
models:
custom_model:
provider: custom_provider
diff --git a/pkg/model/provider/provider.go b/pkg/model/provider/provider.go
index 6a4d9d18c..749bb9536 100644
--- a/pkg/model/provider/provider.go
+++ b/pkg/model/provider/provider.go
@@ -300,23 +300,72 @@ func applyProviderDefaults(cfg *latest.ModelConfig, customProviders map[string]l
"base_url", providerCfg.BaseURL,
)
+ // Apply the underlying provider type if set on the provider config.
+ // This allows the model to inherit the real provider type (e.g., "anthropic")
+ // so that the correct API client is selected.
+ if providerCfg.Provider != "" {
+ enhancedCfg.Provider = providerCfg.Provider
+ }
+
if enhancedCfg.BaseURL == "" && providerCfg.BaseURL != "" {
enhancedCfg.BaseURL = providerCfg.BaseURL
}
if enhancedCfg.TokenKey == "" && providerCfg.TokenKey != "" {
enhancedCfg.TokenKey = providerCfg.TokenKey
}
+ if enhancedCfg.Temperature == nil && providerCfg.Temperature != nil {
+ enhancedCfg.Temperature = providerCfg.Temperature
+ }
+ if enhancedCfg.MaxTokens == nil && providerCfg.MaxTokens != nil {
+ enhancedCfg.MaxTokens = providerCfg.MaxTokens
+ }
+ if enhancedCfg.TopP == nil && providerCfg.TopP != nil {
+ enhancedCfg.TopP = providerCfg.TopP
+ }
+ if enhancedCfg.FrequencyPenalty == nil && providerCfg.FrequencyPenalty != nil {
+ enhancedCfg.FrequencyPenalty = providerCfg.FrequencyPenalty
+ }
+ if enhancedCfg.PresencePenalty == nil && providerCfg.PresencePenalty != nil {
+ enhancedCfg.PresencePenalty = providerCfg.PresencePenalty
+ }
+ if enhancedCfg.ParallelToolCalls == nil && providerCfg.ParallelToolCalls != nil {
+ enhancedCfg.ParallelToolCalls = providerCfg.ParallelToolCalls
+ }
+ if enhancedCfg.TrackUsage == nil && providerCfg.TrackUsage != nil {
+ enhancedCfg.TrackUsage = providerCfg.TrackUsage
+ }
+ if enhancedCfg.ThinkingBudget == nil && providerCfg.ThinkingBudget != nil {
+ enhancedCfg.ThinkingBudget = providerCfg.ThinkingBudget
+ }
- // Set api_type in ProviderOpts if not already set
- if enhancedCfg.ProviderOpts == nil {
- enhancedCfg.ProviderOpts = make(map[string]any)
+ // Merge provider_opts from provider config (model opts take precedence)
+ if len(providerCfg.ProviderOpts) > 0 {
+ if enhancedCfg.ProviderOpts == nil {
+ enhancedCfg.ProviderOpts = make(map[string]any)
+ }
+ for k, v := range providerCfg.ProviderOpts {
+ if _, has := enhancedCfg.ProviderOpts[k]; !has {
+ enhancedCfg.ProviderOpts[k] = v
+ }
+ }
}
- if _, has := enhancedCfg.ProviderOpts["api_type"]; !has {
- apiType := providerCfg.APIType
- if apiType == "" {
- apiType = "openai_chatcompletions"
+
+ // Set api_type in ProviderOpts if not already set.
+ // Only default to openai_chatcompletions for OpenAI-compatible providers.
+ if providerCfg.APIType != "" {
+ if enhancedCfg.ProviderOpts == nil {
+ enhancedCfg.ProviderOpts = make(map[string]any)
+ }
+ if _, has := enhancedCfg.ProviderOpts["api_type"]; !has {
+ enhancedCfg.ProviderOpts["api_type"] = providerCfg.APIType
+ }
+ } else if isOpenAICompatibleProvider(resolveEffectiveProvider(providerCfg)) {
+ if enhancedCfg.ProviderOpts == nil {
+ enhancedCfg.ProviderOpts = make(map[string]any)
+ }
+ if _, has := enhancedCfg.ProviderOpts["api_type"]; !has {
+ enhancedCfg.ProviderOpts["api_type"] = "openai_chatcompletions"
}
- enhancedCfg.ProviderOpts["api_type"] = apiType
}
applyModelDefaults(enhancedCfg)
@@ -470,3 +519,26 @@ func isGeminiProModel(model string) bool {
func isGeminiFlashModel(model string) bool {
return strings.HasPrefix(gemini3Family(model), "flash")
}
+
+// resolveEffectiveProvider returns the effective provider type for a ProviderConfig.
+// If Provider is explicitly set, returns that. Otherwise returns "openai" (backward compat).
+func resolveEffectiveProvider(cfg latest.ProviderConfig) string {
+ if cfg.Provider != "" {
+ return cfg.Provider
+ }
+ return "openai"
+}
+
+// isOpenAICompatibleProvider returns true if the provider type uses the OpenAI API protocol.
+func isOpenAICompatibleProvider(providerType string) bool {
+ switch providerType {
+ case "openai", "openai_chatcompletions", "openai_responses":
+ return true
+ default:
+ // Check if it's an alias that maps to openai
+ if alias, exists := Aliases[providerType]; exists {
+ return alias.APIType == "openai"
+ }
+ return false
+ }
+}