kcode-k1-codex-web-search-values 0.1.0

Concrete values for K1 Codex web search
Documentation
//! Concrete values exchanged by the K1 Codex Web Search consumer.

use std::fmt;
use std::str::FromStr;
use std::time::Duration;

/// A rejected request argument or context-size spelling.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ArgumentValidationError {
    BlankQuestion,
    BlankModel,
    BlankReasoningEffort,
    InvalidSearchContextSize,
}

impl fmt::Display for ArgumentValidationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = match self {
            Self::BlankQuestion => "question must not be blank",
            Self::BlankModel => "model must not be blank",
            Self::BlankReasoningEffort => "reasoning effort must not be blank",
            Self::InvalidSearchContextSize => "search context size must be low, medium, or high",
        };
        formatter.write_str(message)
    }
}

impl std::error::Error for ArgumentValidationError {}

/// The provider-supported search-context size.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchContextSize {
    Low,
    Medium,
    High,
}

impl fmt::Display for SearchContextSize {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
        })
    }
}

impl FromStr for SearchContextSize {
    type Err = ArgumentValidationError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "low" => Ok(Self::Low),
            "medium" => Ok(Self::Medium),
            "high" => Ok(Self::High),
            _ => Err(ArgumentValidationError::InvalidSearchContextSize),
        }
    }
}

/// Validated inputs for one web-search request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchRequest {
    question: String,
    model: String,
    reasoning_effort: String,
    search_context_size: SearchContextSize,
}

impl WebSearchRequest {
    pub fn new(
        question: impl Into<String>,
        model: impl Into<String>,
        reasoning_effort: impl Into<String>,
        search_context_size: SearchContextSize,
    ) -> Result<Self, ArgumentValidationError> {
        let question = question.into();
        let model = model.into();
        let reasoning_effort = reasoning_effort.into();

        if question.trim().is_empty() {
            return Err(ArgumentValidationError::BlankQuestion);
        }
        if model.trim().is_empty() {
            return Err(ArgumentValidationError::BlankModel);
        }
        if reasoning_effort.trim().is_empty() {
            return Err(ArgumentValidationError::BlankReasoningEffort);
        }

        Ok(Self {
            question,
            model,
            reasoning_effort,
            search_context_size,
        })
    }

    pub fn question(&self) -> &str {
        &self.question
    }

    pub fn model(&self) -> &str {
        &self.model
    }

    pub fn reasoning_effort(&self) -> &str {
        &self.reasoning_effort
    }

    pub fn search_context_size(&self) -> SearchContextSize {
        self.search_context_size
    }
}

/// A source observed while an interim message is produced.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchSourceCandidate {
    pub title: String,
    pub url: String,
    pub provenance: String,
}

/// A source retained with the final answer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchSource {
    pub title: String,
    pub url: String,
    pub provenance: String,
}

/// Nonterminal output from an in-progress search.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchMessage {
    pub text: String,
    pub source_candidates: Vec<WebSearchSourceCandidate>,
}

/// Provider token accounting. `None` means that the count is unknown.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebSearchUsage {
    pub input_tokens: Option<u64>,
    pub output_tokens: Option<u64>,
    pub total_tokens: Option<u64>,
}

/// Cost evidence for a completed search.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WebSearchCost {
    Unknown,
    Estimate {
        decimal_amount: String,
        currency: String,
        pricing_revision: String,
    },
}

/// Elapsed time for one named phase.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchPhaseTiming {
    pub phase: String,
    pub duration: Duration,
}

/// Phase observations and aggregate elapsed time.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchTiming {
    pub phases: Vec<WebSearchPhaseTiming>,
    pub aggregate: Duration,
}

/// Stable terminal result recorded in provenance.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebSearchOutcome {
    Success,
    Failure,
}

/// Stable, safe failure classification.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebSearchFailureCategory {
    InvalidRequest,
    Authentication,
    Provider,
    Timeout,
    Unavailable,
    Internal,
}

/// Whether a failed operation could have affected the provider.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebSearchProviderEffect {
    None,
    Possible,
}

/// Evidence tying terminal values to the request and backend execution.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchProvenance {
    pub backend: String,
    pub auth: String,
    pub requested_model: String,
    pub requested_reasoning_effort: String,
    pub requested_search_context_size: SearchContextSize,
    pub ephemeral: bool,
    pub resumable: bool,
    pub outcome: WebSearchOutcome,
}

/// A completed search that produced a final answer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchSuccess {
    pub answer: String,
    pub sources: Vec<WebSearchSource>,
    pub usage: WebSearchUsage,
    pub cost: WebSearchCost,
    pub timing: WebSearchTiming,
    pub provenance: WebSearchProvenance,
}

