Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions src/acp/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ impl AcpService {
let reasoning = model_reasoning(&config, &models, &provider, &model);
let reasoning_selection =
reasoning.unwrap_or(crate::model::reasoning::ReasoningEffort::None);
let context_window = model_context_window(&config, &provider, &model);
let context_window = model_context_window(&config, &models, &provider, &model);
let agent = config
.merged_config
.default_agent
Expand Down Expand Up @@ -474,8 +474,12 @@ impl AcpService {
session.provider.clone_from(&model.provider_id);
session.model.clone_from(&model.id);
session.reasoning = resolved_reasoning(session, session.reasoning_selection);
session.context_window =
model_context_window(&session.config, &session.provider, &session.model);
session.context_window = model_context_window(
&session.config,
&session.models,
&session.provider,
&session.model,
);
Ok(SetSessionConfigOptionResponse::new(session_config_options(
session,
)))
Expand Down Expand Up @@ -551,7 +555,7 @@ impl AcpService {
let reasoning = model_reasoning(&config, &models, &provider, &model);
let reasoning_selection =
reasoning.unwrap_or(crate::model::reasoning::ReasoningEffort::None);
let context_window = model_context_window(&config, &provider, &model);
let context_window = model_context_window(&config, &models, &provider, &model);
let skills = crate::skill::SkillStore::load(&config.xdg_config_home, &config.project_root);
let session = AcpSession {
cwd,
Expand Down Expand Up @@ -969,7 +973,20 @@ fn model_reasoning_capability(
.filter(|capability| !capability.values().is_empty())
}

fn model_context_window(config: &LoadedConfig, provider: &str, model: &str) -> Option<u32> {
fn model_context_window(
config: &LoadedConfig,
models: &[crate::model::types::Model],
provider: &str,
model: &str,
) -> Option<u32> {
if let Some(context_window) = models
.iter()
.find(|candidate| candidate.provider_id == provider && candidate.id == model)
.and_then(|model| model.context_window)
{
return Some(context_window);
}

let discovery = crate::model::discovery::Discovery::new_with_custom(Some(
config.merged_config.custom_providers.clone(),
))
Expand Down Expand Up @@ -1476,6 +1493,7 @@ mod tests {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: None,
}
}

Expand Down Expand Up @@ -1568,6 +1586,12 @@ mod tests {
fn config_with_command(command: crate::command::custom::CustomCommand) -> LoadedConfig {
let mut merged_config = crate::config::configuration::MergedConfig::default();
merged_config.commands.push(command);
config_with_merged(merged_config)
}

fn config_with_merged(
merged_config: crate::config::configuration::MergedConfig,
) -> LoadedConfig {
LoadedConfig {
merged_config,
raw_merged: serde_json::Value::Null,
Expand All @@ -1579,6 +1603,10 @@ mod tests {
}
}

fn empty_config() -> LoadedConfig {
config_with_merged(crate::config::configuration::MergedConfig::default())
}

fn session_with_config(config: LoadedConfig) -> AcpSession {
let skills = crate::skill::SkillStore::load(&config.xdg_config_home, &config.project_root);
AcpSession {
Expand Down Expand Up @@ -1802,6 +1830,17 @@ mod tests {
assert!(find_selectable_model(&models, "other/gpt-5").is_err());
}

#[test]
fn resolves_context_window_from_selectable_models() {
let mut model = model("example", "Example", "large-context", "Large Context");
model.context_window = Some(1_090_000);

assert_eq!(
model_context_window(&empty_config(), &[model], "example", "large-context"),
Some(1_090_000)
);
}

#[test]
fn preserves_selected_reasoning_effort_when_model_cannot_apply_it() {
let model = model("example", "Example", "chat", "Chat");
Expand Down
23 changes: 11 additions & 12 deletions src/config/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::tools::{
use anyhow::{anyhow, Context, Result};
use regex::Regex;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -110,15 +110,15 @@ fn parse_provider_id_set(
value: Option<&Value>,
diagnostics: &mut ConfigDiagnostics,
key: &str,
) -> BTreeSet<String> {
) -> HashSet<String> {
let Some(value) = value else {
return BTreeSet::new();
return HashSet::new();
};
let Some(entries) = value.as_array() else {
diagnostics
.warnings
.push(format!("{key} must be an array of provider IDs"));
return BTreeSet::new();
return HashSet::new();
};

entries
Expand Down Expand Up @@ -422,8 +422,8 @@ pub struct MergedConfig {
pub agent_permission_rules: HashMap<String, PermissionRules>,
pub agent_steps: HashMap<String, usize>,
pub provider_timeouts: HashMap<String, ProviderTimeout>,
pub enabled_providers: BTreeSet<String>,
pub disabled_providers: BTreeSet<String>,
pub disabled_providers: HashSet<String>,
pub enabled_providers: Option<HashSet<String>>,
pub custom_providers: HashMap<String, CustomProviderConfig>,
pub notifications: NotificationsConfig,
pub images: ImagesConfig,
Expand Down Expand Up @@ -1260,12 +1260,11 @@ fn parse_merged_config(merged: &Value, diagnostics: &mut ConfigDiagnostics) -> M
);
out.sync_agent_derived_fields();
out.provider_timeouts = parse_provider_timeouts(obj.get("provider"), diagnostics);
out.enabled_providers = parse_provider_id_set(
obj.get("enabled_providers")
.or_else(|| obj.get("enabledProviders")),
diagnostics,
"enabled_providers",
);
let enabled_providers = obj
.get("enabled_providers")
.or_else(|| obj.get("enabledProviders"));
out.enabled_providers = enabled_providers
.map(|value| parse_provider_id_set(Some(value), diagnostics, "enabled_providers"));
out.disabled_providers = parse_provider_id_set(
obj.get("disabled_providers")
.or_else(|| obj.get("disabledProviders")),
Expand Down
6 changes: 5 additions & 1 deletion src/model/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ fn provider_is_enabled(
provider_id: &str,
) -> bool {
!config.disabled_providers.contains(provider_id)
&& (config.enabled_providers.is_empty() || config.enabled_providers.contains(provider_id))
&& config
.enabled_providers
.as_ref()
.is_none_or(|enabled| enabled.contains(provider_id))
}

#[cfg(test)]
Expand All @@ -124,6 +127,7 @@ mod tests {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: None,
};

assert_eq!(model_ref(&model), "openai/gpt-5");
Expand Down
7 changes: 7 additions & 0 deletions src/model/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ impl Discovery {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: custom_model.context_window,
});
}
}
Expand Down Expand Up @@ -714,6 +715,11 @@ impl Discovery {
free,
local: false,
reasoning_options: model.reasoning_options.clone(),
context_window: model
.limit
.as_ref()
.map(|limit| limit.context)
.filter(|context| *context > 0),
});
}
}
Expand Down Expand Up @@ -882,6 +888,7 @@ mod tests {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: None,
};
let connected_provider_ids = std::collections::HashSet::new();
let configured_provider_ids =
Expand Down
5 changes: 5 additions & 0 deletions src/model/effective_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct SnapshotModel {
free: bool,
local: bool,
reasoning_options: Vec<crate::model::reasoning::ReasoningOption>,
#[serde(default)]
context_window: Option<u32>,
}

impl From<Model> for SnapshotModel {
Expand All @@ -42,6 +44,7 @@ impl From<Model> for SnapshotModel {
free: model.free,
local: model.local,
reasoning_options: model.reasoning_options,
context_window: model.context_window,
}
}
}
Expand All @@ -59,6 +62,7 @@ impl From<SnapshotModel> for Model {
free: model.free,
local: model.local,
reasoning_options: model.reasoning_options,
context_window: model.context_window,
}
}
}
Expand Down Expand Up @@ -179,6 +183,7 @@ mod tests {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/model/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ mod tests {
free: true,
local: false,
reasoning_options: Vec::new(),
context_window: None,
};
let paid_model = crate::model::types::Model {
id: "gpt-5.3-codex".to_string(),
Expand All @@ -402,6 +403,7 @@ mod tests {
free: false,
local: false,
reasoning_options: Vec::new(),
context_window: None,
};

assert!(ModelExtensions::is_available_without_connection(
Expand Down
8 changes: 8 additions & 0 deletions src/model/extensions/ollama.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,19 @@ pub fn model_for_dialog(model: OllamaModel) -> crate::model::types::Model {
free: false,
local: true,
reasoning_options: Vec::new(),
context_window: discovery_model_for_dialog(&model.id)
.and_then(|model| model.limit)
.map(|limit| limit.context)
.filter(|context| *context > 0),
id: model.id,
name: model.name,
}
}

fn discovery_model_for_dialog(id: &str) -> Option<crate::model::discovery::Model> {
cached_discovery_models().and_then(|models| models.get(id).cloned())
}

fn cached_discovery_models(
) -> Option<std::collections::HashMap<String, crate::model::discovery::Model>> {
let models = cache().lock().ok().and_then(|guard| match guard.clone() {
Expand Down
3 changes: 3 additions & 0 deletions src/model/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub struct Model {
pub local: bool,
/// Mirrors models.dev `reasoning_options`.
pub reasoning_options: Vec<crate::model::reasoning::ReasoningOption>,
/// Mirrors models.dev `limit.context` when available.
pub context_window: Option<u32>,
}

impl Model {
Expand Down Expand Up @@ -142,6 +144,7 @@ mod tests {
kind: "effort".to_string(),
values: vec!["low".to_string()],
}],
context_window: Some(128_000),
};

let description = model.dialog_description();
Expand Down
Loading