//! Schema-heavy configuration TOML types used by Codex. use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; use crate::permissions_toml::PermissionsToml; use crate::profile_toml::ConfigProfile; use crate::types::AnalyticsConfigToml; use crate::types::ApprovalsReviewer; use crate::types::AppsConfigToml; use crate::types::AuthCredentialsStoreMode; use crate::types::FeedbackConfigToml; use crate::types::History; use crate::types::McpServerConfig; use crate::types::MemoriesToml; use crate::types::Notice; use crate::types::OAuthCredentialsStoreMode; use crate::types::OtelConfigToml; use crate::types::PluginConfig; use crate::types::SandboxWorkspaceWrite; use crate::types::ShellEnvironmentPolicyToml; use crate::types::SkillsConfig; use crate::types::ToolSuggestConfig; use crate::types::Tui; use crate::types::UriBasedFileOpener; use crate::types::WindowsToml; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; use codex_features::FeaturesToml; use codex_git_utils::resolve_root_git_project_for_trust; use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::OPENAI_PROVIDER_ID; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; use serde::Deserialize; use serde::Deserializer; use serde::Serialize; const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = [ OPENAI_PROVIDER_ID, OLLAMA_OSS_PROVIDER_ID, LMSTUDIO_OSS_PROVIDER_ID, ]; /// Base config deserialized from ~/.codex/config.toml. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ConfigToml { /// Optional override of model selection. pub model: Option, /// Review model override used by the `/review` feature. pub review_model: Option, /// Provider to use from the model_providers map. pub model_provider: Option, /// Size of the context window for the model, in tokens. pub model_context_window: Option, /// Token usage threshold triggering auto-compaction of conversation history. pub model_auto_compact_token_limit: Option, /// Default approval policy for executing commands. pub approval_policy: Option, /// Configures who approval requests are routed to for review once they have /// been escalated. This does not disable separate safety checks such as /// ARC. pub approvals_reviewer: Option, #[serde(default)] pub shell_environment_policy: ShellEnvironmentPolicyToml, /// Whether the model may request a login shell for shell-based tools. /// Default to `true` /// /// If `true`, the model may request a login shell (`login = true`), and /// omitting `login` defaults to using a login shell. /// If `false`, the model can never use a login shell: `login = true` /// requests are rejected, and omitting `login` defaults to a non-login /// shell. pub allow_login_shell: Option, /// Sandbox mode to use. pub sandbox_mode: Option, /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. pub sandbox_workspace_write: Option, /// Default named permissions profile to apply from the `[permissions]` /// table. pub default_permissions: Option, /// Named permissions profiles. #[serde(default)] pub permissions: Option, /// Optional external command to spawn for end-user notifications. #[serde(default)] pub notify: Option>, /// System instructions. pub instructions: Option, /// Developer instructions inserted as a `developer` role message. #[serde(default)] pub developer_instructions: Option, /// Whether to inject the `` developer block. pub include_permissions_instructions: Option, /// Whether to inject the `` developer block. pub include_apps_instructions: Option, /// Whether to inject the `` user block. pub include_environment_context: Option, /// Optional path to a file containing model instructions that will override /// the built-in instructions for the selected model. Users are STRONGLY /// DISCOURAGED from using this field, as deviating from the instructions /// sanctioned by Codex will likely degrade model performance. pub model_instructions_file: Option, /// Compact prompt used for history compaction. pub compact_prompt: Option, /// Optional commit attribution text for commit message co-author trailers. /// /// Set to an empty string to disable automatic commit attribution. pub commit_attribution: Option, /// When set, restricts ChatGPT login to a specific workspace identifier. #[serde(default)] pub forced_chatgpt_workspace_id: Option, /// When set, restricts the login mechanism users may use. #[serde(default)] pub forced_login_method: Option, /// Preferred backend for storing CLI auth credentials. /// file (default): Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. /// auto: Use the keyring if available, otherwise use a file. #[serde(default)] pub cli_auth_credentials_store: Option, /// Definition for MCP servers that Codex can reach out to for tool calls. #[serde(default)] // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`. #[schemars(schema_with = "crate::schema::mcp_servers_schema")] pub mcp_servers: HashMap, /// Preferred backend for storing MCP OAuth credentials. /// keyring: Use an OS-specific keyring service. /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2 /// file: Use a file in the Codex home directory. /// auto (default): Use the OS-specific keyring service if available, otherwise use a file. #[serde(default)] pub mcp_oauth_credentials_store: Option, /// Optional fixed port for the local HTTP callback server used during MCP OAuth login. /// When unset, Codex will bind to an ephemeral port chosen by the OS. pub mcp_oauth_callback_port: Option, /// Optional redirect URI to use during MCP OAuth login. /// When set, this URI is used in the OAuth authorization request instead /// of the local listener address. The local callback listener still binds /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). pub mcp_oauth_callback_url: Option, /// User-defined provider entries that extend the built-in list. Built-in /// IDs cannot be overridden. #[serde(default, deserialize_with = "deserialize_model_providers")] pub model_providers: HashMap, /// Maximum number of bytes to include from an AGENTS.md project doc file. pub project_doc_max_bytes: Option, /// Ordered list of fallback filenames to look for when AGENTS.md is missing. pub project_doc_fallback_filenames: Option>, /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option, /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. /// Default: `300000` (5 minutes). pub background_terminal_max_timeout: Option, /// Optional absolute path to the Node runtime used by `js_repl`. pub js_repl_node_path: Option, /// Ordered list of directories to search for Node modules in `js_repl`. pub js_repl_node_module_dirs: Option>, /// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution. pub zsh_path: Option, /// Profile to use from the `profiles` map. pub profile: Option, /// Named profiles to facilitate switching between different configurations. #[serde(default)] pub profiles: HashMap, /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. #[serde(default)] pub history: Option, /// Directory where Codex stores the SQLite state DB. /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`. pub sqlite_home: Option, /// Directory where Codex writes log files, for example `codex-tui.log`. /// Defaults to `$CODEX_HOME/log`. pub log_dir: Option, /// Optional URI-based file opener. If set, citations to files in the model /// output will be hyperlinked using the specified URI scheme. pub file_opener: Option, /// Collection of settings that are specific to the TUI. pub tui: Option, /// When set to `true`, `AgentReasoning` events will be hidden from the /// UI/output. Defaults to `false`. pub hide_agent_reasoning: Option, /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. /// Defaults to `false`. pub show_raw_agent_reasoning: Option, pub model_reasoning_effort: Option, pub plan_mode_reasoning_effort: Option, pub model_reasoning_summary: Option, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option, /// Override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option, /// Optional path to a JSON model catalog (applied on startup only). /// Per-thread `config` overrides are accepted but do not reapply this (no-ops). pub model_catalog_json: Option, /// Optionally specify a personality for the model pub personality: Option, /// Optional explicit service tier preference for new turns (`fast` or `flex`). pub service_tier: Option, /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: Option, /// Base URL override for the built-in `openai` model provider. pub openai_base_url: Option, /// Machine-local realtime audio device preferences used by realtime voice. #[serde(default)] pub audio: Option, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport base URL (the `Op::RealtimeConversation` /// `/v1/realtime` /// connection) without changing normal provider HTTP requests. pub experimental_realtime_ws_base_url: Option, /// Experimental / do not use. Selects the realtime websocket model/snapshot /// used for the `Op::RealtimeConversation` connection. pub experimental_realtime_ws_model: Option, /// Experimental / do not use. Realtime websocket session selection. /// `version` controls v1/v2 and `type` controls conversational/transcription. #[serde(default)] pub realtime: Option, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport instructions (the `Op::RealtimeConversation` /// `/ws` session.update instructions) without changing normal prompts. pub experimental_realtime_ws_backend_prompt: Option, /// Experimental / do not use. Replaces the synthesized realtime startup /// context appended to websocket session instructions. An empty string /// disables startup context injection entirely. pub experimental_realtime_ws_startup_context: Option, /// Experimental / do not use. Replaces the built-in realtime start /// instructions inserted into developer messages when realtime becomes /// active. pub experimental_realtime_start_instructions: Option, pub projects: Option>, /// Controls the web search tool mode: disabled, cached, or live. pub web_search: Option, /// Nested tools section for feature toggles pub tools: Option, /// Additional discoverable tools that can be suggested for installation. pub tool_suggest: Option, /// Agent-related settings (thread limits, etc.). pub agents: Option, /// Memories subsystem settings. pub memories: Option, /// User-level skill config entries keyed by SKILL.md path. pub skills: Option, /// User-level plugin config entries keyed by plugin name. #[serde(default)] pub plugins: HashMap, /// Centralized feature flags (new). Prefer this over individual toggles. #[serde(default)] // Injects known feature keys into the schema and forbids unknown keys. #[schemars(schema_with = "crate::schema::features_schema")] pub features: Option, /// Suppress warnings about unstable (under development) features. pub suppress_unstable_features_warning: Option, /// Settings for ghost snapshots (used for undo). #[serde(default)] pub ghost_snapshot: Option, /// Markers used to detect the project root when searching parent /// directories for `.codex` folders. Defaults to [".git"] when unset. #[serde(default)] pub project_root_markers: Option>, /// When `true`, checks for Codex updates on startup and surfaces update prompts. /// Set to `false` only if your Codex updates are centrally managed. /// Defaults to `true`. pub check_for_update_on_startup: Option, /// When true, disables burst-paste detection for typed input entirely. /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option, /// When `false`, disables analytics across Codex product surfaces in this machine. /// Defaults to `true`. pub analytics: Option, /// When `false`, disables feedback collection across Codex product surfaces. /// Defaults to `true`. pub feedback: Option, /// Settings for app-specific controls. #[serde(default)] pub apps: Option, /// OTEL configuration. pub otel: Option, /// Windows-specific configuration. #[serde(default)] pub windows: Option, /// Tracks whether the Windows onboarding screen has been acknowledged. pub windows_wsl_setup_acknowledged: Option, /// Collection of in-product notices (different from notifications) /// See [`crate::types::Notice`] for more details pub notice: Option, /// Legacy, now use features /// Deprecated: ignored. Use `model_instructions_file`. #[schemars(skip)] pub experimental_instructions_file: Option, pub experimental_compact_prompt_file: Option, pub experimental_use_unified_exec_tool: Option, pub experimental_use_freeform_apply_patch: Option, /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama". pub oss_provider: Option, } impl From for UserSavedConfig { fn from(config_toml: ConfigToml) -> Self { let profiles = config_toml .profiles .into_iter() .map(|(k, v)| (k, v.into())) .collect(); Self { approval_policy: config_toml.approval_policy, sandbox_mode: config_toml.sandbox_mode, sandbox_settings: config_toml.sandbox_workspace_write.map(From::from), forced_chatgpt_workspace_id: config_toml.forced_chatgpt_workspace_id, forced_login_method: config_toml.forced_login_method, model: config_toml.model, model_reasoning_effort: config_toml.model_reasoning_effort, model_reasoning_summary: config_toml.model_reasoning_summary, model_verbosity: config_toml.model_verbosity, tools: config_toml.tools.map(From::from), profile: config_toml.profile, profiles, } } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ProjectConfig { pub trust_level: Option, } impl ProjectConfig { pub fn is_trusted(&self) -> bool { matches!(self.trust_level, Some(TrustLevel::Trusted)) } pub fn is_untrusted(&self) -> bool { matches!(self.trust_level, Some(TrustLevel::Untrusted)) } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RealtimeAudioConfig { pub microphone: Option, pub speaker: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum RealtimeWsMode { #[default] Conversational, Transcription, } pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct RealtimeConfig { pub version: RealtimeWsVersion, #[serde(rename = "type")] pub session_type: RealtimeWsMode, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct RealtimeToml { pub version: Option, #[serde(rename = "type")] pub session_type: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct RealtimeAudioToml { pub microphone: Option, pub speaker: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ToolsToml { #[serde( default, deserialize_with = "deserialize_optional_web_search_tool_config" )] pub web_search: Option, /// Enable the `view_image` tool that lets the agent attach local images. #[serde(default)] pub view_image: Option, } #[derive(Deserialize)] #[serde(untagged)] enum WebSearchToolConfigInput { Enabled(bool), Config(WebSearchToolConfig), } fn deserialize_optional_web_search_tool_config<'de, D>( deserializer: D, ) -> Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(deserializer)?; Ok(match value { None => None, Some(WebSearchToolConfigInput::Enabled(enabled)) => { let _ = enabled; None } Some(WebSearchToolConfigInput::Config(config)) => Some(config), }) } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct AgentsToml { /// Maximum number of agent threads that can be open concurrently. /// When unset, no limit is enforced. #[schemars(range(min = 1))] pub max_threads: Option, /// Maximum nesting depth allowed for spawned agent threads. /// Root sessions start at depth 0. #[schemars(range(min = 1))] pub max_depth: Option, /// Default maximum runtime in seconds for agent job workers. #[schemars(range(min = 1))] pub job_max_runtime_seconds: Option, /// User-defined role declarations keyed by role name. /// /// Example: /// ```toml /// [agents.researcher] /// description = "Research-focused role." /// config_file = "./agents/researcher.toml" /// nickname_candidates = ["Herodotus", "Ibn Battuta"] /// ``` #[serde(default, flatten)] pub roles: BTreeMap, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct AgentRoleToml { /// Human-facing role documentation used in spawn tool guidance. /// Required unless supplied by the referenced agent role file. pub description: Option, /// Path to a role-specific config layer. /// Relative paths are resolved relative to the `config.toml` that defines them. pub config_file: Option, /// Candidate nicknames for agents spawned with this role. pub nickname_candidates: Option>, } impl From for Tools { fn from(tools_toml: ToolsToml) -> Self { Self { web_search: tools_toml.web_search.is_some().then_some(true), view_image: tools_toml.view_image, } } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct GhostSnapshotToml { /// Exclude untracked files larger than this many bytes from ghost snapshots. #[serde(alias = "ignore_untracked_files_over_bytes")] pub ignore_large_untracked_files: Option, /// Ignore untracked directories that contain this many files or more. /// (Still emits a warning unless warnings are disabled.) #[serde(alias = "large_untracked_dir_warning_threshold")] pub ignore_large_untracked_dirs: Option, /// Disable all ghost snapshot warning events. pub disable_warnings: Option, } impl ConfigToml { /// Derive the effective sandbox policy from the configuration. pub fn derive_sandbox_policy( &self, sandbox_mode_override: Option, profile_sandbox_mode: Option, windows_sandbox_level: WindowsSandboxLevel, resolved_cwd: &Path, sandbox_policy_constraint: Option<&crate::Constrained>, ) -> SandboxPolicy { let sandbox_mode_was_explicit = sandbox_mode_override.is_some() || profile_sandbox_mode.is_some() || self.sandbox_mode.is_some(); let resolved_sandbox_mode = sandbox_mode_override .or(profile_sandbox_mode) .or(self.sandbox_mode) .or_else(|| { // If no sandbox_mode is set but this directory has a trust decision, // default to workspace-write except on unsandboxed Windows where we // default to read-only. self.get_active_project(resolved_cwd).and_then(|p| { if p.is_trusted() || p.is_untrusted() { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled { Some(SandboxMode::ReadOnly) } else { Some(SandboxMode::WorkspaceWrite) } } else { None } }) }) .unwrap_or_default(); let mut sandbox_policy = match resolved_sandbox_mode { SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { Some(SandboxWorkspaceWrite { writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, }) => SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), read_only_access: ReadOnlyAccess::FullAccess, network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, }, None => SandboxPolicy::new_workspace_write_policy(), }, SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, }; let downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| { if cfg!(target_os = "windows") // If the experimental Windows sandbox is enabled, do not force a downgrade. && windows_sandbox_level == WindowsSandboxLevel::Disabled && matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. }) { *policy = SandboxPolicy::new_read_only_policy(); } }; if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) { downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } if !sandbox_mode_was_explicit && let Some(constraint) = sandbox_policy_constraint && let Err(err) = constraint.can_set(&sandbox_policy) { tracing::warn!( error = %err, "default sandbox policy is disallowed by requirements; falling back to required default" ); sandbox_policy = constraint.get().clone(); downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } sandbox_policy } /// Resolves the cwd to an existing project, or returns None if ConfigToml /// does not contain a project corresponding to cwd or a git repo for cwd pub fn get_active_project(&self, resolved_cwd: &Path) -> Option { let projects = self.projects.clone().unwrap_or_default(); let resolved_cwd_key = project_trust_key(resolved_cwd); let resolved_cwd_raw_key = resolved_cwd.to_string_lossy().to_string(); if let Some(project_config) = projects .get(&resolved_cwd_key) .or_else(|| projects.get(&resolved_cwd_raw_key)) { return Some(project_config.clone()); } // If cwd lives inside a git repo/worktree, check whether the root git project // (the primary repository working directory) is trusted. This lets // worktrees inherit trust from the main project. if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) { let repo_root_key = project_trust_key(repo_root.as_path()); let repo_root_raw_key = repo_root.to_string_lossy().to_string(); if let Some(project_config_for_root) = projects .get(&repo_root_key) .or_else(|| projects.get(&repo_root_raw_key)) { return Some(project_config_for_root.clone()); } } None } pub fn get_config_profile( &self, override_profile: Option, ) -> Result { let profile = override_profile.or_else(|| self.profile.clone()); match profile { Some(key) => { if let Some(profile) = self.profiles.get(key.as_str()) { return Ok(profile.clone()); } Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!("config profile `{key}` not found"), )) } None => Ok(ConfigProfile::default()), } } } /// Canonicalize the path and convert it to a string to be used as a key in the /// projects trust map. On Windows, strips UNC, when possible, to try to ensure /// that different paths that point to the same location have the same key. fn project_trust_key(project_path: &Path) -> String { dunce::canonicalize(project_path) .unwrap_or_else(|_| project_path.to_path_buf()) .to_string_lossy() .to_string() } pub fn validate_reserved_model_provider_ids( model_providers: &HashMap, ) -> Result<(), String> { let mut conflicts = model_providers .keys() .filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())) .map(|key| format!("`{key}`")) .collect::>(); conflicts.sort_unstable(); if conflicts.is_empty() { Ok(()) } else { Err(format!( "model_providers contains reserved built-in provider IDs: {}. \ Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).", conflicts.join(", ") )) } } pub fn validate_model_providers( model_providers: &HashMap, ) -> Result<(), String> { validate_reserved_model_provider_ids(model_providers)?; for (key, provider) in model_providers { provider .validate() .map_err(|message| format!("model_providers.{key}: {message}"))?; } Ok(()) } fn deserialize_model_providers<'de, D>( deserializer: D, ) -> Result, D::Error> where D: serde::Deserializer<'de>, { let model_providers = HashMap::::deserialize(deserializer)?; validate_model_providers(&model_providers).map_err(serde::de::Error::custom)?; Ok(model_providers) } pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> { match provider { LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => Ok(()), LEGACY_OLLAMA_CHAT_PROVIDER_ID => Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, OLLAMA_CHAT_PROVIDER_REMOVED_ERROR, )), _ => Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}" ), )), } }