use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use serde_json::Value;
use crate::{
CredentialSpec, Harness, Features, Error, Info, ModelChoice, InstallHint,
Readiness, InstalledModel, ModelManagement, PullProgressCallback,
RunCallback, RunHandle, RunRequest,
};
mod chat;
mod instructions;
pub use instructions::InstructionSources;
mod ollama;
mod openai_models;
mod profile;
pub use profile::{ModelFacts, PromptProfile, COMPACT_AT_OR_BELOW_PARAMS_B, COMPACT_AT_OR_BELOW_TOKENS};
mod run;
mod session;
mod skills;
pub use skills::global_skill_roots;
mod tools;
mod wire;
pub use session::SessionRecord;
pub use tools::mcp::{McpPrompt, McpPromptArg, McpServer, McpTransport, PromptMessage};
const OLLAMA_CTX_CEILING: u64 = 32_768;
const OLLAMA_CTX_DEFAULT: u64 = 8_192;
fn local_server_context(base_url: &str) -> Option<u64> {
let response = ureq::get(&format!("{base_url}/props"))
.timeout(std::time::Duration::from_secs(2))
.call()
.ok()?;
let body: Value = response.into_json().ok()?;
body.get("default_generation_settings")?
.get("n_ctx")?
.as_u64()
.filter(|n| *n > 0)
}
enum Discovery {
OllamaTags,
Static(Vec<ModelChoice>),
ModelsDev(String),
OpenAiModels { fallback: Vec<ModelChoice> },
}
#[derive(Debug, Clone)]
pub struct AgentDef {
pub description: String,
pub system_prompt: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelCost {
pub input_per_mtok: f64,
pub output_per_mtok: f64,
pub cache_read_per_mtok: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Permission {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone)]
pub struct PermissionRule {
pub tool: Option<String>,
pub pattern: Option<String>,
pub effect: Permission,
}
impl PermissionRule {
pub fn deny(tool: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: None, effect: Permission::Deny }
}
pub fn deny_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Deny }
}
pub fn allow_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Allow }
}
pub fn ask_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Ask }
}
pub fn ask(tool: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: None, effect: Permission::Ask }
}
}
#[derive(Debug, Clone)]
pub struct PermissionRequest {
pub tool: String,
pub subject: Option<String>,
}
pub type PermissionPrompt = std::sync::Arc<dyn Fn(&PermissionRequest) -> bool + Send + Sync>;
pub struct OpenHarness {
id: String,
display_name: String,
description: String,
base_url: String,
api_key: ApiKey,
disabled_tools: Vec<String>,
prompt_cache: PromptCache,
instruction_sources: InstructionSources,
global_skill_roots: Vec<std::path::PathBuf>,
profile: PromptProfile,
discovery: Discovery,
default_model: Option<String>,
session_dir: Option<PathBuf>,
context_tokens: Option<u64>,
agents: Vec<(String, AgentDef)>,
mcp_servers: Vec<McpServer>,
model_costs: Vec<(String, ModelCost)>,
permissions: Vec<PermissionRule>,
permission_prompt: Option<PermissionPrompt>,
reasoning_tag: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PromptCache {
#[default]
Implicit,
Ephemeral,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum ApiKey {
#[default]
NotNeeded,
Required,
Value(String),
Env(String),
}
impl ApiKey {
pub fn is_needed(&self) -> bool {
!matches!(self, Self::NotNeeded)
}
pub fn env_var(&self) -> Option<&str> {
match self {
Self::Env(name) => Some(name),
_ => None,
}
}
pub(crate) fn resolve(&self) -> Option<String> {
let raw = match self {
Self::Value(key) => Some(key.clone()),
Self::Env(name) => std::env::var(name).ok(),
Self::NotNeeded | Self::Required => None,
};
raw.filter(|value| !value.trim().is_empty())
}
}
#[derive(Clone, Debug, Default)]
pub struct OpenHarnessConfig {
pub id: String,
pub display_name: String,
pub base_url: String,
pub prompt_cache: PromptCache,
pub api_key: ApiKey,
pub disabled_tools: Vec<String>,
pub instruction_sources: InstructionSources,
pub global_skill_roots: Vec<std::path::PathBuf>,
pub profile: PromptProfile,
pub models: Vec<ModelChoice>,
}
impl OpenHarness {
pub fn builtin_tool_names() -> Vec<String> {
tools::ToolSet::builtin_tool_names()
}
pub fn ollama() -> Self {
Self::ollama_at("http://localhost:11434")
}
pub fn ollama_at(base_url: impl Into<String>) -> Self {
Self {
id: "ollama".to_owned(),
display_name: "Ollama".to_owned(),
description: "Local models served by Ollama via its OpenAI-compatible API.".to_owned(),
base_url: base_url.into(),
api_key: ApiKey::NotNeeded, prompt_cache: PromptCache::default(),
disabled_tools: Vec::new(),
instruction_sources: InstructionSources::default(),
global_skill_roots: Vec::new(),
profile: PromptProfile::default(),
discovery: Discovery::OllamaTags,
default_model: None,
session_dir: None,
context_tokens: None,
agents: Vec::new(),
mcp_servers: Vec::new(),
model_costs: Vec::new(),
permissions: Vec::new(),
permission_prompt: None,
reasoning_tag: Some("think".to_owned()),
}
}
pub fn custom(config: OpenHarnessConfig) -> Self {
let OpenHarnessConfig {
id,
display_name,
base_url,
api_key,
prompt_cache,
disabled_tools,
instruction_sources,
global_skill_roots,
profile,
models,
} = config;
Self {
id,
description: format!("{display_name} via its OpenAI-compatible API."),
display_name,
base_url,
api_key,
prompt_cache,
disabled_tools,
instruction_sources,
global_skill_roots,
profile,
default_model: models.first().map(|m| m.value.clone()),
discovery: Discovery::Static(models),
session_dir: None,
context_tokens: None,
agents: Vec::new(),
mcp_servers: Vec::new(),
model_costs: Vec::new(),
permissions: Vec::new(),
permission_prompt: None,
reasoning_tag: Some("think".to_owned()),
}
}
pub fn with_models_dev(mut self, provider: impl Into<String>) -> Self {
self.discovery = Discovery::ModelsDev(provider.into());
self
}
pub fn with_openai_models(mut self) -> Self {
let fallback = match std::mem::replace(&mut self.discovery, Discovery::Static(Vec::new())) {
Discovery::Static(models) => models,
_ => Vec::new(),
};
self.discovery = Discovery::OpenAiModels { fallback };
self
}
fn require_ollama_management(&self) -> Result<(), Error> {
match &self.discovery {
Discovery::OllamaTags => Ok(()),
Discovery::Static(_)
| Discovery::ModelsDev(_)
| Discovery::OpenAiModels { .. } => Err(Error::Other(format!(
"{} does not support managing models.",
self.display_name
))),
}
}
fn resolve_context(&self, model: &str) -> (Option<u64>, chat::Dialect, Option<f64>) {
match &self.discovery {
Discovery::OllamaTags => {
let (probed_window, parameters) = ollama::model_facts(&self.base_url, model);
let effective = self
.context_tokens
.or_else(|| probed_window.map(|n| n.min(OLLAMA_CTX_CEILING)))
.unwrap_or(OLLAMA_CTX_DEFAULT);
(Some(effective), chat::Dialect::OllamaNative { num_ctx: effective }, parameters)
}
Discovery::Static(_) | Discovery::ModelsDev(_) | Discovery::OpenAiModels { .. } => {
let window = self.context_tokens.or_else(|| {
if profile::is_local_endpoint(&self.base_url) {
local_server_context(&self.base_url)
} else if let Discovery::ModelsDev(provider) = &self.discovery {
crate::models_dev::context_limit(provider, model)
} else {
None
}
});
(window, chat::Dialect::OpenAi, None)
}
}
}
fn model_cost_for(&self, model: &str) -> Option<ModelCost> {
self.model_costs.iter().find(|(m, _)| m == model).map(|(_, c)| *c)
}
pub fn with_session_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.session_dir = Some(dir.into());
self
}
pub fn with_context_tokens(mut self, tokens: u64) -> Self {
self.context_tokens = Some(tokens);
self
}
pub fn with_agent(mut self, name: impl Into<String>, def: AgentDef) -> Self {
self.agents.push((name.into(), def));
self
}
pub fn with_mcp_server(mut self, server: McpServer) -> Self {
self.mcp_servers.push(server);
self
}
pub fn with_model_cost(mut self, model: impl Into<String>, cost: ModelCost) -> Self {
self.model_costs.push((model.into(), cost));
self
}
pub fn with_permission_rule(mut self, rule: PermissionRule) -> Self {
self.permissions.push(rule);
self
}
pub fn with_permission_prompt(
mut self,
prompt: impl Fn(&PermissionRequest) -> bool + Send + Sync + 'static,
) -> Self {
self.permission_prompt = Some(std::sync::Arc::new(prompt));
self
}
pub fn with_reasoning_tag(mut self, tag: impl Into<String>) -> Self {
self.reasoning_tag = Some(tag.into());
self
}
pub fn without_reasoning_extraction(mut self) -> Self {
self.reasoning_tag = None;
self
}
pub fn sessions(&self) -> Result<Vec<SessionRecord>, Error> {
match &self.session_dir {
Some(dir) => session::FileStore::new(dir.clone())
.list_records()
.map_err(Error::Other),
None => Ok(Vec::new()),
}
}
pub fn mcp_prompts(&self) -> Vec<McpPrompt> {
let cwd = std::env::current_dir().unwrap_or_default();
tools::mcp::list_prompts(&self.mcp_servers, &cwd)
}
pub fn get_mcp_prompt(
&self,
server: &str,
name: &str,
arguments: &[(String, String)],
) -> Result<Vec<PromptMessage>, Error> {
let cwd = std::env::current_dir().unwrap_or_default();
tools::mcp::get_prompt(&self.mcp_servers, server, name, arguments, &cwd).map_err(Error::Other)
}
}
impl Harness for OpenHarness {
fn info(&self) -> Info {
Info {
id: self.id.clone(),
display_name: self.display_name.clone(),
description: self.description.clone(),
install_hint: match self.discovery {
Discovery::OllamaTags => Some(InstallHint::url("https://ollama.com/download")),
Discovery::Static(_)
| Discovery::ModelsDev(_)
| Discovery::OpenAiModels { .. } => None,
},
}
}
fn features(&self) -> Features {
Features {
credential_required: self.api_key.is_needed(),
models: match &self.discovery {
Discovery::Static(m) => m.clone(),
Discovery::OllamaTags
| Discovery::ModelsDev(_)
| Discovery::OpenAiModels { .. } => Vec::new(),
},
custom_model: true,
max_turns: true,
custom_instructions: true,
..Default::default()
}
}
fn readiness(&self) -> Readiness {
let base = |ready: bool, error: Option<String>| Readiness {
harness_id: self.id.clone(),
ready,
installed: true,
version: None,
auth_configured: ready,
error,
details: Value::Null,
};
match &self.discovery {
Discovery::OllamaTags => match ollama::list_tags(&self.base_url) {
Ok(_) => base(true, None),
Err(e) => base(
false,
Some(format!(
"Ollama is not reachable at {} — is it running (`ollama serve`)? ({e})",
self.base_url
)),
),
},
Discovery::OpenAiModels { .. } => {
if self.api_key.is_needed() && self.api_key.resolve().is_none() {
base(false, Some(format!("Add an API key for {}.", self.display_name)))
} else if !profile::is_local_endpoint(&self.base_url) {
base(true, None)
} else {
match openai_models::list_models(&self.base_url, self.api_key.resolve().as_deref())
{
Ok(_) => base(true, None),
Err(e) => base(
false,
Some(format!(
"{} is not reachable at {} — is it running? ({e})",
self.display_name, self.base_url
)),
),
}
}
}
Discovery::Static(_) | Discovery::ModelsDev(_) => {
if self.api_key.is_needed() && self.api_key.resolve().is_none() {
let how = match self.api_key.env_var() {
Some(env) => format!("Set {env} to use {}.", self.display_name),
None => format!("Add an API key for {}.", self.display_name),
};
base(false, Some(how))
} else {
base(true, None)
}
}
}
}
fn start(&self, request: RunRequest, on_event: RunCallback) -> Result<RunHandle, Error> {
let RunRequest { run_id, prompt, cwd, mode, tuning, resume, attachments } = request;
let model = tuning
.model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_owned)
.or_else(|| self.default_model.clone())
.ok_or_else(|| {
Error::Other(format!(
"{}: no model selected and no default — set RunTuning.model",
self.id
))
})?;
let (context_tokens, dialect, model_parameters_b) = self.resolve_context(&model);
let model_cost = self.model_cost_for(&model);
let image_data_uris: Vec<String> =
attachments.iter().map(|a| wire::image_data_uri(&a.mime_type, &a.data)).collect();
let cfg = run::LoopConfig {
run_id,
base_url: self.base_url.clone(),
api_key: self.api_key.resolve(),
disabled_tools: self.disabled_tools.clone(),
instruction_sources: self.instruction_sources.clone(),
global_skill_roots: self.global_skill_roots.clone(),
profile: self.profile,
prompt_cache: self.prompt_cache,
model_parameters_b,
model,
prompt,
cwd: cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()),
mode,
max_turns: run::LoopConfig::max_turns_or_default(tuning.max_turns),
resume,
store: self.session_dir.clone().map(session::FileStore::new),
context_tokens,
dialect,
agents: self.agents.clone(),
mcp_servers: self.mcp_servers.clone(),
output_schema: tuning.output_schema,
model_cost,
image_data_uris,
permissions: self.permissions.clone(),
permission_prompt: self.permission_prompt.clone(),
reasoning_tag: self.reasoning_tag.clone(),
extra_instructions: tuning.extra_instructions,
};
let cancel = Arc::new(AtomicBool::new(false));
let thread_cancel = Arc::clone(&cancel);
std::thread::spawn(move || run::drive(cfg, thread_cancel, on_event));
Ok(Box::new(run::OpenAiRun::new(cancel)))
}
fn credential(&self) -> CredentialSpec {
match self.api_key.is_needed() {
true => CredentialSpec {
label: format!("{} API key", self.display_name),
keychain_service: self.id.clone(),
keychain_account: self.api_key.env_var().map_or_else(|| self.id.clone(), str::to_owned),
required: true,
},
false => CredentialSpec {
label: format!("{} (no key required)", self.display_name),
keychain_service: self.id.clone(),
keychain_account: String::new(),
required: false,
},
}
}
fn list_models(&self) -> Result<Vec<ModelChoice>, Error> {
match &self.discovery {
Discovery::OllamaTags => ollama::list_tags(&self.base_url).map_err(Error::Other),
Discovery::Static(_) => Ok(self.features().models),
Discovery::ModelsDev(provider) => Ok(crate::models_dev::provider_models(provider)),
Discovery::OpenAiModels { fallback } => {
Ok(openai_models::list_models(&self.base_url, self.api_key.resolve().as_deref())
.unwrap_or_else(|_| fallback.clone()))
}
}
}
fn model_management(&self) -> Option<ModelManagement> {
match &self.discovery {
Discovery::OllamaTags => Some(ModelManagement { base_url: self.base_url.clone() }),
Discovery::Static(_)
| Discovery::ModelsDev(_)
| Discovery::OpenAiModels { .. } => None,
}
}
fn list_installed_models(&self) -> Result<Vec<InstalledModel>, Error> {
self.require_ollama_management()?;
ollama::list_installed(&self.base_url).map_err(Error::Other)
}
fn pull_model(
&self,
model: &str,
cancel: &std::sync::atomic::AtomicBool,
on_progress: PullProgressCallback<'_>,
) -> Result<(), Error> {
self.require_ollama_management()?;
ollama::pull(&self.base_url, model, cancel, on_progress).map_err(Error::Other)
}
fn delete_model(&self, model: &str) -> Result<(), Error> {
self.require_ollama_management()?;
ollama::delete(&self.base_url, model).map_err(Error::Other)
}
}
#[cfg(test)]
mod tests {
#[test]
fn declared_models_become_the_discovery_fallback() {
let harness = OpenHarness::custom(OpenHarnessConfig {
id: "custom:x".to_owned(),
display_name: "LM Studio".to_owned(),
base_url: "http://127.0.0.1:9".to_owned(),
models: vec![ModelChoice { value: "qwen3:8b".into(), label: "qwen3:8b".into() }],
..Default::default()
})
.with_openai_models();
let models = harness.list_models().expect("a failed probe must not be an error");
assert_eq!(models.len(), 1);
assert_eq!(models[0].value, "qwen3:8b");
}
#[test]
fn an_unreachable_local_endpoint_is_not_ready() {
let harness = OpenHarness::custom(OpenHarnessConfig {
id: "custom:x".to_owned(),
display_name: "LM Studio".to_owned(),
base_url: "http://127.0.0.1:9".to_owned(),
..Default::default()
})
.with_openai_models();
let readiness = harness.readiness();
assert!(!readiness.ready);
assert!(readiness.error.is_some_and(|e| e.contains("not reachable")));
}
use super::*;
#[test]
fn ollama_is_keyless_dynamic_and_editing() {
let h = OpenHarness::ollama();
let info = h.info();
assert_eq!(info.id, "ollama");
assert!(info.install_hint.is_some_and(|h| h.url.contains("ollama.com")));
let can = h.features();
assert!(!can.credential_required);
assert!(!can.previews_edits);
assert!(can.custom_model);
assert!(can.models.is_empty());
assert!(!h.credential().required);
}
#[test]
fn only_ollama_exposes_model_management() {
let ollama = OpenHarness::ollama();
let mgmt = ollama.model_management().expect("Ollama manages models");
assert_eq!(mgmt.base_url, "http://localhost:11434");
let remote = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key: ApiKey::Env("OPENROUTER_API_KEY".to_owned()),
..Default::default()
});
assert!(remote.model_management().is_none());
assert!(remote.list_installed_models().is_err());
assert!(remote.delete_model("whatever").is_err());
let cancel = std::sync::atomic::AtomicBool::new(false);
assert!(remote.pull_model("whatever", &cancel, &mut |_| {}).is_err());
}
#[test]
fn a_key_passed_as_a_value_needs_no_environment_variable() {
let h = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key: ApiKey::Value("sk-or-v1-example".to_owned()),
..Default::default()
});
assert!(h.features().credential_required);
assert!(h.readiness().ready);
let spec = h.credential();
assert!(spec.required && !spec.keychain_account.is_empty());
}
#[test]
fn a_value_only_provider_without_a_key_says_so_without_naming_a_variable() {
let h = OpenHarness::custom(OpenHarnessConfig {
id: "acme".to_owned(),
display_name: "Acme".to_owned(),
base_url: "https://acme.test".to_owned(),
api_key: ApiKey::Required,
..Default::default()
});
let readiness = h.readiness();
assert!(!readiness.ready);
let error = readiness.error.unwrap_or_default();
assert!(error.contains("Add an API key"), "{error}");
assert!(!error.contains("Set "), "{error}");
}
#[test]
fn naming_a_variable_still_implies_a_key_is_needed() {
let h = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key: ApiKey::Env("OPENROUTER_API_KEY".to_owned()),
..Default::default()
});
assert!(h.features().credential_required);
assert!(h.credential().required);
}
#[test]
fn a_value_wins_over_the_environment() {
std::env::set_var("ACME_KEY", "from-the-environment");
let h = OpenHarness::custom(OpenHarnessConfig {
id: "acme".to_owned(),
display_name: "Acme".to_owned(),
base_url: "https://acme.test".to_owned(),
api_key: ApiKey::Value("from-the-host".to_owned()),
..Default::default()
});
assert_eq!(h.api_key.resolve().as_deref(), Some("from-the-host"));
assert!(h.api_key.env_var().is_none(), "a value names no variable to tell the user about");
std::env::remove_var("ACME_KEY");
}
#[test]
fn every_key_state_agrees_with_itself() {
let harness = |key: ApiKey| {
OpenHarness::custom(OpenHarnessConfig {
id: "acme".to_owned(),
display_name: "Acme".to_owned(),
base_url: "https://acme.test".to_owned(),
api_key: key,
..Default::default()
})
};
let local = harness(ApiKey::NotNeeded);
assert!(!local.features().credential_required);
assert!(!local.credential().required);
assert!(local.readiness().ready, "no key needed means ready");
let vaulted = harness(ApiKey::Value("sk-secret".to_owned()));
assert!(vaulted.features().credential_required, "a value still needs a key");
assert!(vaulted.credential().required, "and the slot stays writable");
assert!(vaulted.readiness().ready, "and it is satisfied");
let awaiting = harness(ApiKey::Required);
assert!(awaiting.features().credential_required);
assert!(!awaiting.readiness().ready, "required but absent is not ready");
let error = awaiting.readiness().error.unwrap_or_default();
assert!(error.contains("Add an API key"), "no variable to name: {error}");
std::env::set_var("ACME_ENV_KEY", "sk-from-env");
let from_env = harness(ApiKey::Env("ACME_ENV_KEY".to_owned()));
assert!(from_env.features().credential_required);
assert!(from_env.readiness().ready);
assert_eq!(from_env.credential().keychain_account, "ACME_ENV_KEY");
std::env::remove_var("ACME_ENV_KEY");
}
#[test]
fn an_exported_but_empty_variable_is_not_a_credential() {
std::env::set_var("ACME_BLANK", " ");
let harness = OpenHarness::custom(OpenHarnessConfig {
id: "acme".to_owned(),
display_name: "Acme".to_owned(),
base_url: "https://acme.test".to_owned(),
api_key: ApiKey::Env("ACME_BLANK".to_owned()),
..Default::default()
});
assert!(harness.api_key.resolve().is_none(), "blank is a misconfiguration, not a key");
assert!(!harness.readiness().ready);
std::env::remove_var("ACME_BLANK");
}
#[test]
fn custom_requires_its_key_and_lists_static_models() {
let h = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key: ApiKey::Env("OPENROUTER_API_KEY".to_owned()),
models: vec![ModelChoice { value: "x-ai/grok".to_owned(), label: "Grok".to_owned() }],
..Default::default()
});
assert!(h.features().credential_required);
assert!(h.credential().required);
assert_eq!(h.credential().keychain_account, "OPENROUTER_API_KEY");
assert_eq!(h.list_models().unwrap().len(), 1);
assert_eq!(h.default_model.as_deref(), Some("x-ai/grok"));
}
}