use std::collections::BTreeMap;
use crate::Metadata;
pub const SIM_PROVIDER: &str = "sim";
pub const CLI_PROVIDER: &str = "cli";
#[derive(Clone, Debug, PartialEq)]
pub struct Target {
pub label: String,
pub provider: String,
pub model: String,
pub available: bool,
pub metadata: Metadata,
}
impl Target {
pub fn new(
label: impl Into<String>,
provider: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self {
label: label.into(),
provider: provider.into(),
model: model.into(),
available: true,
metadata: BTreeMap::new(),
}
}
pub fn sim() -> Self {
Self::new("sim", SIM_PROVIDER, "sim")
}
pub fn cli(label: impl Into<String>) -> Self {
Self::new(label, CLI_PROVIDER, "")
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
pub fn anthropic(model: impl Into<String>) -> Self {
Self::cloud("anthropic", model, "ANTHROPIC_API_KEY")
}
pub fn openai(model: impl Into<String>) -> Self {
Self::cloud("openai", model, "OPENAI_API_KEY")
}
pub fn gemini(model: impl Into<String>) -> Self {
Self::cloud("gemini", model, "GEMINI_API_KEY")
}
pub fn cloud(
provider: impl Into<String>,
model: impl Into<String>,
key_env: impl AsRef<str>,
) -> Self {
let provider = provider.into();
let model = model.into();
let available = std::env::var(key_env.as_ref())
.map(|v| !v.trim().is_empty())
.unwrap_or(false);
Self {
label: format!("{provider}/{model}"),
provider,
model,
available,
metadata: BTreeMap::new(),
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn available(mut self, available: bool) -> Self {
self.available = available;
self
}
pub fn meta(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_sim(&self) -> bool {
self.provider == SIM_PROVIDER
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sim_is_available() {
let m = Target::sim();
assert!(m.available);
assert!(m.is_sim());
assert_eq!(m.label, "sim");
}
#[test]
fn cli_is_a_harness_target() {
let t = Target::cli("yolop");
assert!(t.available);
assert!(!t.is_sim());
assert_eq!(t.label, "yolop");
assert_eq!(t.provider, CLI_PROVIDER);
assert_eq!(t.model, "");
let wrapped = Target::cli("codex").with_model("claude-opus-4-8");
assert_eq!(wrapped.model, "claude-opus-4-8");
}
#[test]
fn cloud_gates_on_env() {
let m = Target::cloud("acme", "x", "MIRA_TEST_DEFINITELY_UNSET_KEY");
assert!(!m.available);
assert_eq!(m.label, "acme/x");
}
#[test]
fn builder_overrides() {
let m = Target::new("a", "p", "m")
.label("alias")
.available(false)
.meta("region", "us");
assert_eq!(m.label, "alias");
assert!(!m.available);
assert_eq!(m.metadata.get("region").unwrap(), "us");
}
}