@@ -92,6 +92,119 @@ pub fn llm_system(
9292 llm_call_sys ( prompt, model_override, Some ( system) )
9393}
9494
95+ /// `llm_stream_print(prompt, system?, model?) -> string`
96+ ///
97+ /// Streams the LLM response token-by-token to stdout, then returns the full
98+ /// accumulated text. Uses SSE streaming (stream:true). Supports both Anthropic
99+ /// and OpenAI providers (auto-detected via LLM_PROVIDER env var).
100+ #[ cfg( feature = "native-llm" ) ]
101+ pub fn llm_stream_print (
102+ prompt : & str ,
103+ system : Option < & str > ,
104+ model_override : Option < & str > ,
105+ ) -> Result < Value , String > {
106+ use std:: io:: { BufRead , BufReader , Write } ;
107+
108+ let cfg = Config :: from_env ( ) ?;
109+ let model = model_override. unwrap_or ( & cfg. model ) . to_string ( ) ;
110+
111+ // Build messages list
112+ let mut messages: Vec < ChatMessage > = Vec :: new ( ) ;
113+ if let Some ( sys) = system {
114+ if !sys. is_empty ( ) {
115+ messages. push ( ChatMessage { role : "system" . to_string ( ) , content : sys. to_string ( ) } ) ;
116+ }
117+ }
118+ messages. push ( ChatMessage { role : "user" . to_string ( ) , content : prompt. to_string ( ) } ) ;
119+
120+ match cfg. provider {
121+ Provider :: Anthropic => {
122+ let mut system_parts: Vec < String > = Vec :: new ( ) ;
123+ let mut msgs_json: Vec < serde_json:: Value > = Vec :: new ( ) ;
124+ for m in & messages {
125+ if m. role == "system" {
126+ system_parts. push ( m. content . clone ( ) ) ;
127+ } else {
128+ msgs_json. push ( serde_json:: json!( { "role" : m. role, "content" : m. content } ) ) ;
129+ }
130+ }
131+ let mut body = serde_json:: json!( {
132+ "model" : model, "max_tokens" : 4096 ,
133+ "messages" : msgs_json, "stream" : true
134+ } ) ;
135+ if !system_parts. is_empty ( ) {
136+ body[ "system" ] = serde_json:: Value :: String ( system_parts. join ( "\n \n " ) ) ;
137+ }
138+ let resp = ureq:: post ( & cfg. base_url )
139+ . set ( "Content-Type" , "application/json" )
140+ . set ( "Authorization" , & format ! ( "Bearer {}" , cfg. api_key) )
141+ . set ( "anthropic-version" , "2023-06-01" )
142+ . set ( "x-api-key" , & cfg. api_key )
143+ . send_json ( body)
144+ . map_err ( |e| format ! ( "llm_stream HTTP error: {}" , e) ) ?;
145+ let reader = BufReader :: new ( resp. into_reader ( ) ) ;
146+ let mut full_text = String :: new ( ) ;
147+ for line in reader. lines ( ) {
148+ let line = line. map_err ( |e| format ! ( "llm_stream read error: {}" , e) ) ?;
149+ if let Some ( data) = line. strip_prefix ( "data: " ) {
150+ if data == "[DONE]" { break ; }
151+ if let Ok ( event) = serde_json:: from_str :: < serde_json:: Value > ( data) {
152+ if event[ "type" ] == "content_block_delta" {
153+ if let Some ( text) = event[ "delta" ] [ "text" ] . as_str ( ) {
154+ print ! ( "{}" , text) ;
155+ let _ = std:: io:: stdout ( ) . flush ( ) ;
156+ full_text. push_str ( text) ;
157+ }
158+ }
159+ }
160+ }
161+ }
162+ println ! ( ) ;
163+ Ok ( Value :: String ( full_text) )
164+ }
165+ Provider :: OpenAI => {
166+ let msgs_json: Vec < serde_json:: Value > = messages
167+ . iter ( )
168+ . map ( |m| serde_json:: json!( { "role" : m. role, "content" : m. content } ) )
169+ . collect ( ) ;
170+ let body = serde_json:: json!( {
171+ "model" : model, "messages" : msgs_json, "stream" : true
172+ } ) ;
173+ let resp = ureq:: post ( & cfg. base_url )
174+ . set ( "Content-Type" , "application/json" )
175+ . set ( "Authorization" , & format ! ( "Bearer {}" , cfg. api_key) )
176+ . send_json ( body)
177+ . map_err ( |e| format ! ( "llm_stream HTTP error: {}" , e) ) ?;
178+ let reader = BufReader :: new ( resp. into_reader ( ) ) ;
179+ let mut full_text = String :: new ( ) ;
180+ for line in reader. lines ( ) {
181+ let line = line. map_err ( |e| format ! ( "llm_stream read error: {}" , e) ) ?;
182+ if let Some ( data) = line. strip_prefix ( "data: " ) {
183+ if data == "[DONE]" { break ; }
184+ if let Ok ( event) = serde_json:: from_str :: < serde_json:: Value > ( data) {
185+ if let Some ( text) = event[ "choices" ] [ 0 ] [ "delta" ] [ "content" ] . as_str ( ) {
186+ print ! ( "{}" , text) ;
187+ let _ = std:: io:: stdout ( ) . flush ( ) ;
188+ full_text. push_str ( text) ;
189+ }
190+ }
191+ }
192+ }
193+ println ! ( ) ;
194+ Ok ( Value :: String ( full_text) )
195+ }
196+ }
197+ }
198+
199+ #[ cfg( not( feature = "native-llm" ) ) ]
200+ pub fn llm_stream_print (
201+ _prompt : & str ,
202+ _system : Option < & str > ,
203+ _model_override : Option < & str > ,
204+ ) -> Result < Value , String > {
205+ Err ( "llm_stream_print: recompile with --features native-llm" . to_string ( ) )
206+ }
207+
95208/// `batch_llm_call(prompts, model?, concurrency?) -> string[]`
96209///
97210/// Send multiple prompts to the LLM sequentially and return all responses in
0 commit comments