mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix(windsurf): 修复 native 工具流式回程
This commit is contained in:
@@ -573,6 +573,10 @@ fn build_windsurf_stream_frame_stream(
|
|||||||
.filter(|tool_call| !streamed_native_call_ids.contains(&tool_call.id))
|
.filter(|tool_call| !streamed_native_call_ids.contains(&tool_call.id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
tool_calls.extend(parsed_tool_calls.tool_calls);
|
tool_calls.extend(parsed_tool_calls.tool_calls);
|
||||||
|
let finish_reason = windsurf_stream_finish_reason(
|
||||||
|
!streamed_native_call_ids.is_empty(),
|
||||||
|
!tool_calls.is_empty(),
|
||||||
|
);
|
||||||
if !tool_calls.is_empty() {
|
if !tool_calls.is_empty() {
|
||||||
for frame in sse_tool_call_frames_from_index(
|
for frame in sse_tool_call_frames_from_index(
|
||||||
&prepared.request_id,
|
&prepared.request_id,
|
||||||
@@ -595,9 +599,10 @@ fn build_windsurf_stream_frame_stream(
|
|||||||
&content,
|
&content,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let _ = tx.send(encode_stream_frame_ndjson(&sse_finish_frame(
|
let _ = tx.send(encode_stream_frame_ndjson(&sse_finish_frame_with_reason(
|
||||||
&prepared.request_id,
|
&prepared.request_id,
|
||||||
&prepared.model,
|
&prepared.model,
|
||||||
|
finish_reason,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let _ = tx.send(encode_stream_frame_ndjson(&raw_sse_data_frame(b"data: [DONE]\n\n")));
|
let _ = tx.send(encode_stream_frame_ndjson(&raw_sse_data_frame(b"data: [DONE]\n\n")));
|
||||||
@@ -616,7 +621,7 @@ fn build_windsurf_stream_frame_stream(
|
|||||||
windsurf_terminal_summary(
|
windsurf_terminal_summary(
|
||||||
poll_result.usage,
|
poll_result.usage,
|
||||||
Some(prepared.model.as_str()),
|
Some(prepared.model.as_str()),
|
||||||
Some(if tool_calls.is_empty() { "stop" } else { "tool_calls" }),
|
Some(finish_reason),
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -1092,8 +1097,7 @@ where
|
|||||||
step.response_text.as_str()
|
step.response_text.as_str()
|
||||||
};
|
};
|
||||||
let previous = yielded_by_step.get(&index).copied().unwrap_or_default();
|
let previous = yielded_by_step.get(&index).copied().unwrap_or_default();
|
||||||
if live_text.len() > previous {
|
if let Some(delta) = windsurf_text_delta_from_cursor(live_text, previous) {
|
||||||
let delta = live_text[previous..].to_string();
|
|
||||||
yielded_by_step.insert(index, live_text.len());
|
yielded_by_step.insert(index, live_text.len());
|
||||||
grew = true;
|
grew = true;
|
||||||
on_delta(delta)?;
|
on_delta(delta)?;
|
||||||
@@ -1104,8 +1108,7 @@ where
|
|||||||
&& step.modified_text.starts_with(live_text)
|
&& step.modified_text.starts_with(live_text)
|
||||||
{
|
{
|
||||||
let cursor = yielded_by_step.get(&index).copied().unwrap_or_default();
|
let cursor = yielded_by_step.get(&index).copied().unwrap_or_default();
|
||||||
if step.modified_text.len() > cursor {
|
if let Some(delta) = windsurf_text_delta_from_cursor(&step.modified_text, cursor) {
|
||||||
let delta = step.modified_text[cursor..].to_string();
|
|
||||||
yielded_by_step.insert(index, step.modified_text.len());
|
yielded_by_step.insert(index, step.modified_text.len());
|
||||||
grew = true;
|
grew = true;
|
||||||
on_delta(delta)?;
|
on_delta(delta)?;
|
||||||
@@ -1115,6 +1118,18 @@ where
|
|||||||
Ok(grew)
|
Ok(grew)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn windsurf_text_delta_from_cursor(text: &str, cursor: usize) -> Option<String> {
|
||||||
|
if text.len() <= cursor {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cursor = if text.is_char_boundary(cursor) {
|
||||||
|
cursor
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
Some(text[cursor..].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
async fn ensure_windsurf_language_server(
|
async fn ensure_windsurf_language_server(
|
||||||
plan: &ExecutionPlan,
|
plan: &ExecutionPlan,
|
||||||
) -> Result<LsHandle, ExecutionRuntimeTransportError> {
|
) -> Result<LsHandle, ExecutionRuntimeTransportError> {
|
||||||
@@ -3111,8 +3126,15 @@ fn sse_data_frame(request_id: &str, model: &str, delta: &str) -> StreamFrame {
|
|||||||
raw_sse_data_frame(&body)
|
raw_sse_data_frame(&body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sse_finish_frame(request_id: &str, model: &str) -> StreamFrame {
|
fn windsurf_stream_finish_reason(
|
||||||
sse_finish_frame_with_reason(request_id, model, "stop")
|
streamed_native_tool_call: bool,
|
||||||
|
pending_tool_call: bool,
|
||||||
|
) -> &'static str {
|
||||||
|
if streamed_native_tool_call || pending_tool_call {
|
||||||
|
"tool_calls"
|
||||||
|
} else {
|
||||||
|
"stop"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sse_finish_frame_with_reason(request_id: &str, model: &str, finish_reason: &str) -> StreamFrame {
|
fn sse_finish_frame_with_reason(request_id: &str, model: &str, finish_reason: &str) -> StreamFrame {
|
||||||
@@ -4429,6 +4451,19 @@ mod tests {
|
|||||||
assert!(text.contains(r#""arguments":"{\"query\":\"today tech\"}""#));
|
assert!(text.contains(r#""arguments":"{\"query\":\"today tech\"}""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_tool_stream_finish_reason_stays_tool_calls_after_live_delta() {
|
||||||
|
assert_eq!(
|
||||||
|
super::windsurf_stream_finish_reason(true, false),
|
||||||
|
"tool_calls"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::windsurf_stream_finish_reason(false, true),
|
||||||
|
"tool_calls"
|
||||||
|
);
|
||||||
|
assert_eq!(super::windsurf_stream_finish_reason(false, false), "stop");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windsurf_rate_limit_errors_classify_as_upstream_429() {
|
fn windsurf_rate_limit_errors_classify_as_upstream_429() {
|
||||||
let err = ExecutionRuntimeTransportError::UpstreamRequest(
|
let err = ExecutionRuntimeTransportError::UpstreamRequest(
|
||||||
@@ -4893,6 +4928,18 @@ mod tests {
|
|||||||
assert_eq!(yielded_by_step.get(&0), Some(&12));
|
assert_eq!(yielded_by_step.get(&0), Some(&12));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_delta_cursor_resets_on_non_char_boundary() {
|
||||||
|
assert_eq!(
|
||||||
|
super::windsurf_text_delta_from_cursor("aé", 2).as_deref(),
|
||||||
|
Some("aé")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::windsurf_text_delta_from_cursor("hello", 2).as_deref(),
|
||||||
|
Some("llo")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windsurf_warmup_treats_http_404_as_non_transport_error() {
|
fn windsurf_warmup_treats_http_404_as_non_transport_error() {
|
||||||
let err = ExecutionRuntimeTransportError::UpstreamRequest(
|
let err = ExecutionRuntimeTransportError::UpstreamRequest(
|
||||||
|
|||||||
@@ -2157,6 +2157,11 @@ fn decode_body_for_storage(body_base64: Option<&str>) -> Option<Value> {
|
|||||||
let bytes = base64::engine::general_purpose::STANDARD
|
let bytes = base64::engine::general_purpose::STANDARD
|
||||||
.decode(body_base64)
|
.decode(body_base64)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
if let Some(error_body) =
|
||||||
|
aether_ai_formats::api::extract_provider_private_stream_error_body(None, &bytes)
|
||||||
|
{
|
||||||
|
return Some(error_body);
|
||||||
|
}
|
||||||
if let Ok(json_body) = serde_json::from_slice::<Value>(&bytes) {
|
if let Ok(json_body) = serde_json::from_slice::<Value>(&bytes) {
|
||||||
return Some(json_body);
|
return Some(json_body);
|
||||||
}
|
}
|
||||||
@@ -3014,7 +3019,7 @@ mod tests {
|
|||||||
build_stream_terminal_usage_event, build_streaming_usage_record,
|
build_stream_terminal_usage_event, build_streaming_usage_record,
|
||||||
build_sync_terminal_usage_event, build_sync_terminal_usage_payload_seed,
|
build_sync_terminal_usage_event, build_sync_terminal_usage_payload_seed,
|
||||||
build_sync_terminal_usage_seed, build_terminal_usage_context_seed,
|
build_sync_terminal_usage_seed, build_terminal_usage_context_seed,
|
||||||
build_terminal_usage_event_from_seed, build_usage_event_data_seed,
|
build_terminal_usage_event_from_seed, build_usage_event_data_seed, decode_body_for_storage,
|
||||||
extract_token_counts_from_json, extract_token_counts_from_value, headers_to_json,
|
extract_token_counts_from_json, extract_token_counts_from_value, headers_to_json,
|
||||||
mask_header_value, mask_sensitive_body_fields, mask_sensitive_headers_in_json_value,
|
mask_header_value, mask_sensitive_body_fields, mask_sensitive_headers_in_json_value,
|
||||||
parse_sse_body_for_storage, resolve_error_message, trim_owned_non_empty_string,
|
parse_sse_body_for_storage, resolve_error_message, trim_owned_non_empty_string,
|
||||||
@@ -5403,6 +5408,27 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_body_for_storage_extracts_connect_json_error_frames() {
|
||||||
|
let payload = br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#;
|
||||||
|
let mut framed = Vec::new();
|
||||||
|
framed.push(2);
|
||||||
|
framed.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||||
|
framed.extend_from_slice(payload);
|
||||||
|
let body_base64 = base64::engine::general_purpose::STANDARD.encode(framed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
decode_body_for_storage(Some(body_base64.as_str())),
|
||||||
|
Some(json!({
|
||||||
|
"error": {
|
||||||
|
"code": "resource_exhausted",
|
||||||
|
"message": "quota exhausted",
|
||||||
|
"type": "resource_exhausted"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_sse_body_for_storage_handles_crlf_and_cr_line_endings() {
|
fn parse_sse_body_for_storage_handles_crlf_and_cr_line_endings() {
|
||||||
let sse_body = concat!(
|
let sse_body = concat!(
|
||||||
|
|||||||
Reference in New Issue
Block a user