Skip to main content

vtcode_llm/open_responses/bridge/
mod.rs

1//! Bridge layer for converting VT Code events to Open Responses format.
2//!
3//! This module provides adapters to convert VT Code's internal `ThreadEvent`
4//! and `ThreadItem` types to Open Responses-conformant structures, enabling
5//! backwards compatibility during migration.
6
7use serde_json::json;
8
9use super::{
10    ContentPart, CustomItem, FunctionCallItem, ItemStatus, MessageItem, MessageRole, OpenResponseError, OpenUsage,
11    OutputItem, ReasoningItem, Response, ResponseStatus, ResponseStreamEvent, StreamEventEmitter,
12    response::{generate_item_id, generate_response_id},
13};
14use crate::provider::{FinishReason, NormalizedStreamEvent, ToolCall};
15use vtcode_exec_events::{
16    CommandExecutionStatus, McpToolCallStatus, PatchApplyStatus, ThreadEvent, ThreadItem, ThreadItemDetails,
17    ToolOutputItem,
18};
19
20/// Builder for constructing Open Responses `Response` objects from VT Code events.
21///
22/// Tracks streaming state and maintains the mapping between VT Code item IDs
23/// and Open Responses output item indices.
24#[derive(Debug)]
25pub struct ResponseBuilder {
26    response: Response,
27    next_output_index: usize,
28    item_id_to_index: hashbrown::HashMap<String, usize>,
29    active_items: hashbrown::HashMap<String, ActiveItemState>,
30    tool_call_correlation_ids: hashbrown::HashMap<String, String>,
31    used_tool_call_ids: hashbrown::HashSet<String>,
32    normalized: NormalizedBridgeState,
33}
34
35/// State for an active (in-progress) streaming item.
36#[derive(Debug, Clone)]
37struct ActiveItemState {
38    output_index: usize,
39    content_index: usize,
40    /// Previous text content for safe delta computation (avoids UTF-8 slicing issues)
41    prev_text: String,
42}
43
44#[derive(Debug, Clone)]
45struct NormalizedFunctionCallState {
46    item_id: String,
47    output_index: usize,
48    name: Option<String>,
49    arguments: String,
50}
51
52#[derive(Debug, Default)]
53struct NormalizedBridgeState {
54    response_started: bool,
55    message_item_id: Option<String>,
56    reasoning_item_id: Option<String>,
57    tool_calls: hashbrown::HashMap<String, NormalizedFunctionCallState>,
58}
59impl ResponseBuilder {
60    /// Creates a new response builder with the given model.
61    pub fn new(model: impl Into<String>) -> Self {
62        let response = Response::new(generate_response_id(), model);
63        Self {
64            response,
65            next_output_index: 0,
66            item_id_to_index: hashbrown::HashMap::new(),
67            active_items: hashbrown::HashMap::new(),
68            tool_call_correlation_ids: hashbrown::HashMap::new(),
69            used_tool_call_ids: hashbrown::HashSet::new(),
70            normalized: NormalizedBridgeState::default(),
71        }
72    }
73
74    /// Returns a reference to the current response.
75    pub(crate) fn response(&self) -> &Response {
76        &self.response
77    }
78
79    /// Returns a mutable reference to the current response.
80    pub fn response_mut(&mut self) -> &mut Response {
81        &mut self.response
82    }
83
84    /// Returns the response ID.
85    pub fn response_id(&self) -> &str {
86        &self.response.id
87    }
88
89    /// Consumes the builder and returns the final response.
90    pub fn build(self) -> Response {
91        self.response
92    }
93}
94
95mod emitter;
96mod items;
97mod normalized;
98
99pub use emitter::DualEventEmitter;
100
101#[cfg(test)]
102mod tests;