1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! Runtime selection model for internal agents: which harness runs an agent,
//! and the settings only that harness understands.
//!
//! The on-disk shape of these selections lives in `config_format`.
use {crate::model_aliases::ModelAliases, rho_providers::reasoning::ReasoningLevel};
/// How a decision-model entry, such as the permission screen, asks its model.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ModelKind {
/// Over a decision API (System One or OpenAI's Decisions), on a host
/// that serves decision models.
Decision,
/// As a chat model, through the text adapter.
Text,
}
/// Provider selection for an internal agent that runs on Rho's own stack.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RhoInternalAgentModel {
pub provider: String,
pub model: String,
pub auth: String,
/// The `kind` key. Only decision-model entries read it; `None` takes the
/// entry's default.
pub kind: Option<ModelKind>,
/// The `allow_threshold_percent` key. Only a decision-model permission
/// screen reads it; `None` takes the screen's default.
pub allow_threshold_percent: Option<u8>,
pub(super) model_alias: Option<String>,
}
/// Which harness runs an internal agent, together with the settings only that
/// harness understands.
///
/// The runtime axis travels as one value, mirroring `AgentRuntimeSpec`, so a
/// stored selection cannot pair one harness with another harness's model
/// vocabulary or auth.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InternalAgentTarget {
/// Rho's own provider stack, with an explicit provider, model, and auth.
Rho(RhoInternalAgentModel),
/// The installed `claude` binary under the user's Claude Code sign-in.
/// Rho holds no credential for it and resolves no model name.
ClaudeCli {
/// Pass-through `--model`. `None` omits the flag and lets Claude Code
/// pick.
model: Option<String>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InternalAgentModelConfig {
pub target: InternalAgentTarget,
/// Per-agent reasoning override. `None` keeps the agent definition default.
pub reasoning: Option<ReasoningLevel>,
}
impl InternalAgentModelConfig {
/// A selection on Rho's own runtime.
pub fn new(provider: String, model: String, auth: String) -> Self {
Self {
target: InternalAgentTarget::Rho(RhoInternalAgentModel {
provider,
model,
auth,
kind: None,
allow_threshold_percent: None,
model_alias: None,
}),
reasoning: None,
}
}
/// A selection that delegates to the Claude Code CLI. `model` is passed
/// through as `--model`; `None` lets Claude Code choose.
pub fn claude_cli(model: Option<String>) -> Self {
Self {
target: InternalAgentTarget::ClaudeCli { model },
reasoning: None,
}
}
/// The Rho provider selection, or `None` when this agent delegates.
pub fn rho(&self) -> Option<&RhoInternalAgentModel> {
match &self.target {
InternalAgentTarget::Rho(model) => Some(model),
InternalAgentTarget::ClaudeCli { .. } => None,
}
}
/// How the selection reads on a status line, badge, or picker row.
///
/// Both runtimes render as `<source>/<model>` so one row format covers the
/// whole list.
pub fn display_reference(&self) -> String {
match &self.target {
InternalAgentTarget::Rho(selection) => {
rho_providers::provider::model_reference(&selection.provider, &selection.model)
}
InternalAgentTarget::ClaudeCli { model } => rho_providers::provider::model_reference(
crate::claude_runtime::models::CLAUDE_CODE_SOURCE_LABEL,
model
.as_deref()
.unwrap_or(crate::claude_runtime::models::CLAUDE_DEFAULT_MODEL_BADGE),
),
}
}
/// The Rho selection, for assertions that only make sense on Rho's stack.
#[cfg(test)]
pub fn expect_rho(&self) -> &RhoInternalAgentModel {
self.rho().expect("selection runs on the rho runtime")
}
/// The Rho selection for mutation in tests.
#[cfg(test)]
pub fn expect_rho_mut(&mut self) -> &mut RhoInternalAgentModel {
match &mut self.target {
InternalAgentTarget::Rho(model) => model,
InternalAgentTarget::ClaudeCli { .. } => {
panic!("selection runs on the rho runtime")
}
}
}
pub(super) fn current_alias<'a>(&'a self, aliases: &'a ModelAliases) -> Option<&'a str> {
let selection = self.rho()?;
let name = selection.model_alias.as_deref()?;
let target = aliases.get(name)?;
(target.model == selection.model
&& target.provider.as_deref().unwrap_or(&selection.provider) == selection.provider)
.then_some(name)
}
}