use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{
error::{ConsensusError, Result},
panel::ModelProvider,
round::{DevilAdvocateReview, ModelDeliberationResponse},
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SeatConfig {
pub configured: bool,
pub positions: BTreeMap<String, ModelDeliberationResponse>,
pub devil_advocate_reviews: BTreeMap<String, DevilAdvocateReview>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderSeat {
pub provider: ModelProvider,
pub config: SeatConfig,
}
impl ProviderSeat {
#[must_use]
pub fn unconfigured(provider: ModelProvider) -> Self {
Self {
provider,
config: SeatConfig::default(),
}
}
#[must_use]
pub fn with_mock(
provider: ModelProvider,
positions: BTreeMap<String, ModelDeliberationResponse>,
devil_advocate_reviews: BTreeMap<String, DevilAdvocateReview>,
) -> Self {
Self {
provider,
config: SeatConfig {
configured: true,
positions,
devil_advocate_reviews,
},
}
}
fn provider_label(&self) -> &'static str {
match self.provider {
ModelProvider::Anthropic => "anthropic",
ModelProvider::OpenAI => "openai",
ModelProvider::Google => "google",
ModelProvider::XAI => "xai",
ModelProvider::Other(_) => "other",
}
}
pub fn position_for(&self, model_id: &str) -> Result<ModelDeliberationResponse> {
if !self.config.configured {
return Err(ConsensusError::ProviderError(format!(
"{} seat unconfigured",
self.provider_label()
)));
}
self.config.positions.get(model_id).cloned().ok_or_else(|| {
ConsensusError::ProviderError(format!(
"missing structured response for model {model_id} on {} seat",
self.provider_label()
))
})
}
pub fn devil_advocate_review_for(&self, model_id: &str) -> Result<DevilAdvocateReview> {
if !self.config.configured {
return Err(ConsensusError::ProviderError(format!(
"{} seat unconfigured",
self.provider_label()
)));
}
self.config
.devil_advocate_reviews
.get(model_id)
.cloned()
.ok_or_else(|| {
ConsensusError::ProviderError(format!(
"missing devil's advocate review for model {model_id} on {} seat",
self.provider_label()
))
})
}
}
#[derive(Debug, Clone)]
pub struct PresidentialCohort {
pub xai: ProviderSeat,
pub openai: ProviderSeat,
pub anthropic: ProviderSeat,
pub planned: BTreeMap<String, ProviderSeat>,
}
impl PresidentialCohort {
#[must_use]
pub fn new_fail_closed() -> Self {
let mut planned = BTreeMap::new();
for name in [
"google",
"meta",
"deepseek",
"nvidia",
"qwen",
"mistral",
"amazon",
"microsoft",
] {
planned.insert(
name.to_string(),
ProviderSeat::unconfigured(ModelProvider::Other(name.to_string())),
);
}
Self {
xai: ProviderSeat::unconfigured(ModelProvider::XAI),
openai: ProviderSeat::unconfigured(ModelProvider::OpenAI),
anthropic: ProviderSeat::unconfigured(ModelProvider::Anthropic),
planned,
}
}
pub fn seat_for(&self, provider: &ModelProvider) -> Result<&ProviderSeat> {
match provider {
ModelProvider::XAI => Ok(&self.xai),
ModelProvider::OpenAI => Ok(&self.openai),
ModelProvider::Anthropic => Ok(&self.anthropic),
ModelProvider::Google => self
.planned
.get("google")
.ok_or_else(|| ConsensusError::ProviderError("google seat unavailable".into())),
ModelProvider::Other(name) => self
.planned
.get(name)
.ok_or_else(|| ConsensusError::ProviderError(format!("{name} seat unavailable"))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdvisoryReceipt {
pub provider: String,
pub model_id: String,
pub role: String,
pub response_hash: String,
}
pub fn bind_advisory_receipt(
provider: &ModelProvider,
model_id: &str,
role: &str,
response: &ModelDeliberationResponse,
) -> Result<AdvisoryReceipt> {
let hash = crate::commitment::commit_response(response)?;
let provider_label = match provider {
ModelProvider::Anthropic => "anthropic",
ModelProvider::OpenAI => "openai",
ModelProvider::Google => "google",
ModelProvider::XAI => "xai",
ModelProvider::Other(other) => other.as_str(),
};
Ok(AdvisoryReceipt {
provider: provider_label.to_string(),
model_id: model_id.to_string(),
role: role.to_string(),
response_hash: hash.to_string(),
})
}
pub fn require_dissent_receipt(dissent_receipt_id: &str) -> Result<()> {
if dissent_receipt_id.trim().is_empty() {
return Err(ConsensusError::ProviderError(
"presidential brief item missing dissent_receipt_id".into(),
));
}
Ok(())
}
#[cfg(feature = "xai")]
#[must_use]
pub fn xai_seat(
positions: BTreeMap<String, ModelDeliberationResponse>,
devil_advocate_reviews: BTreeMap<String, DevilAdvocateReview>,
) -> ProviderSeat {
ProviderSeat::with_mock(ModelProvider::XAI, positions, devil_advocate_reviews)
}
#[cfg(feature = "openai")]
#[must_use]
pub fn openai_seat(
positions: BTreeMap<String, ModelDeliberationResponse>,
devil_advocate_reviews: BTreeMap<String, DevilAdvocateReview>,
) -> ProviderSeat {
ProviderSeat::with_mock(ModelProvider::OpenAI, positions, devil_advocate_reviews)
}
#[cfg(feature = "anthropic")]
#[must_use]
pub fn anthropic_seat(
positions: BTreeMap<String, ModelDeliberationResponse>,
devil_advocate_reviews: BTreeMap<String, DevilAdvocateReview>,
) -> ProviderSeat {
ProviderSeat::with_mock(ModelProvider::Anthropic, positions, devil_advocate_reviews)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::panel::ModelRole;
fn sample_response() -> ModelDeliberationResponse {
ModelDeliberationResponse {
position_text: "approve with conditions".into(),
key_claims: vec!["claim-a".into()],
confidence_bps: 7000,
}
}
#[test]
fn unconfigured_seat_fails_closed() {
let seat = ProviderSeat::unconfigured(ModelProvider::XAI);
let err = seat.position_for("grok-2").expect_err("unconfigured");
assert!(err.to_string().contains("xai seat unconfigured"));
}
#[test]
fn configured_mock_seat_returns_response() {
let mut positions = BTreeMap::new();
positions.insert("grok-2".into(), sample_response());
let seat = ProviderSeat::with_mock(ModelProvider::XAI, positions, BTreeMap::new());
let got = seat.position_for("grok-2").expect("mock");
assert_eq!(got.key_claims, vec!["claim-a".to_string()]);
}
#[test]
fn planned_seats_are_non_voting_stubs() {
let cohort = PresidentialCohort::new_fail_closed();
assert!(!cohort.planned["deepseek"].config.configured);
let err = cohort
.seat_for(&ModelProvider::Other("deepseek".into()))
.expect("stub exists")
.position_for("any")
.expect_err("unconfigured");
assert!(err.to_string().contains("unconfigured"));
}
#[test]
fn advisory_receipt_binds_response_hash() {
let response = sample_response();
let receipt = bind_advisory_receipt(
&ModelProvider::OpenAI,
"gpt-4o",
&format!("{:?}", ModelRole::DevilsAdvocate),
&response,
)
.expect("bind");
assert_eq!(receipt.provider, "openai");
assert!(!receipt.response_hash.is_empty());
}
#[test]
fn missing_dissent_receipt_fails_closed() {
assert!(require_dissent_receipt("").is_err());
assert!(require_dissent_receipt("dissent-1").is_ok());
}
#[test]
fn provider_modules_have_no_live_http_clients() {
let source = include_str!("providers.rs");
let production = source.split("#[cfg(test)]").next().expect("prod");
for needle in ["reqwest", "ureq", "hyper::", "SystemTime::now"] {
assert!(
!production.contains(needle),
"providers must not include live HTTP/wall-clock ({needle})"
);
}
}
}