1use super::*;
2use agent_core::reasoning::ReasoningLevel;
3use serde::Deserialize;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AnthropicExecutionMode {
7 Standard,
8 Max,
9 UltraCode,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum AnthropicWireEffort {
14 Low,
15 Medium,
16 High,
17 XHigh,
18 Max,
19}
20
21impl AnthropicWireEffort {
22 pub const fn as_str(self) -> &'static str {
23 match self {
24 Self::Low => "low",
25 Self::Medium => "medium",
26 Self::High => "high",
27 Self::XHigh => "xhigh",
28 Self::Max => "max",
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AnthropicWorkflowPlan {
35 None,
36 Standing,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct AnthropicPlanPrerequisites {
41 pub orchestration_policy: bool,
42 pub foreground_worker_authorized: bool,
43 pub concurrent_limit: usize,
44 pub total_limit: usize,
45 pub lifecycle_start: bool,
46 pub lifecycle_status: bool,
47 pub lifecycle_steer: bool,
48 pub lifecycle_collect: bool,
49 pub lifecycle_resume: bool,
50}
51
52impl AnthropicPlanPrerequisites {
53 pub const fn installed() -> Self {
54 Self {
55 orchestration_policy: true,
56 foreground_worker_authorized: true,
57 concurrent_limit: 1,
58 total_limit: 1,
59 lifecycle_start: true,
60 lifecycle_status: true,
61 lifecycle_steer: true,
62 lifecycle_collect: true,
63 lifecycle_resume: true,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum AnthropicPlanErrorCode {
70 InvalidProviderIdentity,
71 CapabilityMetadataMissing,
72 UnsupportedReasoningLevel,
73 UltraCodeRequiresForeground,
74 UltraCodeRequiresOrchestration,
75 UltraCodeRequiresLifecycleTools,
76 UltraCodeRequiresWorkerAuthorization,
77 UltraCodeRequiresLimits,
78}
79
80impl AnthropicPlanErrorCode {
81 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::InvalidProviderIdentity => "invalid_provider_identity",
84 Self::CapabilityMetadataMissing => "capability_metadata_missing",
85 Self::UnsupportedReasoningLevel => "unsupported_reasoning_level",
86 Self::UltraCodeRequiresForeground => "ultracode_requires_foreground",
87 Self::UltraCodeRequiresOrchestration => "ultracode_requires_orchestration",
88 Self::UltraCodeRequiresLifecycleTools => "ultracode_requires_lifecycle_tools",
89 Self::UltraCodeRequiresWorkerAuthorization => "ultracode_requires_worker_authorization",
90 Self::UltraCodeRequiresLimits => "ultracode_requires_limits",
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct AnthropicPlanError(AnthropicPlanErrorCode);
97impl AnthropicPlanError {
98 pub const fn code(&self) -> AnthropicPlanErrorCode {
99 self.0
100 }
101}
102impl std::fmt::Display for AnthropicPlanError {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.write_str(self.0.as_str())
105 }
106}
107impl std::error::Error for AnthropicPlanError {}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct AnthropicExecutionPlan {
111 pub qualified_model: String,
112 pub requested_level: ReasoningLevel,
113 pub role: ExecutionRole,
114 pub mode: AnthropicExecutionMode,
115 pub wire_effort: Option<AnthropicWireEffort>,
116 pub workflow: AnthropicWorkflowPlan,
117}
118
119pub(super) const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
120pub(super) const ANTHROPIC_MODELS_PAGE_LIMIT: usize = 100;
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct AnthropicModeCapabilities {
125 pub max_supported: bool,
126 pub xhigh_supported: bool,
127 pub workflow_supported: bool,
128}
129
130impl AnthropicModeCapabilities {
131 pub const fn none() -> Self {
132 Self {
133 max_supported: false,
134 xhigh_supported: false,
135 workflow_supported: false,
136 }
137 }
138
139 pub const fn ultracode_supported(self) -> bool {
140 self.xhigh_supported && self.workflow_supported
141 }
142
143 pub const fn narrow_with_live_effort(mut self, supported: Option<bool>) -> Self {
145 if matches!(supported, Some(false)) {
146 self.max_supported = false;
147 self.xhigh_supported = false;
148 self.workflow_supported = false;
149 }
150 self
151 }
152}
153
154const ANTHROPIC_MODE_MANIFEST_VERSION: u16 = 1;
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct AnthropicManifestRow {
159 pub qualified_id: &'static str,
160 pub max_supported: bool,
161 pub xhigh_supported: bool,
162 pub workflow_supported: bool,
163 pub evidence: &'static str,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct AnthropicCapabilityManifest {
168 pub schema_version: u16,
169 pub rows: &'static [AnthropicManifestRow],
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum AnthropicManifestErrorCode {
174 UnsupportedVersion,
175 MalformedQualifiedId,
176 DuplicateId,
177 ContradictoryCapabilities,
178 EvidenceMissing,
179}
180
181#[cfg(test)]
182impl AnthropicManifestErrorCode {
183 pub const fn as_str(self) -> &'static str {
186 match self {
187 Self::UnsupportedVersion => "unsupported_version",
188 Self::MalformedQualifiedId => "malformed_qualified_id",
189 Self::DuplicateId => "duplicate_id",
190 Self::ContradictoryCapabilities => "contradictory_capabilities",
191 Self::EvidenceMissing => "evidence_missing",
192 }
193 }
194}
195
196impl AnthropicCapabilityManifest {
197 pub fn validate(self) -> Result<(), AnthropicManifestErrorCode> {
198 if self.schema_version != ANTHROPIC_MODE_MANIFEST_VERSION {
199 return Err(AnthropicManifestErrorCode::UnsupportedVersion);
200 }
201 let mut ids = std::collections::BTreeSet::new();
202 for row in self.rows {
203 let Some(id) = row.qualified_id.strip_prefix("anthropic/") else {
204 return Err(AnthropicManifestErrorCode::MalformedQualifiedId);
205 };
206 if id.is_empty() || id.contains('/') {
207 return Err(AnthropicManifestErrorCode::MalformedQualifiedId);
208 }
209 if !ids.insert(row.qualified_id) {
210 return Err(AnthropicManifestErrorCode::DuplicateId);
211 }
212 if row.workflow_supported && !row.xhigh_supported {
213 return Err(AnthropicManifestErrorCode::ContradictoryCapabilities);
214 }
215 if (row.max_supported || row.workflow_supported) && row.evidence.trim().is_empty() {
216 return Err(AnthropicManifestErrorCode::EvidenceMissing);
217 }
218 }
219 Ok(())
220 }
221}
222
223const ANTHROPIC_MODE_ROWS: &[AnthropicManifestRow] = &[AnthropicManifestRow {
224 qualified_id: "anthropic/claude-fable-5",
225 max_supported: true,
226 xhigh_supported: true,
227 workflow_supported: true,
228 evidence: "Claude Code 2.1.207; sha256:85e7e988a392d859f90802ca21fb26e89d3c9ab527f5ed0b08df3955e34d5c83",
229}];
230const ANTHROPIC_MODE_MANIFEST: AnthropicCapabilityManifest = AnthropicCapabilityManifest {
231 schema_version: ANTHROPIC_MODE_MANIFEST_VERSION,
232 rows: ANTHROPIC_MODE_ROWS,
233};
234
235pub fn anthropic_mode_capabilities(qualified_model: &str) -> Option<AnthropicModeCapabilities> {
238 ANTHROPIC_MODE_MANIFEST.validate().ok()?;
239 let row = ANTHROPIC_MODE_MANIFEST
240 .rows
241 .iter()
242 .find(|row| row.qualified_id == qualified_model)?;
243 Some(AnthropicModeCapabilities {
244 max_supported: row.max_supported,
245 xhigh_supported: row.xhigh_supported,
246 workflow_supported: row.workflow_supported,
247 })
248}
249
250pub fn plan_standard_anthropic_transport(
251 model: &str,
252 requested_level: ReasoningLevel,
253 role: ExecutionRole,
254) -> Option<AnthropicExecutionPlan> {
255 if matches!(
256 requested_level,
257 ReasoningLevel::Max | ReasoningLevel::UltraCode
258 ) {
259 return None;
260 }
261 let wire_effort = match requested_level {
262 ReasoningLevel::Low => Some(AnthropicWireEffort::Low),
263 ReasoningLevel::Medium => Some(AnthropicWireEffort::Medium),
264 ReasoningLevel::High => Some(AnthropicWireEffort::High),
265 ReasoningLevel::XHigh => Some(AnthropicWireEffort::XHigh),
266 _ => None,
267 };
268 Some(AnthropicExecutionPlan {
269 qualified_model: model.to_owned(),
270 requested_level,
271 role,
272 mode: AnthropicExecutionMode::Standard,
273 wire_effort,
274 workflow: AnthropicWorkflowPlan::None,
275 })
276}
277
278pub fn plan_anthropic_execution(
279 qualified_model: &str,
280 requested_level: ReasoningLevel,
281 role: ExecutionRole,
282 prerequisites: AnthropicPlanPrerequisites,
283 live_effort_supported: Option<bool>,
284) -> Result<AnthropicExecutionPlan, AnthropicPlanError> {
285 let Some(id) = qualified_model.strip_prefix("anthropic/") else {
286 return Err(AnthropicPlanError(
287 AnthropicPlanErrorCode::InvalidProviderIdentity,
288 ));
289 };
290 if id.is_empty() || id.contains('/') {
291 return Err(AnthropicPlanError(
292 AnthropicPlanErrorCode::InvalidProviderIdentity,
293 ));
294 }
295 if requested_level == ReasoningLevel::Ultra {
296 return Err(AnthropicPlanError(
297 AnthropicPlanErrorCode::UnsupportedReasoningLevel,
298 ));
299 }
300 let capabilities = anthropic_mode_capabilities(qualified_model)
301 .map(|caps| caps.narrow_with_live_effort(live_effort_supported));
302 if matches!(
303 requested_level,
304 ReasoningLevel::Max | ReasoningLevel::UltraCode
305 ) && capabilities.is_none()
306 {
307 return Err(AnthropicPlanError(
308 AnthropicPlanErrorCode::CapabilityMetadataMissing,
309 ));
310 }
311 if match requested_level {
312 ReasoningLevel::Max => !capabilities.is_some_and(|caps| caps.max_supported),
313 ReasoningLevel::UltraCode => !capabilities.is_some_and(|caps| caps.ultracode_supported()),
314 _ => false,
315 } {
316 return Err(AnthropicPlanError(
317 AnthropicPlanErrorCode::UnsupportedReasoningLevel,
318 ));
319 }
320 if requested_level == ReasoningLevel::UltraCode {
321 if role != ExecutionRole::Foreground {
322 return Err(AnthropicPlanError(
323 AnthropicPlanErrorCode::UltraCodeRequiresForeground,
324 ));
325 }
326 if !prerequisites.orchestration_policy {
327 return Err(AnthropicPlanError(
328 AnthropicPlanErrorCode::UltraCodeRequiresOrchestration,
329 ));
330 }
331 if !prerequisites.foreground_worker_authorized {
332 return Err(AnthropicPlanError(
333 AnthropicPlanErrorCode::UltraCodeRequiresWorkerAuthorization,
334 ));
335 }
336 if prerequisites.concurrent_limit == 0 || prerequisites.total_limit == 0 {
337 return Err(AnthropicPlanError(
338 AnthropicPlanErrorCode::UltraCodeRequiresLimits,
339 ));
340 }
341 if ![
342 prerequisites.lifecycle_start,
343 prerequisites.lifecycle_status,
344 prerequisites.lifecycle_steer,
345 prerequisites.lifecycle_collect,
346 prerequisites.lifecycle_resume,
347 ]
348 .into_iter()
349 .all(|present| present)
350 {
351 return Err(AnthropicPlanError(
352 AnthropicPlanErrorCode::UltraCodeRequiresLifecycleTools,
353 ));
354 }
355 }
356 let wire_effort = match requested_level {
357 ReasoningLevel::Low => Some(AnthropicWireEffort::Low),
358 ReasoningLevel::Medium => Some(AnthropicWireEffort::Medium),
359 ReasoningLevel::High => Some(AnthropicWireEffort::High),
360 ReasoningLevel::XHigh | ReasoningLevel::UltraCode => Some(AnthropicWireEffort::XHigh),
361 ReasoningLevel::Max => Some(AnthropicWireEffort::Max),
362 _ => None,
363 };
364 Ok(AnthropicExecutionPlan {
365 qualified_model: qualified_model.to_owned(),
366 requested_level,
367 role,
368 mode: match requested_level {
369 ReasoningLevel::Max => AnthropicExecutionMode::Max,
370 ReasoningLevel::UltraCode => AnthropicExecutionMode::UltraCode,
371 _ => AnthropicExecutionMode::Standard,
372 },
373 wire_effort,
374 workflow: if requested_level == ReasoningLevel::UltraCode {
375 AnthropicWorkflowPlan::Standing
376 } else {
377 AnthropicWorkflowPlan::None
378 },
379 })
380}
381
382#[cfg(test)]
383mod manifest_validation_tests {
384 use super::*;
385 const GOOD: AnthropicManifestRow = AnthropicManifestRow {
386 qualified_id: "anthropic/claude-fable-5",
387 max_supported: true,
388 xhigh_supported: true,
389 workflow_supported: true,
390 evidence: "exact evidence",
391 };
392 fn manifest(
393 version: u16,
394 rows: &'static [AnthropicManifestRow],
395 ) -> AnthropicCapabilityManifest {
396 AnthropicCapabilityManifest {
397 schema_version: version,
398 rows,
399 }
400 }
401 #[test]
402 fn dedicated_manifest_failures_and_exact_fable_success() {
403 assert_eq!(
404 manifest(2, &[GOOD]).validate(),
405 Err(AnthropicManifestErrorCode::UnsupportedVersion)
406 );
407 assert_eq!(
408 manifest(
409 1,
410 &[AnthropicManifestRow {
411 qualified_id: "anthropic/fable/5",
412 ..GOOD
413 }]
414 )
415 .validate(),
416 Err(AnthropicManifestErrorCode::MalformedQualifiedId)
417 );
418 assert_eq!(
419 manifest(1, &[GOOD, GOOD]).validate(),
420 Err(AnthropicManifestErrorCode::DuplicateId)
421 );
422 assert_eq!(
423 manifest(
424 1,
425 &[AnthropicManifestRow {
426 xhigh_supported: false,
427 ..GOOD
428 }]
429 )
430 .validate(),
431 Err(AnthropicManifestErrorCode::ContradictoryCapabilities)
432 );
433 assert_eq!(
434 manifest(
435 1,
436 &[AnthropicManifestRow {
437 evidence: " ",
438 ..GOOD
439 }]
440 )
441 .validate(),
442 Err(AnthropicManifestErrorCode::EvidenceMissing)
443 );
444 assert_eq!(manifest(1, &[GOOD]).validate(), Ok(()));
445 assert!(anthropic_mode_capabilities("anthropic/claude-fable-5").is_some());
446 assert!(anthropic_mode_capabilities("anthropic/claude-fable-5-near").is_none());
447 }
448 #[test]
449 fn stable_errors_are_input_free() {
450 assert_eq!(
451 [
452 AnthropicManifestErrorCode::UnsupportedVersion.as_str(),
453 AnthropicManifestErrorCode::MalformedQualifiedId.as_str(),
454 AnthropicManifestErrorCode::DuplicateId.as_str(),
455 AnthropicManifestErrorCode::ContradictoryCapabilities.as_str(),
456 AnthropicManifestErrorCode::EvidenceMissing.as_str()
457 ],
458 [
459 "unsupported_version",
460 "malformed_qualified_id",
461 "duplicate_id",
462 "contradictory_capabilities",
463 "evidence_missing"
464 ]
465 );
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq)]
470pub struct AnthropicCatalogPage {
471 pub models: Vec<CatalogModel>,
472 pub has_more: bool,
473 pub last_id: Option<String>,
474}
475
476#[derive(Debug, Deserialize)]
477struct AnthropicModelsPage {
478 data: Vec<AnthropicModelItem>,
479 #[serde(default)]
480 has_more: bool,
481 #[serde(default)]
482 last_id: Option<String>,
483}
484
485#[derive(Debug, Deserialize)]
486struct AnthropicModelItem {
487 id: String,
488 #[serde(default)]
489 display_name: Option<String>,
490 #[serde(default)]
491 max_input_tokens: Option<u64>,
492 #[serde(default)]
493 max_tokens: Option<u64>,
494 #[serde(default)]
495 capabilities: Option<AnthropicCapabilities>,
496}
497
498#[derive(Debug, Deserialize)]
499struct AnthropicCapabilities {
500 #[serde(default)]
501 thinking: Option<CapabilitySupported>,
502 #[serde(default)]
503 effort: Option<AnthropicEffortCapability>,
504}
505
506#[derive(Debug, Deserialize)]
507struct CapabilitySupported {
508 #[serde(default)]
509 supported: bool,
510}
511
512#[derive(Debug, Deserialize)]
513struct AnthropicEffortCapability {
514 #[serde(default)]
515 supported: bool,
516}
517
518pub fn parse_anthropic_catalog_page(body: &str) -> Result<AnthropicCatalogPage, serde_json::Error> {
519 let page: AnthropicModelsPage = serde_json::from_str(body)?;
520 let models: Vec<CatalogModel> = page
521 .data
522 .into_iter()
523 .filter_map(|item| {
524 let mut m = CatalogModel::new("anthropic", "Anthropic", item.id)?;
525 m.provider_kind = CatalogProviderKind::Anthropic;
526 m.label = item.display_name.filter(|name| !name.trim().is_empty());
527 m.context_tokens = item.max_input_tokens;
528 m.max_output_tokens = item.max_tokens;
529 m.reasoning = match item.capabilities {
530 Some(caps) => match caps.thinking.as_ref() {
531 Some(thinking) if thinking.supported => ReasoningSupport::AnthropicAdaptive {
532 adaptive: caps.effort.as_ref().is_some_and(|c| c.supported),
533 },
534 Some(_) => ReasoningSupport::None,
537 None => ReasoningSupport::Unknown,
539 },
540 _ => ReasoningSupport::Unknown,
541 };
542 m.source = CatalogSource::Live;
543 Some(m)
544 })
545 .collect();
546
547 super::capability_cache::populate(&models);
550
551 Ok(AnthropicCatalogPage {
552 models,
553 has_more: page.has_more,
554 last_id: page.last_id.filter(|id| !id.trim().is_empty()),
555 })
556}
557
558pub fn parse_anthropic_catalog_models(body: &str) -> Result<Vec<CatalogModel>, serde_json::Error> {
559 parse_anthropic_catalog_page(body).map(|page| page.models)
560}
561
562pub fn anthropic_static_capability(model_id: &str) -> Option<ReasoningSupport> {
570 match model_id {
571 "claude-opus-5-5" | "claude-opus-4-7" | "claude-fable-5" | "claude-fable-5-1" => {
572 Some(ReasoningSupport::AnthropicAdaptive { adaptive: true })
573 }
574 "claude-sonnet-4-6" | "claude-opus-4-6" | "claude-haiku-4-5-20251001" => {
575 Some(ReasoningSupport::AnthropicAdaptive { adaptive: false })
576 }
577 _ => None,
578 }
579}
580
581pub fn anthropic_models_url(after_id: Option<&str>) -> String {
582 let mut url = format!("{ANTHROPIC_MODELS_URL}?limit={ANTHROPIC_MODELS_PAGE_LIMIT}");
583 if let Some(after_id) = after_id.filter(|id| !id.trim().is_empty()) {
584 url.push_str("&after_id=");
585 url.push_str(after_id);
586 }
587 url
588}
589
590pub fn merge_catalog_pages(pages: Vec<Vec<CatalogModel>>) -> Vec<CatalogModel> {
591 let mut seen = std::collections::BTreeSet::new();
592 let mut merged = Vec::new();
593 for page in pages {
594 for model in page {
595 if seen.insert(model.id.clone()) {
596 merged.push(model);
597 }
598 }
599 }
600 merged
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn standard_transport_accepts_bare_or_unknown_models_but_special_modes_require_authority() {
609 for model in ["claude-haiku-4-5", "anthropic/unknown-model"] {
610 for level in [
611 ReasoningLevel::Off,
612 ReasoningLevel::Adaptive,
613 ReasoningLevel::Low,
614 ReasoningLevel::Medium,
615 ReasoningLevel::High,
616 ReasoningLevel::XHigh,
617 ] {
618 let plan =
619 plan_standard_anthropic_transport(model, level, ExecutionRole::Foreground)
620 .expect("ordinary transport plan");
621 assert_eq!(plan.mode, AnthropicExecutionMode::Standard);
622 assert_eq!(plan.qualified_model, model);
623 }
624 assert!(plan_standard_anthropic_transport(
625 model,
626 ReasoningLevel::Max,
627 ExecutionRole::Foreground
628 )
629 .is_none());
630 assert!(plan_standard_anthropic_transport(
631 model,
632 ReasoningLevel::UltraCode,
633 ExecutionRole::Foreground
634 )
635 .is_none());
636 }
637 }
638
639 #[test]
640 fn exact_fable_mode_capabilities_are_evidence_locked() {
641 let caps = anthropic_mode_capabilities("anthropic/claude-fable-5")
642 .expect("exact qualified Fable 5 row");
643 assert!(caps.max_supported);
644 assert!(caps.xhigh_supported);
645 assert!(caps.workflow_supported);
646 assert!(caps.ultracode_supported());
647
648 for denied in [
649 "anthropic/claude-fable-5-preview",
650 "anthropic/claude-opus-4-7",
651 "anthropic/claude-opus-5-5",
654 "openai/claude-fable-5",
655 "claude-fable-5",
656 "anthropic/fable-5",
657 ] {
658 assert_eq!(anthropic_mode_capabilities(denied), None, "{denied}");
659 }
660 }
661
662 #[test]
663 fn generic_live_effort_cannot_invent_special_modes_and_can_revoke() {
664 let static_caps = anthropic_mode_capabilities("anthropic/claude-fable-5").unwrap();
665 assert_eq!(static_caps.narrow_with_live_effort(Some(true)), static_caps);
666 let revoked = static_caps.narrow_with_live_effort(Some(false));
667 assert!(!revoked.max_supported);
668 assert!(!revoked.ultracode_supported());
669
670 assert_eq!(
671 AnthropicModeCapabilities::none().narrow_with_live_effort(Some(true)),
672 AnthropicModeCapabilities::none()
673 );
674 }
675
676 #[test]
677 fn typed_planner_maps_exact_modes_and_fails_closed() {
678 let prerequisites = AnthropicPlanPrerequisites::installed();
679 let plan = plan_anthropic_execution(
680 "anthropic/claude-fable-5",
681 ReasoningLevel::UltraCode,
682 ExecutionRole::Foreground,
683 prerequisites,
684 None,
685 )
686 .unwrap();
687 assert_eq!(plan.mode, AnthropicExecutionMode::UltraCode);
688 assert_eq!(plan.wire_effort, Some(AnthropicWireEffort::XHigh));
689 assert_eq!(plan.workflow, AnthropicWorkflowPlan::Standing);
690
691 for (level, wire, mode) in [
692 (
693 ReasoningLevel::Max,
694 AnthropicWireEffort::Max,
695 AnthropicExecutionMode::Max,
696 ),
697 (
698 ReasoningLevel::XHigh,
699 AnthropicWireEffort::XHigh,
700 AnthropicExecutionMode::Standard,
701 ),
702 ] {
703 let plan = plan_anthropic_execution(
704 "anthropic/claude-fable-5",
705 level,
706 ExecutionRole::Foreground,
707 prerequisites,
708 None,
709 )
710 .unwrap();
711 assert_eq!(plan.mode, mode);
712 assert_eq!(plan.wire_effort, Some(wire));
713 assert_eq!(plan.workflow, AnthropicWorkflowPlan::None);
714 }
715
716 for (model, level, role, prerequisites, code) in [
717 (
718 "openai-codex/claude-fable-5",
719 ReasoningLevel::UltraCode,
720 ExecutionRole::Foreground,
721 prerequisites,
722 AnthropicPlanErrorCode::InvalidProviderIdentity,
723 ),
724 (
725 "anthropic/claude-fable-5-preview",
726 ReasoningLevel::Max,
727 ExecutionRole::Foreground,
728 prerequisites,
729 AnthropicPlanErrorCode::CapabilityMetadataMissing,
730 ),
731 (
732 "anthropic/claude-fable-5",
733 ReasoningLevel::Ultra,
734 ExecutionRole::Foreground,
735 prerequisites,
736 AnthropicPlanErrorCode::UnsupportedReasoningLevel,
737 ),
738 (
739 "anthropic/claude-fable-5",
740 ReasoningLevel::UltraCode,
741 ExecutionRole::Worker,
742 prerequisites,
743 AnthropicPlanErrorCode::UltraCodeRequiresForeground,
744 ),
745 (
746 "anthropic/claude-fable-5",
747 ReasoningLevel::UltraCode,
748 ExecutionRole::Internal,
749 prerequisites,
750 AnthropicPlanErrorCode::UltraCodeRequiresForeground,
751 ),
752 (
753 "anthropic/claude-fable-5",
754 ReasoningLevel::UltraCode,
755 ExecutionRole::Foreground,
756 AnthropicPlanPrerequisites {
757 orchestration_policy: false,
758 ..AnthropicPlanPrerequisites::installed()
759 },
760 AnthropicPlanErrorCode::UltraCodeRequiresOrchestration,
761 ),
762 (
763 "anthropic/claude-fable-5",
764 ReasoningLevel::UltraCode,
765 ExecutionRole::Foreground,
766 AnthropicPlanPrerequisites {
767 lifecycle_start: false,
768 ..AnthropicPlanPrerequisites::installed()
769 },
770 AnthropicPlanErrorCode::UltraCodeRequiresLifecycleTools,
771 ),
772 ] {
773 assert_eq!(
774 plan_anthropic_execution(model, level, role, prerequisites, None)
775 .unwrap_err()
776 .code(),
777 code
778 );
779 }
780 }
781
782 #[test]
783 fn live_false_revokes_but_live_true_does_not_invent() {
784 let installed = AnthropicPlanPrerequisites::installed();
785 assert_eq!(
786 plan_anthropic_execution(
787 "anthropic/unknown",
788 ReasoningLevel::Max,
789 ExecutionRole::Foreground,
790 installed,
791 Some(true)
792 )
793 .unwrap_err()
794 .code(),
795 AnthropicPlanErrorCode::CapabilityMetadataMissing
796 );
797 assert_eq!(
798 plan_anthropic_execution(
799 "anthropic/claude-fable-5",
800 ReasoningLevel::Max,
801 ExecutionRole::Foreground,
802 installed,
803 Some(false)
804 )
805 .unwrap_err()
806 .code(),
807 AnthropicPlanErrorCode::UnsupportedReasoningLevel
808 );
809 }
810
811 #[test]
814 fn static_capability_covers_exact_known_models_only() {
815 for id in ["claude-opus-5-5", "claude-opus-4-7", "claude-fable-5"] {
816 assert_eq!(
817 anthropic_static_capability(id),
818 Some(ReasoningSupport::AnthropicAdaptive { adaptive: true }),
819 "{id}"
820 );
821 }
822 for id in [
823 "claude-sonnet-4-6",
824 "claude-opus-4-6",
825 "claude-haiku-4-5-20251001",
826 ] {
827 assert_eq!(
828 anthropic_static_capability(id),
829 Some(ReasoningSupport::AnthropicAdaptive { adaptive: false }),
830 "{id}"
831 );
832 }
833 for id in [
835 "claude-opus-4-7-preview",
836 "claude-haiku-4-5",
837 "opus-4-7",
838 "claude-opus-5.5",
839 "claude-opus-5-5-preview",
840 "opus-5-5",
841 "",
842 ] {
843 assert_eq!(anthropic_static_capability(id), None, "{id}");
844 }
845 }
846
847 #[test]
853 fn static_table_and_wire_shape_classifier_agree_for_known_models() {
854 use agent_core::models::{model_supports_adaptive_thinking, KNOWN_MODELS};
855 for (id, _label) in KNOWN_MODELS {
856 let expected = ReasoningSupport::AnthropicAdaptive {
857 adaptive: model_supports_adaptive_thinking(id),
858 };
859 assert_eq!(
860 anthropic_static_capability(id),
861 Some(expected),
862 "static table and wire-shape classifier disagree for {id}"
863 );
864 }
865 for (id, classifier_infers_adaptive) in [
869 ("claude-opus-4-7-preview", true),
870 ("claude-fable-5-latest", true),
871 ("claude-sonnet-4-6-legacy", false),
872 ] {
873 assert_eq!(
874 anthropic_static_capability(id),
875 None,
876 "near-miss {id} must fail closed in the static table"
877 );
878 assert_eq!(
880 agent_core::models::model_supports_adaptive_thinking(id),
881 classifier_infers_adaptive,
882 "classifier expectation drifted for {id}"
883 );
884 }
885 }
886
887 #[test]
888 fn live_parse_maps_explicit_unsupported_thinking_to_none() {
889 let body = r#"{
890 "data": [
891 {"id": "claude-test-thinker",
892 "capabilities": {"thinking": {"supported": true},
893 "effort": {"supported": true}}},
894 {"id": "claude-test-fixed",
895 "capabilities": {"thinking": {"supported": true},
896 "effort": {"supported": false}}},
897 {"id": "claude-test-nothink",
898 "capabilities": {"thinking": {"supported": false}}},
899 {"id": "claude-test-nocaps"}
900 ],
901 "has_more": false
902 }"#;
903 let models = parse_anthropic_catalog_models(body).expect("parse");
904 let by_id = |id: &str| models.iter().find(|m| m.id == id).unwrap();
905 assert_eq!(
906 by_id("claude-test-thinker").reasoning,
907 ReasoningSupport::AnthropicAdaptive { adaptive: true }
908 );
909 assert_eq!(
910 by_id("claude-test-fixed").reasoning,
911 ReasoningSupport::AnthropicAdaptive { adaptive: false }
912 );
913 assert_eq!(
915 by_id("claude-test-nothink").reasoning,
916 ReasoningSupport::None
917 );
918 assert_eq!(
920 by_id("claude-test-nocaps").reasoning,
921 ReasoningSupport::Unknown
922 );
923 }
924
925 #[test]
926 fn live_parse_populates_capability_cache_with_qualified_ids() {
927 let body = r#"{
928 "data": [
929 {"id": "claude-test-cache-entry",
930 "capabilities": {"thinking": {"supported": true},
931 "effort": {"supported": true}}}
932 ],
933 "has_more": false
934 }"#;
935 parse_anthropic_catalog_models(body).expect("parse");
936 let cached = super::super::capability_cache::get("anthropic/claude-test-cache-entry")
937 .expect("live anthropic parse must populate the capability cache");
938 assert_eq!(
939 cached.reasoning,
940 ReasoningSupport::AnthropicAdaptive { adaptive: true }
941 );
942 }
943}