mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
feat(buzz-agent): send User-Agent on LLM requests (#8116)
## Summary `buzz-agent` builds its LLM `reqwest::Client` without a User-Agent, and reqwest sends none by default, so OpenAI-compatible providers that identify clients by product token see buzz-agent as an anonymous caller. This sets `User-Agent: buzz-agent/<crate version>` on the shared LLM client in `crates/buzz-agent/src/llm.rs`. The version is `CARGO_PKG_VERSION`, the same value `agentInfo` reports over ACP. No request body or endpoint changes. Open point: the crate version is the workspace version (`0.1.0`), so every build reports the same version. A per-build version would need the desktop to pass one through at spawn time; this PR does not do that. ### Related issue None found. ### Testing - The test stub now captures the `User-Agent` request header; new `llm_requests_carry_buzz_agent_user_agent` asserts `buzz-agent/<version>` on a Chat Completions POST. - Verified red without the `.user_agent(...)` line (`left: None`), green with it. - `cargo test -p buzz-agent --lib llm::tests`: 194 passed. `cargo fmt` clean; the one clippy warning is pre-existing (`hints.rs:33`). - Not exercised in a built desktop app. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com>
This commit is contained in:
@@ -59,9 +59,16 @@ pub struct Llm {
|
||||
/// network/reachability problem, not a slow generation.
|
||||
const LLM_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// `User-Agent` sent on every LLM request. reqwest sends no User-Agent by
|
||||
/// default, so OpenAI-compatible providers that identify clients by product
|
||||
/// token saw buzz-agent as an anonymous caller. The version is the crate
|
||||
/// version, matching the `agentInfo` reported over ACP.
|
||||
const LLM_USER_AGENT: &str = concat!("buzz-agent/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
impl Llm {
|
||||
pub fn new(cfg: &Config) -> Result<Self, AgentError> {
|
||||
let http = Client::builder()
|
||||
.user_agent(LLM_USER_AGENT)
|
||||
.connect_timeout(LLM_CONNECT_TIMEOUT)
|
||||
// No client-level read_timeout: we apply a per-request total
|
||||
// timeout via RequestBuilder::timeout() so that escalated budgets
|
||||
@@ -2654,6 +2661,7 @@ mod tests {
|
||||
struct CapturedHttpRequest {
|
||||
method: String,
|
||||
path: String,
|
||||
user_agent: Option<String>,
|
||||
body: Option<Value>,
|
||||
}
|
||||
|
||||
@@ -2728,6 +2736,11 @@ mod tests {
|
||||
Ok(read) => bytes.extend_from_slice(&chunk[..read]),
|
||||
}
|
||||
}
|
||||
let user_agent = header_text.lines().find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
name.eq_ignore_ascii_case("user-agent")
|
||||
.then(|| value.trim().to_string())
|
||||
});
|
||||
let mut request_line = header_text.lines().next().unwrap_or_default().split(' ');
|
||||
let method = request_line.next().unwrap_or_default().to_string();
|
||||
let path = request_line.next().unwrap_or_default().to_string();
|
||||
@@ -2736,10 +2749,12 @@ mod tests {
|
||||
} else {
|
||||
serde_json::from_slice(&bytes[header_end..header_end + content_length]).ok()
|
||||
};
|
||||
captured_for_server
|
||||
.lock()
|
||||
.await
|
||||
.push(CapturedHttpRequest { method, path, body });
|
||||
captured_for_server.lock().await.push(CapturedHttpRequest {
|
||||
method,
|
||||
path,
|
||||
user_agent,
|
||||
body,
|
||||
});
|
||||
|
||||
let response = responses.lock().await.pop_front().unwrap_or_else(|| {
|
||||
StubHttpResponse::error(500, "stub response sequence exhausted")
|
||||
@@ -2800,6 +2815,23 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Every LLM request identifies the client to the provider via the
|
||||
/// first User-Agent product token.
|
||||
#[tokio::test]
|
||||
async fn llm_requests_carry_buzz_agent_user_agent() {
|
||||
let (base_url, captured) =
|
||||
spawn_sequence_stub(vec![StubHttpResponse::ok(chat_response("ok"))]).await;
|
||||
let mut config = cfg(Provider::OpenAi);
|
||||
config.base_url = base_url;
|
||||
let llm = Llm::new(&config).unwrap();
|
||||
|
||||
complete_model(&llm, &config, "gpt-test").await.unwrap();
|
||||
let requests = captured.lock().await;
|
||||
let expected = format!("buzz-agent/{}", env!("CARGO_PKG_VERSION"));
|
||||
assert_eq!(requests.len(), 1);
|
||||
assert_eq!(requests[0].user_agent.as_deref(), Some(expected.as_str()));
|
||||
}
|
||||
|
||||
/// An explicit model is sent verbatim and never rewritten to something
|
||||
/// else. A server error is retried under the *same* model (the ordinary
|
||||
/// transport retry) and then surfaced -- there is no second model to fall
|
||||
|
||||
Reference in New Issue
Block a user