Skip to content
Merged
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
52 changes: 31 additions & 21 deletions src/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,22 @@ impl Agent {
}
}

/// The OpenRouter model slug this agent runs on when it has no usable
/// native login (or is forced there). Single source for the env/args and
/// for the provenance shown to the caller.
pub fn openrouter_model(&self) -> &'static str {
/// The OpenRouter model slug this agent runs on when it has no usable native
/// login (or is forced there), or None for a native-only agent that has no
/// OpenRouter route at all. Single source for the env/args and the provenance
/// shown to the caller; the None forces callers to rule out native-only
/// agents rather than emitting a fake slug.
pub fn openrouter_model(&self) -> Option<&'static str> {
match self {
Agent::Claude => "anthropic/claude-sonnet-4.6",
Agent::Codex => "openai/gpt-5",
Agent::Qwen => "qwen/qwen3-coder",
Agent::Vibe => "mistralai/mistral-medium-3.1",
// Antigravity is native-only, so this is never used for it.
Agent::Antigravity => "native-only (no OpenRouter)",
Agent::Claude => Some("anthropic/claude-sonnet-4.6"),
Agent::Codex => Some("openai/gpt-5"),
Agent::Qwen => Some("qwen/qwen3-coder"),
Agent::Vibe => Some("mistralai/mistral-medium-3.1"),
// Antigravity is native-only: no OpenRouter model. None (not a fake
// slug) so the type forces callers to have already ruled this out.
Agent::Antigravity => None,
// Grok's OpenRouter leg drives its own model through codex's harness.
Agent::Grok => "x-ai/grok-build-0.1",
Agent::Grok => Some("x-ai/grok-build-0.1"),
}
}

Expand Down Expand Up @@ -213,7 +216,7 @@ impl Agent {
let mut cmd = if runner == Agent::Codex {
// Codex's exec harness; the model is passed only on the OpenRouter
// leg (grok supplies its own model, codex supplies gpt-5).
codex_exec_command(repo, openrouter.then(|| self.openrouter_model()))
codex_exec_command(repo, if openrouter { self.openrouter_model() } else { None })
} else {
let mut cmd = runner.base_command();
cmd.args(self.args());
Expand Down Expand Up @@ -245,14 +248,15 @@ impl Agent {
plan
}

/// A native-only agent is a closed-source CLI bound to its vendor's own
/// backend with no way to reach OpenRouter at all - only Antigravity
/// (Google). Grok's own CLI also can't be repointed, but grok's *model*
/// (x-ai/grok-build-0.1) is on OpenRouter, so grok's OpenRouter leg borrows
/// codex's harness (see `command`) - hence grok is NOT native-only. The
/// single source of truth for "has no OpenRouter route".
/// A native-only agent has no OpenRouter route at all - only Antigravity
/// (Google). Derived from `openrouter_model()` so "has an OpenRouter route"
/// has ONE source: an agent with no model slug is native-only, and every
/// capability and guard below follows from it - they can't disagree. (Grok
/// is NOT native-only: its own CLI can't reach OpenRouter, but its model
/// x-ai/grok-build-0.1 is there and its OpenRouter leg borrows codex's
/// harness - see `openrouter_runner`/`command`.)
pub fn is_native_only(&self) -> bool {
matches!(self, Agent::Antigravity)
self.openrouter_model().is_none()
}

/// Can this agent reach OpenRouter in principle? Most can: Claude via the
Expand Down Expand Up @@ -351,7 +355,10 @@ impl Agent {
Agent::Claude => vec![
("ANTHROPIC_BASE_URL", "https://openrouter.ai/api".into()),
("ANTHROPIC_AUTH_TOKEN", key.to_string()),
("ANTHROPIC_MODEL", self.openrouter_model().into()),
(
"ANTHROPIC_MODEL",
self.openrouter_model().expect("claude has an OpenRouter model").into(),
),
("MAX_THINKING_TOKENS", "0".into()),
],
// Grok's OpenRouter leg runs through codex, which reads the same key.
Expand All @@ -364,7 +371,10 @@ impl Agent {
Agent::Qwen => vec![
("OPENAI_API_KEY", key.to_string()),
("OPENAI_BASE_URL", "https://openrouter.ai/api/v1".into()),
("OPENAI_MODEL", self.openrouter_model().into()),
(
"OPENAI_MODEL",
self.openrouter_model().expect("qwen has an OpenRouter model").into(),
),
],
// Vibe reads its provider/model from the scratch VIBE_HOME and the
// key from OPENROUTER_API_KEY (named in that config). VIBE_HOME is
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ fn run(args: RunArgs) -> Result<()> {
/// printed to stdout and, with --out, a file.
fn report_section(r: &Report) -> String {
let provenance = if r.used_openrouter {
format!("via OpenRouter: {}", r.agent.openrouter_model())
let model = r.agent.openrouter_model().expect("ran on OpenRouter, so has a model");
format!("via OpenRouter: {model}")
} else {
"native login".to_string()
};
Expand Down Expand Up @@ -275,7 +276,7 @@ fn run_notes(reports: &[Report], selected: &[Agent], settings: &settings::Settin
if r.fell_back {
notes.push(format!(
"- {name}: native login failed, so it ran on OpenRouter ({}). To restore the native login, {}. To skip the wasted retry on later runs, add `--skip-native {name}`.",
r.agent.openrouter_model(),
r.agent.openrouter_model().expect("fell back to OpenRouter, so has a model"),
r.agent.native_fix_hint(),
));
}
Expand Down Expand Up @@ -555,7 +556,7 @@ fn start_vibe_home(selected: &[Agent]) -> Option<vibe::Home> {
if !(selected.contains(&Agent::Vibe) && openrouter::key().is_some()) {
return None;
}
match vibe::Home::create(Agent::Vibe.openrouter_model()) {
match vibe::Home::create(Agent::Vibe.openrouter_model().expect("vibe has an OpenRouter model")) {
Ok(home) => Some(home),
Err(e) => {
eprintln!("postmortemthis: could not prepare vibe home ({e}); the vibe leg will fail");
Expand Down
7 changes: 4 additions & 3 deletions src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ fn poke(agent: Agent, has_key: bool) -> String {
if agent.authed() {
return format!("{} - native login will be used", agent.auth_hint());
}
if has_key && agent.supports_openrouter() {
return format!("no login - will run on OpenRouter ({})", agent.openrouter_model());
if has_key && let Some(model) = agent.openrouter_model() {
return format!("no login - will run on OpenRouter ({model})");
}
if agent.has_native_login() {
"not logged in - won't run until you log in".into()
Expand All @@ -196,7 +196,8 @@ fn effective(agent: Agent, settings: &Settings, has_key: bool) -> String {
// Forced OpenRouter only takes effect with a usable route; plan_run falls
// back to native otherwise, so the summary must say so, not overstate it.
Mode::Openrouter if has_key && agent.supports_openrouter() => {
format!("forced OpenRouter ({})", agent.openrouter_model())
let model = agent.openrouter_model().expect("supports_openrouter, so has a model");
format!("forced OpenRouter ({model})")
}
Mode::Openrouter => format!("forced OpenRouter (inactive, no key) - {}", poke(agent, has_key)),
Mode::Auto => poke(agent, has_key),
Expand Down
Loading