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 + } +}