Let typed worker budget own provider timeout
This commit is contained in:
parent
c65259e51c
commit
08958b07b7
4 changed files with 50 additions and 3 deletions
|
|
@ -233,19 +233,26 @@ impl EpiphanyCodexOpenAiTransport {
|
|||
pub struct EpiphanyOpenRouterTransport {
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl EpiphanyOpenRouterTransport {
|
||||
pub fn new(api_key: impl Into<String>) -> Result<Self> {
|
||||
pub fn new(api_key: impl Into<String>, request_timeout: Option<Duration>) -> Result<Self> {
|
||||
let api_key = api_key.into();
|
||||
if api_key.trim().is_empty() || api_key.trim() != api_key {
|
||||
return Err(anyhow::anyhow!(
|
||||
"OpenRouter API key must be a non-empty trimmed credential"
|
||||
));
|
||||
}
|
||||
if request_timeout.is_some_and(|timeout| timeout.is_zero()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"OpenRouter request timeout must be positive when present"
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
api_key,
|
||||
base_url: OPENROUTER_API_BASE_URL.to_string(),
|
||||
request_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +277,7 @@ impl EpiphanyOpenRouterTransport {
|
|||
outbound
|
||||
.headers
|
||||
.insert(http::header::AUTHORIZATION, authorization);
|
||||
outbound.timeout = Some(Duration::from_secs(90));
|
||||
outbound.timeout = self.request_timeout;
|
||||
let transport = ReqwestTransport::new(build_reqwest_client());
|
||||
let response = transport
|
||||
.execute(outbound)
|
||||
|
|
@ -1551,6 +1558,16 @@ mod tests {
|
|||
assert!(!status.codex_transport_attached);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openrouter_whole_request_timeout_is_explicit() -> Result<()> {
|
||||
let bounded = EpiphanyOpenRouterTransport::new("test-key", Some(Duration::from_secs(90)))?;
|
||||
assert_eq!(bounded.request_timeout, Some(Duration::from_secs(90)));
|
||||
let outer_budget_owned = EpiphanyOpenRouterTransport::new("test-key", None)?;
|
||||
assert_eq!(outer_budget_owned.request_timeout, None);
|
||||
assert!(EpiphanyOpenRouterTransport::new("test-key", Some(Duration::ZERO)).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openrouter_terminal_tool_owns_typed_output_instead_of_response_format() {
|
||||
let mut request = openrouter_request(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use epiphany_openai_adapter::EpiphanyOpenAiModelReceipt;
|
|||
use epiphany_openai_adapter::EpiphanyOpenAiModelRequest;
|
||||
use epiphany_openai_adapter::EpiphanyOpenAiStreamEvent;
|
||||
use epiphany_openai_adapter::EpiphanyOpenAiStreamPayload;
|
||||
use epiphany_openai_runtime::DEFAULT_PROVIDER_REQUEST_TIMEOUT;
|
||||
use epiphany_openai_runtime::EpiphanyOpenAiRuntimeOptions;
|
||||
use epiphany_openai_runtime::EpiphanyWorkerRuntimeOptions;
|
||||
use epiphany_openai_runtime::OPENAI_RUNTIME_ROLE;
|
||||
|
|
@ -916,6 +917,7 @@ async fn run_worker_launch_with_tool_continuation(
|
|||
options: RunWorkerCliOptions,
|
||||
pass_progress: WorkerPassProgress,
|
||||
) -> Result<serde_json::Value> {
|
||||
let request_timeout = provider_request_timeout(options.max_runtime_seconds);
|
||||
let tool_adapter_bin = options
|
||||
.tool_adapter_bin
|
||||
.clone()
|
||||
|
|
@ -964,6 +966,7 @@ async fn run_worker_launch_with_tool_continuation(
|
|||
coordinator_note: "Native worker runtime route; Codex is auth/model transport only."
|
||||
.to_string(),
|
||||
default_model: Some(options.model.clone()),
|
||||
request_timeout,
|
||||
};
|
||||
let mut current_request_id = initial_request.request_id.clone();
|
||||
pass_progress.note_model_request(¤t_request_id);
|
||||
|
|
@ -1379,6 +1382,15 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_worker_budget_is_the_only_whole_request_deadline() {
|
||||
assert_eq!(
|
||||
provider_request_timeout(None),
|
||||
Some(DEFAULT_PROVIDER_REQUEST_TIMEOUT)
|
||||
);
|
||||
assert_eq!(provider_request_timeout(Some(600)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_turn_json_ingress_accepts_published_native_object() -> Result<()> {
|
||||
let request = parse_model_turn_request_json(
|
||||
|
|
@ -1518,6 +1530,7 @@ mod tests {
|
|||
objective: format!("Run model execution for {worker_job_id}."),
|
||||
coordinator_note: "test".to_string(),
|
||||
default_model: Some("gpt-test".to_string()),
|
||||
request_timeout: Some(DEFAULT_PROVIDER_REQUEST_TIMEOUT),
|
||||
},
|
||||
&model_request,
|
||||
&[
|
||||
|
|
@ -2010,6 +2023,7 @@ async fn run_worker_options(
|
|||
if options.auto_tools {
|
||||
run_worker_launch_with_tool_continuation(options, pass_progress).await
|
||||
} else {
|
||||
let request_timeout = provider_request_timeout(options.max_runtime_seconds);
|
||||
Ok(serde_json::to_value(
|
||||
run_worker_launch_observed(
|
||||
EpiphanyWorkerRuntimeOptions {
|
||||
|
|
@ -2019,6 +2033,7 @@ async fn run_worker_options(
|
|||
provider: options.provider,
|
||||
job_id: options.job_id,
|
||||
model: options.model,
|
||||
request_timeout,
|
||||
},
|
||||
|request_id| pass_progress.note_model_request(request_id),
|
||||
)
|
||||
|
|
@ -2027,6 +2042,12 @@ async fn run_worker_options(
|
|||
}
|
||||
}
|
||||
|
||||
fn provider_request_timeout(max_runtime_seconds: Option<u64>) -> Option<Duration> {
|
||||
max_runtime_seconds
|
||||
.is_none()
|
||||
.then_some(DEFAULT_PROVIDER_REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
fn run_tool_adapter(
|
||||
tool_adapter_bin: &PathBuf,
|
||||
store_path: &PathBuf,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
|
@ -61,6 +62,7 @@ mod persona_executor;
|
|||
pub use persona_executor::*;
|
||||
|
||||
pub const OPENAI_RUNTIME_ROLE: &str = "openai-model-adapter";
|
||||
pub const DEFAULT_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
pub const OPENAI_RUNTIME_SOURCE: &str = "epiphany-openai-runtime";
|
||||
pub const DEFAULT_MODEL_PROVIDER: &str = "openai-codex";
|
||||
pub const OPENROUTER_MODEL_PROVIDER: &str = "openrouter";
|
||||
|
|
@ -101,6 +103,9 @@ pub struct EpiphanyOpenAiRuntimeOptions {
|
|||
pub objective: String,
|
||||
pub coordinator_note: String,
|
||||
pub default_model: Option<String>,
|
||||
/// Whole-request transport bound. Worker passes set this to `None` when
|
||||
/// their outer typed runtime budget is the sole deadline owner.
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
|
|
@ -126,6 +131,7 @@ pub struct EpiphanyWorkerRuntimeOptions {
|
|||
pub provider: String,
|
||||
pub job_id: String,
|
||||
pub model: String,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
|
|
@ -260,7 +266,7 @@ async fn run_openai_model_turn_bound(
|
|||
),
|
||||
},
|
||||
)?;
|
||||
let transport = EpiphanyOpenRouterTransport::new(credential)?;
|
||||
let transport = EpiphanyOpenRouterTransport::new(credential, options.request_timeout)?;
|
||||
let events = collect_transport_events(
|
||||
transport.collect_model_events_with_frame_observer(
|
||||
request.clone(),
|
||||
|
|
@ -462,6 +468,7 @@ where
|
|||
coordinator_note: "Native worker runtime route; Codex is auth/model transport only."
|
||||
.to_string(),
|
||||
default_model: Some(options.model),
|
||||
request_timeout: options.request_timeout,
|
||||
};
|
||||
on_model_request(&model_request.request_id);
|
||||
let openai_summary = run_model_turn(
|
||||
|
|
@ -1315,6 +1322,7 @@ pub fn default_options(
|
|||
coordinator_note: "Native OpenAI runtime route; Codex is auth/model transport only."
|
||||
.to_string(),
|
||||
default_model: Some(request.model.clone()),
|
||||
request_timeout: Some(DEFAULT_PROVIDER_REQUEST_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ impl PersonaModelRunner for NativePersonaModelRunner {
|
|||
coordinator_note: "Native Persona model executor; transport owns inference only."
|
||||
.to_string(),
|
||||
default_model: Some(self.model.clone()),
|
||||
request_timeout: Some(crate::DEFAULT_PROVIDER_REQUEST_TIMEOUT),
|
||||
};
|
||||
let summary = run_model_turn(&self.provider, options.clone(), request.clone()).await?;
|
||||
let output = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
|
||||
|
|
|
|||
Reference in a new issue