Ignore empty reasoning heartbeats
This commit is contained in:
@@ -732,6 +732,13 @@ impl ProviderRunCoordinator {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AgentEvent::ReasoningCompleted { text, signature }
|
||||||
|
if text.is_empty() && signature.is_none() =>
|
||||||
|
{
|
||||||
|
// Ignore provider heartbeat-shaped empty reasoning events. They are not
|
||||||
|
// meaningful model progress and must not keep an otherwise idle turn alive.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
AgentEvent::ReasoningCompleted { text, signature } => {
|
AgentEvent::ReasoningCompleted { text, signature } => {
|
||||||
if !self
|
if !self
|
||||||
.ensure_model_started_acknowledged(
|
.ensure_model_started_acknowledged(
|
||||||
|
|||||||
@@ -86,6 +86,45 @@ async fn rig_stream_maps_reasoning_text_usage_and_stop() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rig_stream_ignores_empty_reasoning_heartbeats() {
|
||||||
|
let http_client = MockStreamingClient {
|
||||||
|
sse_bytes: sse(&[
|
||||||
|
r#"{"id":"cmpl-1","model":"test-model","choices":[{"delta":{"reasoning_content":"","tool_calls":[]},"finish_reason":null}],"usage":null}"#,
|
||||||
|
r#"{"id":"cmpl-1","model":"test-model","choices":[{"delta":{"content":"done","tool_calls":[]},"finish_reason":"stop"}],"usage":null}"#,
|
||||||
|
"[DONE]",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
let client = openai::CompletionsClient::builder()
|
||||||
|
.api_key("test-key")
|
||||||
|
.base_url("http://localhost/v1")
|
||||||
|
.http_client(http_client)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let model = client.completion_model("test-model");
|
||||||
|
let (_, control) = galaxy_agent_core::turn_control();
|
||||||
|
|
||||||
|
let events = start_model_turn(model, text_request(), control, None, true)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!events.iter().any(|event| matches!(
|
||||||
|
event,
|
||||||
|
AgentEvent::ReasoningCompleted { text, signature }
|
||||||
|
if text.is_empty() && signature.is_none()
|
||||||
|
)));
|
||||||
|
assert!(
|
||||||
|
events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, AgentEvent::TextDelta { text } if text == "done"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn cancellation_before_stream_start_is_a_normal_stop() {
|
async fn cancellation_before_stream_start_is_a_normal_stop() {
|
||||||
let runtime = OpenAICompatibleRuntime::new(OpenAICompatibleRuntimeConfig {
|
let runtime = OpenAICompatibleRuntime::new(OpenAICompatibleRuntimeConfig {
|
||||||
|
|||||||
@@ -99,10 +99,13 @@ where
|
|||||||
}
|
}
|
||||||
Ok(StreamedAssistantContent::Reasoning(reasoning)) => {
|
Ok(StreamedAssistantContent::Reasoning(reasoning)) => {
|
||||||
let text = reasoning.display_text();
|
let text = reasoning.display_text();
|
||||||
yield Ok(AgentEvent::ReasoningCompleted {
|
let signature = reasoning.first_signature().map(str::to_string);
|
||||||
text,
|
// Some Responses providers send empty reasoning blocks as heartbeats.
|
||||||
signature: reasoning.first_signature().map(str::to_string),
|
// Do not expose them as model progress: doing so continually resets the
|
||||||
});
|
// coordinator's idle timeout and can leave a turn stuck forever.
|
||||||
|
if !text.is_empty() || signature.is_some() {
|
||||||
|
yield Ok(AgentEvent::ReasoningCompleted { text, signature });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => {
|
Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => {
|
||||||
if !reasoning.is_empty() {
|
if !reasoning.is_empty() {
|
||||||
@@ -236,10 +239,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rig_core::completion::AssistantContent::Reasoning(reasoning) => {
|
rig_core::completion::AssistantContent::Reasoning(reasoning) => {
|
||||||
yield Ok(AgentEvent::ReasoningCompleted {
|
let text = reasoning.display_text();
|
||||||
text: reasoning.display_text(),
|
let signature = reasoning.first_signature().map(str::to_string);
|
||||||
signature: reasoning.first_signature().map(str::to_string),
|
if !text.is_empty() || signature.is_some() {
|
||||||
});
|
yield Ok(AgentEvent::ReasoningCompleted { text, signature });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rig_core::completion::AssistantContent::ToolCall(tool_call) => {
|
rig_core::completion::AssistantContent::ToolCall(tool_call) => {
|
||||||
yield Ok(AgentEvent::Tool {
|
yield Ok(AgentEvent::Tool {
|
||||||
|
|||||||
Reference in New Issue
Block a user