From 2512a4b94c7c9307fd89bff1250b263a6e76e139 Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 3 Apr 2026 17:11:09 +0530 Subject: [PATCH 1/3] feat(config): add inline provider definitions to forge config --- crates/forge_config/src/config.rs | 55 ++++++++++++ .../forge_repo/src/provider/provider_repo.rs | 37 ++++++++- forge.schema.json | 83 +++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 2e2af49569..c9fe905b6f 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use derive_setters::Setters; @@ -11,6 +12,52 @@ use crate::{ AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, ReasoningConfig, RetryConfig, Update, }; +/// 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, +} + +/// 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, + /// 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, + /// Wire protocol used by this provider. Accepted values: `"OpenAI"`, + /// `"Anthropic"`, `"Google"`, `"Bedrock"`, `"OpenAIResponses"`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub response_type: Option, + /// 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, + /// Additional HTTP headers sent with every request to this provider. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom_headers: Option>, +} + /// Top-level Forge configuration merged from all sources (defaults, file, /// environment). #[derive(Default, Debug, Setters, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)] @@ -170,6 +217,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, + + /// 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, } impl ForgeConfig { diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 8af74b5b07..bc33f361fd 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -165,6 +165,39 @@ impl 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 { + self.infra + .get_config() + .providers + .into_iter() + .map(|entry| ProviderConfig { + id: ProviderId::from(entry.id), + provider_type: forge_domain::ProviderType::Llm, + api_key_vars: entry.api_key_var, + url_param_vars: entry + .url_param_vars + .into_iter() + .map(|p| { + if p.options.is_empty() { + UrlParamVarConfig::Plain(p.name) + } else { + UrlParamVarConfig::WithOptions { name: p.name, options: p.options } + } + }) + .collect(), + response_type: entry + .response_type + .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()), + url: entry.url, + models: entry.models.map(Models::Url), + auth_methods: vec![forge_domain::AuthMethod::ApiKey], + custom_headers: entry.custom_headers, + }) + .collect() + } + async fn get_providers(&self) -> Vec { let configs = self.get_merged_configs().await; @@ -420,10 +453,12 @@ impl /// Returns merged provider configs (embedded + custom) async fn get_merged_configs(&self) -> Vec { 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 } diff --git a/forge.schema.json b/forge.schema.json index b5851006b1..cd4498a09c 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -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": [ @@ -582,6 +589,82 @@ } } }, + "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" + ] + }, + "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" + ] + }, + "response_type": { + "description": "Wire protocol used by this provider. Accepted values: `\"OpenAI\"`,\n`\"Anthropic\"`, `\"Google\"`, `\"Bedrock\"`, `\"OpenAIResponses\"`.", + "type": [ + "string", + "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" + ] + }, + "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", From 21fe9c32dd696898e3a17a81c65bf84eae6c1b0f Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 3 Apr 2026 17:12:31 +0530 Subject: [PATCH 2/3] refactor(provider_repo): convert forge provider entries into config --- .../forge_repo/src/provider/provider_repo.rs | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index bc33f361fd..517c3bea63 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -103,6 +103,34 @@ fn merge_configs(base: &mut Vec, other: Vec) { base.extend(map.into_values()); } +impl From 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 for ProviderConfig { + fn from(entry: forge_config::ProviderEntry) -> Self { + ProviderConfig { + id: ProviderId::from(entry.id), + provider_type: forge_domain::ProviderType::Llm, + api_key_vars: entry.api_key_var, + url_param_vars: entry.url_param_vars.into_iter().map(Into::into).collect(), + response_type: entry + .response_type + .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()), + url: entry.url, + models: entry.models.map(Models::Url), + auth_methods: vec![forge_domain::AuthMethod::ApiKey], + 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 { @@ -172,29 +200,7 @@ impl .get_config() .providers .into_iter() - .map(|entry| ProviderConfig { - id: ProviderId::from(entry.id), - provider_type: forge_domain::ProviderType::Llm, - api_key_vars: entry.api_key_var, - url_param_vars: entry - .url_param_vars - .into_iter() - .map(|p| { - if p.options.is_empty() { - UrlParamVarConfig::Plain(p.name) - } else { - UrlParamVarConfig::WithOptions { name: p.name, options: p.options } - } - }) - .collect(), - response_type: entry - .response_type - .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()), - url: entry.url, - models: entry.models.map(Models::Url), - auth_methods: vec![forge_domain::AuthMethod::ApiKey], - custom_headers: entry.custom_headers, - }) + .map(Into::into) .collect() } From 3eba7abccda90e0388a6e0dee16661d446a6fa2e Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 3 Apr 2026 17:23:01 +0530 Subject: [PATCH 3/3] feat(config): add provider_type and auth_methods fields --- crates/forge_config/src/config.rs | 46 ++++++++++++- .../forge_repo/src/provider/provider_repo.rs | 41 ++++++++++-- forge.schema.json | 65 +++++++++++++++++-- 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index c9fe905b6f..0f189de0e3 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -12,6 +12,40 @@ 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)] @@ -45,10 +79,9 @@ pub struct ProviderEntry { /// placeholders. #[serde(default, skip_serializing_if = "Option::is_none")] pub models: Option, - /// Wire protocol used by this provider. Accepted values: `"OpenAI"`, - /// `"Anthropic"`, `"Google"`, `"Bedrock"`, `"OpenAIResponses"`. + /// Wire protocol used by this provider. #[serde(default, skip_serializing_if = "Option::is_none")] - pub response_type: Option, + pub response_type: Option, /// Environment variables whose values are substituted into `{{VAR}}` /// placeholders in the `url` and `models` templates. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -56,6 +89,13 @@ pub struct ProviderEntry { /// Additional HTTP headers sent with every request to this provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_headers: Option>, + /// Provider category; defaults to `llm` when omitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider_type: Option, + /// Authentication methods supported by this provider; defaults to + /// `["api_key"]` when omitted. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub auth_methods: Vec, } /// Top-level Forge configuration merged from all sources (defaults, file, diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 517c3bea63..5ff2a23982 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -115,17 +115,48 @@ impl From for UrlParamVarConfig { impl From 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: forge_domain::ProviderType::Llm, + provider_type, api_key_vars: entry.api_key_var, url_param_vars: entry.url_param_vars.into_iter().map(Into::into).collect(), - response_type: entry - .response_type - .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()), + response_type, url: entry.url, models: entry.models.map(Models::Url), - auth_methods: vec![forge_domain::AuthMethod::ApiKey], + auth_methods, custom_headers: entry.custom_headers, } } diff --git a/forge.schema.json b/forge.schema.json index cd4498a09c..f925ff6a5f 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -589,6 +589,14 @@ } } }, + "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", @@ -600,6 +608,13 @@ "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": [ @@ -621,11 +636,26 @@ "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. Accepted values: `\"OpenAI\"`,\n`\"Anthropic\"`, `\"Google\"`, `\"Bedrock\"`, `\"OpenAIResponses\"`.", - "type": [ - "string", - "null" + "description": "Wire protocol used by this provider.", + "anyOf": [ + { + "$ref": "#/$defs/ProviderResponseType" + }, + { + "type": "null" + } ] }, "url": { @@ -645,6 +675,33 @@ "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",