use serde_json::{Value, json};
use crate::config::{Config, types::ProviderConfig};
pub const CONFIG_OPTION_MODEL: &str = "model";
pub fn commands_payload() -> Vec<Value> {
let mut out: Vec<Value> = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut push = |name: &str, description: &str| {
let name = name.trim().trim_start_matches('/');
if name.is_empty() || name.contains([' ', '/', '\\']) || !seen.insert(name.to_string()) {
return;
}
out.push(json!({ "name": name, "description": description }));
};
for cmd in crate::tui::app::state::SLASH_COMMANDS {
push(cmd.name, cmd.description);
}
for skill in crate::brain::skills::load_all_skills() {
push(&skill.slash_name, &skill.description);
}
let brain_path = crate::brain::BrainLoader::resolve_path();
for cmd in crate::brain::CommandLoader::from_brain_path(&brain_path).load() {
push(&cmd.name, &cmd.description);
}
out
}
struct ModelEntry {
provider: String,
display: String,
model: String,
pair: String,
}
fn collect_models(config: &Config) -> Vec<ModelEntry> {
let mut entries: Vec<ModelEntry> = Vec::new();
for (id, display, requires_api_key, cfg) in config.providers.provider_registry() {
let Some(c) = cfg else { continue };
if !c.enabled || (requires_api_key && c.api_key.is_none()) {
continue;
}
push_provider_models(&mut entries, id, display, c);
}
if let Some((name, cfg)) = config.providers.active_custom() {
push_provider_models(&mut entries, name, name, cfg);
}
entries
}
fn current_pair(entries: &[ModelEntry], current_override: Option<&str>) -> String {
current_override
.map(str::to_string)
.or_else(|| entries.first().map(|e| e.pair.clone()))
.unwrap_or_default()
}
pub fn models_payload(config: &Config, current_override: Option<&str>) -> Value {
let entries = collect_models(config);
let available: Vec<Value> = entries
.iter()
.map(|e| {
json!({
"modelId": e.pair,
"name": format!("{} / {}", e.display, e.model),
})
})
.collect();
json!({
"availableModels": available,
"currentModelId": current_pair(&entries, current_override),
})
}
pub fn config_options_payload(config: &Config, current_override: Option<&str>) -> Value {
let entries = collect_models(config);
if entries.is_empty() {
return json!([]);
}
let mut groups: Vec<Value> = Vec::new();
for e in &entries {
let option = json!({ "value": e.pair, "name": e.model });
if let Some(existing) = groups
.iter_mut()
.find(|g| g["group"].as_str() == Some(&e.provider))
{
existing["options"]
.as_array_mut()
.expect("group built with an options array")
.push(option);
} else {
groups.push(json!({
"group": e.provider,
"name": e.display,
"options": [option],
}));
}
}
json!([{
"id": CONFIG_OPTION_MODEL,
"name": "Model",
"description": "Provider and model used for this session",
"category": "model",
"type": "select",
"currentValue": current_pair(&entries, current_override),
"options": groups,
}])
}
fn push_provider_models(
entries: &mut Vec<ModelEntry>,
id: &str,
display: &str,
cfg: &ProviderConfig,
) {
let mut models: Vec<&str> = cfg
.models
.iter()
.map(String::as_str)
.map(str::trim)
.filter(|m| !m.is_empty())
.collect();
if models.is_empty()
&& let Some(d) = cfg.default_model.as_deref().map(str::trim)
&& !d.is_empty()
{
models.push(d);
}
for model in models {
entries.push(ModelEntry {
provider: id.to_string(),
display: display.to_string(),
model: model.to_string(),
pair: format!("{id}/{model}"),
});
}
}