Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions crates/forge_config/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf;

use derive_setters::Setters;
Expand All @@ -11,6 +12,92 @@ use crate::{
AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, ReasoningConfig, RetryConfig, Update,
};

/// Wire protocol a provider uses for chat completions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)]
pub enum ProviderResponseType {
OpenAI,
OpenAIResponses,
Anthropic,
Bedrock,
Google,
OpenCode,
}

/// Category of a provider.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Dummy)]
#[serde(rename_all = "snake_case")]
pub enum ProviderTypeEntry {
/// LLM provider for chat completions.
#[default]
Llm,
/// Context engine provider for code indexing and search.
ContextEngine,
}

/// Authentication method supported by a provider.
///
/// Only the simple (non-OAuth) methods are available here; providers that
/// require OAuth device or authorization-code flows must be configured via the
/// file-based `provider.json` override instead.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)]
#[serde(rename_all = "snake_case")]
pub enum ProviderAuthMethod {
ApiKey,
GoogleAdc,
}

/// A URL parameter variable for a provider, used to substitute template
/// variables in URL strings.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)]
#[serde(rename_all = "snake_case")]
pub struct ProviderUrlParam {
/// The environment variable name used as the template variable key.
pub name: String,
/// Optional preset values for this parameter shown as suggestions in the
/// UI.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
}

/// A single provider entry defined inline in `forge.toml`.
///
/// Inline providers are merged with the built-in provider list; entries with
/// the same `id` override the corresponding built-in entry field-by-field,
/// while entries with a new `id` are appended to the list.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)]
#[serde(rename_all = "snake_case")]
pub struct ProviderEntry {
/// Unique provider identifier used in model paths (e.g. `"my_provider"`).
pub id: String,
/// Environment variable holding the API key for this provider.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_var: Option<String>,
/// URL template for chat completions; may contain `{{VAR}}` placeholders
/// that are substituted from the credential's url params.
pub url: String,
/// URL template for fetching the model list; may contain `{{VAR}}`
/// placeholders.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub models: Option<String>,
/// Wire protocol used by this provider.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_type: Option<ProviderResponseType>,
/// Environment variables whose values are substituted into `{{VAR}}`
/// placeholders in the `url` and `models` templates.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub url_param_vars: Vec<ProviderUrlParam>,
/// Additional HTTP headers sent with every request to this provider.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_headers: Option<HashMap<String, String>>,
/// Provider category; defaults to `llm` when omitted.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_type: Option<ProviderTypeEntry>,
/// Authentication methods supported by this provider; defaults to
/// `["api_key"]` when omitted.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub auth_methods: Vec<ProviderAuthMethod>,
}

/// Top-level Forge configuration merged from all sources (defaults, file,
/// environment).
#[derive(Default, Debug, Setters, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)]
Expand Down Expand Up @@ -170,6 +257,14 @@ pub struct ForgeConfig {
/// token budget, and visibility of the model's thinking process.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,

/// Additional provider definitions merged with the built-in provider list.
///
/// Entries with an `id` matching a built-in provider override its fields;
/// entries with a new `id` are appended and become available for model
/// selection.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub providers: Vec<ProviderEntry>,
}

