From 619be054b41ab33e0d09c9b11f75faed3e531cd4 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Thu, 19 Mar 2026 13:38:28 +0530 Subject: [PATCH 1/5] feat(provider): add JsonSchema derivations and generate provider.schema.json --- Cargo.lock | 1 + crates/forge_domain/src/auth/auth_method.rs | 8 +- crates/forge_domain/src/auth/oauth_config.rs | 26 +- crates/forge_domain/src/model.rs | 9 +- crates/forge_domain/src/provider.rs | 20 +- crates/forge_repo/Cargo.toml | 1 + crates/forge_repo/src/lib.rs | 3 + .../src/provider/provider.schema.json | 360 ++++++++++++++++++ .../forge_repo/src/provider/provider_repo.rs | 37 +- crates/forge_repo/tests/provider_schema.rs | 29 ++ 10 files changed, 474 insertions(+), 20 deletions(-) create mode 100644 crates/forge_repo/src/provider/provider.schema.json create mode 100644 crates/forge_repo/tests/provider_schema.rs diff --git a/Cargo.lock b/Cargo.lock index cbbb1dc096..48fe750286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2114,6 +2114,7 @@ dependencies = [ "gray_matter", "handlebars", "insta", + "is_ci", "lazy_static", "merge", "mockito", diff --git a/crates/forge_domain/src/auth/auth_method.rs b/crates/forge_domain/src/auth/auth_method.rs index 5c82b5e6ff..fca0131c11 100644 --- a/crates/forge_domain/src/auth/auth_method.rs +++ b/crates/forge_domain/src/auth/auth_method.rs @@ -1,18 +1,24 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::OAuthConfig; /// Authentication method configuration -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AuthMethod { + /// Authenticate using an API key ApiKey, + /// Authenticate using OAuth device flow #[serde(rename = "oauth_device")] OAuthDevice(OAuthConfig), + /// Authenticate using OAuth authorization code flow #[serde(rename = "oauth_code")] OAuthCode(OAuthConfig), + /// Authenticate using Google Application Default Credentials #[serde(rename = "google_adc")] GoogleAdc, + /// Authenticate using OpenAI Codex device flow #[serde(rename = "codex_device")] CodexDevice(OAuthConfig), } diff --git a/crates/forge_domain/src/auth/oauth_config.rs b/crates/forge_domain/src/auth/oauth_config.rs index f9c99344e3..18263e475b 100644 --- a/crates/forge_domain/src/auth/oauth_config.rs +++ b/crates/forge_domain/src/auth/oauth_config.rs @@ -1,29 +1,51 @@ use std::collections::HashMap; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use url::Url; #[derive( - Clone, Serialize, Deserialize, derive_more::From, derive_more::Deref, PartialEq, Eq, Debug, + Clone, + Serialize, + Deserialize, + derive_more::From, + derive_more::Deref, + PartialEq, + Eq, + Debug, + JsonSchema, )] #[serde(transparent)] +#[schemars(with = "String")] pub struct ClientId(String); /// OAuth configuration for authentication flows -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OAuthConfig { + /// The URL to initiate the OAuth authorization or device code flow + #[schemars(with = "String")] pub auth_url: Url, + /// The URL to exchange authorization codes or device codes for access tokens + #[schemars(with = "String")] pub token_url: Url, + /// The OAuth client identifier pub client_id: ClientId, + /// The OAuth scopes to request pub scopes: Vec, + /// The redirect URI for authorization code flows #[serde(skip_serializing_if = "Option::is_none")] pub redirect_uri: Option, + /// Whether to use PKCE (Proof Key for Code Exchange) #[serde(default)] pub use_pkce: bool, + /// The URL to refresh the token after OAuth-with-API-Key flow + #[schemars(with = "Option")] #[serde(skip_serializing_if = "Option::is_none")] pub token_refresh_url: Option, + /// Custom HTTP headers to include in OAuth requests #[serde(skip_serializing_if = "Option::is_none")] pub custom_headers: Option>, + /// Extra parameters to include in the authorization request #[serde(skip_serializing_if = "Option::is_none")] pub extra_auth_params: Option>, } diff --git a/crates/forge_domain/src/model.rs b/crates/forge_domain/src/model.rs index 1d2600939d..df240f6e5d 100644 --- a/crates/forge_domain/src/model.rs +++ b/crates/forge_domain/src/model.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use strum_macros::EnumString; /// Represents input modalities that a model can accept -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumString)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumString, JsonSchema)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase", ascii_case_insensitive)] pub enum InputModality { @@ -20,13 +20,18 @@ fn default_input_modalities() -> Vec { vec![InputModality::Text] } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Setters)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Setters, JsonSchema)] pub struct Model { + /// Unique model identifier pub id: ModelId, + /// Human-readable display name for the model pub name: Option, + /// Description of the model's capabilities pub description: Option, + /// Maximum context window size in tokens pub context_length: Option, // TODO: add provider information to the model + /// Whether the model supports tool/function calls pub tools_supported: Option, /// Whether the model supports parallel tool calls pub supports_parallel_tool_calls: Option, diff --git a/crates/forge_domain/src/provider.rs b/crates/forge_domain/src/provider.rs index e55d6de1e2..6840ec55c9 100644 --- a/crates/forge_domain/src/provider.rs +++ b/crates/forge_domain/src/provider.rs @@ -10,7 +10,18 @@ use crate::{ApiKey, AuthCredential, AuthDetails, Model, Template}; /// Distinguishes between different categories of providers #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, Default, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Display, + EnumString, + Default, + JsonSchema, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -176,12 +187,17 @@ impl From for ProviderId { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum ProviderResponse { + /// OpenAI-compatible chat completions format OpenAI, + /// OpenAI Responses API format OpenAIResponses, + /// Anthropic messages format Anthropic, + /// AWS Bedrock format Bedrock, + /// Google AI format Google, } diff --git a/crates/forge_repo/Cargo.toml b/crates/forge_repo/Cargo.toml index ed9cc726a9..4de92898ab 100644 --- a/crates/forge_repo/Cargo.toml +++ b/crates/forge_repo/Cargo.toml @@ -69,3 +69,4 @@ fake = { version = "5.1.0", features = ["derive"] } derive_setters.workspace = true mockito = { workspace = true } regex = { workspace = true } +is_ci.workspace = true diff --git a/crates/forge_repo/src/lib.rs b/crates/forge_repo/src/lib.rs index 8041672897..4cf31305a0 100644 --- a/crates/forge_repo/src/lib.rs +++ b/crates/forge_repo/src/lib.rs @@ -16,3 +16,6 @@ mod proto_generated { // Only expose forge_repo container pub use forge_repo::*; + +// Expose ProviderConfig for schema generation +pub use provider::ProviderConfig; diff --git a/crates/forge_repo/src/provider/provider.schema.json b/crates/forge_repo/src/provider/provider.schema.json new file mode 100644 index 0000000000..78f30f44d8 --- /dev/null +++ b/crates/forge_repo/src/provider/provider.schema.json @@ -0,0 +1,360 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Array_of_ProviderConfig", + "type": "array", + "items": { + "$ref": "#/$defs/ProviderConfig" + }, + "$defs": { + "AuthMethod": { + "description": "Authentication method configuration", + "oneOf": [ + { + "description": "Authenticate using an API key", + "type": "string", + "const": "api_key" + }, + { + "description": "Authenticate using OAuth device flow", + "type": "object", + "properties": { + "oauth_device": { + "$ref": "#/$defs/OAuthConfig" + } + }, + "additionalProperties": false, + "required": [ + "oauth_device" + ] + }, + { + "description": "Authenticate using OAuth authorization code flow", + "type": "object", + "properties": { + "oauth_code": { + "$ref": "#/$defs/OAuthConfig" + } + }, + "additionalProperties": false, + "required": [ + "oauth_code" + ] + }, + { + "description": "Authenticate using Google Application Default Credentials", + "type": "string", + "const": "google_adc" + }, + { + "description": "Authenticate using OpenAI Codex device flow", + "type": "object", + "properties": { + "codex_device": { + "$ref": "#/$defs/OAuthConfig" + } + }, + "additionalProperties": false, + "required": [ + "codex_device" + ] + } + ] + }, + "InputModality": { + "description": "Represents input modalities that a model can accept", + "oneOf": [ + { + "description": "Text input (all models support this)", + "type": "string", + "const": "text" + }, + { + "description": "Image input (vision-capable models)", + "type": "string", + "const": "image" + } + ] + }, + "Model": { + "type": "object", + "properties": { + "context_length": { + "description": "Maximum context window size in tokens", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "description": { + "description": "Description of the model's capabilities", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Unique model identifier", + "type": "string" + }, + "input_modalities": { + "description": "Input modalities supported by the model (defaults to text-only)", + "type": "array", + "default": [ + "text" + ], + "items": { + "$ref": "#/$defs/InputModality" + } + }, + "name": { + "description": "Human-readable display name for the model", + "type": [ + "string", + "null" + ] + }, + "supports_parallel_tool_calls": { + "description": "Whether the model supports parallel tool calls", + "type": [ + "boolean", + "null" + ] + }, + "supports_reasoning": { + "description": "Whether the model supports reasoning", + "type": [ + "boolean", + "null" + ] + }, + "tools_supported": { + "description": "Whether the model supports tool/function calls", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id" + ] + }, + "Models": { + "description": "Represents the source of models for a provider", + "anyOf": [ + { + "description": "Models are fetched from a URL", + "type": "string" + }, + { + "description": "Models are hardcoded in the configuration", + "type": "array", + "items": { + "$ref": "#/$defs/Model" + } + } + ] + }, + "OAuthConfig": { + "description": "OAuth configuration for authentication flows", + "type": "object", + "properties": { + "auth_url": { + "description": "The URL to initiate the OAuth authorization or device code flow", + "type": "string" + }, + "client_id": { + "description": "The OAuth client identifier", + "type": "string" + }, + "custom_headers": { + "description": "Custom HTTP headers to include in OAuth requests", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "extra_auth_params": { + "description": "Extra parameters to include in the authorization request", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "redirect_uri": { + "description": "The redirect URI for authorization code flows", + "type": [ + "string", + "null" + ] + }, + "scopes": { + "description": "The OAuth scopes to request", + "type": "array", + "items": { + "type": "string" + } + }, + "token_refresh_url": { + "description": "The URL to refresh the token after OAuth-with-API-Key flow", + "type": [ + "string", + "null" + ] + }, + "token_url": { + "description": "The URL to exchange authorization codes or device codes for access tokens", + "type": "string" + }, + "use_pkce": { + "description": "Whether to use PKCE (Proof Key for Code Exchange)", + "type": "boolean", + "default": false + } + }, + "required": [ + "auth_url", + "token_url", + "client_id", + "scopes" + ] + }, + "ProviderConfig": { + "description": "Configuration for a single provider entry in provider.json", + "type": "object", + "properties": { + "api_key_vars": { + "description": "Environment variable name holding the API key", + "type": [ + "string", + "null" + ], + "default": null + }, + "auth_methods": { + "description": "Authentication methods supported by this provider", + "type": "array", + "items": { + "$ref": "#/$defs/AuthMethod" + } + }, + "custom_headers": { + "description": "Custom HTTP headers to include in all requests to this provider", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + }, + "default": null + }, + "id": { + "description": "Unique identifier for this provider", + "$ref": "#/$defs/ProviderId" + }, + "models": { + "description": "Source of models for this provider (URL or hardcoded list)", + "anyOf": [ + { + "$ref": "#/$defs/Models" + }, + { + "type": "null" + } + ] + }, + "provider_type": { + "description": "The type of provider (llm or context_engine)", + "$ref": "#/$defs/ProviderType", + "default": "llm" + }, + "response_type": { + "description": "The response format type used by this provider", + "anyOf": [ + { + "$ref": "#/$defs/ProviderResponse" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "description": "The endpoint URL (supports template variables like {{VAR}})", + "type": "string" + }, + "url_param_vars": { + "description": "Environment variable names holding URL parameters", + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "url", + "auth_methods" + ] + }, + "ProviderId": { + "description": "--- IMPORTANT ---\nThe order of providers is important because that would be order in which the\nproviders will be resolved", + "type": "string" + }, + "ProviderResponse": { + "oneOf": [ + { + "description": "OpenAI-compatible chat completions format", + "type": "string", + "const": "OpenAI" + }, + { + "description": "OpenAI Responses API format", + "type": "string", + "const": "OpenAIResponses" + }, + { + "description": "Anthropic messages format", + "type": "string", + "const": "Anthropic" + }, + { + "description": "AWS Bedrock format", + "type": "string", + "const": "Bedrock" + }, + { + "description": "Google AI format", + "type": "string", + "const": "Google" + } + ] + }, + "ProviderType": { + "description": "Distinguishes between different categories of providers", + "oneOf": [ + { + "description": "LLM providers for chat completions (default for backward compatibility)", + "type": "string", + "const": "llm" + }, + { + "description": "Context engine providers for code indexing and search", + "type": "string", + "const": "context_engine" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 57863a0ba4..ae8443b198 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -8,44 +8,55 @@ use forge_domain::{ ProviderRepository, ProviderType, URLParam, URLParamValue, }; use merge::Merge; +use schemars::JsonSchema; use serde::Deserialize; /// Represents the source of models for a provider -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(untagged)] -enum Models { +pub enum Models { /// Models are fetched from a URL Url(String), /// Models are hardcoded in the configuration Hardcoded(Vec), } -#[derive(Debug, Clone, Deserialize, Merge)] -struct ProviderConfig { +/// Configuration for a single provider entry in provider.json +#[derive(Debug, Clone, Deserialize, Merge, JsonSchema)] +pub struct ProviderConfig { + /// Unique identifier for this provider #[merge(strategy = overwrite)] - id: ProviderId, + pub id: ProviderId, + /// The type of provider (llm or context_engine) #[serde(default)] #[merge(strategy = overwrite)] - provider_type: ProviderType, + pub provider_type: ProviderType, + /// Environment variable name holding the API key #[serde(default)] #[merge(strategy = overwrite)] - api_key_vars: Option, + pub api_key_vars: Option, + /// Environment variable names holding URL parameters #[serde(default)] #[merge(strategy = merge::vec::append)] - url_param_vars: Vec, + pub url_param_vars: Vec, + /// The response format type used by this provider #[serde(default)] #[merge(strategy = overwrite)] - response_type: Option, + pub response_type: Option, + /// The endpoint URL (supports template variables like {{VAR}}) #[merge(strategy = overwrite)] - url: String, + pub url: String, + /// Source of models for this provider (URL or hardcoded list) #[serde(default)] #[merge(strategy = overwrite)] - models: Option, + pub models: Option, + /// Authentication methods supported by this provider #[merge(strategy = merge::vec::append)] - auth_methods: Vec, + pub auth_methods: Vec, + /// Custom HTTP headers to include in all requests to this provider #[serde(default)] #[merge(strategy = overwrite)] - custom_headers: Option>, + pub custom_headers: Option>, } fn overwrite(base: &mut T, other: T) { diff --git a/crates/forge_repo/tests/provider_schema.rs b/crates/forge_repo/tests/provider_schema.rs new file mode 100644 index 0000000000..0a3abfbf44 --- /dev/null +++ b/crates/forge_repo/tests/provider_schema.rs @@ -0,0 +1,29 @@ +use std::path::Path; + +use forge_repo::ProviderConfig; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn generate_provider_schema() -> anyhow::Result<()> { + let schema = schemars::schema_for!(Vec); + let generated_schema = serde_json::to_string_pretty(&schema)?; + + let crate_root = env!("CARGO_MANIFEST_DIR"); + let schema_path = Path::new(crate_root).join("src/provider/provider.schema.json"); + + if is_ci::uncached() { + // On CI: validate that the generated schema matches the committed file + let existing_schema = tokio::fs::read_to_string(&schema_path).await?; + assert_eq!( + generated_schema.trim(), + existing_schema.trim(), + "Generated provider schema does not match the committed schema file. \ + Please run the test locally to update the schema file." + ); + } else { + // Locally: generate and write the schema file + tokio::fs::write(&schema_path, generated_schema).await?; + } + + Ok(()) +} From bd1269112ec1200ca02044a499cd71fbc9bf0ed9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:12:30 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- crates/forge_domain/src/auth/oauth_config.rs | 3 ++- crates/forge_repo/src/lib.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge_domain/src/auth/oauth_config.rs b/crates/forge_domain/src/auth/oauth_config.rs index 18263e475b..109ce3cb82 100644 --- a/crates/forge_domain/src/auth/oauth_config.rs +++ b/crates/forge_domain/src/auth/oauth_config.rs @@ -25,7 +25,8 @@ pub struct OAuthConfig { /// The URL to initiate the OAuth authorization or device code flow #[schemars(with = "String")] pub auth_url: Url, - /// The URL to exchange authorization codes or device codes for access tokens + /// The URL to exchange authorization codes or device codes for access + /// tokens #[schemars(with = "String")] pub token_url: Url, /// The OAuth client identifier diff --git a/crates/forge_repo/src/lib.rs b/crates/forge_repo/src/lib.rs index 4cf31305a0..833c827204 100644 --- a/crates/forge_repo/src/lib.rs +++ b/crates/forge_repo/src/lib.rs @@ -16,6 +16,5 @@ mod proto_generated { // Only expose forge_repo container pub use forge_repo::*; - // Expose ProviderConfig for schema generation pub use provider::ProviderConfig; From 184a338775d88339fe25acf8ad24efc7d8f622a9 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Thu, 19 Mar 2026 13:43:46 +0530 Subject: [PATCH 3/5] fix(provider): add OpenCode variant to ProviderResponse JSON schema --- crates/forge_domain/src/provider.rs | 1 + crates/forge_repo/src/provider/provider.schema.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/crates/forge_domain/src/provider.rs b/crates/forge_domain/src/provider.rs index 3041e3c7cf..dfd1929a4d 100644 --- a/crates/forge_domain/src/provider.rs +++ b/crates/forge_domain/src/provider.rs @@ -201,6 +201,7 @@ pub enum ProviderResponse { Bedrock, /// Google AI format Google, + /// OpenCode proxy format — response type is determined by the selected model OpenCode, } diff --git a/crates/forge_repo/src/provider/provider.schema.json b/crates/forge_repo/src/provider/provider.schema.json index 78f30f44d8..c195cf02eb 100644 --- a/crates/forge_repo/src/provider/provider.schema.json +++ b/crates/forge_repo/src/provider/provider.schema.json @@ -338,6 +338,11 @@ "description": "Google AI format", "type": "string", "const": "Google" + }, + { + "description": "OpenCode proxy format — response type is determined by the selected model", + "type": "string", + "const": "OpenCode" } ] }, From fc8053a38e2e0974d8529cf9f8ee93f721e87628 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:16:00 +0000 Subject: [PATCH 4/5] [autofix.ci] apply automated fixes --- crates/forge_domain/src/provider.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/forge_domain/src/provider.rs b/crates/forge_domain/src/provider.rs index dfd1929a4d..7980e66531 100644 --- a/crates/forge_domain/src/provider.rs +++ b/crates/forge_domain/src/provider.rs @@ -201,7 +201,8 @@ pub enum ProviderResponse { Bedrock, /// Google AI format Google, - /// OpenCode proxy format — response type is determined by the selected model + /// OpenCode proxy format — response type is determined by the selected + /// model OpenCode, } From a2a3b3880f0a5497a9a35a1cd4348dcfdd788e81 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Thu, 19 Mar 2026 13:50:48 +0530 Subject: [PATCH 5/5] fix(provider): update descriptions in ProviderConfig for better readability --- crates/forge_repo/src/provider/provider.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge_repo/src/provider/provider.schema.json b/crates/forge_repo/src/provider/provider.schema.json index c195cf02eb..c17b560876 100644 --- a/crates/forge_repo/src/provider/provider.schema.json +++ b/crates/forge_repo/src/provider/provider.schema.json @@ -211,7 +211,7 @@ ] }, "token_url": { - "description": "The URL to exchange authorization codes or device codes for access tokens", + "description": "The URL to exchange authorization codes or device codes for access\ntokens", "type": "string" }, "use_pkce": { @@ -340,7 +340,7 @@ "const": "Google" }, { - "description": "OpenCode proxy format — response type is determined by the selected model", + "description": "OpenCode proxy format — response type is determined by the selected\nmodel", "type": "string", "const": "OpenCode" }