Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
193 lines
6.5 KiB
Rust
193 lines
6.5 KiB
Rust
//! Deterministic-vs-transient failure classification for compaction
|
|
//! LLM calls.
|
|
//!
|
|
//! The *policy* lives here, shared across harnesses; per-harness error types
|
|
//! and their wrapping (e.g. kigi's `SamplingError` →
|
|
//! `CompactFailure(acp::Error)`) stay in thin host wrappers that delegate the
|
|
//! status/message decisions to these functions.
|
|
|
|
/// Whether a compaction-call failure is worth retrying.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum FailureKind {
|
|
/// Retrying the same payload will hit the same failure — the retry loop
|
|
/// should bail without sleeping or re-issuing.
|
|
Deterministic,
|
|
/// Failure may resolve on retry (network blips, 5xx, rate limits).
|
|
Transient,
|
|
}
|
|
|
|
impl FailureKind {
|
|
pub fn is_deterministic(self) -> bool {
|
|
matches!(self, Self::Deterministic)
|
|
}
|
|
}
|
|
|
|
/// True when an error message indicates a context-window overflow.
|
|
///
|
|
/// Backends report this inconsistently with no stable error code, so the match
|
|
/// is on message text. Re-sending the same payload always fails, so callers
|
|
/// must not retry.
|
|
pub fn is_context_length_error(message: &str) -> bool {
|
|
let m = message.to_ascii_lowercase();
|
|
m.contains("too long for this model")
|
|
|| m.contains("prompt is too long")
|
|
|| m.contains("maximum prompt length")
|
|
|| m.contains("maximum context length")
|
|
|| m.contains("context_length_exceeded")
|
|
}
|
|
|
|
/// Classify an HTTP API failure (status + message) for the compaction retry
|
|
/// loop.
|
|
///
|
|
/// A context-length overflow message is deterministic regardless of status,
|
|
/// because backends sometimes dress it as a synthesized 500.
|
|
pub fn classify_http_status(status: u16, message: &str) -> FailureKind {
|
|
if is_context_length_error(message)
|
|
|| ((400..500).contains(&status) && status != 408 && status != 429)
|
|
{
|
|
FailureKind::Deterministic
|
|
} else {
|
|
FailureKind::Transient
|
|
}
|
|
}
|
|
|
|
/// Classify a provider-style stream error event (`ResponseError` /
|
|
/// `ResponseFailed.error`) for the compaction retry loop.
|
|
///
|
|
/// `code` is the structured `code` field on the event: typically a numeric HTTP
|
|
/// status as a string, but some providers send error-type strings like
|
|
/// `"invalid_request_error"` instead, and that marker can also arrive buried in
|
|
/// `message` alone. It always means a schema violation, which re-sending the
|
|
/// same payload cannot fix.
|
|
pub fn classify_stream_event_error(code: Option<&str>, message: &str) -> FailureKind {
|
|
if matches!(code, Some("invalid_request_error")) || message.contains("invalid_request_error") {
|
|
return FailureKind::Deterministic;
|
|
}
|
|
|
|
if let Some(status_code) = code.and_then(|c| c.parse::<u16>().ok())
|
|
&& (400..500).contains(&status_code)
|
|
&& status_code != 408
|
|
&& status_code != 429
|
|
{
|
|
return FailureKind::Deterministic;
|
|
}
|
|
|
|
// Size overflow arrives here with no parseable code (`code="none"`); the
|
|
// message is the only signal that re-sending cannot help.
|
|
if is_context_length_error(message) {
|
|
return FailureKind::Deterministic;
|
|
}
|
|
|
|
FailureKind::Transient
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn det_status(status: u16) -> bool {
|
|
classify_http_status(status, "test").is_deterministic()
|
|
}
|
|
|
|
#[test]
|
|
fn http_4xx_is_deterministic_except_408_and_429() {
|
|
assert!(det_status(400));
|
|
assert!(det_status(401));
|
|
assert!(det_status(403));
|
|
assert!(det_status(404));
|
|
assert!(det_status(413));
|
|
assert!(!det_status(408));
|
|
assert!(!det_status(429));
|
|
assert!(!det_status(500));
|
|
assert!(!det_status(502));
|
|
assert!(!det_status(503));
|
|
}
|
|
|
|
#[test]
|
|
fn http_500_with_context_length_message_is_deterministic() {
|
|
// The sampler synthesizes status=500 from a streamed size overflow, so
|
|
// status alone reads transient; the message must still short-circuit.
|
|
assert!(
|
|
classify_http_status(
|
|
500,
|
|
"API error (status 500 Internal Server Error): \
|
|
The prompt is too long for this model's context window."
|
|
)
|
|
.is_deterministic()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_event_invalid_request_error_marker_is_deterministic() {
|
|
assert!(
|
|
classify_stream_event_error(
|
|
Some("invalid_request_error"),
|
|
"messages.27.content.1: ..."
|
|
)
|
|
.is_deterministic()
|
|
);
|
|
assert!(
|
|
classify_stream_event_error(
|
|
Some("400"),
|
|
"Provider returned invalid_request_error: messages.X..."
|
|
)
|
|
.is_deterministic()
|
|
);
|
|
assert!(
|
|
classify_stream_event_error(None, "messages.X.content.Y: invalid_request_error: ...")
|
|
.is_deterministic()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_event_numeric_codes_match_http_classification() {
|
|
let det = |c: &str| classify_stream_event_error(Some(c), "msg").is_deterministic();
|
|
assert!(det("400"));
|
|
assert!(det("401"));
|
|
assert!(det("403"));
|
|
assert!(det("404"));
|
|
assert!(!det("408"));
|
|
assert!(!det("429"));
|
|
assert!(!det("500"));
|
|
assert!(!det("503"));
|
|
}
|
|
|
|
#[test]
|
|
fn stream_event_unknown_code_defaults_to_transient() {
|
|
assert!(!classify_stream_event_error(None, "msg").is_deterministic());
|
|
assert!(!classify_stream_event_error(Some("error"), "msg").is_deterministic());
|
|
assert!(!classify_stream_event_error(Some("overloaded_error"), "msg").is_deterministic());
|
|
}
|
|
|
|
#[test]
|
|
fn stream_event_context_length_message_is_deterministic() {
|
|
assert!(
|
|
classify_stream_event_error(
|
|
None,
|
|
"The prompt is too long for this model's context window."
|
|
)
|
|
.is_deterministic()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn context_length_error_matches_known_messages() {
|
|
for msg in [
|
|
"The prompt is too long for this model's context window.",
|
|
"prompt is too long: 250000 tokens > 200000 maximum",
|
|
"exceeds the maximum prompt length",
|
|
"This model's maximum context length is 128000 tokens",
|
|
"error code: context_length_exceeded",
|
|
] {
|
|
assert!(is_context_length_error(msg), "should match: {msg}");
|
|
}
|
|
for msg in [
|
|
"internal server error",
|
|
"rate limited",
|
|
"connection reset by peer",
|
|
] {
|
|
assert!(!is_context_length_error(msg), "should not match: {msg}");
|
|
}
|
|
}
|
|
}
|