vtcode_core/llm/providers/
openrouter.rs

1use crate::config::constants::{models, urls};
2use crate::llm::client::LLMClient;
3use crate::llm::error_display;
4use crate::llm::provider::{
5    FinishReason, LLMError, LLMProvider, LLMRequest, LLMResponse, LLMStream, LLMStreamEvent,
6    Message, MessageRole, ToolCall, ToolChoice, ToolDefinition, Usage,
7};
8use crate::llm::types as llm_types;
9use async_stream::try_stream;
10use async_trait::async_trait;
11use futures::StreamExt;
12use reqwest::Client as HttpClient;
13use serde_json::{Map, Value, json};
14
15use super::{extract_reasoning_trace, gpt5_codex_developer_prompt};
16
17#[derive(Default, Clone)]
18struct ToolCallBuilder {
19    id: Option<String>,
20    name: Option<String>,
21    arguments: String,
22}
23
24impl ToolCallBuilder {
25    fn finalize(self, fallback_index: usize) -> Option<ToolCall> {
26        let name = self.name?;
27        let id = self
28            .id
29            .unwrap_or_else(|| format!("tool_call_{}", fallback_index));
30        let arguments = if self.arguments.is_empty() {
31            "{}".to_string()
32        } else {
33            self.arguments
34        };
35        Some(ToolCall::function(id, name, arguments))
36    }
37}
38
39fn update_tool_calls(builders: &mut Vec<ToolCallBuilder>, deltas: &[Value]) {
40    for (index, delta) in deltas.iter().enumerate() {
41        if builders.len() <= index {
42            builders.push(ToolCallBuilder::default());
43        }
44        let builder = builders
45            .get_mut(index)
46            .expect("tool call builder must exist after push");
47
48        if let Some(id) = delta.get("id").and_then(|v| v.as_str()) {
49            builder.id = Some(id.to_string());
50        }
51
52        if let Some(function) = delta.get("function") {
53            if let Some(name) = function.get("name").and_then(|v| v.as_str()) {
54                builder.name = Some(name.to_string());
55            }
56
57            if let Some(arguments_value) = function.get("arguments") {
58                if let Some(arguments) = arguments_value.as_str() {
59                    builder.arguments.push_str(arguments);
60                } else if arguments_value.is_object() || arguments_value.is_array() {
61                    builder.arguments.push_str(&arguments_value.to_string());
62                }
63            }
64        }
65    }
66}
67
68fn finalize_tool_calls(builders: Vec<ToolCallBuilder>) -> Option<Vec<ToolCall>> {
69    let calls: Vec<ToolCall> = builders
70        .into_iter()
71        .enumerate()
72        .filter_map(|(index, builder)| builder.finalize(index))
73        .collect();
74
75    if calls.is_empty() { None } else { Some(calls) }
76}
77
78#[derive(Debug, PartialEq, Eq)]
79enum StreamFragment {
80    Content(String),
81    Reasoning(String),
82}
83
84#[derive(Default, Debug)]
85struct StreamDelta {
86    fragments: Vec<StreamFragment>,
87}
88
89impl StreamDelta {
90    fn push_content(&mut self, text: &str) {
91        if text.is_empty() {
92            return;
93        }
94
95        match self.fragments.last_mut() {
96            Some(StreamFragment::Content(existing)) => existing.push_str(text),
97            _ => self
98                .fragments
99                .push(StreamFragment::Content(text.to_string())),
100        }
101    }
102
103    fn push_reasoning(&mut self, text: &str) {
104        if text.is_empty() {
105            return;
106        }
107
108        match self.fragments.last_mut() {
109            Some(StreamFragment::Reasoning(existing)) => existing.push_str(text),
110            _ => self
111                .fragments
112                .push(StreamFragment::Reasoning(text.to_string())),
113        }
114    }
115
116    fn is_empty(&self) -> bool {
117        self.fragments.is_empty()
118    }
119
120    fn into_fragments(self) -> Vec<StreamFragment> {
121        self.fragments
122    }
123
124    fn extend(&mut self, other: StreamDelta) {
125        self.fragments.extend(other.fragments);
126    }
127}
128
129#[derive(Default, Clone)]
130struct ReasoningBuffer {
131    text: String,
132    last_chunk: Option<String>,
133}
134
135impl ReasoningBuffer {
136    fn push(&mut self, chunk: &str) -> Option<String> {
137        if chunk.trim().is_empty() {
138            return None;
139        }
140
141        let normalized = Self::normalize_chunk(chunk);
142
143        if normalized.is_empty() {
144            return None;
145        }
146
147        if self.last_chunk.as_deref() == Some(&normalized) {
148            return None;
149        }
150
151        let last_has_spacing = self.text.ends_with(' ') || self.text.ends_with('\n');
152        let chunk_starts_with_space = chunk
153            .chars()
154            .next()
155            .map(|value| value.is_whitespace())
156            .unwrap_or(false);
157        let leading_punctuation = Self::is_leading_punctuation(chunk);
158        let trailing_connector = Self::ends_with_connector(&self.text);
159
160        let mut delta = String::new();
161
162        if !self.text.is_empty()
163            && !last_has_spacing
164            && !chunk_starts_with_space
165            && !leading_punctuation
166            && !trailing_connector
167        {
168            delta.push(' ');
169        }
170
171        delta.push_str(&normalized);
172        self.text.push_str(&delta);
173        self.last_chunk = Some(normalized);
174
175        Some(delta)
176    }
177
178    fn finalize(self) -> Option<String> {
179        let trimmed = self.text.trim();
180        if trimmed.is_empty() {
181            None
182        } else {
183            Some(trimmed.to_string())
184        }
185    }
186
187    fn normalize_chunk(chunk: &str) -> String {
188        let mut normalized = String::new();
189        for part in chunk.split_whitespace() {
190            if !normalized.is_empty() {
191                normalized.push(' ');
192            }
193            normalized.push_str(part);
194        }
195        normalized
196    }
197
198    fn is_leading_punctuation(chunk: &str) -> bool {
199        chunk
200            .chars()
201            .find(|ch| !ch.is_whitespace())
202            .map(|ch| matches!(ch, ',' | '.' | '!' | '?' | ':' | ';' | ')' | ']' | '}'))
203            .unwrap_or(false)
204    }
205
206    fn ends_with_connector(text: &str) -> bool {
207        text.chars()
208            .rev()
209            .find(|ch| !ch.is_whitespace())
210            .map(|ch| matches!(ch, '(' | '[' | '{' | '/' | '-'))
211            .unwrap_or(false)
212    }
213}
214
215fn apply_tool_call_delta_from_content(
216    builders: &mut Vec<ToolCallBuilder>,
217    container: &Map<String, Value>,
218) {
219    if let Some(nested) = container.get("delta").and_then(|value| value.as_object()) {
220        apply_tool_call_delta_from_content(builders, nested);
221    }
222
223    let (index, delta_source) = if let Some(tool_call_value) = container.get("tool_call") {
224        match tool_call_value.as_object() {
225            Some(tool_call) => {
226                let idx = tool_call
227                    .get("index")
228                    .and_then(|value| value.as_u64())
229                    .unwrap_or(0) as usize;
230                (idx, tool_call)
231            }
232            None => (0usize, container),
233        }
234    } else {
235        let idx = container
236            .get("index")
237            .and_then(|value| value.as_u64())
238            .unwrap_or(0) as usize;
239        (idx, container)
240    };
241
242    let mut delta_map = Map::new();
243
244    if let Some(id_value) = delta_source.get("id") {
245        delta_map.insert("id".to_string(), id_value.clone());
246    }
247
248    if let Some(function_value) = delta_source.get("function") {
249        delta_map.insert("function".to_string(), function_value.clone());
250    }
251
252    if delta_map.is_empty() {
253        return;
254    }
255
256    if builders.len() <= index {
257        builders.resize_with(index + 1, ToolCallBuilder::default);
258    }
259
260    let mut deltas = vec![Value::Null; index + 1];
261    deltas[index] = Value::Object(delta_map);
262    update_tool_calls(builders, &deltas);
263}
264
265fn process_content_object(
266    map: &Map<String, Value>,
267    aggregated_content: &mut String,
268    reasoning: &mut ReasoningBuffer,
269    tool_call_builders: &mut Vec<ToolCallBuilder>,
270    deltas: &mut StreamDelta,
271) {
272    if let Some(content_type) = map.get("type").and_then(|value| value.as_str()) {
273        match content_type {
274            "reasoning" | "thinking" | "analysis" => {
275                if let Some(text_value) = map.get("text").and_then(|value| value.as_str()) {
276                    if let Some(delta) = reasoning.push(text_value) {
277                        deltas.push_reasoning(&delta);
278                    }
279                } else if let Some(text_value) =
280                    map.get("output_text").and_then(|value| value.as_str())
281                {
282                    if let Some(delta) = reasoning.push(text_value) {
283                        deltas.push_reasoning(&delta);
284                    }
285                }
286                return;
287            }
288            "tool_call_delta" | "tool_call" => {
289                apply_tool_call_delta_from_content(tool_call_builders, map);
290                return;
291            }
292            _ => {}
293        }
294    }
295
296    if let Some(tool_call_value) = map.get("tool_call").and_then(|value| value.as_object()) {
297        apply_tool_call_delta_from_content(tool_call_builders, tool_call_value);
298        return;
299    }
300
301    if let Some(text_value) = map.get("text").and_then(|value| value.as_str()) {
302        if !text_value.is_empty() {
303            aggregated_content.push_str(text_value);
304            deltas.push_content(text_value);
305        }
306        return;
307    }
308
309    if let Some(text_value) = map.get("output_text").and_then(|value| value.as_str()) {
310        if !text_value.is_empty() {
311            aggregated_content.push_str(text_value);
312            deltas.push_content(text_value);
313        }
314        return;
315    }
316
317    if let Some(text_value) = map
318        .get("output_text_delta")
319        .and_then(|value| value.as_str())
320    {
321        if !text_value.is_empty() {
322            aggregated_content.push_str(text_value);
323            deltas.push_content(text_value);
324        }
325        return;
326    }
327
328    for key in ["content", "items", "output", "outputs", "delta"] {
329        if let Some(inner) = map.get(key) {
330            process_content_value(
331                inner,
332                aggregated_content,
333                reasoning,
334                tool_call_builders,
335                deltas,
336            );
337        }
338    }
339}
340
341fn process_content_part(
342    part: &Value,
343    aggregated_content: &mut String,
344    reasoning: &mut ReasoningBuffer,
345    tool_call_builders: &mut Vec<ToolCallBuilder>,
346    deltas: &mut StreamDelta,
347) {
348    if let Some(text) = part.as_str() {
349        if !text.is_empty() {
350            aggregated_content.push_str(text);
351            deltas.push_content(text);
352        }
353        return;
354    }
355
356    if let Some(map) = part.as_object() {
357        process_content_object(
358            map,
359            aggregated_content,
360            reasoning,
361            tool_call_builders,
362            deltas,
363        );
364        return;
365    }
366
367    if part.is_array() {
368        process_content_value(
369            part,
370            aggregated_content,
371            reasoning,
372            tool_call_builders,
373            deltas,
374        );
375    }
376}
377
378fn process_content_value(
379    value: &Value,
380    aggregated_content: &mut String,
381    reasoning: &mut ReasoningBuffer,
382    tool_call_builders: &mut Vec<ToolCallBuilder>,
383    deltas: &mut StreamDelta,
384) {
385    match value {
386        Value::String(text) => {
387            if !text.is_empty() {
388                aggregated_content.push_str(text);
389                deltas.push_content(text);
390            }
391        }
392        Value::Array(parts) => {
393            for part in parts {
394                process_content_part(
395                    part,
396                    aggregated_content,
397                    reasoning,
398                    tool_call_builders,
399                    deltas,
400                );
401            }
402        }
403        Value::Object(map) => {
404            process_content_object(
405                map,
406                aggregated_content,
407                reasoning,
408                tool_call_builders,
409                deltas,
410            );
411        }
412        _ => {}
413    }
414}
415
416fn extract_tool_calls_from_content(message: &Value) -> Option<Vec<ToolCall>> {
417    let parts = message.get("content").and_then(|value| value.as_array())?;
418    let mut calls: Vec<ToolCall> = Vec::new();
419
420    for (index, part) in parts.iter().enumerate() {
421        let map = match part.as_object() {
422            Some(value) => value,
423            None => continue,
424        };
425
426        let content_type = map.get("type").and_then(|value| value.as_str());
427        let is_tool_call = matches!(content_type, Some("tool_call") | Some("function_call"))
428            || (content_type.is_none()
429                && map.contains_key("name")
430                && map.contains_key("arguments"));
431
432        if !is_tool_call {
433            continue;
434        }
435
436        let id = map
437            .get("id")
438            .and_then(|value| value.as_str())
439            .map(|value| value.to_string())
440            .unwrap_or_else(|| format!("tool_call_{}", index));
441
442        let (name, arguments_value) =
443            if let Some(function) = map.get("function").and_then(|value| value.as_object()) {
444                (
445                    function
446                        .get("name")
447                        .and_then(|value| value.as_str())
448                        .map(|value| value.to_string()),
449                    function.get("arguments"),
450                )
451            } else {
452                (
453                    map.get("name")
454                        .and_then(|value| value.as_str())
455                        .map(|value| value.to_string()),
456                    map.get("arguments"),
457                )
458            };
459
460        let Some(name) = name else {
461            continue;
462        };
463
464        let arguments = arguments_value
465            .map(|value| {
466                if let Some(text) = value.as_str() {
467                    text.to_string()
468                } else if value.is_null() {
469                    "{}".to_string()
470                } else {
471                    value.to_string()
472                }
473            })
474            .unwrap_or_else(|| "{}".to_string());
475
476        calls.push(ToolCall::function(id, name, arguments));
477    }
478
479    if calls.is_empty() { None } else { Some(calls) }
480}
481
482fn extract_reasoning_from_message_content(message: &Value) -> Option<String> {
483    let parts = message.get("content")?.as_array()?;
484    let mut segments: Vec<String> = Vec::new();
485
486    for part in parts {
487        match part {
488            Value::Object(map) => {
489                let part_type = map
490                    .get("type")
491                    .and_then(|value| value.as_str())
492                    .unwrap_or("");
493
494                if matches!(part_type, "reasoning" | "thinking" | "analysis") {
495                    if let Some(extracted) = extract_reasoning_trace(part) {
496                        if !extracted.trim().is_empty() {
497                            segments.push(extracted);
498                            continue;
499                        }
500                    }
501
502                    if let Some(text) = map.get("text").and_then(|value| value.as_str()) {
503                        let trimmed = text.trim();
504                        if !trimmed.is_empty() {
505                            segments.push(trimmed.to_string());
506                        }
507                    }
508                }
509            }
510            Value::String(text) => {
511                let trimmed = text.trim();
512                if !trimmed.is_empty() {
513                    segments.push(trimmed.to_string());
514                }
515            }
516            _ => {}
517        }
518    }
519
520    if segments.is_empty() {
521        None
522    } else {
523        let mut combined = String::new();
524        for (idx, segment) in segments.iter().enumerate() {
525            if idx > 0 {
526                combined.push('\n');
527            }
528            combined.push_str(segment);
529        }
530        Some(combined)
531    }
532}
533
534fn parse_usage_value(value: &Value) -> Usage {
535    Usage {
536        prompt_tokens: value
537            .get("prompt_tokens")
538            .and_then(|pt| pt.as_u64())
539            .unwrap_or(0) as u32,
540        completion_tokens: value
541            .get("completion_tokens")
542            .and_then(|ct| ct.as_u64())
543            .unwrap_or(0) as u32,
544        total_tokens: value
545            .get("total_tokens")
546            .and_then(|tt| tt.as_u64())
547            .unwrap_or(0) as u32,
548    }
549}
550
551fn map_finish_reason(reason: &str) -> FinishReason {
552    match reason {
553        "stop" | "completed" | "done" | "finished" => FinishReason::Stop,
554        "length" => FinishReason::Length,
555        "tool_calls" => FinishReason::ToolCalls,
556        "content_filter" => FinishReason::ContentFilter,
557        other => FinishReason::Error(other.to_string()),
558    }
559}
560
561fn push_reasoning_value(reasoning: &mut ReasoningBuffer, value: &Value, deltas: &mut StreamDelta) {
562    if let Some(reasoning_text) = extract_reasoning_trace(value) {
563        if let Some(delta) = reasoning.push(&reasoning_text) {
564            deltas.push_reasoning(&delta);
565        }
566    } else if let Some(text_value) = value.get("text").and_then(|v| v.as_str()) {
567        if let Some(delta) = reasoning.push(text_value) {
568            deltas.push_reasoning(&delta);
569        }
570    }
571}
572
573fn parse_chat_completion_chunk(
574    payload: &Value,
575    aggregated_content: &mut String,
576    tool_call_builders: &mut Vec<ToolCallBuilder>,
577    reasoning: &mut ReasoningBuffer,
578    finish_reason: &mut FinishReason,
579) -> StreamDelta {
580    let mut deltas = StreamDelta::default();
581
582    if let Some(choices) = payload.get("choices").and_then(|c| c.as_array()) {
583        if let Some(choice) = choices.first() {
584            if let Some(delta) = choice.get("delta") {
585                if let Some(content_value) = delta.get("content") {
586                    process_content_value(
587                        content_value,
588                        aggregated_content,
589                        reasoning,
590                        tool_call_builders,
591                        &mut deltas,
592                    );
593                }
594
595                if let Some(reasoning_value) = delta.get("reasoning") {
596                    push_reasoning_value(reasoning, reasoning_value, &mut deltas);
597                }
598
599                if let Some(tool_calls_value) = delta.get("tool_calls").and_then(|v| v.as_array()) {
600                    update_tool_calls(tool_call_builders, tool_calls_value);
601                }
602            }
603
604            if let Some(reasoning_value) = choice.get("reasoning") {
605                push_reasoning_value(reasoning, reasoning_value, &mut deltas);
606            }
607
608            if let Some(reason) = choice.get("finish_reason").and_then(|v| v.as_str()) {
609                *finish_reason = map_finish_reason(reason);
610            }
611        }
612    }
613
614    deltas
615}
616
617fn parse_response_chunk(
618    payload: &Value,
619    aggregated_content: &mut String,
620    tool_call_builders: &mut Vec<ToolCallBuilder>,
621    reasoning: &mut ReasoningBuffer,
622    finish_reason: &mut FinishReason,
623) -> StreamDelta {
624    let mut deltas = StreamDelta::default();
625
626    if let Some(delta_value) = payload.get("delta") {
627        process_content_value(
628            delta_value,
629            aggregated_content,
630            reasoning,
631            tool_call_builders,
632            &mut deltas,
633        );
634    }
635
636    if let Some(event_type) = payload.get("type").and_then(|v| v.as_str()) {
637        match event_type {
638            "response.reasoning.delta" => {
639                if let Some(delta_value) = payload.get("delta") {
640                    push_reasoning_value(reasoning, delta_value, &mut deltas);
641                }
642            }
643            "response.tool_call.delta" => {
644                if let Some(delta_object) = payload.get("delta").and_then(|v| v.as_object()) {
645                    apply_tool_call_delta_from_content(tool_call_builders, delta_object);
646                }
647            }
648            "response.completed" | "response.done" | "response.finished" => {
649                if let Some(response_obj) = payload.get("response") {
650                    if aggregated_content.is_empty() {
651                        process_content_value(
652                            response_obj,
653                            aggregated_content,
654                            reasoning,
655                            tool_call_builders,
656                            &mut deltas,
657                        );
658                    }
659
660                    if let Some(reason) = response_obj
661                        .get("stop_reason")
662                        .and_then(|value| value.as_str())
663                        .or_else(|| response_obj.get("status").and_then(|value| value.as_str()))
664                    {
665                        *finish_reason = map_finish_reason(reason);
666                    }
667                }
668            }
669            _ => {}
670        }
671    }
672
673    if let Some(response_obj) = payload.get("response") {
674        if aggregated_content.is_empty() {
675            if let Some(content_value) = response_obj
676                .get("output_text")
677                .or_else(|| response_obj.get("output"))
678                .or_else(|| response_obj.get("content"))
679            {
680                process_content_value(
681                    content_value,
682                    aggregated_content,
683                    reasoning,
684                    tool_call_builders,
685                    &mut deltas,
686                );
687            }
688        }
689    }
690
691    if let Some(reasoning_value) = payload.get("reasoning") {
692        push_reasoning_value(reasoning, reasoning_value, &mut deltas);
693    }
694
695    deltas
696}
697
698fn update_usage_from_value(source: &Value, usage: &mut Option<Usage>) {
699    if let Some(usage_value) = source.get("usage") {
700        *usage = Some(parse_usage_value(usage_value));
701    }
702}
703
704fn extract_data_payload(event: &str) -> Option<String> {
705    let mut data_lines: Vec<String> = Vec::new();
706
707    for raw_line in event.lines() {
708        let line = raw_line.trim_end_matches('\r');
709        if line.is_empty() || line.starts_with(':') {
710            continue;
711        }
712
713        if let Some(value) = line.strip_prefix("data:") {
714            data_lines.push(value.trim_start().to_string());
715        }
716    }
717
718    if data_lines.is_empty() {
719        None
720    } else {
721        Some(data_lines.join("\n"))
722    }
723}
724
725fn parse_stream_payload(
726    payload: &Value,
727    aggregated_content: &mut String,
728    tool_call_builders: &mut Vec<ToolCallBuilder>,
729    reasoning: &mut ReasoningBuffer,
730    usage: &mut Option<Usage>,
731    finish_reason: &mut FinishReason,
732) -> Option<StreamDelta> {
733    let mut emitted_delta = StreamDelta::default();
734
735    let chat_delta = parse_chat_completion_chunk(
736        payload,
737        aggregated_content,
738        tool_call_builders,
739        reasoning,
740        finish_reason,
741    );
742    emitted_delta.extend(chat_delta);
743
744    let response_delta = parse_response_chunk(
745        payload,
746        aggregated_content,
747        tool_call_builders,
748        reasoning,
749        finish_reason,
750    );
751    emitted_delta.extend(response_delta);
752
753    update_usage_from_value(payload, usage);
754    if let Some(response_obj) = payload.get("response") {
755        update_usage_from_value(response_obj, usage);
756        if let Some(reason) = response_obj
757            .get("finish_reason")
758            .and_then(|value| value.as_str())
759        {
760            *finish_reason = map_finish_reason(reason);
761        }
762    }
763
764    if emitted_delta.is_empty() {
765        None
766    } else {
767        Some(emitted_delta)
768    }
769}
770
771fn finalize_stream_response(
772    aggregated_content: String,
773    tool_call_builders: Vec<ToolCallBuilder>,
774    usage: Option<Usage>,
775    finish_reason: FinishReason,
776    reasoning: ReasoningBuffer,
777) -> LLMResponse {
778    let content = if aggregated_content.is_empty() {
779        None
780    } else {
781        Some(aggregated_content)
782    };
783
784    let reasoning = reasoning.finalize();
785
786    LLMResponse {
787        content,
788        tool_calls: finalize_tool_calls(tool_call_builders),
789        usage,
790        finish_reason,
791        reasoning,
792    }
793}
794
795pub struct OpenRouterProvider {
796    api_key: String,
797    http_client: HttpClient,
798    base_url: String,
799    model: String,
800}
801
802impl OpenRouterProvider {
803    pub fn new(api_key: String) -> Self {
804        Self::with_model(api_key, models::openrouter::DEFAULT_MODEL.to_string())
805    }
806
807    pub fn with_model(api_key: String, model: String) -> Self {
808        Self {
809            api_key,
810            http_client: HttpClient::new(),
811            base_url: urls::OPENROUTER_API_BASE.to_string(),
812            model,
813        }
814    }
815
816    pub fn from_config(
817        api_key: Option<String>,
818        model: Option<String>,
819        base_url: Option<String>,
820    ) -> Self {
821        let api_key_value = api_key.unwrap_or_default();
822        let mut provider = if let Some(model_value) = model {
823            Self::with_model(api_key_value, model_value)
824        } else {
825            Self::new(api_key_value)
826        };
827        if let Some(base) = base_url {
828            provider.base_url = base;
829        }
830        provider
831    }
832
833    fn default_request(&self, prompt: &str) -> LLMRequest {
834        LLMRequest {
835            messages: vec![Message::user(prompt.to_string())],
836            system_prompt: None,
837            tools: None,
838            model: self.model.clone(),
839            max_tokens: None,
840            temperature: None,
841            stream: false,
842            tool_choice: None,
843            parallel_tool_calls: None,
844            parallel_tool_config: None,
845            reasoning_effort: None,
846        }
847    }
848
849    fn parse_client_prompt(&self, prompt: &str) -> LLMRequest {
850        let trimmed = prompt.trim_start();
851        if trimmed.starts_with('{') {
852            if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
853                if let Some(request) = self.parse_chat_request(&value) {
854                    return request;
855                }
856            }
857        }
858
859        self.default_request(prompt)
860    }
861
862    fn is_gpt5_codex_model(model: &str) -> bool {
863        model == models::openrouter::OPENAI_GPT_5_CODEX
864    }
865
866    fn resolve_model<'a>(&'a self, request: &'a LLMRequest) -> &'a str {
867        if request.model.trim().is_empty() {
868            self.model.as_str()
869        } else {
870            request.model.as_str()
871        }
872    }
873
874    fn uses_responses_api_for(&self, request: &LLMRequest) -> bool {
875        Self::is_gpt5_codex_model(self.resolve_model(request))
876    }
877
878    fn parse_chat_request(&self, value: &Value) -> Option<LLMRequest> {
879        let messages_value = value.get("messages")?.as_array()?;
880        let mut system_prompt = None;
881        let mut messages = Vec::new();
882
883        for entry in messages_value {
884            let role = entry
885                .get("role")
886                .and_then(|r| r.as_str())
887                .unwrap_or(crate::config::constants::message_roles::USER);
888            let content = entry.get("content");
889            let text_content = content.map(Self::extract_content_text).unwrap_or_default();
890
891            match role {
892                "system" => {
893                    if system_prompt.is_none() && !text_content.is_empty() {
894                        system_prompt = Some(text_content);
895                    }
896                }
897                "assistant" => {
898                    let tool_calls = entry
899                        .get("tool_calls")
900                        .and_then(|tc| tc.as_array())
901                        .map(|calls| {
902                            calls
903                                .iter()
904                                .filter_map(|call| {
905                                    let id = call.get("id").and_then(|v| v.as_str())?;
906                                    let function = call.get("function")?;
907                                    let name = function.get("name").and_then(|v| v.as_str())?;
908                                    let arguments = function.get("arguments");
909                                    let serialized = arguments.map_or("{}".to_string(), |value| {
910                                        if value.is_string() {
911                                            value.as_str().unwrap_or("").to_string()
912                                        } else {
913                                            value.to_string()
914                                        }
915                                    });
916                                    Some(ToolCall::function(
917                                        id.to_string(),
918                                        name.to_string(),
919                                        serialized,
920                                    ))
921                                })
922                                .collect::<Vec<_>>()
923                        })
924                        .filter(|calls| !calls.is_empty());
925
926                    let message = if let Some(calls) = tool_calls {
927                        Message {
928                            role: MessageRole::Assistant,
929                            content: text_content,
930                            tool_calls: Some(calls),
931                            tool_call_id: None,
932                        }
933                    } else {
934                        Message::assistant(text_content)
935                    };
936                    messages.push(message);
937                }
938                "tool" => {
939                    let tool_call_id = entry
940                        .get("tool_call_id")
941                        .and_then(|id| id.as_str())
942                        .map(|s| s.to_string());
943                    let content_value = entry
944                        .get("content")
945                        .map(|value| {
946                            if text_content.is_empty() {
947                                value.to_string()
948                            } else {
949                                text_content.clone()
950                            }
951                        })
952                        .unwrap_or_else(|| text_content.clone());
953                    messages.push(Message {
954                        role: MessageRole::Tool,
955                        content: content_value,
956                        tool_calls: None,
957                        tool_call_id,
958                    });
959                }
960                _ => {
961                    messages.push(Message::user(text_content));
962                }
963            }
964        }
965
966        if messages.is_empty() {
967            return None;
968        }
969
970        let tools = value.get("tools").and_then(|tools_value| {
971            let tools_array = tools_value.as_array()?;
972            let converted: Vec<_> = tools_array
973                .iter()
974                .filter_map(|tool| {
975                    let function = tool.get("function")?;
976                    let name = function.get("name").and_then(|n| n.as_str())?;
977                    let description = function
978                        .get("description")
979                        .and_then(|d| d.as_str())
980                        .unwrap_or("")
981                        .to_string();
982                    let parameters = function
983                        .get("parameters")
984                        .cloned()
985                        .unwrap_or_else(|| json!({}));
986                    Some(ToolDefinition::function(
987                        name.to_string(),
988                        description,
989                        parameters,
990                    ))
991                })
992                .collect();
993
994            if converted.is_empty() {
995                None
996            } else {
997                Some(converted)
998            }
999        });
1000
1001        let max_tokens = value
1002            .get("max_tokens")
1003            .and_then(|v| v.as_u64())
1004            .map(|v| v as u32);
1005        let temperature = value
1006            .get("temperature")
1007            .and_then(|v| v.as_f64())
1008            .map(|v| v as f32);
1009        let stream = value
1010            .get("stream")
1011            .and_then(|v| v.as_bool())
1012            .unwrap_or(false);
1013        let tool_choice = value.get("tool_choice").and_then(Self::parse_tool_choice);
1014        let parallel_tool_calls = value.get("parallel_tool_calls").and_then(|v| v.as_bool());
1015        let reasoning_effort = value
1016            .get("reasoning_effort")
1017            .and_then(|v| v.as_str())
1018            .map(|s| s.to_string())
1019            .or_else(|| {
1020                value
1021                    .get("reasoning")
1022                    .and_then(|r| r.get("effort"))
1023                    .and_then(|effort| effort.as_str())
1024                    .map(|s| s.to_string())
1025            });
1026
1027        let model = value
1028            .get("model")
1029            .and_then(|m| m.as_str())
1030            .unwrap_or(&self.model)
1031            .to_string();
1032
1033        Some(LLMRequest {
1034            messages,
1035            system_prompt,
1036            tools,
1037            model,
1038            max_tokens,
1039            temperature,
1040            stream,
1041            tool_choice,
1042            parallel_tool_calls,
1043            parallel_tool_config: None,
1044            reasoning_effort,
1045        })
1046    }
1047
1048    fn extract_content_text(content: &Value) -> String {
1049        match content {
1050            Value::String(text) => text.to_string(),
1051            Value::Array(parts) => parts
1052                .iter()
1053                .filter_map(|part| {
1054                    if let Some(text) = part.get("text").and_then(|t| t.as_str()) {
1055                        Some(text.to_string())
1056                    } else if let Some(Value::String(text)) = part.get("content") {
1057                        Some(text.clone())
1058                    } else {
1059                        None
1060                    }
1061                })
1062                .collect::<Vec<_>>()
1063                .join(""),
1064            _ => String::new(),
1065        }
1066    }
1067
1068    fn parse_tool_choice(choice: &Value) -> Option<ToolChoice> {
1069        match choice {
1070            Value::String(value) => match value.as_str() {
1071                "auto" => Some(ToolChoice::auto()),
1072                "none" => Some(ToolChoice::none()),
1073                "required" => Some(ToolChoice::any()),
1074                _ => None,
1075            },
1076            Value::Object(map) => {
1077                let choice_type = map.get("type").and_then(|t| t.as_str())?;
1078                match choice_type {
1079                    "function" => map
1080                        .get("function")
1081                        .and_then(|f| f.get("name"))
1082                        .and_then(|n| n.as_str())
1083                        .map(|name| ToolChoice::function(name.to_string())),
1084                    "auto" => Some(ToolChoice::auto()),
1085                    "none" => Some(ToolChoice::none()),
1086                    "any" | "required" => Some(ToolChoice::any()),
1087                    _ => None,
1088                }
1089            }
1090            _ => None,
1091        }
1092    }
1093
1094    fn build_standard_responses_input(&self, request: &LLMRequest) -> Result<Vec<Value>, LLMError> {
1095        let mut input = Vec::new();
1096
1097        if let Some(system_prompt) = &request.system_prompt {
1098            if !system_prompt.trim().is_empty() {
1099                input.push(json!({
1100                    "role": "developer",
1101                    "content": [{
1102                        "type": "input_text",
1103                        "text": system_prompt.clone()
1104                    }]
1105                }));
1106            }
1107        }
1108
1109        for msg in &request.messages {
1110            match msg.role {
1111                MessageRole::System => {
1112                    if !msg.content.trim().is_empty() {
1113                        input.push(json!({
1114                            "role": "developer",
1115                            "content": [{
1116                                "type": "input_text",
1117                                "text": msg.content.clone()
1118                            }]
1119                        }));
1120                    }
1121                }
1122                MessageRole::User => {
1123                    input.push(json!({
1124                        "role": "user",
1125                        "content": [{
1126                            "type": "input_text",
1127                            "text": msg.content.clone()
1128                        }]
1129                    }));
1130                }
1131                MessageRole::Assistant => {
1132                    let mut content_parts = Vec::new();
1133                    if !msg.content.is_empty() {
1134                        content_parts.push(json!({
1135                            "type": "output_text",
1136                            "text": msg.content.clone()
1137                        }));
1138                    }
1139
1140                    if let Some(tool_calls) = &msg.tool_calls {
1141                        for call in tool_calls {
1142                            content_parts.push(json!({
1143                                "type": "tool_call",
1144                                "id": call.id.clone(),
1145                                "name": call.function.name.clone(),
1146                                "arguments": call.function.arguments.clone()
1147                            }));
1148                        }
1149                    }
1150
1151                    if !content_parts.is_empty() {
1152                        input.push(json!({
1153                            "role": "assistant",
1154                            "content": content_parts
1155                        }));
1156                    }
1157                }
1158                MessageRole::Tool => {
1159                    let tool_call_id = msg.tool_call_id.clone().ok_or_else(|| {
1160                        let formatted_error = error_display::format_llm_error(
1161                            "OpenRouter",
1162                            "Tool messages must include tool_call_id for Responses API",
1163                        );
1164                        LLMError::InvalidRequest(formatted_error)
1165                    })?;
1166
1167                    let mut tool_content = Vec::new();
1168                    if !msg.content.trim().is_empty() {
1169                        tool_content.push(json!({
1170                            "type": "output_text",
1171                            "text": msg.content.clone()
1172                        }));
1173                    }
1174
1175                    let mut tool_result = json!({
1176                        "type": "tool_result",
1177                        "tool_call_id": tool_call_id
1178                    });
1179
1180                    if !tool_content.is_empty() {
1181                        if let Value::Object(ref mut map) = tool_result {
1182                            map.insert("content".to_string(), json!(tool_content));
1183                        }
1184                    }
1185
1186                    input.push(json!({
1187                        "role": "tool",
1188                        "content": [tool_result]
1189                    }));
1190                }
1191            }
1192        }
1193
1194        Ok(input)
1195    }
1196
1197    fn build_codex_responses_input(&self, request: &LLMRequest) -> Result<Vec<Value>, LLMError> {
1198        let mut additional_guidance = Vec::new();
1199
1200        if let Some(system_prompt) = &request.system_prompt {
1201            let trimmed = system_prompt.trim();
1202            if !trimmed.is_empty() {
1203                additional_guidance.push(trimmed.to_string());
1204            }
1205        }
1206
1207        let mut input = Vec::new();
1208
1209        for msg in &request.messages {
1210            match msg.role {
1211                MessageRole::System => {
1212                    let trimmed = msg.content.trim();
1213                    if !trimmed.is_empty() {
1214                        additional_guidance.push(trimmed.to_string());
1215                    }
1216                }
1217                MessageRole::User => {
1218                    input.push(json!({
1219                        "role": "user",
1220                        "content": [{
1221                            "type": "input_text",
1222                            "text": msg.content.clone()
1223                        }]
1224                    }));
1225                }
1226                MessageRole::Assistant => {
1227                    let mut content_parts = Vec::new();
1228                    if !msg.content.is_empty() {
1229                        content_parts.push(json!({
1230                            "type": "output_text",
1231                            "text": msg.content.clone()
1232                        }));
1233                    }
1234
1235                    if let Some(tool_calls) = &msg.tool_calls {
1236                        for call in tool_calls {
1237                            content_parts.push(json!({
1238                                "type": "tool_call",
1239                                "id": call.id.clone(),
1240                                "name": call.function.name.clone(),
1241                                "arguments": call.function.arguments.clone()
1242                            }));
1243                        }
1244                    }
1245
1246                    if !content_parts.is_empty() {
1247                        input.push(json!({
1248                            "role": "assistant",
1249                            "content": content_parts
1250                        }));
1251                    }
1252                }
1253                MessageRole::Tool => {
1254                    let tool_call_id = msg.tool_call_id.clone().ok_or_else(|| {
1255                        let formatted_error = error_display::format_llm_error(
1256                            "OpenRouter",
1257                            "Tool messages must include tool_call_id for Responses API",
1258                        );
1259                        LLMError::InvalidRequest(formatted_error)
1260                    })?;
1261
1262                    let mut tool_content = Vec::new();
1263                    if !msg.content.trim().is_empty() {
1264                        tool_content.push(json!({
1265                            "type": "output_text",
1266                            "text": msg.content.clone()
1267                        }));
1268                    }
1269
1270                    let mut tool_result = json!({
1271                        "type": "tool_result",
1272                        "tool_call_id": tool_call_id
1273                    });
1274
1275                    if !tool_content.is_empty() {
1276                        if let Value::Object(ref mut map) = tool_result {
1277                            map.insert("content".to_string(), json!(tool_content));
1278                        }
1279                    }
1280
1281                    input.push(json!({
1282                        "role": "tool",
1283                        "content": [tool_result]
1284                    }));
1285                }
1286            }
1287        }
1288
1289        let developer_prompt = gpt5_codex_developer_prompt(&additional_guidance);
1290        input.insert(
1291            0,
1292            json!({
1293                "role": "developer",
1294                "content": [{
1295                    "type": "input_text",
1296                    "text": developer_prompt
1297                }]
1298            }),
1299        );
1300
1301        Ok(input)
1302    }
1303
1304    fn convert_to_openrouter_responses_format(
1305        &self,
1306        request: &LLMRequest,
1307    ) -> Result<Value, LLMError> {
1308        let resolved_model = self.resolve_model(request);
1309        let input = if Self::is_gpt5_codex_model(resolved_model) {
1310            self.build_codex_responses_input(request)?
1311        } else {
1312            self.build_standard_responses_input(request)?
1313        };
1314
1315        if input.is_empty() {
1316            let formatted_error = error_display::format_llm_error(
1317                "OpenRouter",
1318                "No messages provided for Responses API",
1319            );
1320            return Err(LLMError::InvalidRequest(formatted_error));
1321        }
1322
1323        let mut provider_request = json!({
1324            "model": resolved_model,
1325            "input": input,
1326            "stream": request.stream
1327        });
1328
1329        if let Some(max_tokens) = request.max_tokens {
1330            provider_request["max_output_tokens"] = json!(max_tokens);
1331        }
1332
1333        if let Some(temperature) = request.temperature {
1334            provider_request["temperature"] = json!(temperature);
1335        }
1336
1337        if let Some(tools) = &request.tools {
1338            if !tools.is_empty() {
1339                let tools_json: Vec<Value> = tools
1340                    .iter()
1341                    .map(|tool| {
1342                        json!({
1343                            "type": "function",
1344                            "function": {
1345                                "name": tool.function.name,
1346                                "description": tool.function.description,
1347                                "parameters": tool.function.parameters
1348                            }
1349                        })
1350                    })
1351                    .collect();
1352                provider_request["tools"] = Value::Array(tools_json);
1353            }
1354        }
1355
1356        if let Some(tool_choice) = &request.tool_choice {
1357            provider_request["tool_choice"] = tool_choice.to_provider_format("openai");
1358        }
1359
1360        if let Some(parallel) = request.parallel_tool_calls {
1361            provider_request["parallel_tool_calls"] = Value::Bool(parallel);
1362        }
1363
1364        if let Some(effort) = request.reasoning_effort.as_deref() {
1365            if self.supports_reasoning_effort(resolved_model) {
1366                provider_request["reasoning"] = json!({ "effort": effort });
1367            }
1368        }
1369
1370        if Self::is_gpt5_codex_model(resolved_model) {
1371            provider_request["reasoning"] = json!({ "effort": "medium" });
1372        }
1373
1374        Ok(provider_request)
1375    }
1376
1377    fn convert_to_openrouter_format(&self, request: &LLMRequest) -> Result<Value, LLMError> {
1378        let resolved_model = self.resolve_model(request);
1379        let mut messages = Vec::new();
1380
1381        if let Some(system_prompt) = &request.system_prompt {
1382            messages.push(json!({
1383                "role": crate::config::constants::message_roles::SYSTEM,
1384                "content": system_prompt
1385            }));
1386        }
1387
1388        for msg in &request.messages {
1389            let role = msg.role.as_openai_str();
1390            let mut message = json!({
1391                "role": role,
1392                "content": msg.content
1393            });
1394
1395            if msg.role == MessageRole::Assistant {
1396                if let Some(tool_calls) = &msg.tool_calls {
1397                    if !tool_calls.is_empty() {
1398                        let tool_calls_json: Vec<Value> = tool_calls
1399                            .iter()
1400                            .map(|tc| {
1401                                json!({
1402                                    "id": tc.id,
1403                                    "type": "function",
1404                                    "function": {
1405                                        "name": tc.function.name,
1406                                        "arguments": tc.function.arguments
1407                                    }
1408                                })
1409                            })
1410                            .collect();
1411                        message["tool_calls"] = Value::Array(tool_calls_json);
1412                    }
1413                }
1414            }
1415
1416            if msg.role == MessageRole::Tool {
1417                if let Some(tool_call_id) = &msg.tool_call_id {
1418                    message["tool_call_id"] = Value::String(tool_call_id.clone());
1419                }
1420            }
1421
1422            messages.push(message);
1423        }
1424
1425        if messages.is_empty() {
1426            let formatted_error =
1427                error_display::format_llm_error("OpenRouter", "No messages provided");
1428            return Err(LLMError::InvalidRequest(formatted_error));
1429        }
1430
1431        let mut provider_request = json!({
1432            "model": resolved_model,
1433            "messages": messages,
1434            "stream": request.stream
1435        });
1436
1437        if let Some(max_tokens) = request.max_tokens {
1438            provider_request["max_tokens"] = json!(max_tokens);
1439        }
1440
1441        if let Some(temperature) = request.temperature {
1442            provider_request["temperature"] = json!(temperature);
1443        }
1444
1445        if let Some(tools) = &request.tools {
1446            if !tools.is_empty() {
1447                let tools_json: Vec<Value> = tools
1448                    .iter()
1449                    .map(|tool| {
1450                        json!({
1451                            "type": "function",
1452                            "function": {
1453                                "name": tool.function.name,
1454                                "description": tool.function.description,
1455                                "parameters": tool.function.parameters
1456                            }
1457                        })
1458                    })
1459                    .collect();
1460                provider_request["tools"] = Value::Array(tools_json);
1461            }
1462        }
1463
1464        if let Some(tool_choice) = &request.tool_choice {
1465            provider_request["tool_choice"] = tool_choice.to_provider_format("openai");
1466        }
1467
1468        if let Some(parallel) = request.parallel_tool_calls {
1469            provider_request["parallel_tool_calls"] = Value::Bool(parallel);
1470        }
1471
1472        if let Some(effort) = request.reasoning_effort.as_deref() {
1473            if self.supports_reasoning_effort(resolved_model) {
1474                provider_request["reasoning"] = json!({ "effort": effort });
1475            }
1476        }
1477
1478        Ok(provider_request)
1479    }
1480
1481    fn parse_openrouter_response(&self, response_json: Value) -> Result<LLMResponse, LLMError> {
1482        if let Some(choices) = response_json
1483            .get("choices")
1484            .and_then(|value| value.as_array())
1485        {
1486            if choices.is_empty() {
1487                let formatted_error =
1488                    error_display::format_llm_error("OpenRouter", "No choices in response");
1489                return Err(LLMError::Provider(formatted_error));
1490            }
1491
1492            let choice = &choices[0];
1493            let message = choice.get("message").ok_or_else(|| {
1494                let formatted_error = error_display::format_llm_error(
1495                    "OpenRouter",
1496                    "Invalid response format: missing message",
1497                );
1498                LLMError::Provider(formatted_error)
1499            })?;
1500
1501            let content = match message.get("content") {
1502                Some(Value::String(text)) => Some(text.to_string()),
1503                Some(Value::Array(parts)) => {
1504                    let text = parts
1505                        .iter()
1506                        .filter_map(|part| part.get("text").and_then(|t| t.as_str()))
1507                        .collect::<Vec<_>>()
1508                        .join("");
1509                    if text.is_empty() { None } else { Some(text) }
1510                }
1511                _ => None,
1512            };
1513
1514            let tool_calls = message
1515                .get("tool_calls")
1516                .and_then(|tc| tc.as_array())
1517                .map(|calls| {
1518                    calls
1519                        .iter()
1520                        .filter_map(|call| {
1521                            let id = call.get("id").and_then(|v| v.as_str())?;
1522                            let function = call.get("function")?;
1523                            let name = function.get("name").and_then(|v| v.as_str())?;
1524                            let arguments = function.get("arguments");
1525                            let serialized = arguments.map_or("{}".to_string(), |value| {
1526                                if value.is_string() {
1527                                    value.as_str().unwrap_or("").to_string()
1528                                } else {
1529                                    value.to_string()
1530                                }
1531                            });
1532                            Some(ToolCall::function(
1533                                id.to_string(),
1534                                name.to_string(),
1535                                serialized,
1536                            ))
1537                        })
1538                        .collect::<Vec<_>>()
1539                })
1540                .filter(|calls| !calls.is_empty());
1541
1542            let mut reasoning = message
1543                .get("reasoning")
1544                .and_then(extract_reasoning_trace)
1545                .or_else(|| choice.get("reasoning").and_then(extract_reasoning_trace));
1546
1547            if reasoning.is_none() {
1548                reasoning = extract_reasoning_from_message_content(message);
1549            }
1550
1551            let finish_reason = choice
1552                .get("finish_reason")
1553                .and_then(|fr| fr.as_str())
1554                .map(map_finish_reason)
1555                .unwrap_or(FinishReason::Stop);
1556
1557            let usage = response_json.get("usage").map(parse_usage_value);
1558
1559            return Ok(LLMResponse {
1560                content,
1561                tool_calls,
1562                usage,
1563                finish_reason,
1564                reasoning,
1565            });
1566        }
1567
1568        self.parse_responses_api_response(&response_json)
1569    }
1570
1571    fn parse_responses_api_response(&self, payload: &Value) -> Result<LLMResponse, LLMError> {
1572        let response_container = payload.get("response").unwrap_or(payload);
1573
1574        let outputs = response_container
1575            .get("output")
1576            .or_else(|| response_container.get("outputs"))
1577            .and_then(|value| value.as_array())
1578            .ok_or_else(|| {
1579                let formatted_error = error_display::format_llm_error(
1580                    "OpenRouter",
1581                    "Invalid response format: missing output",
1582                );
1583                LLMError::Provider(formatted_error)
1584            })?;
1585
1586        if outputs.is_empty() {
1587            let formatted_error =
1588                error_display::format_llm_error("OpenRouter", "No output in response");
1589            return Err(LLMError::Provider(formatted_error));
1590        }
1591
1592        let message = outputs
1593            .iter()
1594            .find(|value| {
1595                value
1596                    .get("role")
1597                    .and_then(|role| role.as_str())
1598                    .map(|role| role == "assistant")
1599                    .unwrap_or(true)
1600            })
1601            .unwrap_or(&outputs[0]);
1602
1603        let mut aggregated_content = String::new();
1604        let mut reasoning_buffer = ReasoningBuffer::default();
1605        let mut tool_call_builders: Vec<ToolCallBuilder> = Vec::new();
1606        let mut deltas = StreamDelta::default();
1607
1608        if let Some(content_value) = message.get("content") {
1609            process_content_value(
1610                content_value,
1611                &mut aggregated_content,
1612                &mut reasoning_buffer,
1613                &mut tool_call_builders,
1614                &mut deltas,
1615            );
1616        } else {
1617            process_content_value(
1618                message,
1619                &mut aggregated_content,
1620                &mut reasoning_buffer,
1621                &mut tool_call_builders,
1622                &mut deltas,
1623            );
1624        }
1625
1626        let mut tool_calls = finalize_tool_calls(tool_call_builders);
1627        if tool_calls.is_none() {
1628            tool_calls = extract_tool_calls_from_content(message);
1629        }
1630
1631        let mut reasoning = reasoning_buffer.finalize();
1632        if reasoning.is_none() {
1633            reasoning = extract_reasoning_from_message_content(message)
1634                .or_else(|| message.get("reasoning").and_then(extract_reasoning_trace))
1635                .or_else(|| payload.get("reasoning").and_then(extract_reasoning_trace));
1636        }
1637
1638        let content = if aggregated_content.is_empty() {
1639            message
1640                .get("output_text")
1641                .and_then(|value| value.as_str())
1642                .map(|value| value.to_string())
1643        } else {
1644            Some(aggregated_content)
1645        };
1646
1647        let mut usage = payload.get("usage").map(parse_usage_value);
1648        if usage.is_none() {
1649            usage = response_container.get("usage").map(parse_usage_value);
1650        }
1651
1652        let finish_reason = payload
1653            .get("stop_reason")
1654            .or_else(|| payload.get("finish_reason"))
1655            .or_else(|| payload.get("status"))
1656            .or_else(|| response_container.get("stop_reason"))
1657            .or_else(|| response_container.get("finish_reason"))
1658            .or_else(|| message.get("stop_reason"))
1659            .or_else(|| message.get("finish_reason"))
1660            .and_then(|value| value.as_str())
1661            .map(map_finish_reason)
1662            .unwrap_or(FinishReason::Stop);
1663
1664        Ok(LLMResponse {
1665            content,
1666            tool_calls,
1667            usage,
1668            finish_reason,
1669            reasoning,
1670        })
1671    }
1672}
1673
1674#[async_trait]
1675impl LLMProvider for OpenRouterProvider {
1676    fn name(&self) -> &str {
1677        "openrouter"
1678    }
1679
1680    fn supports_streaming(&self) -> bool {
1681        true
1682    }
1683
1684    fn supports_reasoning(&self, model: &str) -> bool {
1685        let requested = if model.trim().is_empty() {
1686            self.model.as_str()
1687        } else {
1688            model
1689        };
1690
1691        models::openrouter::REASONING_MODELS
1692            .iter()
1693            .any(|candidate| *candidate == requested)
1694    }
1695
1696    fn supports_reasoning_effort(&self, model: &str) -> bool {
1697        let requested = if model.trim().is_empty() {
1698            self.model.as_str()
1699        } else {
1700            model
1701        };
1702        models::openrouter::REASONING_MODELS
1703            .iter()
1704            .any(|candidate| *candidate == requested)
1705    }
1706
1707    async fn stream(&self, request: LLMRequest) -> Result<LLMStream, LLMError> {
1708        let (provider_request, url) = if self.uses_responses_api_for(&request) {
1709            let mut req = self.convert_to_openrouter_responses_format(&request)?;
1710            req["stream"] = Value::Bool(true);
1711            (req, format!("{}/responses", self.base_url))
1712        } else {
1713            let mut req = self.convert_to_openrouter_format(&request)?;
1714            req["stream"] = Value::Bool(true);
1715            (req, format!("{}/chat/completions", self.base_url))
1716        };
1717
1718        let response = self
1719            .http_client
1720            .post(&url)
1721            .bearer_auth(&self.api_key)
1722            .json(&provider_request)
1723            .send()
1724            .await
1725            .map_err(|e| {
1726                let formatted_error =
1727                    error_display::format_llm_error("OpenRouter", &format!("Network error: {}", e));
1728                LLMError::Network(formatted_error)
1729            })?;
1730
1731        if !response.status().is_success() {
1732            let status = response.status();
1733            let error_text = response.text().await.unwrap_or_default();
1734
1735            if status.as_u16() == 429 || error_text.contains("quota") {
1736                return Err(LLMError::RateLimit);
1737            }
1738
1739            let formatted_error = error_display::format_llm_error(
1740                "OpenRouter",
1741                &format!("HTTP {}: {}", status, error_text),
1742            );
1743            return Err(LLMError::Provider(formatted_error));
1744        }
1745
1746        fn find_sse_boundary(buffer: &str) -> Option<(usize, usize)> {
1747            let newline_boundary = buffer.find("\n\n").map(|idx| (idx, 2));
1748            let carriage_boundary = buffer.find("\r\n\r\n").map(|idx| (idx, 4));
1749
1750            match (newline_boundary, carriage_boundary) {
1751                (Some((n_idx, n_len)), Some((c_idx, c_len))) => {
1752                    if n_idx <= c_idx {
1753                        Some((n_idx, n_len))
1754                    } else {
1755                        Some((c_idx, c_len))
1756                    }
1757                }
1758                (Some(boundary), None) => Some(boundary),
1759                (None, Some(boundary)) => Some(boundary),
1760                (None, None) => None,
1761            }
1762        }
1763
1764        let stream = try_stream! {
1765            let mut body_stream = response.bytes_stream();
1766            let mut buffer = String::new();
1767            let mut aggregated_content = String::new();
1768            let mut tool_call_builders: Vec<ToolCallBuilder> = Vec::new();
1769            let mut reasoning = ReasoningBuffer::default();
1770            let mut usage: Option<Usage> = None;
1771            let mut finish_reason = FinishReason::Stop;
1772            let mut done = false;
1773
1774            while let Some(chunk_result) = body_stream.next().await {
1775                let chunk = chunk_result.map_err(|err| {
1776                    let formatted_error = error_display::format_llm_error(
1777                        "OpenRouter",
1778                        &format!("Streaming error: {}", err),
1779                    );
1780                    LLMError::Network(formatted_error)
1781                })?;
1782
1783                buffer.push_str(&String::from_utf8_lossy(&chunk));
1784
1785                while let Some((split_idx, delimiter_len)) = find_sse_boundary(&buffer) {
1786                    let event = buffer[..split_idx].to_string();
1787                    buffer.drain(..split_idx + delimiter_len);
1788
1789                    if let Some(data_payload) = extract_data_payload(&event) {
1790                        let trimmed_payload = data_payload.trim();
1791                        if trimmed_payload == "[DONE]" {
1792                            done = true;
1793                            break;
1794                        }
1795
1796                        if !trimmed_payload.is_empty() {
1797                            let payload: Value = serde_json::from_str(trimmed_payload).map_err(|err| {
1798                                let formatted_error = error_display::format_llm_error(
1799                                    "OpenRouter",
1800                                    &format!("Failed to parse stream payload: {}", err),
1801                                );
1802                                LLMError::Provider(formatted_error)
1803                            })?;
1804
1805                            if let Some(delta) = parse_stream_payload(
1806                                &payload,
1807                                &mut aggregated_content,
1808                                &mut tool_call_builders,
1809                                &mut reasoning,
1810                                &mut usage,
1811                                &mut finish_reason,
1812                            ) {
1813                                for fragment in delta.into_fragments() {
1814                                    match fragment {
1815                                        StreamFragment::Content(text) if !text.is_empty() => {
1816                                            yield LLMStreamEvent::Token { delta: text };
1817                                        }
1818                                        StreamFragment::Reasoning(text) if !text.is_empty() => {
1819                                            yield LLMStreamEvent::Reasoning { delta: text };
1820                                        }
1821                                        _ => {}
1822                                    }
1823                                }
1824                            }
1825                        }
1826                    }
1827                }
1828
1829                if done {
1830                    break;
1831                }
1832            }
1833
1834            if !done && !buffer.trim().is_empty() {
1835                if let Some(data_payload) = extract_data_payload(&buffer) {
1836                    let trimmed_payload = data_payload.trim();
1837                    if trimmed_payload != "[DONE]" && !trimmed_payload.is_empty() {
1838                        let payload: Value = serde_json::from_str(trimmed_payload).map_err(|err| {
1839                            let formatted_error = error_display::format_llm_error(
1840                                "OpenRouter",
1841                                &format!("Failed to parse stream payload: {}", err),
1842                            );
1843                            LLMError::Provider(formatted_error)
1844                        })?;
1845
1846                        if let Some(delta) = parse_stream_payload(
1847                            &payload,
1848                            &mut aggregated_content,
1849                            &mut tool_call_builders,
1850                            &mut reasoning,
1851                            &mut usage,
1852                            &mut finish_reason,
1853                        ) {
1854                            for fragment in delta.into_fragments() {
1855                                match fragment {
1856                                    StreamFragment::Content(text) if !text.is_empty() => {
1857                                        yield LLMStreamEvent::Token { delta: text };
1858                                    }
1859                                    StreamFragment::Reasoning(text) if !text.is_empty() => {
1860                                        yield LLMStreamEvent::Reasoning { delta: text };
1861                                    }
1862                                    _ => {}
1863                                }
1864                            }
1865                        }
1866                    }
1867                }
1868            }
1869
1870            let response = finalize_stream_response(
1871                aggregated_content,
1872                tool_call_builders,
1873                usage,
1874                finish_reason,
1875                reasoning,
1876            );
1877
1878            yield LLMStreamEvent::Completed { response };
1879        };
1880
1881        Ok(Box::pin(stream))
1882    }
1883
1884    async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
1885        let (provider_request, url) = if self.uses_responses_api_for(&request) {
1886            (
1887                self.convert_to_openrouter_responses_format(&request)?,
1888                format!("{}/responses", self.base_url),
1889            )
1890        } else {
1891            (
1892                self.convert_to_openrouter_format(&request)?,
1893                format!("{}/chat/completions", self.base_url),
1894            )
1895        };
1896
1897        let response = self
1898            .http_client
1899            .post(&url)
1900            .bearer_auth(&self.api_key)
1901            .json(&provider_request)
1902            .send()
1903            .await
1904            .map_err(|e| {
1905                let formatted_error =
1906                    error_display::format_llm_error("OpenRouter", &format!("Network error: {}", e));
1907                LLMError::Network(formatted_error)
1908            })?;
1909
1910        if !response.status().is_success() {
1911            let status = response.status();
1912            let error_text = response.text().await.unwrap_or_default();
1913
1914            if status.as_u16() == 429 || error_text.contains("quota") {
1915                return Err(LLMError::RateLimit);
1916            }
1917
1918            let formatted_error = error_display::format_llm_error(
1919                "OpenRouter",
1920                &format!("HTTP {}: {}", status, error_text),
1921            );
1922            return Err(LLMError::Provider(formatted_error));
1923        }
1924
1925        let openrouter_response: Value = response.json().await.map_err(|e| {
1926            let formatted_error = error_display::format_llm_error(
1927                "OpenRouter",
1928                &format!("Failed to parse response: {}", e),
1929            );
1930            LLMError::Provider(formatted_error)
1931        })?;
1932
1933        self.parse_openrouter_response(openrouter_response)
1934    }
1935
1936    fn supported_models(&self) -> Vec<String> {
1937        models::openrouter::SUPPORTED_MODELS
1938            .iter()
1939            .map(|s| s.to_string())
1940            .collect()
1941    }
1942
1943    fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
1944        if request.messages.is_empty() {
1945            let formatted_error =
1946                error_display::format_llm_error("OpenRouter", "Messages cannot be empty");
1947            return Err(LLMError::InvalidRequest(formatted_error));
1948        }
1949
1950        for message in &request.messages {
1951            if let Err(err) = message.validate_for_provider("openai") {
1952                let formatted = error_display::format_llm_error("OpenRouter", &err);
1953                return Err(LLMError::InvalidRequest(formatted));
1954            }
1955        }
1956
1957        if request.model.trim().is_empty() {
1958            let formatted_error =
1959                error_display::format_llm_error("OpenRouter", "Model must be provided");
1960            return Err(LLMError::InvalidRequest(formatted_error));
1961        }
1962
1963        Ok(())
1964    }
1965}
1966
1967#[async_trait]
1968impl LLMClient for OpenRouterProvider {
1969    async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, LLMError> {
1970        let request = self.parse_client_prompt(prompt);
1971        let request_model = request.model.clone();
1972        let response = LLMProvider::generate(self, request).await?;
1973
1974        Ok(llm_types::LLMResponse {
1975            content: response.content.unwrap_or_default(),
1976            model: request_model,
1977            usage: response.usage.map(|u| llm_types::Usage {
1978                prompt_tokens: u.prompt_tokens as usize,
1979                completion_tokens: u.completion_tokens as usize,
1980                total_tokens: u.total_tokens as usize,
1981            }),
1982            reasoning: response.reasoning,
1983        })
1984    }
1985
1986    fn backend_kind(&self) -> llm_types::BackendKind {
1987        llm_types::BackendKind::OpenRouter
1988    }
1989
1990    fn model_id(&self) -> &str {
1991        &self.model
1992    }
1993}
1994
1995#[cfg(test)]
1996mod tests {
1997    use super::*;
1998    use serde_json::json;
1999
2000    #[test]
2001    fn test_parse_stream_payload_chat_chunk() {
2002        let payload = json!({
2003            "choices": [{
2004                "delta": {
2005                    "content": [
2006                        {"type": "output_text", "text": "Hello"}
2007                    ]
2008                }
2009            }]
2010        });
2011
2012        let mut aggregated = String::new();
2013        let mut builders = Vec::new();
2014        let mut reasoning = ReasoningBuffer::default();
2015        let mut usage = None;
2016        let mut finish_reason = FinishReason::Stop;
2017
2018        let delta = parse_stream_payload(
2019            &payload,
2020            &mut aggregated,
2021            &mut builders,
2022            &mut reasoning,
2023            &mut usage,
2024            &mut finish_reason,
2025        );
2026
2027        let fragments = delta.expect("delta should exist").into_fragments();
2028        assert_eq!(
2029            fragments,
2030            vec![StreamFragment::Content("Hello".to_string())]
2031        );
2032        assert_eq!(aggregated, "Hello");
2033        assert!(builders.is_empty());
2034        assert!(usage.is_none());
2035        assert!(reasoning.finalize().is_none());
2036    }
2037
2038    #[test]
2039    fn test_parse_stream_payload_response_delta() {
2040        let payload = json!({
2041            "type": "response.delta",
2042            "delta": {
2043                "type": "output_text_delta",
2044                "text": "Stream"
2045            }
2046        });
2047
2048        let mut aggregated = String::new();
2049        let mut builders = Vec::new();
2050        let mut reasoning = ReasoningBuffer::default();
2051        let mut usage = None;
2052        let mut finish_reason = FinishReason::Stop;
2053
2054        let delta = parse_stream_payload(
2055            &payload,
2056            &mut aggregated,
2057            &mut builders,
2058            &mut reasoning,
2059            &mut usage,
2060            &mut finish_reason,
2061        );
2062
2063        let fragments = delta.expect("delta should exist").into_fragments();
2064        assert_eq!(
2065            fragments,
2066            vec![StreamFragment::Content("Stream".to_string())]
2067        );
2068        assert_eq!(aggregated, "Stream");
2069    }
2070
2071    #[test]
2072    fn test_extract_data_payload_joins_multiline_events() {
2073        let event = ": keep-alive\n".to_string() + "data: {\"a\":1}\n" + "data: {\"b\":2}\n";
2074        let payload = extract_data_payload(&event);
2075        assert_eq!(payload.as_deref(), Some("{\"a\":1}\n{\"b\":2}"));
2076    }
2077}