1use crate::routing::probe_match::select_probe_slug;
2use crate::routing::slug;
3
4use super::availability::{ResolvedRunnablePath, RunnableConfidence, RunnablePathSource};
5use super::probes::{OpenCodeProbeResult, PiProbeResult};
6
7pub struct HarnessModelInput<'a> {
8 pub harness: &'a str,
9 pub model_id: &'a str,
10 pub provider_constraint: Option<&'a str>,
11 pub provider_for_order: Option<&'a str>,
12 pub settings_provider_order: Option<&'a [String]>,
13 pub opencode_probe: Option<&'a OpenCodeProbeResult>,
14 pub pi_probe: Option<&'a PiProbeResult>,
15}
16
17pub fn resolve_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
18 let model_id = input.model_id.trim();
19 if model_id.is_empty() {
20 return ResolvedRunnablePath {
21 harness_model_id: String::new(),
22 source: RunnablePathSource::Passthrough,
23 confidence: RunnableConfidence::Unknown,
24 };
25 }
26
27 let harness = input.harness;
28 if harness.eq_ignore_ascii_case("pi") {
29 return resolve_pi_harness_model(input);
30 }
31 if harness.eq_ignore_ascii_case("opencode") {
32 return resolve_opencode_harness_model(input);
33 }
34
35 if native_provider_matches_harness(input, harness) {
36 return ResolvedRunnablePath {
37 harness_model_id: model_id.to_string(),
38 source: RunnablePathSource::ProviderMatch,
39 confidence: RunnableConfidence::Likely,
40 };
41 }
42
43 ResolvedRunnablePath {
44 harness_model_id: model_id.to_string(),
45 source: RunnablePathSource::Passthrough,
46 confidence: RunnableConfidence::Unknown,
47 }
48}
49
50fn native_provider_matches_harness(input: HarnessModelInput<'_>, harness: &str) -> bool {
51 let provider_matches = |provider: &str| {
52 !provider.trim().is_empty() && slug::provider_matches_native_harness(provider, harness)
53 };
54 input.provider_constraint.is_some_and(provider_matches)
55 || input.provider_for_order.is_some_and(provider_matches)
56}
57
58fn resolve_pi_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
59 let model_id = input.model_id.trim();
60 let Some(pi_probe) = input.pi_probe else {
61 return constraint_qualified_passthrough(model_id, input.provider_constraint);
62 };
63 if !pi_probe.compatible {
64 return constraint_qualified_passthrough(model_id, input.provider_constraint);
65 }
66
67 probe_slug_or_passthrough(
68 model_id,
69 input.provider_constraint,
70 input.provider_for_order,
71 input.settings_provider_order,
72 pi_probe.model_slugs.iter().map(String::as_str),
73 )
74}
75
76fn resolve_opencode_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
77 let model_id = input.model_id.trim();
78 let Some(opencode_probe) = input.opencode_probe else {
79 return constraint_qualified_passthrough(model_id, input.provider_constraint);
80 };
81 if !opencode_probe.model_probe_success {
82 return constraint_qualified_passthrough(model_id, input.provider_constraint);
83 }
84
85 probe_slug_or_passthrough(
86 model_id,
87 input.provider_constraint,
88 input.provider_for_order,
89 input.settings_provider_order,
90 opencode_probe.model_slugs.iter().map(String::as_str),
91 )
92}
93
94fn probe_slug_or_passthrough<'a>(
95 model_id: &str,
96 provider_constraint: Option<&str>,
97 provider_for_order: Option<&str>,
98 settings_provider_order: Option<&[String]>,
99 slugs: impl IntoIterator<Item = &'a str>,
100) -> ResolvedRunnablePath {
101 let selection = select_probe_slug(
102 model_id,
103 probe_constraint_for_selection(provider_constraint, provider_for_order),
104 provider_for_order,
105 settings_provider_order,
106 slugs,
107 );
108 if let Some(slug) = selection.chosen_slug {
109 return ResolvedRunnablePath {
110 harness_model_id: slug,
111 source: RunnablePathSource::CachedProbe,
112 confidence: RunnableConfidence::Confirmed,
113 };
114 }
115
116 constraint_qualified_passthrough(model_id, provider_constraint)
117}
118
119fn probe_constraint_for_selection<'a>(
122 provider_constraint: Option<&'a str>,
123 provider_for_order: Option<&'a str>,
124) -> Option<&'a str> {
125 let constraint = provider_constraint.filter(|provider| !provider.trim().is_empty())?;
126 let order = provider_for_order.filter(|provider| !provider.trim().is_empty());
127 if order.is_some_and(|order| slug::providers_exact_match(constraint, order)) {
128 return None;
129 }
130 Some(constraint)
131}
132
133fn constraint_qualified_passthrough(
134 model_id: &str,
135 provider_constraint: Option<&str>,
136) -> ResolvedRunnablePath {
137 if model_id.contains('/') {
138 return passthrough_bare(model_id);
139 }
140 if let Some(constraint) = provider_constraint.filter(|provider| !provider.trim().is_empty()) {
141 return ResolvedRunnablePath {
142 harness_model_id: format!("{}/{}", constraint.trim(), model_id),
143 source: RunnablePathSource::Passthrough,
144 confidence: RunnableConfidence::Confirmed,
145 };
146 }
147 passthrough_bare(model_id)
148}
149
150fn passthrough_bare(model_id: &str) -> ResolvedRunnablePath {
151 ResolvedRunnablePath {
152 harness_model_id: model_id.to_string(),
153 source: RunnablePathSource::Passthrough,
154 confidence: RunnableConfidence::Unknown,
155 }
156}
157#[cfg(test)]
158mod tests {
159 use std::collections::HashSet;
160
161 use super::*;
162 use crate::models::probes::PiProbeResult;
163
164 #[test]
165 fn qualified_provider_constraint_passthrough_without_probe() {
166 let resolved = resolve_harness_model(HarnessModelInput {
167 harness: "pi",
168 model_id: "gpt-5.4-mini",
169 provider_constraint: Some("openai-codex"),
170 provider_for_order: Some("openai-codex"),
171 settings_provider_order: None,
172 opencode_probe: None,
173 pi_probe: None,
174 });
175
176 assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
177 assert_eq!(resolved.source, RunnablePathSource::Passthrough);
178 assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
179 }
180
181 #[test]
182 fn pi_bare_model_uses_probe_slug() {
183 let mut model_slugs = HashSet::new();
184 model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
185 model_slugs.insert("openai/gpt-5.4-mini".to_string());
186 let pi_probe = PiProbeResult {
187 compatible: true,
188 model_slugs,
189 ..PiProbeResult::default()
190 };
191
192 let resolved = resolve_harness_model(HarnessModelInput {
193 harness: "pi",
194 model_id: "gpt-5.4-mini",
195 provider_constraint: None,
196 provider_for_order: Some("openai"),
197 settings_provider_order: None,
198 opencode_probe: None,
199 pi_probe: Some(&pi_probe),
200 });
201
202 assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
203 assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
204 assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
205 }
206
207 #[test]
208 fn pi_constraint_prefers_matching_provider_slug() {
209 let mut model_slugs = HashSet::new();
210 model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
211 model_slugs.insert("openai/gpt-5.4-mini".to_string());
212 let pi_probe = PiProbeResult {
213 compatible: true,
214 model_slugs,
215 ..PiProbeResult::default()
216 };
217
218 let resolved = resolve_harness_model(HarnessModelInput {
219 harness: "pi",
220 model_id: "gpt-5.4-mini",
221 provider_constraint: Some("openai-codex"),
222 provider_for_order: Some("openai-codex"),
223 settings_provider_order: None,
224 opencode_probe: None,
225 pi_probe: Some(&pi_probe),
226 });
227
228 assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
229 }
230
231 #[test]
232 fn opencode_uses_probe_slug_without_provider_constraint() {
233 let opencode_probe = OpenCodeProbeResult {
234 model_slugs: vec![
235 "openai/gpt-5.4-mini".to_string(),
236 "openai/gpt-5.5".to_string(),
237 ],
238 model_probe_success: true,
239 error: None,
240 };
241
242 let resolved = resolve_harness_model(HarnessModelInput {
243 harness: "opencode",
244 model_id: "gpt-5.4-mini",
245 provider_constraint: None,
246 provider_for_order: Some("openai"),
247 settings_provider_order: None,
248 opencode_probe: Some(&opencode_probe),
249 pi_probe: None,
250 });
251
252 assert_eq!(resolved.harness_model_id, "openai/gpt-5.4-mini");
253 assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
254 }
255
256 #[test]
257 fn codex_native_provider_match_returns_bare_model() {
258 let resolved = resolve_harness_model(HarnessModelInput {
259 harness: "codex",
260 model_id: "gpt-5.4-mini",
261 provider_constraint: None,
262 provider_for_order: Some("openai"),
263 settings_provider_order: None,
264 opencode_probe: None,
265 pi_probe: None,
266 });
267
268 assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
269 assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
270 assert_eq!(resolved.confidence, RunnableConfidence::Likely);
271 }
272
273 #[test]
274 fn codex_provider_constraint_native_match_returns_bare_model() {
275 let resolved = resolve_harness_model(HarnessModelInput {
276 harness: "codex",
277 model_id: "gpt-5.4-mini",
278 provider_constraint: Some("openai"),
279 provider_for_order: Some("openai"),
280 settings_provider_order: None,
281 opencode_probe: None,
282 pi_probe: None,
283 });
284
285 assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
286 assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
287 assert_eq!(resolved.confidence, RunnableConfidence::Likely);
288 }
289
290 #[test]
291 fn pi_provider_constraint_uses_probe_slug_not_blind_prefix() {
292 let mut model_slugs = HashSet::new();
293 model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
294 model_slugs.insert("openai/gpt-5.4-mini".to_string());
295 let pi_probe = PiProbeResult {
296 compatible: true,
297 model_slugs,
298 ..PiProbeResult::default()
299 };
300
301 let resolved = resolve_harness_model(HarnessModelInput {
302 harness: "pi",
303 model_id: "gpt-5.4-mini",
304 provider_constraint: Some("openai"),
305 provider_for_order: Some("openai"),
306 settings_provider_order: None,
307 opencode_probe: None,
308 pi_probe: Some(&pi_probe),
309 });
310
311 assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
312 assert_ne!(resolved.harness_model_id, "openai/gpt-5.4-mini");
313 assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
314 assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
315 }
316}