impl ForgeConfig {
Expand Down
74 changes: 73 additions & 1 deletion crates/forge_repo/src/provider/provider_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,65 @@ fn merge_configs(base: &mut Vec<ProviderConfig>, other: Vec<ProviderConfig>) {
base.extend(map.into_values());
}

impl From<forge_config::ProviderUrlParam> for UrlParamVarConfig {
fn from(param: forge_config::ProviderUrlParam) -> Self {
if param.options.is_empty() {
UrlParamVarConfig::Plain(param.name)
} else {
UrlParamVarConfig::WithOptions { name: param.name, options: param.options }
}
}
}

impl From<forge_config::ProviderEntry> for ProviderConfig {
fn from(entry: forge_config::ProviderEntry) -> Self {
let provider_type = match entry.provider_type {
Some(forge_config::ProviderTypeEntry::ContextEngine) => {
forge_domain::ProviderType::ContextEngine
}
Some(forge_config::ProviderTypeEntry::Llm) | None => forge_domain::ProviderType::Llm,
};

let auth_methods = if entry.auth_methods.is_empty() {
vec![forge_domain::AuthMethod::ApiKey]
} else {
entry
.auth_methods
.into_iter()
.map(|m| match m {
forge_config::ProviderAuthMethod::ApiKey => forge_domain::AuthMethod::ApiKey,
forge_config::ProviderAuthMethod::GoogleAdc => {
forge_domain::AuthMethod::GoogleAdc
}
})
.collect()
};

let response_type = entry.response_type.map(|r| match r {
forge_config::ProviderResponseType::OpenAI => ProviderResponse::OpenAI,
forge_config::ProviderResponseType::OpenAIResponses => {
ProviderResponse::OpenAIResponses
}
forge_config::ProviderResponseType::Anthropic => ProviderResponse::Anthropic,
forge_config::ProviderResponseType::Bedrock => ProviderResponse::Bedrock,
forge_config::ProviderResponseType::Google => ProviderResponse::Google,
forge_config::ProviderResponseType::OpenCode => ProviderResponse::OpenCode,
});

ProviderConfig {
id: ProviderId::from(entry.id),
provider_type,
api_key_vars: entry.api_key_var,
url_param_vars: entry.url_param_vars.into_iter().map(Into::into).collect(),
response_type,
url: entry.url,
models: entry.models.map(Models::Url),
auth_methods,
custom_headers: entry.custom_headers,
}
}
}

impl From<&ProviderConfig> for forge_domain::ProviderTemplate {
fn from(config: &ProviderConfig) -> Self {
let models = config.models.as_ref().map(|m| match m {
Expand Down Expand Up @@ -165,6 +224,17 @@ impl<F: EnvironmentInfra + FileReaderInfra + FileWriterInfra + HttpInfra>
Ok(configs)
}

/// Converts provider entries from `ForgeConfig` into `ProviderConfig`
/// instances that can be merged into the provider list.
fn get_config_provider_configs(&self) -> Vec<ProviderConfig> {
self.infra
.get_config()
.providers
.into_iter()
.map(Into::into)
.collect()
}

async fn get_providers(&self) -> Vec<AnyProvider> {
let configs = self.get_merged_configs().await;

Expand Down Expand Up @@ -420,10 +490,12 @@ impl<F: EnvironmentInfra + FileReaderInfra + FileWriterInfra + HttpInfra>
/// Returns merged provider configs (embedded + custom)
async fn get_merged_configs(&self) -> Vec<ProviderConfig> {
let mut configs = ProviderConfigs(get_provider_configs().clone());
// Merge custom configs into embedded configs
// Merge custom file configs into embedded configs
configs.merge(ProviderConfigs(
self.get_custom_provider_configs().await.unwrap_or_default(),
));
// Merge inline configs from ForgeConfig (forge.toml `providers` field)
configs.merge(ProviderConfigs(self.get_config_provider_configs()));

configs.0
}
Expand Down
140 changes: 140 additions & 0 deletions forge.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@
"default": 0,
"minimum": 0
},
"providers": {
"description": "Additional provider definitions merged with the built-in provider list.\n\nEntries with an `id` matching a built-in provider override its fields;\nentries with a new `id` are appended and become available for model\nselection.",
"type": "array",
"items": {
"$ref": "#/$defs/ProviderEntry"
}
},
"reasoning": {
"description": "Reasoning configuration applied to all agents; controls effort level,\ntoken budget, and visibility of the model's thinking process.",
"anyOf": [
Expand Down Expand Up @@ -582,6 +589,139 @@
}
}
},
"ProviderAuthMethod": {
"description": "Authentication method supported by a provider.\n\nOnly the simple (non-OAuth) methods are available here; providers that\nrequire OAuth device or authorization-code flows must be configured via the\nfile-based `provider.json` override instead.",
"type": "string",
"enum": [
"api_key",
"google_adc"
]
},
"ProviderEntry": {
"description": "A single provider entry defined inline in `forge.toml`.\n\nInline providers are merged with the built-in provider list; entries with\nthe same `id` override the corresponding built-in entry field-by-field,\nwhile entries with a new `id` are appended to the list.",
"type": "object",
"properties": {
"api_key_var": {
"description": "Environment variable holding the API key for this provider.",
"type": [
"string",
"null"
]
},
"auth_methods": {
"description": "Authentication methods supported by this provider; defaults to\n`[\"api_key\"]` when omitted.",
"type": "array",
"items": {
"$ref": "#/$defs/ProviderAuthMethod"
}
},
"custom_headers": {
"description": "Additional HTTP headers sent with every request to this provider.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "string"
}
},
"id": {
"description": "Unique provider identifier used in model paths (e.g. `\"my_provider\"`).",
"type": "string"
},
"models": {
"description": "URL template for fetching the model list; may contain `{{VAR}}`\nplaceholders.",
"type": [
"string",
"null"
]
},
"provider_type": {
"description": "Provider category; defaults to `llm` when omitted.",
"anyOf": [
{
"$ref": "#/$defs/ProviderTypeEntry"
},
{
"type": "null"
}
]
},
"response_type": {
"description": "Wire protocol used by this provider.",
"anyOf": [
{
"$ref": "#/$defs/ProviderResponseType"
},
{
"type": "null"
}
]
},
"url": {
"description": "URL template for chat completions; may contain `{{VAR}}` placeholders\nthat are substituted from the credential's url params.",
"type": "string"
},
"url_param_vars": {
"description": "Environment variables whose values are substituted into `{{VAR}}`\nplaceholders in the `url` and `models` templates.",
"type": "array",
"items": {
"$ref": "#/$defs/ProviderUrlParam"
}
}
},
"required": [
"id",
"url"
]
},
"ProviderResponseType": {
"description": "Wire protocol a provider uses for chat completions.",
"type": "string",
"enum": [
"OpenAI",
"OpenAIResponses",
"Anthropic",
"Bedrock",
"Google",
"OpenCode"
]
},
"ProviderTypeEntry": {
"description": "Category of a provider.",
"oneOf": [
{
"description": "LLM provider for chat completions.",
"type": "string",
"const": "llm"
},
{
"description": "Context engine provider for code indexing and search.",
"type": "string",
"const": "context_engine"
}
]
},
"ProviderUrlParam": {
"description": "A URL parameter variable for a provider, used to substitute template\nvariables in URL strings.",
"type": "object",
"properties": {
"name": {
"description": "The environment variable name used as the template variable key.",
"type": "string"
},
"options": {
"description": "Optional preset values for this parameter shown as suggestions in the\nUI.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"name"
]
},
"ReasoningConfig": {
"description": "Controls the reasoning behaviour of a model, including effort level, token\nbudget, and visibility of the thinking process.",
"type": "object",
Expand Down
Loading