@@ -14,8 +14,9 @@ use crate::rate_limit::{ApiResponse, RateLimitInfo};
1414use crate :: streaming:: parse_sse_stream;
1515use crate :: transport:: Transport ;
1616use crate :: types:: {
17- ApiErrorResponse , CountTokensRequest , CountTokensResponse , MessagesRequest , MessagesResponse ,
18- ModelListResponse , StreamEvent ,
17+ ApiErrorResponse , ContentBlock , CountTokensRequest , CountTokensResponse , Message ,
18+ MessageContent , MessagesRequest , MessagesResponse , ModelListResponse , Role , StreamEvent ,
19+ SystemPrompt , TextBlock , TypedContentBlock ,
1920} ;
2021
2122/// Anthropic API client.
@@ -66,7 +67,9 @@ impl Client {
6667 & self ,
6768 req : & MessagesRequest ,
6869 ) -> Result < ApiResponse < MessagesResponse > , Error > {
69- self . post_json ( "/v1/messages" , req) . await
70+ let mut req = req. clone ( ) ;
71+ normalize_inline_system_messages ( & mut req. messages , & mut req. system ) ?;
72+ self . post_json ( "/v1/messages" , & req) . await
7073 }
7174
7275 /// Send a streaming messages request, returning a stream of events.
@@ -77,7 +80,9 @@ impl Client {
7780 & self ,
7881 req : & MessagesRequest ,
7982 ) -> Result < ApiResponse < impl Stream < Item = Result < StreamEvent , Error > > + Send > , Error > {
80- let response = self . send_post ( "/v1/messages" , req) . await ?;
83+ let mut req = req. clone ( ) ;
84+ normalize_inline_system_messages ( & mut req. messages , & mut req. system ) ?;
85+ let response = self . send_post ( "/v1/messages" , & req) . await ?;
8186
8287 let rate_limit = RateLimitInfo :: from_headers ( response. headers ( ) ) ;
8388
@@ -98,7 +103,9 @@ impl Client {
98103 & self ,
99104 req : & CountTokensRequest ,
100105 ) -> Result < ApiResponse < CountTokensResponse > , Error > {
101- self . post_json ( "/v1/messages/count_tokens" , req) . await
106+ let mut req = req. clone ( ) ;
107+ normalize_inline_system_messages ( & mut req. messages , & mut req. system ) ?;
108+ self . post_json ( "/v1/messages/count_tokens" , & req) . await
102109 }
103110
104111 /// List available models.
@@ -219,6 +226,71 @@ impl Client {
219226 }
220227}
221228
229+ fn normalize_inline_system_messages (
230+ messages : & mut Vec < Message > ,
231+ system : & mut Option < SystemPrompt > ,
232+ ) -> Result < ( ) , Error > {
233+ let mut conversation = Vec :: with_capacity ( messages. len ( ) ) ;
234+ let mut inline_system = Vec :: new ( ) ;
235+ let mut found_inline_system = false ;
236+
237+ for message in std:: mem:: take ( messages) {
238+ if message. role != Role :: System {
239+ conversation. push ( message) ;
240+ continue ;
241+ }
242+
243+ found_inline_system = true ;
244+ match message. content {
245+ MessageContent :: Text ( text) => inline_system. push ( TextBlock {
246+ r#type : "text" . to_owned ( ) ,
247+ text,
248+ cache_control : None ,
249+ } ) ,
250+ MessageContent :: Blocks ( blocks) => {
251+ for block in blocks {
252+ match block {
253+ ContentBlock :: Typed ( TypedContentBlock :: Text {
254+ text,
255+ cache_control,
256+ } ) => inline_system. push ( TextBlock {
257+ r#type : "text" . to_owned ( ) ,
258+ text,
259+ cache_control,
260+ } ) ,
261+ _ => {
262+ return Err ( Error :: Json (
263+ <serde_json:: Error as serde:: ser:: Error >:: custom (
264+ "inline system messages may only contain text blocks" ,
265+ ) ,
266+ ) ) ;
267+ }
268+ }
269+ }
270+ }
271+ }
272+ }
273+
274+ * messages = conversation;
275+ if !found_inline_system {
276+ return Ok ( ( ) ) ;
277+ }
278+
279+ let mut system_blocks = match system. take ( ) {
280+ Some ( SystemPrompt :: Text ( text) ) => vec ! [ TextBlock {
281+ r#type: "text" . to_owned( ) ,
282+ text,
283+ cache_control: None ,
284+ } ] ,
285+ Some ( SystemPrompt :: Blocks ( blocks) ) => blocks,
286+ None => Vec :: new ( ) ,
287+ } ;
288+ system_blocks. extend ( inline_system) ;
289+ * system = Some ( SystemPrompt :: Blocks ( system_blocks) ) ;
290+
291+ Ok ( ( ) )
292+ }
293+
222294#[ cfg( test) ]
223295mod tests {
224296 use futures_util:: StreamExt ;
@@ -229,7 +301,10 @@ mod tests {
229301
230302 use super :: * ;
231303 use crate :: transport:: TransportConfig ;
232- use crate :: types:: { Message , MessageContent , MessagesRequest , Role } ;
304+ use crate :: types:: {
305+ ContentBlock , Message , MessageContent , MessagesRequest , Role , SystemPrompt ,
306+ TypedContentBlock ,
307+ } ;
233308
234309 fn transport ( base_url : String ) -> Transport {
235310 Transport :: new ( TransportConfig {
@@ -310,6 +385,11 @@ mod tests {
310385 String :: from_utf8 ( buf) . unwrap ( )
311386 }
312387
388+ fn request_body_json ( request : & str ) -> serde_json:: Value {
389+ let ( _, body) = request. split_once ( "\r \n \r \n " ) . unwrap ( ) ;
390+ serde_json:: from_str ( body) . unwrap ( )
391+ }
392+
313393 fn http_response ( status : & str , content_type : & str , body : & str ) -> String {
314394 format ! (
315395 "HTTP/1.1 {status}\r \n content-type: {content_type}\r \n content-length: {}\r \n connection: close\r \n \r \n {body}" ,
@@ -348,16 +428,64 @@ mod tests {
348428 spawn_server ( http_response ( "200 OK" , "application/json" , body) ) . await ;
349429 let client = Client :: new ( transport ( base_url) ) . unwrap ( ) ;
350430
351- let resp = client. messages ( & messages_request ( ) ) . await . unwrap ( ) ;
431+ let mut req = messages_request ( ) ;
432+ req. system = Some ( SystemPrompt :: Text ( "top-level instructions" . into ( ) ) ) ;
433+ req. messages . insert (
434+ 0 ,
435+ Message {
436+ role : Role :: System ,
437+ content : MessageContent :: Blocks ( vec ! [ ContentBlock :: Typed (
438+ TypedContentBlock :: Text {
439+ text: "block instructions" . into( ) ,
440+ cache_control: None ,
441+ } ,
442+ ) ] ) ,
443+ } ,
444+ ) ;
445+ req. messages . insert (
446+ 0 ,
447+ Message {
448+ role : Role :: System ,
449+ content : MessageContent :: Text ( "inline instructions" . into ( ) ) ,
450+ } ,
451+ ) ;
452+
453+ let resp = client. messages ( & req) . await . unwrap ( ) ;
352454 assert_eq ! ( resp. body. id, "msg_01XFDUDYJgAACzvnptvVoYEL" ) ;
353455 assert_eq ! ( resp. body. usage. input_tokens, 10 ) ;
354456
355- let raw = req_rx. await . unwrap ( ) . to_lowercase ( ) ;
356- assert ! ( raw. contains( "post /v1/messages http/1.1" ) ) ;
357- assert ! ( raw. contains( "x-api-key: sk-ant-test-key" ) ) ;
358- assert ! ( raw. contains( "anthropic-version: 2023-06-01" ) ) ;
359- assert ! ( raw. contains( "content-type: application/json" ) ) ;
360- assert ! ( raw. contains( "claude-sonnet-4-20250514" ) ) ;
457+ let raw = req_rx. await . unwrap ( ) ;
458+ let lowercase = raw. to_lowercase ( ) ;
459+ assert ! ( lowercase. contains( "post /v1/messages http/1.1" ) ) ;
460+ assert ! ( lowercase. contains( "x-api-key: sk-ant-test-key" ) ) ;
461+ assert ! ( lowercase. contains( "anthropic-version: 2023-06-01" ) ) ;
462+ assert ! ( lowercase. contains( "content-type: application/json" ) ) ;
463+ assert ! ( lowercase. contains( "claude-sonnet-4-20250514" ) ) ;
464+
465+ let sent = request_body_json ( & raw ) ;
466+ assert_eq ! (
467+ sent[ "system" ] ,
468+ serde_json:: json!( [
469+ { "type" : "text" , "text" : "top-level instructions" } ,
470+ { "type" : "text" , "text" : "inline instructions" } ,
471+ { "type" : "text" , "text" : "block instructions" }
472+ ] )
473+ ) ;
474+ assert_eq ! (
475+ sent[ "messages" ] ,
476+ serde_json:: json!( [ { "role" : "user" , "content" : "Hello" } ] )
477+ ) ;
478+ }
479+
480+ #[ test]
481+ fn inline_system_role_cannot_be_serialized_directly ( ) {
482+ let error = serde_json:: to_value ( Message {
483+ role : Role :: System ,
484+ content : MessageContent :: Text ( "instructions" . into ( ) ) ,
485+ } )
486+ . unwrap_err ( ) ;
487+
488+ assert ! ( error. to_string( ) . contains( "inbound-only" ) ) ;
361489 }
362490
363491 #[ tokio:: test]
@@ -415,17 +543,39 @@ mod tests {
415543
416544 let req = crate :: types:: CountTokensRequest :: builder ( )
417545 . model ( "claude-sonnet-4-20250514" )
418- . messages ( vec ! [ Message {
419- role: Role :: User ,
420- content: MessageContent :: Text ( "Hello" . into( ) ) ,
421- } ] )
546+ . system ( SystemPrompt :: Text ( "top-level instructions" . into ( ) ) )
547+ . messages ( vec ! [
548+ Message {
549+ role: Role :: System ,
550+ content: MessageContent :: Text ( "inline instructions" . into( ) ) ,
551+ } ,
552+ Message {
553+ role: Role :: User ,
554+ content: MessageContent :: Text ( "Hello" . into( ) ) ,
555+ } ,
556+ ] )
422557 . build ( ) ;
423558
424559 let resp = client. count_tokens ( & req) . await . unwrap ( ) ;
425560 assert_eq ! ( resp. body. input_tokens, 42 ) ;
426561
427- let raw = req_rx. await . unwrap ( ) . to_lowercase ( ) ;
428- assert ! ( raw. contains( "post /v1/messages/count_tokens http/1.1" ) ) ;
562+ let raw = req_rx. await . unwrap ( ) ;
563+ assert ! (
564+ raw. to_lowercase( )
565+ . contains( "post /v1/messages/count_tokens http/1.1" )
566+ ) ;
567+ let sent = request_body_json ( & raw ) ;
568+ assert_eq ! (
569+ sent[ "system" ] ,
570+ serde_json:: json!( [
571+ { "type" : "text" , "text" : "top-level instructions" } ,
572+ { "type" : "text" , "text" : "inline instructions" }
573+ ] )
574+ ) ;
575+ assert_eq ! (
576+ sent[ "messages" ] ,
577+ serde_json:: json!( [ { "role" : "user" , "content" : "Hello" } ] )
578+ ) ;
429579 }
430580
431581 #[ tokio:: test]
@@ -521,12 +671,19 @@ mod tests {
521671 "event: message_stop\n " ,
522672 "data: {\" type\" :\" message_stop\" }\n \n " ,
523673 ) ;
524- let ( base_url, _ ) =
674+ let ( base_url, req_rx ) =
525675 spawn_server ( http_response ( "200 OK" , "text/event-stream" , events) ) . await ;
526676 let client = Client :: new ( transport ( base_url) ) . unwrap ( ) ;
527677
528678 let mut req = messages_request ( ) ;
529679 req. stream = Some ( true ) ;
680+ req. messages . insert (
681+ 0 ,
682+ Message {
683+ role : Role :: System ,
684+ content : MessageContent :: Text ( "streaming instructions" . into ( ) ) ,
685+ } ,
686+ ) ;
530687
531688 let resp = client. messages_stream ( & req) . await . unwrap ( ) ;
532689 assert_eq ! ( resp. rate_limit. requests_limit, None ) ;
@@ -555,6 +712,16 @@ mod tests {
555712 }
556713 other => panic ! ( "expected ContentBlockDelta, got: {other:?}" ) ,
557714 }
715+
716+ let sent = request_body_json ( & req_rx. await . unwrap ( ) ) ;
717+ assert_eq ! (
718+ sent[ "system" ] ,
719+ serde_json:: json!( [ { "type" : "text" , "text" : "streaming instructions" } ] )
720+ ) ;
721+ assert_eq ! (
722+ sent[ "messages" ] ,
723+ serde_json:: json!( [ { "role" : "user" , "content" : "Hello" } ] )
724+ ) ;
558725 }
559726
560727 #[ tokio:: test]
0 commit comments