Synced from monorepo
Synced from monorepo Changes: - grok-shell: send an expired external-provider credential to the sign-in flow, not a 401 loop - pager: clickable ▲ jumps to the top of the response being read - grok-shell: keep a large task log from making the completion message too long - Plan viewer scrollbar: widen grab zone to the border column; fix striped thumb in Terminal.app - pager: poll the tmux probe teardown grace instead of sleeping it - security: vendor-compat MCP kill switch is now actually enforced when reported as on - grok-shell: restore session eviction when a leader client disconnects - Bump rust-toolchain to 1.93.0 - workspace: lexical-normalize permission path patterns before glob matching - pager: reject garbage Enter in the /resume picker - pager: show Mermaid affordances in plan mode preview - pager: drop manage-account link from /session-info - workspace: auto-approve read-only git queries; defer write floor to auto classifier - Add free-form pattern editor to the "Always allow" command prompt - grok-shell: fix /btw caching - pager: Tab walks answers in the ask_user_question card - External-provider auth refresh: single 7s attempt instead of 3×5s - pager: don't resurrect finished background tasks as Running when completion arrives first - pager: report tmux truecolor clamping in Doctor - Fix plan viewer scrollbar click+drag hijacked by comment gutter - pager/shell: stop double Recap after the same last turn - sampler: preserve x-should-retry through stream collection - pager: clear plan-mode indicator immediately when the user approves a plan - pager: tmux does not re-read its config on reattach Source-Revision: 64c4de99cc822b25ce9c54ab5a4f372093d0885d
This commit is contained in:
parent
a422116582
commit
780d1388ff
323 changed files with 12258 additions and 7226 deletions
|
|
@ -714,7 +714,7 @@ fn synthesize_from_info(info: &SamplingErrorInfo) -> SamplingError {
|
|||
message: info.message.clone(),
|
||||
model_metadata: info.model_metadata.clone(),
|
||||
retry_after_secs: info.retry_after_secs,
|
||||
should_retry: None,
|
||||
should_retry: info.should_retry,
|
||||
}
|
||||
}
|
||||
SamplingErrorKind::EmptyResponse => {
|
||||
|
|
@ -825,6 +825,7 @@ fn handle_cancellation(
|
|||
message: "request cancelled".to_string(),
|
||||
is_retryable: false,
|
||||
retry_after_secs: None,
|
||||
should_retry: None,
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
|
|
@ -863,6 +864,7 @@ mod tests {
|
|||
message: "inference idle timeout after 240s with no chunks".to_string(),
|
||||
is_retryable: false,
|
||||
retry_after_secs: None,
|
||||
should_retry: None,
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
|
|
@ -884,6 +886,7 @@ mod tests {
|
|||
message: "boom".to_string(),
|
||||
is_retryable: true,
|
||||
retry_after_secs: None,
|
||||
should_retry: Some(false),
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
|
|
@ -893,10 +896,14 @@ mod tests {
|
|||
let err = synthesize_from_info(&info);
|
||||
match err {
|
||||
SamplingError::Api {
|
||||
status, message, ..
|
||||
status,
|
||||
message,
|
||||
should_retry,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(status.as_u16(), 500);
|
||||
assert_eq!(message, "boom");
|
||||
assert_eq!(should_retry, Some(false), "server veto must survive");
|
||||
}
|
||||
other => panic!("expected Api, got {other:?}"),
|
||||
}
|
||||
|
|
@ -910,6 +917,7 @@ mod tests {
|
|||
message: "slow down".to_string(),
|
||||
is_retryable: true,
|
||||
retry_after_secs: Some(7),
|
||||
should_retry: None,
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use xai_grok_sampling_types::{
|
|||
|
||||
use crate::attribution::bearer_tail_fragment;
|
||||
use crate::config::{AuthScheme, OriginClientInfo, SamplerConfig};
|
||||
use crate::events::SamplingErrorInfo;
|
||||
|
||||
// Re-export ApiBackend from the shared types crate for downstream callers.
|
||||
pub use xai_grok_sampling_types::ApiBackend;
|
||||
|
|
@ -2056,16 +2057,22 @@ impl SamplingClient {
|
|||
};
|
||||
result
|
||||
.map(|(response, _metrics)| response)
|
||||
.map_err(|info| SamplingError::Api {
|
||||
status: info
|
||||
.status_code
|
||||
.and_then(|c| reqwest::StatusCode::from_u16(c).ok())
|
||||
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
message: info.message,
|
||||
model_metadata: info.model_metadata,
|
||||
retry_after_secs: info.retry_after_secs,
|
||||
should_retry: None,
|
||||
})
|
||||
.map_err(stream_collect_error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild `Api` from stream-collected info, preserving status,
|
||||
/// `Retry-After`, and `x-should-retry` (kind is lost on this path).
|
||||
fn stream_collect_error(info: SamplingErrorInfo) -> SamplingError {
|
||||
SamplingError::Api {
|
||||
status: info
|
||||
.status_code
|
||||
.and_then(|c| reqwest::StatusCode::from_u16(c).ok())
|
||||
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
message: info.message,
|
||||
model_metadata: info.model_metadata,
|
||||
retry_after_secs: info.retry_after_secs,
|
||||
should_retry: info.should_retry,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2075,6 +2082,45 @@ mod tests {
|
|||
use indexmap::IndexMap;
|
||||
use xai_grok_sampling_types::types::ChatRequestMessage;
|
||||
|
||||
#[test]
|
||||
fn stream_collect_error_preserves_should_retry() {
|
||||
let info = SamplingErrorInfo {
|
||||
kind: crate::events::SamplingErrorKind::Api,
|
||||
status_code: Some(529),
|
||||
message: "Overloaded".into(),
|
||||
is_retryable: true,
|
||||
retry_after_secs: Some(3),
|
||||
should_retry: Some(false),
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
doom_loop_aborted_at_chunk: None,
|
||||
credential: xai_grok_sampling_types::SentCredential::Unknown,
|
||||
};
|
||||
// SamplingError is not PartialEq (it carries reqwest/serde errors),
|
||||
// so destructure once and compare all fields in a single assert.
|
||||
let SamplingError::Api {
|
||||
status,
|
||||
message,
|
||||
model_metadata,
|
||||
retry_after_secs,
|
||||
should_retry,
|
||||
} = stream_collect_error(info)
|
||||
else {
|
||||
panic!("expected Api");
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
status.as_u16(),
|
||||
message.as_str(),
|
||||
model_metadata.is_none(),
|
||||
retry_after_secs,
|
||||
should_retry,
|
||||
),
|
||||
(529, "Overloaded", true, Some(3), Some(false)),
|
||||
);
|
||||
}
|
||||
|
||||
fn minimal_config() -> SamplerConfig {
|
||||
SamplerConfig {
|
||||
api_key: Some("test-key".to_string()),
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@ pub struct SamplingErrorInfo {
|
|||
pub message: String,
|
||||
pub is_retryable: bool,
|
||||
pub retry_after_secs: Option<u64>,
|
||||
/// Parsed `x-should-retry` response header. `Some(false)` = the server
|
||||
/// says the failure is request-content-caused; never retry. `None` =
|
||||
/// header absent, or payload from an older peer.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub should_retry: Option<bool>,
|
||||
pub model_metadata: Option<ResponseModelMetadata>,
|
||||
/// Present only when `kind == EmptyResponse`. Carries the structured
|
||||
/// context from the L2 stream so downstream consumers can distinguish
|
||||
|
|
@ -279,6 +284,7 @@ impl From<&SamplingError> for SamplingErrorInfo {
|
|||
kind,
|
||||
status_code,
|
||||
message,
|
||||
should_retry: err.should_retry_header(),
|
||||
is_retryable,
|
||||
retry_after_secs,
|
||||
model_metadata,
|
||||
|
|
@ -295,6 +301,26 @@ mod tests {
|
|||
use super::*;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[test]
|
||||
fn from_sampling_error_carries_should_retry_header() {
|
||||
let err = SamplingError::Api {
|
||||
status: StatusCode::from_u16(529).expect("valid status"),
|
||||
message: "Overloaded".into(),
|
||||
model_metadata: None,
|
||||
retry_after_secs: None,
|
||||
should_retry: Some(false),
|
||||
};
|
||||
let info = SamplingErrorInfo::from(&err);
|
||||
assert_eq!(info.should_retry, Some(false));
|
||||
|
||||
// Non-Api variants have no header — stays None.
|
||||
let stream_err = SamplingError::StreamError {
|
||||
error_type: "overloaded_error".into(),
|
||||
message: "Overloaded".into(),
|
||||
};
|
||||
assert_eq!(SamplingErrorInfo::from(&stream_err).should_retry, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_variant_classified_as_auth() {
|
||||
let err = SamplingError::auth_unknown("bad token");
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ pub async fn collect_response(
|
|||
message: "stream ended without Completed or Failed".to_string(),
|
||||
is_retryable: false,
|
||||
retry_after_secs: None,
|
||||
should_retry: None,
|
||||
model_metadata: None,
|
||||
empty_response_context: None,
|
||||
doom_loop_triggers: None,
|
||||
|
|
|
|||
Loading…
Reference in a new issue