mod opencode;
mod registry;
use std::cell::RefCell;
use std::sync::Arc;
use crate::config::types::{
ModelOption, NaviConfig, ProviderConfig, ProviderKind, ProviderModelConfig, ToolCallingMode,
};
use crate::registry::RegistryStore;
pub use opencode::{is_free_model_name, model_can_run_publicly, provider_request_model_name};
pub use registry::default_request_options_for;
thread_local! {
static REGISTRY_STORE: RefCell<Option<Arc<RegistryStore>>> = const { RefCell::new(None) };
}
pub fn set_registry_store(store: Arc<RegistryStore>) {
REGISTRY_STORE.with(|cell| {
*cell.borrow_mut() = Some(store);
});
}
pub fn provider_catalog(config: &NaviConfig) -> Vec<ProviderConfig> {
let mut providers = base_provider_catalog();
merge_provider_configs(&mut providers, config.providers.clone());
apply_default_request_options(&mut providers);
providers
}
fn base_provider_catalog() -> Vec<ProviderConfig> {
let registry_providers = REGISTRY_STORE.with(|cell| {
cell.borrow()
.as_ref()
.and_then(|store| match store.load_all_providers() {
Ok(providers) if !providers.is_empty() => Some(providers),
Ok(_) => {
tracing::debug!("registry cache is empty, falling back to built-in providers");
None
}
Err(err) => {
tracing::warn!(
error = %err,
"failed to load from registry cache, falling back to built-in"
);
None
}
})
});
registry_providers.unwrap_or_else(registry::built_in_providers)
}
pub fn canonical_provider_id(id: &str) -> &str {
match id {
"opencode-zen" => "opencode",
other => other,
}
}
pub fn resolve_provider_config(config: &NaviConfig, id: &str) -> Option<ProviderConfig> {
let canonical_id = canonical_provider_id(id);
provider_catalog(config)
.into_iter()
.find(|provider| canonical_provider_id(&provider.id) == canonical_id)
}
pub fn available_model_options(config: &NaviConfig) -> Vec<ModelOption> {
provider_catalog(config)
.into_iter()
.flat_map(|provider| {
let desc = provider.description.clone();
provider
.models
.clone()
.into_iter()
.map(move |model| ModelOption {
name: model.name,
provider_id: provider.id.clone(),
provider_label: provider.label.clone(),
provider_description: desc.clone(),
task_size: model.task_size,
context_window_tokens: model.context_window_tokens,
})
})
.collect()
}
pub fn effective_context_window(config: &NaviConfig) -> u64 {
let selected_provider = &config.model.provider;
let selected_model = &config.model.name;
available_model_options(config)
.into_iter()
.find(|m| m.provider_id == *selected_provider && m.name == *selected_model)
.and_then(|m| m.context_window_tokens)
.unwrap_or(crate::config::defaults::DEFAULT_CONTEXT_WINDOW)
}
pub(crate) fn effective_tool_prompt_manifest(config: &NaviConfig) -> bool {
use crate::config::types::ToolPromptManifest;
match config.harness.tool_prompt_manifest {
ToolPromptManifest::Always => return true,
ToolPromptManifest::Never => return false,
ToolPromptManifest::Auto => {}
}
match effective_tool_calling_mode(config) {
ToolCallingMode::TextExtracted | ToolCallingMode::ManifestOnly => return true,
ToolCallingMode::Disabled => return false,
ToolCallingMode::Native => {}
}
let selected_provider = &config.model.provider;
let selected_model = &config.model.name;
provider_catalog(config)
.into_iter()
.find(|provider| {
canonical_provider_id(&provider.id) == canonical_provider_id(selected_provider)
})
.and_then(|provider| {
provider
.models
.iter()
.find(|model| model.name == *selected_model)
.and_then(|model| model.tool_prompt_manifest)
.or(provider.tool_prompt_manifest)
})
.unwrap_or(false)
}
pub fn effective_tool_calling_mode(config: &NaviConfig) -> ToolCallingMode {
let selected_provider = &config.model.provider;
provider_catalog(config)
.into_iter()
.find(|provider| {
canonical_provider_id(&provider.id) == canonical_provider_id(selected_provider)
})
.and_then(|provider| provider.tool_calling_mode)
.unwrap_or(ToolCallingMode::Native)
}
impl NaviConfig {
pub fn update_provider_models(&mut self, provider_id: &str, model_names: &[String]) {
let mut existing_models = std::collections::HashMap::new();
let provider_id = canonical_provider_id(provider_id).to_string();
if let Some(existing_override) = self
.providers
.iter()
.find(|p| canonical_provider_id(&p.id) == provider_id)
{
for m in &existing_override.models {
existing_models.insert(m.name.clone(), m.clone());
}
}
if let Some(registry_provider) = base_provider_catalog()
.into_iter()
.find(|p| canonical_provider_id(&p.id) == provider_id)
{
for m in registry_provider.models {
existing_models.insert(m.name.clone(), m);
}
}
let mut new_models = Vec::new();
for name in model_names {
if let Some(model) = existing_models.get(name) {
let mut model = model.clone();
model.name = name.clone();
new_models.push(model);
} else {
new_models.push(ProviderModelConfig {
name: name.clone(),
task_size: registry::determine_task_size(name),
context_window_tokens: None,
max_output_tokens: None,
recommended_temperature: None,
supports_thinking: None,
tool_prompt_manifest: None,
});
}
}
if let Some(p) = self
.providers
.iter_mut()
.find(|p| canonical_provider_id(&p.id) == provider_id)
{
p.id = provider_id.clone();
p.models = new_models;
} else {
if let Some(mut resolved) = resolve_provider_config(self, &provider_id) {
resolved.models = new_models;
self.providers.push(resolved);
} else {
self.providers.push(ProviderConfig {
id: provider_id.to_string(),
label: provider_id.to_string(),
description: "Synced dynamically".to_string(),
kind: ProviderKind::OpenAiChatCompletions,
api_key_env: format!(
"{}_API_KEY",
provider_id.to_uppercase().replace('-', "_")
),
base_url: None,
models: new_models,
..Default::default()
});
}
}
}
}
pub(crate) fn merge_provider_configs(
providers: &mut Vec<ProviderConfig>,
overrides: Vec<ProviderConfig>,
) {
for override_config in overrides {
if let Some(existing) = providers.iter_mut().find(|provider| {
canonical_provider_id(&provider.id) == canonical_provider_id(&override_config.id)
}) {
let existing_models: std::collections::HashMap<String, ProviderModelConfig> = existing
.models
.drain(..)
.map(|m| (m.name.clone(), m))
.collect();
let mut merged_models = Vec::new();
for override_model in override_config.models {
if let Some(registry_model) = existing_models.get(&override_model.name) {
merged_models.push(ProviderModelConfig {
name: override_model.name,
task_size: override_model.task_size,
context_window_tokens: override_model
.context_window_tokens
.or(registry_model.context_window_tokens),
max_output_tokens: override_model
.max_output_tokens
.or(registry_model.max_output_tokens),
recommended_temperature: override_model
.recommended_temperature
.or(registry_model.recommended_temperature),
supports_thinking: override_model
.supports_thinking
.or(registry_model.supports_thinking),
tool_prompt_manifest: override_model
.tool_prompt_manifest
.or(registry_model.tool_prompt_manifest),
});
} else {
merged_models.push(override_model);
}
}
existing.id = canonical_provider_id(&existing.id).to_string();
existing.label = override_config.label;
existing.description = override_config.description;
existing.kind = override_config.kind;
existing.api_key_env = override_config.api_key_env;
if override_config.base_url.is_some() {
existing.base_url = override_config.base_url;
}
existing.models = merged_models;
if override_config.request_options.is_some() {
existing.request_options = override_config.request_options;
}
if override_config.request_timeout_ms.is_some() {
existing.request_timeout_ms = override_config.request_timeout_ms;
}
if override_config.request_max_retries.is_some() {
existing.request_max_retries = override_config.request_max_retries;
}
if override_config.stream_idle_timeout_ms.is_some() {
existing.stream_idle_timeout_ms = override_config.stream_idle_timeout_ms;
}
if override_config.stream_max_retries.is_some() {
existing.stream_max_retries = override_config.stream_max_retries;
}
if override_config.websocket_connect_timeout_ms.is_some() {
existing.websocket_connect_timeout_ms =
override_config.websocket_connect_timeout_ms;
}
if override_config.retry_429.is_some() {
existing.retry_429 = override_config.retry_429;
}
if override_config.tool_prompt_manifest.is_some() {
existing.tool_prompt_manifest = override_config.tool_prompt_manifest;
}
if override_config.tool_calling_mode.is_some() {
existing.tool_calling_mode = override_config.tool_calling_mode;
}
} else {
providers.push(override_config);
}
}
}
fn apply_default_request_options(providers: &mut [ProviderConfig]) {
for provider in providers {
let id = canonical_provider_id(&provider.id);
if provider.request_options.is_none()
&& let Some(defaults) = default_request_options_for(id)
{
provider.request_options = Some(defaults);
}
if provider.tool_calling_mode.is_none() && id == "commandcode" {
provider.tool_calling_mode = Some(ToolCallingMode::Native);
}
}
}
#[cfg(test)]
mod tests {
use super::{effective_tool_calling_mode, effective_tool_prompt_manifest};
use crate::config::types::{
ModelTaskSize, NaviConfig, ProviderConfig, ProviderModelConfig, ToolCallingMode,
};
#[test]
fn update_provider_models_prefers_registry_metadata_over_stale_override() {
let mut config = NaviConfig::default();
config.providers.push(ProviderConfig {
id: "commandcode".to_string(),
models: vec![ProviderModelConfig {
name: "claude-sonnet-4-6".to_string(),
task_size: ModelTaskSize::Large,
context_window_tokens: Some(128_000),
max_output_tokens: None,
recommended_temperature: None,
supports_thinking: None,
tool_prompt_manifest: None,
}],
..Default::default()
});
config.update_provider_models("commandcode", &["claude-sonnet-4-6".to_string()]);
let provider = config
.providers
.iter()
.find(|provider| provider.id == "commandcode")
.expect("commandcode override");
assert_eq!(provider.models[0].context_window_tokens, Some(1_000_000));
}
#[test]
fn commandcode_uses_native_tool_mode() {
let mut config = NaviConfig::default();
config.model.provider = "commandcode".to_string();
assert_eq!(
effective_tool_calling_mode(&config),
ToolCallingMode::Native
);
assert!(!effective_tool_prompt_manifest(&config));
}
}