Everything is streaming (#7247)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Douwe Osinga
2026-02-17 19:53:38 +01:00
committed by GitHub
parent 65d3a1550f
commit 9a91fccfb5
42 changed files with 801 additions and 1718 deletions
+6 -18
View File
@@ -343,7 +343,8 @@ mod tests {
use goose::conversation::message::{Message, MessageContent};
use goose::model::ModelConfig;
use goose::providers::base::{
Provider, ProviderDef, ProviderMetadata, ProviderUsage, Usage,
stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata,
ProviderUsage, Usage,
};
use goose::providers::errors::ProviderError;
use goose::session::session_manager::SessionType;
@@ -385,13 +386,14 @@ mod tests {
#[async_trait]
impl Provider for MockToolProvider {
async fn complete(
async fn stream(
&self,
_model_config: &ModelConfig,
_session_id: &str,
_system_prompt: &str,
_messages: &[Message],
_tools: &[Tool],
) -> Result<(Message, ProviderUsage), ProviderError> {
) -> Result<MessageStream, ProviderError> {
let tool_call = CallToolRequestParams {
meta: None,
task: None,
@@ -405,21 +407,7 @@ mod tests {
Usage::new(Some(10), Some(5), Some(15)),
);
Ok((message, usage))
}
async fn complete_with_model(
&self,
session_id: Option<&str>,
_model_config: &ModelConfig,
system_prompt: &str,
messages: &[Message],
tools: &[Tool],
) -> anyhow::Result<(Message, ProviderUsage), ProviderError> {
// Test-only: coerce missing session_id to empty so complete() can be reused.
let session_id = session_id.unwrap_or("");
self.complete(session_id, system_prompt, messages, tools)
.await
Ok(stream_from_single_message(message, usage))
}
fn get_model_config(&self) -> ModelConfig {
+8 -5
View File
@@ -5,7 +5,10 @@ use goose::agents::{Agent, AgentEvent, SessionConfig};
use goose::conversation::message::{Message, MessageContent};
use goose::conversation::Conversation;
use goose::model::ModelConfig;
use goose::providers::base::{Provider, ProviderDef, ProviderMetadata, ProviderUsage, Usage};
use goose::providers::base::{
stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata,
ProviderUsage, Usage,
};
use goose::providers::errors::ProviderError;
use goose::session::session_manager::SessionType;
use goose::session::Session;
@@ -94,14 +97,14 @@ impl MockCompactionProvider {
#[async_trait]
impl Provider for MockCompactionProvider {
async fn complete_with_model(
async fn stream(
&self,
_session_id: Option<&str>,
_model_config: &ModelConfig,
_session_id: &str,
system_prompt: &str,
messages: &[Message],
_tools: &[Tool],
) -> Result<(Message, ProviderUsage), ProviderError> {
) -> Result<MessageStream, ProviderError> {
// Check if this is a compaction call (message contains "summarize")
let is_compaction = messages.iter().any(|msg| {
msg.content.iter().any(|content| {
@@ -163,7 +166,7 @@ impl Provider for MockCompactionProvider {
),
);
Ok((message, usage))
Ok(stream_from_single_message(message, usage))
}
fn get_model_config(&self) -> ModelConfig {
+10 -8
View File
@@ -19,7 +19,10 @@ use test_case::test_case;
use async_trait::async_trait;
use goose::conversation::message::Message;
use goose::providers::base::{Provider, ProviderDef, ProviderMetadata, ProviderUsage, Usage};
use goose::providers::base::{
stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata,
ProviderUsage, Usage,
};
use goose::providers::errors::ProviderError;
use once_cell::sync::Lazy;
use std::process::Command;
@@ -69,18 +72,17 @@ impl Provider for MockProvider {
"mock"
}
async fn complete_with_model(
async fn stream(
&self,
_session_id: Option<&str>,
_model_config: &ModelConfig,
_session_id: &str,
_system: &str,
_messages: &[Message],
_tools: &[Tool],
) -> anyhow::Result<(Message, ProviderUsage), ProviderError> {
Ok((
Message::assistant().with_text("\"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"),
ProviderUsage::new("mock".to_string(), Usage::default()),
))
) -> Result<MessageStream, ProviderError> {
let message = Message::assistant().with_text("\"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)");
let usage = ProviderUsage::new("mock".to_string(), Usage::default());
Ok(stream_from_single_message(message, usage))
}
fn get_model_config(&self) -> ModelConfig {
+27 -5
View File
@@ -127,9 +127,16 @@ impl ProviderTester {
.build();
let message = Message::user().with_text(prompt);
let model_config = self.provider.get_model_config();
let (response1, _) = self
.provider
.complete(session_id, &system, std::slice::from_ref(&message), &tools)
.complete(
&model_config,
session_id,
&system,
std::slice::from_ref(&message),
&tools,
)
.await?;
// Agentic CLI providers (claude-code, codex) call tools internally and
@@ -163,6 +170,7 @@ impl ProviderTester {
let (response2, _) = self
.provider
.complete(
&model_config,
session_id,
&system,
&[message, response1, tool_response],
@@ -174,10 +182,17 @@ impl ProviderTester {
async fn test_basic_response(&self, session_id: &str) -> Result<()> {
let message = Message::user().with_text("Just say hello!");
let model_config = self.provider.get_model_config();
let (response, _) = self
.provider
.complete(session_id, "You are a helpful assistant.", &[message], &[])
.complete(
&model_config,
session_id,
"You are a helpful assistant.",
&[message],
&[],
)
.await?;
assert!(
@@ -227,10 +242,17 @@ impl ProviderTester {
};
let messages = vec![Message::user().with_text(&large_message_content)];
let model_config = self.provider.get_model_config();
let result = self
.provider
.complete(session_id, "You are a helpful assistant.", &messages, &[])
.complete(
&model_config,
session_id,
"You are a helpful assistant.",
&messages,
&[],
)
.await;
println!("=== {}::context_length_exceeded_error ===", self.name);
@@ -288,9 +310,9 @@ impl ProviderTester {
let message = Message::user().with_text("Just say hello!");
let (response, _) = self
.provider
.complete_with_model(
Some(session_id),
.complete(
&alt_config,
session_id,
"You are a helpful assistant.",
&[message],
&[],
@@ -54,24 +54,33 @@ async fn setup_mock_server() -> (MockServer, HeaderCapture, Box<dyn Provider>) {
.and(path("/v1/chat/completions"))
.respond_with(move |req: &Request| {
capture_clone.capture_session_header(req);
ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "Hi there! How can I help you today?",
"role": "assistant"
// Return SSE streaming format
let sse_response = format!(
"data: {}\n\ndata: {}\n\ndata: [DONE]\n\n",
json!({
"choices": [{
"delta": {
"content": "Hi there! How can I help you today?",
"role": "assistant"
},
"index": 0
}],
"created": 1755133833,
"id": "chatcmpl-test",
"model": "gpt-5-nano"
}),
json!({
"choices": [],
"usage": {
"completion_tokens": 10,
"prompt_tokens": 8,
"total_tokens": 18
}
}],
"created": 1755133833,
"id": "chatcmpl-test",
"model": "gpt-5-nano",
"usage": {
"completion_tokens": 10,
"prompt_tokens": 8,
"total_tokens": 18
}
}))
})
);
ResponseTemplate::new(200)
.set_body_string(sse_response)
.insert_header("content-type", "text/event-stream")
})
.mount(&mock_server)
.await;
@@ -82,8 +91,15 @@ async fn setup_mock_server() -> (MockServer, HeaderCapture, Box<dyn Provider>) {
async fn make_request(provider: &dyn Provider, session_id: &str) {
let message = Message::user().with_text("test message");
let model_config = provider.get_model_config();
let _ = provider
.complete(session_id, "You are a helpful assistant.", &[message], &[])
.complete(
&model_config,
session_id,
"You are a helpful assistant.",
&[message],
&[],
)
.await
.unwrap();
}
+13
View File
@@ -27,9 +27,11 @@ mod tetrate_streaming_tests {
let provider = create_test_provider().await?;
let messages = vec![Message::user().with_text("Count from 1 to 5, one number at a time.")];
let model_config = provider.get_model_config();
let mut stream = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant that counts numbers.",
&messages,
@@ -99,9 +101,11 @@ mod tetrate_streaming_tests {
);
let messages = vec![Message::user().with_text("What's the weather in San Francisco?")];
let model_config = provider.get_model_config();
let mut stream = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant with access to weather information.",
&messages,
@@ -147,9 +151,11 @@ mod tetrate_streaming_tests {
// This might result in a very short or empty response
let messages = vec![Message::user().with_text("")];
let model_config = provider.get_model_config();
let mut stream = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant.",
&messages,
@@ -182,9 +188,11 @@ mod tetrate_streaming_tests {
let messages = vec![Message::user().with_text(
"Write a detailed 3-paragraph essay about the importance of streaming in modern APIs.",
)];
let model_config = provider.get_model_config();
let mut stream = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant that writes detailed essays.",
&messages,
@@ -243,9 +251,11 @@ mod tetrate_streaming_tests {
let provider = TetrateProvider::from_env(model_config).await?;
let messages = vec![Message::user().with_text("Hello")];
let model_config = provider.get_model_config();
let result = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant.",
&messages,
@@ -271,9 +281,11 @@ mod tetrate_streaming_tests {
// Create multiple concurrent streams
let messages1 = vec![Message::user().with_text("Say 'Stream 1'")];
let messages2 = vec![Message::user().with_text("Say 'Stream 2'")];
let model_config = provider.get_model_config();
let stream1 = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant.",
&messages1,
@@ -283,6 +295,7 @@ mod tetrate_streaming_tests {
let stream2 = provider
.stream(
&model_config,
"test-session-id",
"You are a helpful assistant.",
&messages2,