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 true
585 }
586
587 fn supports_reasoning(&self, model: &str) -> bool {
588 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 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 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 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;