/// A completed search that did not produce a final answer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebSearchFailure {
    pub safe_detail: String,
    pub category: WebSearchFailureCategory,
    pub provider_effect: WebSearchProviderEffect,
    pub sources: Vec<WebSearchSource>,
    pub usage: WebSearchUsage,
    pub cost: WebSearchCost,
    pub timing: WebSearchTiming,
    pub provenance: WebSearchProvenance,
}

/// Terminal search result.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WebSearchFinished {
    Success(WebSearchSuccess),
    Failure(WebSearchFailure),
}

/// Stream value emitted by one search.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WebSearchEvent {
    Message(WebSearchMessage),
    Finished(Box<WebSearchFinished>),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn request_rejects_blank_fields_and_preserves_accepted_strings() {
        assert_eq!(
            WebSearchRequest::new(" \t", "model", "effort", SearchContextSize::Low),
            Err(ArgumentValidationError::BlankQuestion)
        );
        assert_eq!(
            WebSearchRequest::new("question", "\n", "effort", SearchContextSize::Low),
            Err(ArgumentValidationError::BlankModel)
        );
        assert_eq!(
            WebSearchRequest::new("question", "model", "  ", SearchContextSize::Low),
            Err(ArgumentValidationError::BlankReasoningEffort)
        );

        let request = WebSearchRequest::new(
            "  keep this question  ",
            " model-name ",
            " effort-name ",
            SearchContextSize::High,
        )
        .expect("nonblank fields are valid");
        assert_eq!(request.question(), "  keep this question  ");
        assert_eq!(request.model(), " model-name ");
        assert_eq!(request.reasoning_effort(), " effort-name ");
        assert_eq!(request.search_context_size(), SearchContextSize::High);
    }

    #[test]
    fn context_size_has_exact_spellings() {
        for (text, value) in [
            ("low", SearchContextSize::Low),
            ("medium", SearchContextSize::Medium),
            ("high", SearchContextSize::High),
        ] {
            assert_eq!(text.parse::<SearchContextSize>(), Ok(value));
            assert_eq!(value.to_string(), text);
        }
        for invalid in ["Low", "HIGH", " high", "high ", ""] {
            assert_eq!(
                invalid.parse::<SearchContextSize>(),
                Err(ArgumentValidationError::InvalidSearchContextSize)
            );
        }
    }

    #[test]
    fn unknown_accounting_is_explicit() {
        let usage = WebSearchUsage {
            input_tokens: None,
            output_tokens: None,
            total_tokens: None,
        };
        assert_eq!(usage.input_tokens, None);
        assert_eq!(usage.output_tokens, None);
        assert_eq!(usage.total_tokens, None);
        assert_eq!(WebSearchCost::Unknown, WebSearchCost::Unknown);
    }

    #[test]
    fn message_is_interim_and_boxed_success_carries_final_answer() {
        let message = WebSearchEvent::Message(WebSearchMessage {
            text: "searching".to_owned(),
            source_candidates: vec![WebSearchSourceCandidate {
                title: "candidate".to_owned(),
                url: "https://example.test/candidate".to_owned(),
                provenance: "backend-result".to_owned(),
            }],
        });
        assert!(matches!(message, WebSearchEvent::Message(_)));

        let success = WebSearchSuccess {
            answer: "final answer".to_owned(),
            sources: vec![WebSearchSource {
                title: "source".to_owned(),
                url: "https://example.test/source".to_owned(),
                provenance: "final-selection".to_owned(),
            }],
            usage: WebSearchUsage {
                input_tokens: Some(7),
                output_tokens: Some(3),
                total_tokens: Some(10),
            },
            cost: WebSearchCost::Unknown,
            timing: WebSearchTiming {
                phases: vec![WebSearchPhaseTiming {
                    phase: "search".to_owned(),
                    duration: Duration::from_millis(20),
                }],
                aggregate: Duration::from_millis(25),
            },
            provenance: WebSearchProvenance {
                backend: "codex".to_owned(),
                auth: "configured".to_owned(),
                requested_model: "model-name".to_owned(),
                requested_reasoning_effort: "effort-name".to_owned(),
                requested_search_context_size: SearchContextSize::Medium,
                ephemeral: true,
                resumable: false,
                outcome: WebSearchOutcome::Success,
            },
        };
        let finished = WebSearchEvent::Finished(Box::new(WebSearchFinished::Success(success)));

        match finished {
            WebSearchEvent::Finished(result) => match *result {
                WebSearchFinished::Success(value) => {
                    assert_eq!(value.answer, "final answer");
                    assert_eq!(value.sources.len(), 1);
                }
                WebSearchFinished::Failure(_) => panic!("expected success"),
            },
            WebSearchEvent::Message(_) => panic!("expected terminal event"),
        }
    }
}