use serde::{Deserialize, Serialize};
use crate::{Cursor, RequestSize, StepSource, Verdict};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StepRow {
pub cursor: Cursor,
pub harness: String,
pub source: StepSource,
pub step_ref: String,
pub session: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
pub occurred_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_5m: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_1h: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_limit: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_body_len: Option<u64>,
pub prompt: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<f64>,
pub verdict: Verdict,
pub prompt_dropped: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_size: Option<RequestSize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BadCounts {
ReasoningExceedsOutput {
harness: String,
step_ref: String,
reasoning: u64,
output: u64,
},
}
impl std::fmt::Display for BadCounts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReasoningExceedsOutput {
harness,
step_ref,
reasoning,
output,
} => write!(
f,
"row {harness} {step_ref}: reasoning {reasoning} exceeds output {output}"
),
}
}
}
impl std::error::Error for BadCounts {}
pub fn live_row_identity(
runner: &str,
harness: &str,
harness_step_ref: Option<&str>,
attempt_id: Option<&str>,
) -> Option<(String, String)> {
match (harness_step_ref, attempt_id) {
(Some(host_record), _) => Some((harness.to_owned(), host_record.to_owned())),
(None, Some(attempt_id)) => Some((runner.to_owned(), attempt_id.to_owned())),
(None, None) => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NotClosed {
NotAnObject { object: &'static str },
UnknownMember {
object: &'static str,
member: String,
},
}
impl std::fmt::Display for NotClosed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAnObject { object } => write!(f, "{object} is not a JSON object"),
Self::UnknownMember { object, member } => {
write!(f, "{object} carries unknown member `{member}`")
}
}
}
}
impl std::error::Error for NotClosed {}
pub(crate) fn check_members(
object: &'static str,
value: &serde_json::Value,
allowed: &[&str],
) -> Result<(), NotClosed> {
let members = value.as_object().ok_or(NotClosed::NotAnObject { object })?;
match members.keys().find(|key| !allowed.contains(&key.as_str())) {
Some(member) => Err(NotClosed::UnknownMember {
object,
member: member.clone(),
}),
None => Ok(()),
}
}
impl StepRow {
pub const REQUIRED_MEMBERS: &'static [&'static str] = &[
"cursor",
"harness",
"source",
"step_ref",
"session",
"occurred_at_ms",
"prompt",
"verdict",
"prompt_dropped",
];
pub const OPTIONAL_MEMBERS: &'static [&'static str] = &[
"run_id",
"provider",
"model",
"input",
"cache_read",
"cache_write",
"cache_write_5m",
"cache_write_1h",
"output",
"reasoning",
"finish",
"context_limit",
"request_body_len",
"retention",
"request_size",
];
#[allow(clippy::too_many_arguments)]
pub fn new(
cursor: impl Into<Cursor>,
harness: impl Into<String>,
source: StepSource,
step_ref: impl Into<String>,
session: impl Into<String>,
occurred_at_ms: u64,
prompt: u64,
verdict: Verdict,
prompt_dropped: bool,
) -> Self {
Self {
cursor: cursor.into(),
harness: harness.into(),
source,
step_ref: step_ref.into(),
session: session.into(),
run_id: None,
occurred_at_ms,
provider: None,
model: None,
input: None,
cache_read: None,
cache_write: None,
cache_write_5m: None,
cache_write_1h: None,
output: None,
reasoning: None,
finish: None,
context_limit: None,
request_body_len: None,
prompt,
retention: None,
verdict,
prompt_dropped,
request_size: None,
}
}
pub fn identity(&self) -> (&str, &str) {
(&self.harness, &self.step_ref)
}
pub fn check_closed(value: &serde_json::Value) -> Result<(), NotClosed> {
let allowed: Vec<&str> = Self::REQUIRED_MEMBERS
.iter()
.chain(Self::OPTIONAL_MEMBERS)
.copied()
.collect();
check_members("row", value, &allowed)
}
pub fn check_counts(&self) -> Result<(), BadCounts> {
match (self.reasoning, self.output) {
(Some(reasoning), Some(output)) if reasoning > output => {
Err(BadCounts::ReasoningExceedsOutput {
harness: self.harness.clone(),
step_ref: self.step_ref.clone(),
reasoning,
output,
})
}
_ => Ok(()),
}
}
pub fn with_run_id(mut self, run_id: impl Into<String>) -> Self {
self.run_id = Some(run_id.into());
self
}
pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn with_input(mut self, input: u64) -> Self {
self.input = Some(input);
self
}
pub fn with_cache_read(mut self, cache_read: u64) -> Self {
self.cache_read = Some(cache_read);
self
}
pub fn with_cache_write(mut self, cache_write: u64) -> Self {
self.cache_write = Some(cache_write);
self
}
pub fn with_cache_write_5m(mut self, cache_write_5m: u64) -> Self {
self.cache_write_5m = Some(cache_write_5m);
self
}
pub fn with_cache_write_1h(mut self, cache_write_1h: u64) -> Self {
self.cache_write_1h = Some(cache_write_1h);
self
}
pub fn with_output(mut self, output: u64) -> Self {
self.output = Some(output);
self
}
pub fn with_reasoning(mut self, reasoning: u64) -> Self {
self.reasoning = Some(reasoning);
self
}
pub fn with_finish(mut self, finish: impl Into<String>) -> Self {
self.finish = Some(finish.into());
self
}
pub fn with_context_limit(mut self, context_limit: u64) -> Self {
self.context_limit = Some(context_limit);
self
}
pub fn with_request_body_len(mut self, request_body_len: u64) -> Self {
self.request_body_len = Some(request_body_len);
self
}
pub fn with_retention(mut self, retention: f64) -> Self {
self.retention = Some(retention);
self
}
pub fn with_request_size(mut self, request_size: RequestSize) -> Self {
self.request_size = Some(request_size);
self
}
}