client_integration.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. use std::collections::HashMap;
  2. use std::sync::Arc;
  3. use std::sync::{Mutex as StdMutex, OnceLock};
  4. use std::time::Duration;
  5. use api::{
  6. AnthropicClient, ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent,
  7. InputContentBlock, InputMessage, MessageDeltaEvent, MessageRequest, OutputContentBlock,
  8. PromptCache, StreamEvent, ToolChoice, ToolDefinition,
  9. };
  10. use serde_json::json;
  11. use tokio::io::{AsyncReadExt, AsyncWriteExt};
  12. use tokio::net::TcpListener;
  13. use tokio::sync::Mutex;
  14. fn env_lock() -> std::sync::MutexGuard<'static, ()> {
  15. static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
  16. LOCK.get_or_init(|| StdMutex::new(()))
  17. .lock()
  18. .unwrap_or_else(std::sync::PoisonError::into_inner)
  19. }
  20. #[tokio::test]
  21. async fn send_message_posts_json_and_parses_response() {
  22. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  23. let body = concat!(
  24. "{",
  25. "\"id\":\"msg_test\",",
  26. "\"type\":\"message\",",
  27. "\"role\":\"assistant\",",
  28. "\"content\":[{\"type\":\"text\",\"text\":\"Hello from Claude\"}],",
  29. "\"model\":\"claude-3-7-sonnet-latest\",",
  30. "\"stop_reason\":\"end_turn\",",
  31. "\"stop_sequence\":null,",
  32. "\"usage\":{\"input_tokens\":12,\"output_tokens\":4},",
  33. "\"request_id\":\"req_body_123\"",
  34. "}"
  35. );
  36. let server = spawn_server(
  37. state.clone(),
  38. vec![http_response("200 OK", "application/json", body)],
  39. )
  40. .await;
  41. let client = AnthropicClient::new("test-key")
  42. .with_auth_token(Some("proxy-token".to_string()))
  43. .with_base_url(server.base_url());
  44. let response = client
  45. .send_message(&sample_request(false))
  46. .await
  47. .expect("request should succeed");
  48. assert_eq!(response.id, "msg_test");
  49. assert_eq!(response.total_tokens(), 16);
  50. assert_eq!(response.request_id.as_deref(), Some("req_body_123"));
  51. assert_eq!(response.usage.cache_creation_input_tokens, 0);
  52. assert_eq!(response.usage.cache_read_input_tokens, 0);
  53. assert_eq!(
  54. response.content,
  55. vec![OutputContentBlock::Text {
  56. text: "Hello from Claude".to_string(),
  57. }]
  58. );
  59. let captured = state.lock().await;
  60. let request = captured.first().expect("server should capture request");
  61. assert_eq!(request.method, "POST");
  62. assert_eq!(request.path, "/v1/messages");
  63. assert_eq!(
  64. request.headers.get("x-api-key").map(String::as_str),
  65. Some("test-key")
  66. );
  67. assert_eq!(
  68. request.headers.get("authorization").map(String::as_str),
  69. Some("Bearer proxy-token")
  70. );
  71. let body: serde_json::Value =
  72. serde_json::from_str(&request.body).expect("request body should be json");
  73. assert_eq!(
  74. body.get("model").and_then(serde_json::Value::as_str),
  75. Some("claude-3-7-sonnet-latest")
  76. );
  77. assert!(body.get("stream").is_none());
  78. assert_eq!(body["tools"][0]["name"], json!("get_weather"));
  79. assert_eq!(body["tool_choice"]["type"], json!("auto"));
  80. }
  81. #[tokio::test]
  82. async fn send_message_parses_prompt_cache_token_usage_from_response() {
  83. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  84. let body = concat!(
  85. "{",
  86. "\"id\":\"msg_cache_tokens\",",
  87. "\"type\":\"message\",",
  88. "\"role\":\"assistant\",",
  89. "\"content\":[{\"type\":\"text\",\"text\":\"Cache tokens\"}],",
  90. "\"model\":\"claude-3-7-sonnet-latest\",",
  91. "\"stop_reason\":\"end_turn\",",
  92. "\"stop_sequence\":null,",
  93. "\"usage\":{\"input_tokens\":12,\"cache_creation_input_tokens\":321,\"cache_read_input_tokens\":654,\"output_tokens\":4}",
  94. "}"
  95. );
  96. let server = spawn_server(
  97. state,
  98. vec![http_response("200 OK", "application/json", body)],
  99. )
  100. .await;
  101. let client = AnthropicClient::new("test-key").with_base_url(server.base_url());
  102. let response = client
  103. .send_message(&sample_request(false))
  104. .await
  105. .expect("request should succeed");
  106. assert_eq!(response.usage.input_tokens, 12);
  107. assert_eq!(response.usage.cache_creation_input_tokens, 321);
  108. assert_eq!(response.usage.cache_read_input_tokens, 654);
  109. assert_eq!(response.usage.output_tokens, 4);
  110. }
  111. #[tokio::test]
  112. #[allow(clippy::await_holding_lock)]
  113. async fn stream_message_parses_sse_events_with_tool_use() {
  114. let _guard = env_lock();
  115. let temp_root = std::env::temp_dir().join(format!(
  116. "api-stream-cache-{}-{}",
  117. std::process::id(),
  118. std::time::SystemTime::now()
  119. .duration_since(std::time::UNIX_EPOCH)
  120. .expect("time")
  121. .as_nanos()
  122. ));
  123. std::env::set_var("CLAUDE_CONFIG_HOME", &temp_root);
  124. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  125. let sse = concat!(
  126. "event: message_start\n",
  127. "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stream\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3-7-sonnet-latest\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":8,\"cache_creation_input_tokens\":13,\"cache_read_input_tokens\":21,\"output_tokens\":0}}}\n\n",
  128. "event: content_block_start\n",
  129. "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_123\",\"name\":\"get_weather\",\"input\":{}}}\n\n",
  130. "event: content_block_delta\n",
  131. "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\n",
  132. "event: content_block_stop\n",
  133. "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
  134. "event: message_delta\n",
  135. "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":8,\"cache_creation_input_tokens\":34,\"cache_read_input_tokens\":55,\"output_tokens\":1}}\n\n",
  136. "event: message_stop\n",
  137. "data: {\"type\":\"message_stop\"}\n\n",
  138. "data: [DONE]\n\n"
  139. );
  140. let server = spawn_server(
  141. state.clone(),
  142. vec![http_response_with_headers(
  143. "200 OK",
  144. "text/event-stream",
  145. sse,
  146. &[("request-id", "req_stream_456")],
  147. )],
  148. )
  149. .await;
  150. let client = AnthropicClient::new("test-key")
  151. .with_auth_token(Some("proxy-token".to_string()))
  152. .with_base_url(server.base_url())
  153. .with_prompt_cache(PromptCache::new("stream-session"));
  154. let mut stream = client
  155. .stream_message(&sample_request(false))
  156. .await
  157. .expect("stream should start");
  158. assert_eq!(stream.request_id(), Some("req_stream_456"));
  159. let mut events = Vec::new();
  160. while let Some(event) = stream
  161. .next_event()
  162. .await
  163. .expect("stream event should parse")
  164. {
  165. events.push(event);
  166. }
  167. assert_eq!(events.len(), 6);
  168. assert!(matches!(events[0], StreamEvent::MessageStart(_)));
  169. assert!(matches!(
  170. events[1],
  171. StreamEvent::ContentBlockStart(ContentBlockStartEvent {
  172. content_block: OutputContentBlock::ToolUse { .. },
  173. ..
  174. })
  175. ));
  176. assert!(matches!(
  177. events[2],
  178. StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
  179. delta: ContentBlockDelta::InputJsonDelta { .. },
  180. ..
  181. })
  182. ));
  183. assert!(matches!(events[3], StreamEvent::ContentBlockStop(_)));
  184. assert!(matches!(
  185. events[4],
  186. StreamEvent::MessageDelta(MessageDeltaEvent { .. })
  187. ));
  188. assert!(matches!(events[5], StreamEvent::MessageStop(_)));
  189. match &events[1] {
  190. StreamEvent::ContentBlockStart(ContentBlockStartEvent {
  191. content_block: OutputContentBlock::ToolUse { name, input, .. },
  192. ..
  193. }) => {
  194. assert_eq!(name, "get_weather");
  195. assert_eq!(input, &json!({}));
  196. }
  197. other => panic!("expected tool_use block, got {other:?}"),
  198. }
  199. let captured = state.lock().await;
  200. let request = captured.first().expect("server should capture request");
  201. assert!(request.body.contains("\"stream\":true"));
  202. let cache_stats = client
  203. .prompt_cache_stats()
  204. .expect("prompt cache stats should exist");
  205. assert_eq!(cache_stats.tracked_requests, 1);
  206. assert_eq!(cache_stats.last_cache_creation_input_tokens, Some(34));
  207. assert_eq!(cache_stats.last_cache_read_input_tokens, Some(55));
  208. assert_eq!(
  209. cache_stats.last_cache_source.as_deref(),
  210. Some("api-response")
  211. );
  212. std::fs::remove_dir_all(temp_root).expect("cleanup temp root");
  213. std::env::remove_var("CLAUDE_CONFIG_HOME");
  214. }
  215. #[tokio::test]
  216. async fn retries_retryable_failures_before_succeeding() {
  217. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  218. let server = spawn_server(
  219. state.clone(),
  220. vec![
  221. http_response(
  222. "429 Too Many Requests",
  223. "application/json",
  224. "{\"type\":\"error\",\"error\":{\"type\":\"rate_limit_error\",\"message\":\"slow down\"}}",
  225. ),
  226. http_response(
  227. "200 OK",
  228. "application/json",
  229. "{\"id\":\"msg_retry\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Recovered\"}],\"model\":\"claude-3-7-sonnet-latest\",\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":3,\"output_tokens\":2}}",
  230. ),
  231. ],
  232. )
  233. .await;
  234. let client = AnthropicClient::new("test-key")
  235. .with_base_url(server.base_url())
  236. .with_retry_policy(2, Duration::from_millis(1), Duration::from_millis(2));
  237. let response = client
  238. .send_message(&sample_request(false))
  239. .await
  240. .expect("retry should eventually succeed");
  241. assert_eq!(response.total_tokens(), 5);
  242. assert_eq!(state.lock().await.len(), 2);
  243. }
  244. #[tokio::test]
  245. async fn surfaces_retry_exhaustion_for_persistent_retryable_errors() {
  246. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  247. let server = spawn_server(
  248. state.clone(),
  249. vec![
  250. http_response(
  251. "503 Service Unavailable",
  252. "application/json",
  253. "{\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"busy\"}}",
  254. ),
  255. http_response(
  256. "503 Service Unavailable",
  257. "application/json",
  258. "{\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"still busy\"}}",
  259. ),
  260. ],
  261. )
  262. .await;
  263. let client = AnthropicClient::new("test-key")
  264. .with_base_url(server.base_url())
  265. .with_retry_policy(1, Duration::from_millis(1), Duration::from_millis(2));
  266. let error = client
  267. .send_message(&sample_request(false))
  268. .await
  269. .expect_err("persistent 503 should fail");
  270. match error {
  271. ApiError::RetriesExhausted {
  272. attempts,
  273. last_error,
  274. } => {
  275. assert_eq!(attempts, 2);
  276. assert!(matches!(
  277. *last_error,
  278. ApiError::Api {
  279. status: reqwest::StatusCode::SERVICE_UNAVAILABLE,
  280. retryable: true,
  281. ..
  282. }
  283. ));
  284. }
  285. other => panic!("expected retries exhausted, got {other:?}"),
  286. }
  287. }
  288. #[tokio::test]
  289. #[allow(clippy::await_holding_lock)]
  290. async fn send_message_reuses_recent_completion_cache_entries() {
  291. let _guard = env_lock();
  292. let temp_root = std::env::temp_dir().join(format!(
  293. "api-prompt-cache-{}-{}",
  294. std::process::id(),
  295. std::time::SystemTime::now()
  296. .duration_since(std::time::UNIX_EPOCH)
  297. .expect("time")
  298. .as_nanos()
  299. ));
  300. std::env::set_var("CLAUDE_CONFIG_HOME", &temp_root);
  301. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  302. let server = spawn_server(
  303. state.clone(),
  304. vec![http_response(
  305. "200 OK",
  306. "application/json",
  307. "{\"id\":\"msg_cached\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Cached once\"}],\"model\":\"claude-3-7-sonnet-latest\",\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":5,\"cache_read_input_tokens\":4000,\"output_tokens\":2}}",
  308. )],
  309. )
  310. .await;
  311. let client = AnthropicClient::new("test-key")
  312. .with_base_url(server.base_url())
  313. .with_prompt_cache(PromptCache::new("integration-session"));
  314. let first = client
  315. .send_message(&sample_request(false))
  316. .await
  317. .expect("first request should succeed");
  318. let second = client
  319. .send_message(&sample_request(false))
  320. .await
  321. .expect("second request should reuse cache");
  322. assert_eq!(first.content, second.content);
  323. assert_eq!(state.lock().await.len(), 1);
  324. let cache_stats = client
  325. .prompt_cache_stats()
  326. .expect("prompt cache stats should exist");
  327. assert_eq!(cache_stats.completion_cache_hits, 1);
  328. assert_eq!(cache_stats.completion_cache_misses, 1);
  329. assert_eq!(cache_stats.completion_cache_writes, 1);
  330. std::fs::remove_dir_all(temp_root).expect("cleanup temp root");
  331. std::env::remove_var("CLAUDE_CONFIG_HOME");
  332. }
  333. #[tokio::test]
  334. #[allow(clippy::await_holding_lock)]
  335. async fn send_message_tracks_unexpected_prompt_cache_breaks() {
  336. let _guard = env_lock();
  337. let temp_root = std::env::temp_dir().join(format!(
  338. "api-prompt-break-{}-{}",
  339. std::process::id(),
  340. std::time::SystemTime::now()
  341. .duration_since(std::time::UNIX_EPOCH)
  342. .expect("time")
  343. .as_nanos()
  344. ));
  345. std::env::set_var("CLAUDE_CONFIG_HOME", &temp_root);
  346. let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
  347. let server = spawn_server(
  348. state,
  349. vec![
  350. http_response(
  351. "200 OK",
  352. "application/json",
  353. "{\"id\":\"msg_one\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"One\"}],\"model\":\"claude-3-7-sonnet-latest\",\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":5,\"cache_read_input_tokens\":6000,\"output_tokens\":2}}",
  354. ),
  355. http_response(
  356. "200 OK",
  357. "application/json",
  358. "{\"id\":\"msg_two\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Two\"}],\"model\":\"claude-3-7-sonnet-latest\",\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":1000,\"output_tokens\":2}}",
  359. ),
  360. ],
  361. )
  362. .await;
  363. let request = sample_request(false);
  364. let client = AnthropicClient::new("test-key")
  365. .with_base_url(server.base_url())
  366. .with_prompt_cache(PromptCache::with_config(api::PromptCacheConfig {
  367. session_id: "break-session".to_string(),
  368. completion_ttl: Duration::from_secs(0),
  369. ..api::PromptCacheConfig::default()
  370. }));
  371. client
  372. .send_message(&request)
  373. .await
  374. .expect("first response should succeed");
  375. client
  376. .send_message(&request)
  377. .await
  378. .expect("second response should succeed");
  379. let cache_stats = client
  380. .prompt_cache_stats()
  381. .expect("prompt cache stats should exist");
  382. assert_eq!(cache_stats.unexpected_cache_breaks, 1);
  383. assert_eq!(
  384. cache_stats.last_break_reason.as_deref(),
  385. Some("cache read tokens dropped while prompt fingerprint remained stable")
  386. );
  387. std::fs::remove_dir_all(temp_root).expect("cleanup temp root");
  388. std::env::remove_var("CLAUDE_CONFIG_HOME");
  389. }
  390. #[tokio::test]
  391. #[ignore = "requires ANTHROPIC_API_KEY and network access"]
  392. async fn live_stream_smoke_test() {
  393. let client = AnthropicClient::from_env().expect("ANTHROPIC_API_KEY must be set");
  394. let mut stream = client
  395. .stream_message(&MessageRequest {
  396. model: std::env::var("ANTHROPIC_MODEL")
  397. .unwrap_or_else(|_| "claude-3-7-sonnet-latest".to_string()),
  398. max_tokens: 32,
  399. messages: vec![InputMessage::user_text(
  400. "Reply with exactly: hello from rust",
  401. )],
  402. system: None,
  403. tools: None,
  404. tool_choice: None,
  405. stream: false,
  406. })
  407. .await
  408. .expect("live stream should start");
  409. while let Some(_event) = stream
  410. .next_event()
  411. .await
  412. .expect("live stream should yield events")
  413. {}
  414. }
  415. #[derive(Debug, Clone, PartialEq, Eq)]
  416. struct CapturedRequest {
  417. method: String,
  418. path: String,
  419. headers: HashMap<String, String>,
  420. body: String,
  421. }
  422. struct TestServer {
  423. base_url: String,
  424. join_handle: tokio::task::JoinHandle<()>,
  425. }
  426. impl TestServer {
  427. fn base_url(&self) -> String {
  428. self.base_url.clone()
  429. }
  430. }
  431. impl Drop for TestServer {
  432. fn drop(&mut self) {
  433. self.join_handle.abort();
  434. }
  435. }
  436. async fn spawn_server(
  437. state: Arc<Mutex<Vec<CapturedRequest>>>,
  438. responses: Vec<String>,
  439. ) -> TestServer {
  440. let listener = TcpListener::bind("127.0.0.1:0")
  441. .await
  442. .expect("listener should bind");
  443. let address = listener
  444. .local_addr()
  445. .expect("listener should have local addr");
  446. let join_handle = tokio::spawn(async move {
  447. for response in responses {
  448. let (mut socket, _) = listener.accept().await.expect("server should accept");
  449. let mut buffer = Vec::new();
  450. let mut header_end = None;
  451. loop {
  452. let mut chunk = [0_u8; 1024];
  453. let read = socket
  454. .read(&mut chunk)
  455. .await
  456. .expect("request read should succeed");
  457. if read == 0 {
  458. break;
  459. }
  460. buffer.extend_from_slice(&chunk[..read]);
  461. if let Some(position) = find_header_end(&buffer) {
  462. header_end = Some(position);
  463. break;
  464. }
  465. }
  466. let header_end = header_end.expect("request should include headers");
  467. let (header_bytes, remaining) = buffer.split_at(header_end);
  468. let header_text =
  469. String::from_utf8(header_bytes.to_vec()).expect("headers should be utf8");
  470. let mut lines = header_text.split("\r\n");
  471. let request_line = lines.next().expect("request line should exist");
  472. let mut parts = request_line.split_whitespace();
  473. let method = parts.next().expect("method should exist").to_string();
  474. let path = parts.next().expect("path should exist").to_string();
  475. let mut headers = HashMap::new();
  476. let mut content_length = 0_usize;
  477. for line in lines {
  478. if line.is_empty() {
  479. continue;
  480. }
  481. let (name, value) = line.split_once(':').expect("header should have colon");
  482. let value = value.trim().to_string();
  483. if name.eq_ignore_ascii_case("content-length") {
  484. content_length = value.parse().expect("content length should parse");
  485. }
  486. headers.insert(name.to_ascii_lowercase(), value);
  487. }
  488. let mut body = remaining[4..].to_vec();
  489. while body.len() < content_length {
  490. let mut chunk = vec![0_u8; content_length - body.len()];
  491. let read = socket
  492. .read(&mut chunk)
  493. .await
  494. .expect("body read should succeed");
  495. if read == 0 {
  496. break;
  497. }
  498. body.extend_from_slice(&chunk[..read]);
  499. }
  500. state.lock().await.push(CapturedRequest {
  501. method,
  502. path,
  503. headers,
  504. body: String::from_utf8(body).expect("body should be utf8"),
  505. });
  506. socket
  507. .write_all(response.as_bytes())
  508. .await
  509. .expect("response write should succeed");
  510. }
  511. });
  512. TestServer {
  513. base_url: format!("http://{address}"),
  514. join_handle,
  515. }
  516. }
  517. fn find_header_end(bytes: &[u8]) -> Option<usize> {
  518. bytes.windows(4).position(|window| window == b"\r\n\r\n")
  519. }
  520. fn http_response(status: &str, content_type: &str, body: &str) -> String {
  521. http_response_with_headers(status, content_type, body, &[])
  522. }
  523. fn http_response_with_headers(
  524. status: &str,
  525. content_type: &str,
  526. body: &str,
  527. headers: &[(&str, &str)],
  528. ) -> String {
  529. let mut extra_headers = String::new();
  530. for (name, value) in headers {
  531. use std::fmt::Write as _;
  532. write!(&mut extra_headers, "{name}: {value}\r\n").expect("header write should succeed");
  533. }
  534. format!(
  535. "HTTP/1.1 {status}\r\ncontent-type: {content_type}\r\n{extra_headers}content-length: {}\r\nconnection: close\r\n\r\n{body}",
  536. body.len()
  537. )
  538. }
  539. fn sample_request(stream: bool) -> MessageRequest {
  540. MessageRequest {
  541. model: "claude-3-7-sonnet-latest".to_string(),
  542. max_tokens: 64,
  543. messages: vec![InputMessage {
  544. role: "user".to_string(),
  545. content: vec![
  546. InputContentBlock::Text {
  547. text: "Say hello".to_string(),
  548. },
  549. InputContentBlock::ToolResult {
  550. tool_use_id: "toolu_prev".to_string(),
  551. content: vec![api::ToolResultContentBlock::Json {
  552. value: json!({"forecast": "sunny"}),
  553. }],
  554. is_error: false,
  555. },
  556. ],
  557. }],
  558. system: Some("Use tools when needed".to_string()),
  559. tools: Some(vec![ToolDefinition {
  560. name: "get_weather".to_string(),
  561. description: Some("Fetches the weather".to_string()),
  562. input_schema: json!({
  563. "type": "object",
  564. "properties": {"city": {"type": "string"}},
  565. "required": ["city"]
  566. }),
  567. }]),
  568. tool_choice: Some(ToolChoice::Auto),
  569. stream,
  570. }
  571. }