mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
wip: #6784 force_http1 stream-open fix (unverified, local only)
This commit is contained in:
1 parent
61ecea887f
commit
d0cc92f2a6
6 files changed
+73
-20
No files matched your search
@@ -353,6 +353,9 @@ pub struct CodewhaleClient {
|
||||
/// Bounded wait for SSE response headers, resolved once from
|
||||
/// `[tui].stream_open_timeout_secs` / `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`.
|
||||
pub(super) stream_open_timeout: Duration,
|
||||
/// HTTP/1.1 pin resolved once from `Config::force_http1` (#6700); the
|
||||
/// single source every client builder and stream open reads.
|
||||
pub(super) force_http1: bool,
|
||||
}
|
||||
|
||||
const CONNECTION_FAILURE_THRESHOLD: u32 = 2;
|
||||
@@ -642,6 +645,7 @@ impl Clone for CodewhaleClient {
|
||||
reasoning_stream_style: self.reasoning_stream_style.clone(),
|
||||
stream_idle_timeout: self.stream_idle_timeout,
|
||||
stream_open_timeout: self.stream_open_timeout,
|
||||
force_http1: self.force_http1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1389,9 +1393,9 @@ fn build_speech_synthesis_body(
|
||||
// === CodewhaleClient ===
|
||||
|
||||
/// Returns true when CODEWHALE_FORCE_HTTP1 (legacy alias: DEEPSEEK_FORCE_HTTP1)
|
||||
/// is set to a truthy value (`1`, `true`, `yes`, `on`, case-insensitive). Used
|
||||
/// by `build_http_client` to opt out of HTTP/2 entirely when a provider's edge
|
||||
/// mishandles long-lived H2 streams (#103). Anything else (unset, `0`,
|
||||
/// is set to a truthy value (`1`, `true`, `yes`, `on`, case-insensitive). Read
|
||||
/// only by `Config::force_http1`, which ORs it with `[tui].force_http1`; every
|
||||
/// client builder and stream open takes that resolved value (#103, #6700). Anything else (unset, `0`,
|
||||
/// `false`, ...) leaves HTTP/2 on.
|
||||
pub(crate) fn force_http1_from_env() -> bool {
|
||||
std::env::var("CODEWHALE_FORCE_HTTP1")
|
||||
@@ -1668,7 +1672,7 @@ impl CodewhaleClient {
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
// Always keep an HTTP/1.1 twin for automatic stream-header fallback
|
||||
// when H2 stalls. When CODEWHALE_FORCE_HTTP1 is set, both clients are
|
||||
// when H2 stalls. When `force_http1` is pinned, both clients are
|
||||
// HTTP/1.1 and the fallback is a no-op retry path.
|
||||
let http1_client = Self::http_client_builder_with_auth_mode(
|
||||
&api_key,
|
||||
@@ -1722,6 +1726,7 @@ impl CodewhaleClient {
|
||||
reasoning_stream_style,
|
||||
stream_idle_timeout,
|
||||
stream_open_timeout,
|
||||
force_http1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2048,7 +2053,7 @@ impl CodewhaleClient {
|
||||
.http2_keep_alive_interval(Some(Duration::from_secs(15)))
|
||||
.http2_keep_alive_timeout(Duration::from_secs(20))
|
||||
.min_tls_version(reqwest::tls::Version::TLS_1_2);
|
||||
if force_http1 || force_http1_from_env() {
|
||||
if force_http1 {
|
||||
builder = builder.http1_only();
|
||||
}
|
||||
if let Ok(cert_path) = std::env::var("SSL_CERT_FILE")
|
||||
@@ -13774,6 +13779,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// #6700: `[tui].force_http1` alone (no env var) must reach the stream-open
|
||||
/// policy every adapter uses, not just the client builders.
|
||||
#[test]
|
||||
fn tui_force_http1_key_pins_stream_open_policy() {
|
||||
let _lock = FORCE_HTTP1_ENV_LOCK.lock().unwrap();
|
||||
let _env = crate::test_support::lock_test_env();
|
||||
let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_FORCE_HTTP1");
|
||||
let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_FORCE_HTTP1");
|
||||
let client_for = |force_http1: Option<bool>| {
|
||||
CodewhaleClient::new(&Config {
|
||||
provider: Some("zai".to_string()),
|
||||
providers: Some(ProvidersConfig {
|
||||
zai: ProviderConfig {
|
||||
api_key: Some("zai-force-http1-key".to_string()),
|
||||
..ProviderConfig::default()
|
||||
},
|
||||
..ProvidersConfig::default()
|
||||
}),
|
||||
tui: Some(crate::config::TuiConfig {
|
||||
force_http1,
|
||||
..crate::config::TuiConfig::default()
|
||||
}),
|
||||
..Config::default()
|
||||
})
|
||||
.expect("client builds")
|
||||
};
|
||||
|
||||
let pinned = client_for(Some(true));
|
||||
assert!(pinned.force_http1);
|
||||
assert_eq!(
|
||||
pinned.stream_open_request().policy,
|
||||
stream_entry::StreamHttpPolicy::Http1Only
|
||||
);
|
||||
|
||||
let unpinned = client_for(None);
|
||||
assert!(!unpinned.force_http1);
|
||||
assert_eq!(
|
||||
unpinned.stream_open_request().policy,
|
||||
stream_entry::StreamHttpPolicy::DualWithH1Fallback
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redact_url_for_display_masks_userinfo_and_sensitive_query_values() {
|
||||
let redacted = redact_url_for_display(
|
||||
|
||||
@@ -256,10 +256,7 @@ impl CodewhaleClient {
|
||||
body: &Value,
|
||||
) -> Result<reqwest::Response> {
|
||||
let url = self.messages_transport_url(url);
|
||||
let open_req = super::stream_entry::StreamOpenRequest::new(
|
||||
self.stream_open_timeout,
|
||||
self.stream_idle_timeout,
|
||||
);
|
||||
let open_req = self.stream_open_request();
|
||||
let opened = super::stream_entry::open_sse_response(&open_req, |policy| {
|
||||
let url = url.clone();
|
||||
async move {
|
||||
|
||||
@@ -1330,10 +1330,7 @@ impl CodewhaleClient {
|
||||
url: &str,
|
||||
body: &Value,
|
||||
) -> Result<(reqwest::Response, Duration)> {
|
||||
let open_req = super::stream_entry::StreamOpenRequest::new(
|
||||
self.stream_open_timeout,
|
||||
self.stream_idle_timeout,
|
||||
);
|
||||
let open_req = self.stream_open_request();
|
||||
let idle_timeout = open_req.idle_timeout;
|
||||
let response = super::stream_entry::open_sse_response(&open_req, |policy| async move {
|
||||
match policy {
|
||||
|
||||
@@ -180,10 +180,7 @@ impl CodewhaleClient {
|
||||
let account_id = self.codex_account_id.clone();
|
||||
let request_body =
|
||||
serde_json::to_vec(&body).context("Failed to serialize Responses API request body")?;
|
||||
let open_req = super::stream_entry::StreamOpenRequest::new(
|
||||
self.stream_open_timeout,
|
||||
self.stream_idle_timeout,
|
||||
);
|
||||
let open_req = self.stream_open_request();
|
||||
let response = super::stream_entry::open_sse_response(&open_req, |policy| {
|
||||
let url = url.clone();
|
||||
let account_id = account_id.clone();
|
||||
|
||||
@@ -159,10 +159,12 @@ pub struct StreamOpenRequest {
|
||||
}
|
||||
|
||||
impl StreamOpenRequest {
|
||||
/// `force_http1` is the client's resolved pin (`Config::force_http1`:
|
||||
/// `[tui].force_http1` or `CODEWHALE_FORCE_HTTP1`), never re-read here.
|
||||
#[must_use]
|
||||
pub fn new(open_timeout: Duration, idle_timeout: Duration) -> Self {
|
||||
pub fn new(force_http1: bool, open_timeout: Duration, idle_timeout: Duration) -> Self {
|
||||
Self {
|
||||
policy: if super::force_http1_from_env() {
|
||||
policy: if force_http1 {
|
||||
StreamHttpPolicy::Http1Only
|
||||
} else {
|
||||
StreamHttpPolicy::DualWithH1Fallback
|
||||
@@ -180,6 +182,19 @@ impl StreamOpenRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl super::CodewhaleClient {
|
||||
/// The open request every streaming adapter starts from, carrying this
|
||||
/// client's resolved HTTP/1.1 pin and timeouts (#6700).
|
||||
#[must_use]
|
||||
pub(super) fn stream_open_request(&self) -> StreamOpenRequest {
|
||||
StreamOpenRequest::new(
|
||||
self.force_http1,
|
||||
self.stream_open_timeout,
|
||||
self.stream_idle_timeout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the HTTP client for a stream open attempt.
|
||||
#[must_use]
|
||||
pub fn client_for_policy<'a>(
|
||||
|
||||
@@ -207,7 +207,7 @@ impl CodewhaleClient {
|
||||
&base_url,
|
||||
WireFormat::ChatCompletions,
|
||||
false,
|
||||
false,
|
||||
client.force_http1,
|
||||
config.connect_timeout(),
|
||||
)?
|
||||
.build()?;
|
||||
|
||||
Reference in new issue
Block a user