Let model transports own attempt deadlines

This commit is contained in:
Meta Vens 2026-08-22 20:16:04 +02:00
commit 33a761bf4f
2 changed files with 55 additions and 1 deletions

View file

@ -117,13 +117,16 @@ pub trait ModelPort: Send + Sync {
})
}
fn provider(&self) -> &'static str;
fn attempt_timeout(&self, _request: &ModelStageRequest) -> std::time::Duration {
std::time::Duration::from_secs(45)
}
}
pub async fn run_validated_stage(
port: &dyn ModelPort,
request: &ModelStageRequest,
) -> Result<ModelStageOutput> {
run_validated_stage_with_timeout(port, request, std::time::Duration::from_secs(45)).await
run_validated_stage_with_timeout(port, request, port.attempt_timeout(request)).await
}
pub async fn run_validated_stage_with_timeout(
@ -304,6 +307,7 @@ mod tests {
}
struct NeverReturns;
struct ShortDeadlineNeverReturns;
struct CorrectionAware {
calls: AtomicUsize,
}
@ -320,6 +324,21 @@ mod tests {
}
}
#[async_trait]
impl ModelPort for ShortDeadlineNeverReturns {
async fn run(&self, _: &ModelStageRequest) -> Result<String> {
std::future::pending().await
}
fn provider(&self) -> &'static str {
"slow-fixture"
}
fn attempt_timeout(&self, _: &ModelStageRequest) -> std::time::Duration {
std::time::Duration::from_millis(5)
}
}
#[async_trait]
impl ModelPort for InvalidThenValid {
async fn run(&self, _: &ModelStageRequest) -> Result<String> {
@ -446,6 +465,24 @@ mod tests {
assert!(error.to_string().contains("timed out"));
}
#[tokio::test]
async fn validated_stage_uses_the_transport_owned_attempt_deadline() {
let request = ModelStageRequest {
stage: "transport-timeout-stage".into(),
model: "fixture".into(),
snapshot_binding: "campaign:one:revision:4".into(),
lived_stream: "fixture".into(),
output_schema: None,
source_receipt_ids: vec![],
temperature: None,
max_output_tokens: None,
};
let error = run_validated_stage(&ShortDeadlineNeverReturns, &request)
.await
.unwrap_err();
assert!(error.to_string().contains("timed out"));
}
#[tokio::test]
async fn receipt_hashes_the_physical_provider_model_not_the_logical_class() {
let request = ModelStageRequest {
@ -563,6 +600,11 @@ mod tests {
assert_eq!(body["plugins"][0]["id"], "response-healing");
assert_eq!(body["temperature"].as_f64(), Some(0.0));
assert!(body.get("thinking").is_none());
let port = OpenRouterPort::new("test-key".into(), "stealth/ox-alpha", "stealth/ox-alpha");
assert_eq!(
port.attempt_timeout(&request),
std::time::Duration::from_secs(120)
);
let mut narrative = request.clone();
narrative.output_schema = None;
@ -882,6 +924,10 @@ impl ModelPort for OpenRouterPort {
fn provider(&self) -> &'static str {
"openrouter"
}
fn attempt_timeout(&self, _request: &ModelStageRequest) -> std::time::Duration {
std::time::Duration::from_secs(120)
}
}
fn resolve_model(request: &ModelStageRequest, fast_model: &str, capable_model: &str) -> String {

View file

@ -733,6 +733,14 @@ Structured stages request JSON Output and then validate locally. Empty or
malformed output gets one retry against the same snapshot. A second failure,
timeout, retrieval outage, or stale result produces no mutation.
The provider port owns its per-attempt transport deadline because queueing and
generation throughput are properties of the selected transport, not fictional
state or stage semantics. The default and DeepSeek deadline is 45 seconds; the
OpenRouter test port allows 120 seconds so a congested route remains slow
instead of being misclassified as invalid. Stage orchestration still owns the
same-snapshot retry and local validation, and an expired attempt still produces
no receipt or mutation.
Prompt projection follows the authority boundary instead of shipping one large
state packet to every stage: