Skip to main content

vtcode_llm/providers/huggingface/
mod.rs

1#![allow(
2    clippy::bind_instead_of_map,
3    clippy::collapsible_if,
4    reason = "Intentional compatibility, platform, or test-only suppression."
5)]
6
7use crate::error_display::format_llm_error;
8use crate::provider::{
9    LLMError, LLMErrorMetadata, LLMProvider, LLMRequest, LLMResponse, LLMStream, LLMStreamEvent, MessageRole,
10    ToolDefinition,
11};
12use crate::providers::shared::{
13    NoopStreamTelemetry, StreamTelemetry, Utf8StreamDecoder, function_output_value_from_message_content,
14};
15use async_stream::try_stream;
16use async_trait::async_trait;
17use futures::StreamExt;
18use reqwest::{Client as HttpClient, Response, StatusCode};
19use serde_json::{Value, json};
20use vtcode_commons::sanitizer::sanitize_provider_diagnostic;
21use vtcode_config::TimeoutsConfig;
22use vtcode_config::constants::{env_vars, models, urls};
23use vtcode_config::core::{AnthropicConfig, ModelConfig, PromptCachingConfig};
24
25use super::common::{
26    assistant_interleaved_history_text, ensure_model, impl_llm_client, is_minimax_m2_model, map_finish_reason_common,
27    normalize_reasoning_detail_objects, override_base_url, parse_response_openai_format, resolve_model,
28};
29use super::error_handling::{format_network_error, format_parse_error};
30
31const PROVIDER_NAME: &str = "HuggingFace";
32const PROVIDER_KEY: &str = "huggingface";
33const JSON_INSTRUCTION: &str = "Return JSON that matches the provided schema.";
34mod responses_format;
35
36pub struct HuggingFaceProvider {
37    api_key: String,
38    http_client: HttpClient,
39    base_url: String,
40    model: String,
41    _timeouts: TimeoutsConfig,
42    model_behavior: Option<ModelConfig>,
43}
44
45impl HuggingFaceProvider {
46    pub fn new(api_key: String) -> Self {
47        Self::with_model_internal(api_key, models::huggingface::DEFAULT_MODEL.to_string(), None, None, None)
48    }
49
50    fn with_model(api_key: String, model: String) -> Self {
51        Self::with_model_internal(api_key, model, None, None, None)
52    }
53
54    pub fn with_timeouts(api_key: String, timeouts: TimeoutsConfig) -> Self {
55        Self::with_model_internal(api_key, models::huggingface::DEFAULT_MODEL.to_string(), None, Some(timeouts), None)
56    }
57
58    fn with_model_internal(
59        api_key: String,
60        model: String,
61        base_url: Option<String>,
62        timeouts: Option<TimeoutsConfig>,
63        model_behavior: Option<ModelConfig>,
64    ) -> Self {
65        use crate::http_client::HttpClientFactory;
66
67        let timeouts = timeouts.unwrap_or_default();
68
69        Self {
70            api_key,
71            http_client: HttpClientFactory::for_llm(&timeouts),
72            base_url: override_base_url(urls::HUGGINGFACE_API_BASE, base_url, Some(env_vars::HUGGINGFACE_BASE_URL)),
73            model,
74            _timeouts: timeouts,
75            model_behavior,
76        }
77    }
78
79    pub fn from_config(
80        api_key: Option<String>,
81        model: Option<String>,
82        base_url: Option<String>,
83        _prompt_cache: Option<PromptCachingConfig>,
84        timeouts: Option<TimeoutsConfig>,
85        _anthropic: Option<AnthropicConfig>,
86        model_behavior: Option<ModelConfig>,
87    ) -> Self {
88        let api_key_value = api_key.unwrap_or_default();
89        let model_value = resolve_model(model, models::huggingface::DEFAULT_MODEL);
90        Self::with_model_internal(api_key_value, model_value, base_url, timeouts, model_behavior)
91    }
92
93    fn normalize_model_id(&self, model: &str) -> Result<String, LLMError> {
94        let model = model.trim();
95        let lower = model.to_ascii_lowercase();
96
97        if lower.contains("minimax-m2") && !model.contains(':') {
98            return Err(LLMError::Provider {
99                message: format_llm_error(
100                    PROVIDER_NAME,
101                    "MiniMax models require explicit provider selection (:novita suffix). \n                    Use 'MiniMaxAI/MiniMax-M2.5:novita'.",
102                ),
103                metadata: None,
104            });
105        }
106
107        if lower.contains("glm-5") && !model.contains(':') {
108            return Err(LLMError::Provider {
109                message: format_llm_error(
110                    PROVIDER_NAME,
111                    "GLM models require explicit provider selection on HuggingFace.",
112                ),
113                metadata: None,
114            });
115        }
116
117        Ok(model.to_string())
118    }
119
120    pub(super) fn serialize_tools_huggingface(&self, tools: &[ToolDefinition]) -> Option<Vec<Value>> {
121        crate::providers::common::serialize_tools_openai_format(tools)
122    }
123
124    fn serialize_messages_huggingface_chat(&self, request: &LLMRequest) -> Result<Vec<Value>, LLMError> {
125        use serde_json::{Map, json};
126
127        let mut messages = Vec::with_capacity(request.messages.len());
128
129        for message in request.messages.iter() {
130            message
131                .validate_for_provider(PROVIDER_KEY)
132                .map_err(|e| LLMError::InvalidRequest { message: e, metadata: None })?;
133
134            let mut message_map = Map::with_capacity(4);
135            message_map.insert("role".to_owned(), Value::String(message.role.as_generic_str().to_owned()));
136
137            if let Some(interleaved_content) = assistant_interleaved_history_text(message, &request.model) {
138                message_map.insert("content".to_owned(), Value::String(interleaved_content));
139            } else {
140                match &message.content {
141                    crate::provider::MessageContent::Text(text) => {
142                        message_map.insert("content".to_owned(), Value::String(text.clone()));
143                    }
144                    crate::provider::MessageContent::Parts(parts) => {
145                        let has_images = parts.iter().any(crate::provider::ContentPart::is_image);
146                        if has_images {
147                            let parts_json: Vec<Value> = parts
148                            .iter()
149                            .map(|part| match part {
150                                crate::provider::ContentPart::Text { text } => {
151                                    json!({ "type": "text", "text": text })
152                                }
153                                crate::provider::ContentPart::Image {
154                                    data,
155                                    mime_type,
156                                    ..
157                                } => {
158                                    json!({
159                                        "type": "image_url",
160                                        "image_url": {
161                                            "url": format!("data:{};base64,{}", mime_type, data)
162                                        }
163                                    })
164                                }
165                                crate::provider::ContentPart::File {
166                                    filename,
167                                    file_id,
168                                    file_url,
169                                    ..
170                                } => {
171                                    let fallback = filename
172                                        .clone()
173                                        .or_else(|| file_id.clone())
174                                        .or_else(|| file_url.clone())
175                                        .unwrap_or_else(|| "attached file".to_string());
176                                    json!({ "type": "text", "text": format!("[File input not directly supported: {}]", fallback) })
177                                }
178                            })
179                            .collect();
180                            message_map.insert("content".to_owned(), Value::Array(parts_json));
181                        } else {
182                            let text = message.content.as_text().into_owned();
183                            message_map.insert("content".to_owned(), Value::String(text));
184                        }
185                    }
186                }
187            }
188
189            if let Some(tool_calls) = &message.tool_calls {
190                let serialized_calls = tool_calls
191                    .iter()
192                    .filter_map(|call| {
193                        call.function.as_ref().map(|func| {
194                            json!({
195                                "id": &call.id,
196                                "type": "function",
197                                "function": {
198                                    "name": &func.name,
199                                    "arguments": &func.arguments
200                                }
201                            })
202                        })
203                    })
204                    .collect::<Vec<_>>();
205                message_map.insert("tool_calls".to_owned(), Value::Array(serialized_calls));
206            }
207
208            if let Some(tool_call_id) = &message.tool_call_id {
209                message_map.insert("tool_call_id".to_owned(), Value::String(tool_call_id.clone()));
210            }
211
212            if message.role == MessageRole::Assistant
213                && is_minimax_m2_model(&request.model)
214                && let Some(reasoning_details) = &message.reasoning_details
215                && !reasoning_details.is_empty()
216            {
217                let normalized_details = normalize_reasoning_detail_objects(reasoning_details);
218                if !normalized_details.is_empty() {
219                    message_map.insert("reasoning_details".to_owned(), Value::Array(normalized_details));
220                }
221            }
222
223            messages.push(Value::Object(message_map));
224        }
225
226        Ok(messages)
227    }
228
229    fn format_for_chat_completions(&self, request: &LLMRequest) -> Result<Value, LLMError> {
230        let mut messages = self.serialize_messages_huggingface_chat(request)?;
231        let is_glm = self.is_glm_model(&request.model);
232
233        if let Some(system) = &request.system_prompt {
234            let has_system = messages.first().and_then(|m| m.get("role")).and_then(|r| r.as_str()) == Some("system");
235            if !has_system {
236                messages.insert(
237                    0,
238                    json!({
239                        "role": "system",
240                        "content": system
241                    }),
242                );
243            }
244        }
245
246        let mut payload = json!({
247            "model": request.model,
248            "messages": messages,
249            "stream": request.stream,
250        });
251
252        if request.stream && request.tools.is_some() && is_glm {
253            payload["tool_stream"] = json!(true);
254        }
255
256        if let Some(max_tokens) = request.max_tokens {
257            payload["max_tokens"] = json!(max_tokens);
258        }
259
260        if let Some(tools) = &request.tools {
261            if let Some(serialized) = self.serialize_tools_huggingface(tools) {
262                payload["tools"] = json!(serialized);
263
264                if let Some(choice) = &request.tool_choice {
265                    payload["tool_choice"] = choice.to_provider_format("openai");
266                }
267            }
268        }
269
270        if let Some(temperature) = request.temperature {
271            payload["temperature"] = json!(super::common::sampling_param_f64(temperature));
272        }
273
274        if let Some(top_p) = request.top_p {
275            payload["top_p"] = json!(super::common::sampling_param_f64(top_p));
276        }
277
278        if let Some(top_k) = request.top_k {
279            payload["top_k"] = json!(top_k);
280        }
281
282        if let Some(effort) = request.reasoning_effort {
283            use crate::rig_adapter::RigProviderCapabilities;
284            use vtcode_config::models::Provider;
285            let supported = self.supported_reasoning_efforts(&request.model);
286            if let Some(reasoning_params) = RigProviderCapabilities::new(Provider::HuggingFace, &request.model)
287                .reasoning_parameters_for_supported_efforts(effort, supported)?
288            {
289                if let Some(params_obj) = reasoning_params.as_object() {
290                    for (k, v) in params_obj {
291                        payload[k] = v.clone();
292                    }
293                }
294            }
295        }
296
297        if request.output_format.is_some() && !is_glm {
298            payload["response_format"] = json!({ "type": "json_object" });
299        }
300
301        Ok(payload)
302    }
303
304    pub(super) fn is_glm_model(&self, model: &str) -> bool {
305        let lower = model.to_ascii_lowercase();
306        lower.contains("glm")
307    }
308
309    fn is_deepseek_model(&self, model: &str) -> bool {
310        let lower = model.to_ascii_lowercase();
311        lower.contains("deepseek")
312    }
313
314    fn is_minimax_model(&self, model: &str) -> bool {
315        let lower = model.to_ascii_lowercase();
316        lower.contains("minimax")
317    }
318
319    fn is_muse_spark_open_model(&self, model: &str) -> bool {
320        let lower = model.to_ascii_lowercase();
321        lower.contains("muse-spark-open") || lower.contains("muse-spark")
322    }
323
324    fn apply_model_defaults(&self, request: &mut LLMRequest) {
325        if self.is_minimax_model(&request.model) {
326            if request.temperature.is_none() {
327                request.temperature = Some(1.0);
328            }
329            if request.top_p.is_none() {
330                request.top_p = Some(0.95);
331            }
332            if request.top_k.is_none() {
333                request.top_k = Some(40);
334            }
335        }
336    }
337
338    pub(super) fn add_json_instruction(&self, payload: &mut Value) -> Result<(), LLMError> {
339        if let Some(instructions) = payload.get_mut("instructions") {
340            if let Some(text) = instructions.as_str() {
341                if !text.contains("Return JSON") {
342                    *instructions = json!(format!("{}\n\n{}", text, JSON_INSTRUCTION));
343                }
344            }
345        } else {
346            payload["instructions"] = json!(JSON_INSTRUCTION);
347        }
348
349        Ok(())
350    }
351
352    fn should_use_responses_api(&self, _request: &LLMRequest) -> bool {
353        false
354    }
355
356    fn format_error(&self, status: StatusCode, body: &str) -> LLMError {
357        let message = format!("HuggingFace API error ({status}): {body}");
358
359        LLMError::Provider {
360            message: format_llm_error(PROVIDER_NAME, &message),
361            metadata: Some(LLMErrorMetadata::new(
362                PROVIDER_NAME,
363                Some(status.as_u16()),
364                None,
365                None,
366                None,
367                None,
368                Some(sanitize_provider_diagnostic(body.as_bytes())),
369            )),
370        }
371    }
372
373    fn parse_responses_api_format(json: &Value, model: String) -> Result<LLMResponse, LLMError> {
374        let convenience_text = json.get("output_text").and_then(|t| t.as_str());
375
376        let json_obj = json.get("response").unwrap_or(json);
377
378        let output = json_obj.get("output").and_then(|v| v.as_array());
379
380        let output_arr = match output {
381            Some(arr) => arr,
382            None => {
383                if let Some(text) = convenience_text {
384                    return Ok(LLMResponse {
385                        content: Some(text.to_string()),
386                        tool_calls: None,
387                        model,
388                        usage: None,
389                        finish_reason: crate::provider::FinishReason::Stop,
390                        reasoning: None,
391                        reasoning_details: None,
392                        tool_references: Vec::new(),
393                        request_id: None,
394                        organization_id: None,
395                        compaction: None,
396                    });
397                }
398
399                return Err(LLMError::Provider {
400                    message: format_llm_error(PROVIDER_NAME, "Not a Responses API format"),
401                    metadata: None,
402                });
403            }
404        };
405
406        let mut content_fragments: Vec<String> = Vec::new();
407        let mut reasoning_fragments: Vec<String> = Vec::new();
408        let mut tool_calls: Vec<crate::provider::ToolCall> = Vec::new();
409
410        for item in output_arr {
411            let item_type = item.get("type").and_then(|t| t.as_str()).unwrap_or("");
412
413            match item_type {
414                "message" => {
415                    if let Some(content_arr) = item.get("content").and_then(|c| c.as_array()) {
416                        for entry in content_arr {
417                            let entry_type = entry.get("type").and_then(|t| t.as_str()).unwrap_or("");
418                            match entry_type {
419                                "text" | "output_text" => {
420                                    if let Some(text) = entry.get("text").and_then(|t| t.as_str()) {
421                                        if !text.is_empty() {
422                                            content_fragments.push(text.to_string());
423                                        }
424                                    }
425                                }
426                                "reasoning" => {
427                                    if let Some(text) = entry.get("text").and_then(|t| t.as_str()) {
428                                        if !text.is_empty() {
429                                            reasoning_fragments.push(text.to_string());
430                                        }
431                                    }
432                                }
433                                "function_call" | "tool_call" => {
434                                    if let Some(call) = Self::parse_responses_tool_call(entry) {
435                                        tool_calls.push(call);
436                                    }
437                                }
438                                _ => {}
439                            }
440                        }
441                    }
442                }
443                "function_call" | "tool_call" => {
444                    if let Some(call) = Self::parse_responses_tool_call(item) {
445                        tool_calls.push(call);
446                    }
447                }
448                "reasoning" => {
449                    if let Some(summary_arr) = item.get("summary").and_then(|s| s.as_array()) {
450                        for summary in summary_arr {
451                            if let Some(text) = summary.get("text").and_then(|t| t.as_str()) {
452                                if !text.is_empty() {
453                                    reasoning_fragments.push(text.to_string());
454                                }
455                            }
456                        }
457                    } else if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
458                        reasoning_fragments.push(text.to_string());
459                    }
460                }
461                _ => {}
462            }
463        }
464
465        let content = if content_fragments.is_empty() {
466            convenience_text.map(|t| t.to_string())
467        } else {
468            Some(content_fragments.join(""))
469        };
470
471        let reasoning = if reasoning_fragments.is_empty() {
472            None
473        } else {
474            Some(reasoning_fragments.join("\n\n"))
475        };
476
477        let finish_reason = if !tool_calls.is_empty() {
478            crate::provider::FinishReason::ToolCalls
479        } else {
480            crate::provider::FinishReason::Stop
481        };
482
483        let usage_value = json.get("usage").or_else(|| json_obj.get("usage"));
484        let usage = usage_value.map(|usage_value| crate::provider::Usage {
485            prompt_tokens: usage_value
486                .get("input_tokens")
487                .or_else(|| usage_value.get("prompt_tokens"))
488                .and_then(|pt| pt.as_u64())
489                .unwrap_or(0) as u32,
490            completion_tokens: usage_value
491                .get("output_tokens")
492                .or_else(|| usage_value.get("completion_tokens"))
493                .and_then(|ct| ct.as_u64())
494                .unwrap_or(0) as u32,
495            total_tokens: usage_value.get("total_tokens").and_then(|tt| tt.as_u64()).unwrap_or(0) as u32,
496            cached_prompt_tokens: None,
497            cache_creation_tokens: None,
498            cache_read_tokens: None,
499            iterations: None,
500        });
501
502        Ok(LLMResponse {
503            content,
504            tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
505            model,
506            usage,
507            finish_reason,
508            reasoning,
509            reasoning_details: None,
510            tool_references: Vec::new(),
511            request_id: None,
512            organization_id: None,
513            compaction: None,
514        })
515    }
516
517    fn parse_responses_tool_call(item: &Value) -> Option<crate::provider::ToolCall> {
518        let call_id = item.get("id").and_then(|v| v.as_str()).unwrap_or("");
519        let function_obj = item.get("function").and_then(|v| v.as_object());
520        let name = function_obj.and_then(|f| f.get("name").and_then(|n| n.as_str()))?;
521        let arguments = function_obj.and_then(|f| f.get("arguments"));
522
523        let serialized = arguments.map_or("{}".to_owned(), |args| {
524            if args.is_string() {
525                args.as_str().unwrap_or("{}").to_string()
526            } else {
527                args.to_string()
528            }
529        });
530
531        Some(crate::provider::ToolCall::function(call_id.to_string(), name.to_string(), serialized))
532    }
533
534    async fn parse_response(
535        &self,
536        response: Response,
537        model: String,
538        use_responses_api: bool,
539    ) -> Result<LLMResponse, LLMError> {
540        let status = response.status();
541
542        if !status.is_success() {
543            let body = crate::providers::common::read_provider_error_body(response).await;
544            return Err(self.format_error(status, &body));
545        }
546
547        let json: Value = response.json().await.map_err(|err| format_parse_error(PROVIDER_NAME, &err))?;
548
549        if use_responses_api {
550            if json.get("output").is_some() {
551                return Self::parse_responses_api_format(&json, model);
552            }
553        }
554
555        parse_response_openai_format::<fn(&Value, &Value) -> Option<String>>(json, PROVIDER_NAME, model, false, None)
556    }
557
558    fn available_models() -> Vec<String> {
559        models::huggingface::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
560    }
561
562    fn get_endpoint(&self, use_responses_api: bool) -> String {
563        let base = self.base_url.trim_end_matches('/');
564        if use_responses_api {
565            format!("{base}/responses")
566        } else {
567            super::common::chat_completions_url(base)
568        }
569    }
570}
571
572#[async_trait]
573impl LLMProvider for HuggingFaceProvider {
574    fn name(&self) -> &str {
575        PROVIDER_KEY
576    }
577
578    fn supports_streaming(&self) -> bool {
579        true
580    }
581
582    fn supports_non_streaming(&self, _model: &str) -> bool {
583        // Pinned so the stream-timeout fallback cannot silently regress.
584        true
585    }
586
587    fn supports_reasoning(&self, model: &str) -> bool {
588        // Codex-inspired robustness: Setting model_supports_reasoning to false
589        // does NOT disable it for known reasoning models.
590        models::huggingface::REASONING_MODELS.contains(&model)
591            || self
592                .model_behavior
593                .as_ref()
594                .and_then(|b| b.model_supports_reasoning)
595                .unwrap_or(false)
596    }
597
598    fn supports_reasoning_effort(&self, model: &str) -> bool {
599        // Same robustness logic for reasoning effort
600        self.is_glm_model(model)
601            || self.is_deepseek_model(model)
602            || self.is_muse_spark_open_model(model)
603            || self
604                .model_behavior
605                .as_ref()
606                .and_then(|b| b.model_supports_reasoning_effort)
607                .unwrap_or(false)
608    }
609
610    fn supports_tools(&self, _model: &str) -> bool {
611        true
612    }
613
614    fn supports_parallel_tool_config(&self, _model: &str) -> bool {
615        false
616    }
617
618    fn supports_structured_output(&self, _model: &str) -> bool {
619        true
620    }
621
622    fn supports_context_caching(&self, _model: &str) -> bool {
623        false
624    }
625
626    fn effective_context_size(&self, model: &str) -> usize {
627        crate::provider::catalog_context_window("huggingface", model, 128_000)
628    }
629
630    async fn generate(&self, mut request: LLMRequest) -> Result<LLMResponse, LLMError> {
631        let model = ensure_model(&mut request, &self.model);
632
633        self.apply_model_defaults(&mut request);
634        self.validate_request(&request)?;
635
636        let model_id = self.normalize_model_id(&request.model)?;
637        request.model = model_id;
638
639        let use_responses_api = self.should_use_responses_api(&request);
640        let payload = if use_responses_api {
641            self.format_for_responses_api(&request)?
642        } else {
643            self.format_for_chat_completions(&request)?
644        };
645
646        let endpoint = self.get_endpoint(use_responses_api);
647
648        let response = self
649            .http_client
650            .post(&endpoint)
651            .header("Authorization", format!("Bearer {}", self.api_key))
652            .json(&payload)
653            .send()
654            .await
655            .map_err(|err| format_network_error(PROVIDER_NAME, &err))?;
656
657        self.parse_response(response, model, use_responses_api).await
658    }
659
660    async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream, LLMError> {
661        let model = ensure_model(&mut request, &self.model);
662
663        self.apply_model_defaults(&mut request);
664        self.validate_request(&request)?;
665        request.stream = true;
666
667        let model_id = self.normalize_model_id(&request.model)?;
668        request.model = model_id;
669
670        let use_responses_api = self.should_use_responses_api(&request);
671        let payload = if use_responses_api {
672            self.format_for_responses_api(&request)?
673        } else {
674            self.format_for_chat_completions(&request)?
675        };
676
677        let endpoint = self.get_endpoint(use_responses_api);
678
679        let response = self
680            .http_client
681            .post(&endpoint)
682            .header("Authorization", format!("Bearer {}", self.api_key))
683            .json(&payload)
684            .send()
685            .await
686            .map_err(|err| format_network_error(PROVIDER_NAME, &err))?;
687
688        if !response.status().is_success() {
689            let status = response.status();
690            let body = crate::providers::common::read_provider_error_body(response).await;
691            return Err(self.format_error(status, &body));
692        }
693
694        self.create_stream(response, model, use_responses_api).await
695    }
696
697    fn supported_models(&self) -> Vec<String> {
698        Self::available_models()
699    }
700
701    fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
702        if request.messages.is_empty() {
703            return Err(LLMError::InvalidRequest {
704                message: format_llm_error(PROVIDER_NAME, "Messages cannot be empty"),
705                metadata: None,
706            });
707        }
708
709        if request.model.trim().is_empty() {
710            return Err(LLMError::InvalidRequest {
711                message: format_llm_error(PROVIDER_NAME, "Model identifier cannot be empty"),
712                metadata: None,
713            });
714        }
715
716        Ok(())
717    }
718}
719
720impl HuggingFaceProvider {
721    async fn create_stream(
722        &self,
723        response: Response,
724        model: String,
725        use_responses_api: bool,
726    ) -> Result<LLMStream, LLMError> {
727        let mut bytes_stream = response.bytes_stream();
728        let mut buffer = String::with_capacity(4096);
729        let mut decoder = Utf8StreamDecoder::new();
730        let mut aggregator = crate::providers::shared::StreamAggregator::new(model.clone());
731        let telemetry = NoopStreamTelemetry;
732
733        let stream = try_stream! {
734            'outer: while let Some(chunk_result) = bytes_stream.next().await {
735                let chunk = chunk_result.map_err(|err| format_network_error(PROVIDER_NAME, &err))?;
736                buffer.push_str(&decoder.push(&chunk));
737
738                if buffer.len() > 128_000 {
739                    Err(LLMError::Provider {
740                        message: format_llm_error(PROVIDER_NAME, "Stream buffer exceeded maximum size (128KB)"),
741                        metadata: None,
742                    })?;
743                }
744
745                while let Some(newline_pos) = buffer.find('\n') {
746                    // Borrow the line from `buffer` instead of copying it into a
747                    // `String` per SSE event. The `drain` is deferred until
748                    // `serde_json::from_str` produces an owned `Value`, so the
749                    // borrow (`line`/`data`) is dead before `buffer` mutates.
750                    let line = buffer[..newline_pos].trim();
751
752                    if line.is_empty() || line.starts_with(':') {
753                        buffer.drain(..=newline_pos);
754                        continue;
755                    }
756
757                    let data = match line.strip_prefix("data: ") {
758                        Some(stripped) => stripped,
759                        None => {
760                            buffer.drain(..=newline_pos);
761                            continue;
762                        }
763                    };
764
765                    if data == "[DONE]" {
766                        buffer.drain(..=newline_pos);
767                        break 'outer;
768                    }
769
770                    let event: Value = match serde_json::from_str(data) {
771                        Ok(v) => v,
772                        Err(_) => {
773                            buffer.drain(..=newline_pos);
774                            continue;
775                        }
776                    };
777
778                    // `event` is now owned; all borrows of `buffer` (via
779                    // `line`/`data`) are dead. Safe to drain the consumed line.
780                    buffer.drain(..=newline_pos);
781
782                    if use_responses_api {
783                        let event_type = event.get("type").and_then(|t| t.as_str()).unwrap_or("");
784
785                        match event_type {
786                            "response.output_text.delta" | "output_text.delta" => {
787                                if let Some(delta) = event.get("delta").and_then(|d| d.as_str()) {
788                                    telemetry.on_content_delta(delta);
789                                    for ev in aggregator.handle_content(delta) {
790                                        yield ev;
791                                    }
792                                }
793                                continue;
794                            }
795                            "response.reasoning.delta" | "reasoning.delta" => {
796                                if let Some(delta) = event.get("delta").and_then(|d| d.as_str()) {
797                                    if let Some(d) = aggregator.handle_reasoning(delta) {
798                                        telemetry.on_reasoning_delta(&d);
799                                        yield LLMStreamEvent::Reasoning { delta: d };
800                                    }
801                                }
802                                continue;
803                            }
804                            "response.function_call_arguments.delta" | "tool_call.delta" => {
805                                telemetry.on_tool_call_delta();
806                                continue;
807                            }
808                            "response.completed" => {
809                                if let Some(response_obj) = event.get("response") {
810                                    if let Ok(response) = Self::parse_responses_api_format(response_obj, model.clone()) {
811                                        let final_agg_response = aggregator.finalize();
812                                        let mut merged_response = response;
813                                        if merged_response.content.is_none() {
814                                            merged_response.content = final_agg_response.content;
815                                        }
816                                        if merged_response.reasoning.is_none() {
817                                            merged_response.reasoning = final_agg_response.reasoning;
818                                        }
819                                        if merged_response.tool_calls.is_none() {
820                                            merged_response.tool_calls = final_agg_response.tool_calls;
821                                        }
822                                        if merged_response.usage.is_none() {
823                                            merged_response.usage = final_agg_response.usage;
824                                        }
825                                        yield LLMStreamEvent::Completed { response: Box::new(merged_response) };
826                                        return;
827                                    }
828                                }
829                                break 'outer;
830                            }
831                            "response.done" => {
832                                break 'outer;
833                            }
834                            _ => {}
835                        }
836                    }
837
838                    if let Some(choices_arr) = event.get("choices").and_then(|c| c.as_array()) {
839                        if let Some(choice) = choices_arr.first() {
840                            if let Some(delta_obj) = choice.get("delta") {
841                                if let Some(content) = delta_obj.get("content").and_then(|c| c.as_str()) {
842                                    telemetry.on_content_delta(content);
843                                    for ev in aggregator.handle_content(content) {
844                                        yield ev;
845                                    }
846                                }
847
848                                if let Some(reason) = delta_obj.get("reasoning_content").and_then(|r| r.as_str()) {
849                                    if let Some(d) = aggregator.handle_reasoning(reason) {
850                                        telemetry.on_reasoning_delta(&d);
851                                        yield LLMStreamEvent::Reasoning { delta: d };
852                                    }
853                                }
854
855                                if let Some(reasoning_details) = delta_obj
856                                    .get("reasoning_details")
857                                    .and_then(|details| details.as_array())
858                                {
859                                    aggregator.set_reasoning_details(reasoning_details);
860                                }
861
862                                if let Some(tool_calls_arr) = delta_obj.get("tool_calls").and_then(|tc| tc.as_array()) {
863                                    aggregator.handle_tool_calls(tool_calls_arr);
864                                    telemetry.on_tool_call_delta();
865                                }
866                            }
867
868                            if let Some(finish_reason_str) = choice.get("finish_reason").and_then(|fr| fr.as_str()) {
869                                aggregator.set_finish_reason(map_finish_reason_common(finish_reason_str));
870                                if let Some(usage_value) = event.get("usage") {
871                                    aggregator.set_usage(crate::provider::Usage {
872                                        prompt_tokens: usage_value.get("prompt_tokens").and_then(|pt| pt.as_u64()).unwrap_or(0) as u32,
873                                        completion_tokens: usage_value.get("completion_tokens").and_then(|ct| ct.as_u64()).unwrap_or(0) as u32,
874                                        total_tokens: usage_value.get("total_tokens").and_then(|tt| tt.as_u64()).unwrap_or(0) as u32,
875                                        cached_prompt_tokens: None,
876                                        cache_creation_tokens: None,
877                                        cache_read_tokens: None,
878                                        iterations: None,
879                                    });
880                                }
881
882                                break 'outer;
883                            }
884                        }
885                    }
886                }
887            }
888
889            yield LLMStreamEvent::Completed { response: Box::new(aggregator.finalize()) };
890        };
891
892        Ok(Box::pin(stream))
893    }
894}
895
896impl_llm_client!(HuggingFaceProvider);
897
898#[cfg(test)]
899mod tests;