assistant.rs

  1use crate::{
  2    assistant_settings::{AssistantDockPosition, AssistantSettings},
  3    OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role,
  4};
  5use anyhow::{anyhow, Result};
  6use chrono::{DateTime, Local};
  7use collections::HashMap;
  8use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer};
  9use fs::Fs;
 10use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
 11use gpui::{
 12    actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity,
 13    ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 14    WindowContext,
 15};
 16use isahc::{http::StatusCode, Request, RequestExt};
 17use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
 18use settings::SettingsStore;
 19use std::{cell::Cell, io, rc::Rc, sync::Arc};
 20use util::{post_inc, ResultExt, TryFutureExt};
 21use workspace::{
 22    dock::{DockPosition, Panel},
 23    item::Item,
 24    pane, Pane, Workspace,
 25};
 26
 27const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
 28
 29actions!(
 30    assistant,
 31    [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey]
 32);
 33
 34pub fn init(cx: &mut AppContext) {
 35    settings::register::<AssistantSettings>(cx);
 36    cx.add_action(
 37        |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
 38            if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
 39                this.update(cx, |this, cx| this.add_context(cx))
 40            }
 41
 42            workspace.focus_panel::<AssistantPanel>(cx);
 43        },
 44    );
 45    cx.add_action(AssistantEditor::assist);
 46    cx.capture_action(AssistantEditor::cancel_last_assist);
 47    cx.add_action(AssistantEditor::quote_selection);
 48    cx.add_action(AssistantPanel::save_api_key);
 49    cx.add_action(AssistantPanel::reset_api_key);
 50}
 51
 52pub enum AssistantPanelEvent {
 53    ZoomIn,
 54    ZoomOut,
 55    Focus,
 56    Close,
 57    DockPositionChanged,
 58}
 59
 60pub struct AssistantPanel {
 61    width: Option<f32>,
 62    height: Option<f32>,
 63    pane: ViewHandle<Pane>,
 64    api_key: Rc<Cell<Option<String>>>,
 65    api_key_editor: Option<ViewHandle<Editor>>,
 66    has_read_credentials: bool,
 67    languages: Arc<LanguageRegistry>,
 68    fs: Arc<dyn Fs>,
 69    _subscriptions: Vec<Subscription>,
 70}
 71
 72impl AssistantPanel {
 73    pub fn load(
 74        workspace: WeakViewHandle<Workspace>,
 75        cx: AsyncAppContext,
 76    ) -> Task<Result<ViewHandle<Self>>> {
 77        cx.spawn(|mut cx| async move {
 78            // TODO: deserialize state.
 79            workspace.update(&mut cx, |workspace, cx| {
 80                cx.add_view::<Self, _>(|cx| {
 81                    let weak_self = cx.weak_handle();
 82                    let pane = cx.add_view(|cx| {
 83                        let mut pane = Pane::new(
 84                            workspace.weak_handle(),
 85                            workspace.project().clone(),
 86                            workspace.app_state().background_actions,
 87                            Default::default(),
 88                            cx,
 89                        );
 90                        pane.set_can_split(false, cx);
 91                        pane.set_can_navigate(false, cx);
 92                        pane.on_can_drop(move |_, _| false);
 93                        pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 94                            let weak_self = weak_self.clone();
 95                            Flex::row()
 96                                .with_child(Pane::render_tab_bar_button(
 97                                    0,
 98                                    "icons/plus_12.svg",
 99                                    false,
100                                    Some(("New Context".into(), Some(Box::new(NewContext)))),
101                                    cx,
102                                    move |_, cx| {
103                                        let weak_self = weak_self.clone();
104                                        cx.window_context().defer(move |cx| {
105                                            if let Some(this) = weak_self.upgrade(cx) {
106                                                this.update(cx, |this, cx| this.add_context(cx));
107                                            }
108                                        })
109                                    },
110                                    None,
111                                ))
112                                .with_child(Pane::render_tab_bar_button(
113                                    1,
114                                    if pane.is_zoomed() {
115                                        "icons/minimize_8.svg"
116                                    } else {
117                                        "icons/maximize_8.svg"
118                                    },
119                                    pane.is_zoomed(),
120                                    Some((
121                                        "Toggle Zoom".into(),
122                                        Some(Box::new(workspace::ToggleZoom)),
123                                    )),
124                                    cx,
125                                    move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
126                                    None,
127                                ))
128                                .into_any()
129                        });
130                        let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
131                        pane.toolbar()
132                            .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
133                        pane
134                    });
135
136                    let mut this = Self {
137                        pane,
138                        api_key: Rc::new(Cell::new(None)),
139                        api_key_editor: None,
140                        has_read_credentials: false,
141                        languages: workspace.app_state().languages.clone(),
142                        fs: workspace.app_state().fs.clone(),
143                        width: None,
144                        height: None,
145                        _subscriptions: Default::default(),
146                    };
147
148                    let mut old_dock_position = this.position(cx);
149                    this._subscriptions = vec![
150                        cx.observe(&this.pane, |_, _, cx| cx.notify()),
151                        cx.subscribe(&this.pane, Self::handle_pane_event),
152                        cx.observe_global::<SettingsStore, _>(move |this, cx| {
153                            let new_dock_position = this.position(cx);
154                            if new_dock_position != old_dock_position {
155                                old_dock_position = new_dock_position;
156                                cx.emit(AssistantPanelEvent::DockPositionChanged);
157                            }
158                        }),
159                    ];
160
161                    this
162                })
163            })
164        })
165    }
166
167    fn handle_pane_event(
168        &mut self,
169        _pane: ViewHandle<Pane>,
170        event: &pane::Event,
171        cx: &mut ViewContext<Self>,
172    ) {
173        match event {
174            pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn),
175            pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut),
176            pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus),
177            pane::Event::Remove => cx.emit(AssistantPanelEvent::Close),
178            _ => {}
179        }
180    }
181
182    fn add_context(&mut self, cx: &mut ViewContext<Self>) {
183        let focus = self.has_focus(cx);
184        let editor = cx
185            .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx));
186        self.pane.update(cx, |pane, cx| {
187            pane.add_item(Box::new(editor), true, focus, None, cx)
188        });
189    }
190
191    fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
192        if let Some(api_key) = self
193            .api_key_editor
194            .as_ref()
195            .map(|editor| editor.read(cx).text(cx))
196        {
197            if !api_key.is_empty() {
198                cx.platform()
199                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
200                    .log_err();
201                self.api_key.set(Some(api_key));
202                self.api_key_editor.take();
203                cx.focus_self();
204                cx.notify();
205            }
206        }
207    }
208
209    fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
210        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
211        self.api_key.take();
212        self.api_key_editor = Some(build_api_key_editor(cx));
213        cx.focus_self();
214        cx.notify();
215    }
216}
217
218fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
219    cx.add_view(|cx| {
220        let mut editor = Editor::single_line(
221            Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())),
222            cx,
223        );
224        editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
225        editor
226    })
227}
228
229impl Entity for AssistantPanel {
230    type Event = AssistantPanelEvent;
231}
232
233impl View for AssistantPanel {
234    fn ui_name() -> &'static str {
235        "AssistantPanel"
236    }
237
238    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
239        let style = &theme::current(cx).assistant;
240        if let Some(api_key_editor) = self.api_key_editor.as_ref() {
241            Flex::column()
242                .with_child(
243                    Text::new(
244                        "Paste your OpenAI API key and press Enter to use the assistant",
245                        style.api_key_prompt.text.clone(),
246                    )
247                    .aligned(),
248                )
249                .with_child(
250                    ChildView::new(api_key_editor, cx)
251                        .contained()
252                        .with_style(style.api_key_editor.container)
253                        .aligned(),
254                )
255                .contained()
256                .with_style(style.api_key_prompt.container)
257                .aligned()
258                .into_any()
259        } else {
260            ChildView::new(&self.pane, cx).into_any()
261        }
262    }
263
264    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
265        if cx.is_self_focused() {
266            if let Some(api_key_editor) = self.api_key_editor.as_ref() {
267                cx.focus(api_key_editor);
268            } else {
269                cx.focus(&self.pane);
270            }
271        }
272    }
273}
274
275impl Panel for AssistantPanel {
276    fn position(&self, cx: &WindowContext) -> DockPosition {
277        match settings::get::<AssistantSettings>(cx).dock {
278            AssistantDockPosition::Left => DockPosition::Left,
279            AssistantDockPosition::Bottom => DockPosition::Bottom,
280            AssistantDockPosition::Right => DockPosition::Right,
281        }
282    }
283
284    fn position_is_valid(&self, _: DockPosition) -> bool {
285        true
286    }
287
288    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
289        settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
290            let dock = match position {
291                DockPosition::Left => AssistantDockPosition::Left,
292                DockPosition::Bottom => AssistantDockPosition::Bottom,
293                DockPosition::Right => AssistantDockPosition::Right,
294            };
295            settings.dock = Some(dock);
296        });
297    }
298
299    fn size(&self, cx: &WindowContext) -> f32 {
300        let settings = settings::get::<AssistantSettings>(cx);
301        match self.position(cx) {
302            DockPosition::Left | DockPosition::Right => {
303                self.width.unwrap_or_else(|| settings.default_width)
304            }
305            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
306        }
307    }
308
309    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
310        match self.position(cx) {
311            DockPosition::Left | DockPosition::Right => self.width = Some(size),
312            DockPosition::Bottom => self.height = Some(size),
313        }
314        cx.notify();
315    }
316
317    fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool {
318        matches!(event, AssistantPanelEvent::ZoomIn)
319    }
320
321    fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool {
322        matches!(event, AssistantPanelEvent::ZoomOut)
323    }
324
325    fn is_zoomed(&self, cx: &WindowContext) -> bool {
326        self.pane.read(cx).is_zoomed()
327    }
328
329    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
330        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
331    }
332
333    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
334        if active {
335            if self.api_key.clone().take().is_none() && !self.has_read_credentials {
336                self.has_read_credentials = true;
337                let api_key = if let Some((_, api_key)) = cx
338                    .platform()
339                    .read_credentials(OPENAI_API_URL)
340                    .log_err()
341                    .flatten()
342                {
343                    String::from_utf8(api_key).log_err()
344                } else {
345                    None
346                };
347                if let Some(api_key) = api_key {
348                    self.api_key.set(Some(api_key));
349                } else if self.api_key_editor.is_none() {
350                    self.api_key_editor = Some(build_api_key_editor(cx));
351                    cx.notify();
352                }
353            }
354
355            if self.pane.read(cx).items_len() == 0 {
356                self.add_context(cx);
357            }
358        }
359    }
360
361    fn icon_path(&self) -> &'static str {
362        "icons/speech_bubble_12.svg"
363    }
364
365    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
366        ("Assistant Panel".into(), Some(Box::new(ToggleFocus)))
367    }
368
369    fn should_change_position_on_event(event: &Self::Event) -> bool {
370        matches!(event, AssistantPanelEvent::DockPositionChanged)
371    }
372
373    fn should_activate_on_event(_: &Self::Event) -> bool {
374        false
375    }
376
377    fn should_close_on_event(event: &AssistantPanelEvent) -> bool {
378        matches!(event, AssistantPanelEvent::Close)
379    }
380
381    fn has_focus(&self, cx: &WindowContext) -> bool {
382        self.pane.read(cx).has_focus()
383            || self
384                .api_key_editor
385                .as_ref()
386                .map_or(false, |editor| editor.is_focused(cx))
387    }
388
389    fn is_focus_event(event: &Self::Event) -> bool {
390        matches!(event, AssistantPanelEvent::Focus)
391    }
392}
393
394struct Assistant {
395    buffer: ModelHandle<MultiBuffer>,
396    messages: Vec<Message>,
397    messages_by_id: HashMap<ExcerptId, Message>,
398    completion_count: usize,
399    pending_completions: Vec<PendingCompletion>,
400    languages: Arc<LanguageRegistry>,
401    api_key: Rc<Cell<Option<String>>>,
402}
403
404impl Entity for Assistant {
405    type Event = ();
406}
407
408impl Assistant {
409    fn new(
410        api_key: Rc<Cell<Option<String>>>,
411        language_registry: Arc<LanguageRegistry>,
412        cx: &mut ModelContext<Self>,
413    ) -> Self {
414        let mut this = Self {
415            buffer: cx.add_model(|_| MultiBuffer::new(0)),
416            messages: Default::default(),
417            messages_by_id: Default::default(),
418            completion_count: Default::default(),
419            pending_completions: Default::default(),
420            languages: language_registry,
421            api_key,
422        };
423        this.push_message(Role::User, cx);
424        this
425    }
426
427    fn assist(&mut self, cx: &mut ModelContext<Self>) {
428        let messages = self
429            .messages
430            .iter()
431            .map(|message| RequestMessage {
432                role: message.role,
433                content: message.content.read(cx).text(),
434            })
435            .collect();
436        let request = OpenAIRequest {
437            model: "gpt-3.5-turbo".into(),
438            messages,
439            stream: true,
440        };
441
442        if let Some(api_key) = self.api_key.clone().take() {
443            let stream = stream_completion(api_key, cx.background().clone(), request);
444            let response = self.push_message(Role::Assistant, cx);
445            self.push_message(Role::User, cx);
446            let task = cx.spawn(|this, mut cx| {
447                async move {
448                    let mut messages = stream.await?;
449
450                    while let Some(message) = messages.next().await {
451                        let mut message = message?;
452                        if let Some(choice) = message.choices.pop() {
453                            response.content.update(&mut cx, |content, cx| {
454                                let text: Arc<str> = choice.delta.content?.into();
455                                content.edit([(content.len()..content.len(), text)], None, cx);
456                                Some(())
457                            });
458                        }
459                    }
460
461                    this.update(&mut cx, |this, _| {
462                        this.pending_completions
463                            .retain(|completion| completion.id != this.completion_count);
464                    });
465
466                    anyhow::Ok(())
467                }
468                .log_err()
469            });
470
471            self.pending_completions.push(PendingCompletion {
472                id: post_inc(&mut self.completion_count),
473                _task: task,
474            });
475        }
476    }
477
478    fn cancel_last_assist(&mut self) -> bool {
479        self.pending_completions.pop().is_some()
480    }
481
482    fn push_message(&mut self, role: Role, cx: &mut ModelContext<Self>) -> Message {
483        let content = cx.add_model(|cx| {
484            let mut buffer = Buffer::new(0, "", cx);
485            let markdown = self.languages.language_for_name("Markdown");
486            cx.spawn_weak(|buffer, mut cx| async move {
487                let markdown = markdown.await?;
488                let buffer = buffer
489                    .upgrade(&cx)
490                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
491                buffer.update(&mut cx, |buffer, cx| {
492                    buffer.set_language(Some(markdown), cx)
493                });
494                anyhow::Ok(())
495            })
496            .detach_and_log_err(cx);
497            buffer.set_language_registry(self.languages.clone());
498            buffer
499        });
500        let excerpt_id = self.buffer.update(cx, |buffer, cx| {
501            buffer
502                .push_excerpts(
503                    content.clone(),
504                    vec![ExcerptRange {
505                        context: 0..0,
506                        primary: None,
507                    }],
508                    cx,
509                )
510                .pop()
511                .unwrap()
512        });
513
514        let message = Message {
515            role,
516            content: content.clone(),
517            sent_at: Local::now(),
518        };
519        self.messages.push(message.clone());
520        self.messages_by_id.insert(excerpt_id, message.clone());
521        message
522    }
523}
524
525struct PendingCompletion {
526    id: usize,
527    _task: Task<Option<()>>,
528}
529
530struct AssistantEditor {
531    assistant: ModelHandle<Assistant>,
532    editor: ViewHandle<Editor>,
533}
534
535impl AssistantEditor {
536    fn new(
537        api_key: Rc<Cell<Option<String>>>,
538        language_registry: Arc<LanguageRegistry>,
539        cx: &mut ViewContext<Self>,
540    ) -> Self {
541        let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
542        let editor = cx.add_view(|cx| {
543            let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx);
544            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
545            editor.set_show_gutter(false, cx);
546            editor.set_render_excerpt_header(
547                {
548                    let assistant = assistant.clone();
549                    move |_editor, params: editor::RenderExcerptHeaderParams, cx| {
550                        let style = &theme::current(cx).assistant;
551                        if let Some(message) = assistant.read(cx).messages_by_id.get(&params.id) {
552                            let sender = match message.role {
553                                Role::User => Label::new("You", style.user_sender.text.clone())
554                                    .contained()
555                                    .with_style(style.user_sender.container),
556                                Role::Assistant => {
557                                    Label::new("Assistant", style.assistant_sender.text.clone())
558                                        .contained()
559                                        .with_style(style.assistant_sender.container)
560                                }
561                                Role::System => {
562                                    Label::new("System", style.assistant_sender.text.clone())
563                                        .contained()
564                                        .with_style(style.assistant_sender.container)
565                                }
566                            };
567
568                            Flex::row()
569                                .with_child(sender.aligned())
570                                .with_child(
571                                    Label::new(
572                                        message.sent_at.format("%I:%M%P").to_string(),
573                                        style.sent_at.text.clone(),
574                                    )
575                                    .contained()
576                                    .with_style(style.sent_at.container)
577                                    .aligned(),
578                                )
579                                .aligned()
580                                .left()
581                                .contained()
582                                .with_style(style.header)
583                                .into_any()
584                        } else {
585                            Empty::new().into_any()
586                        }
587                    }
588                },
589                cx,
590            );
591            editor
592        });
593        Self { assistant, editor }
594    }
595
596    fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
597        self.assistant
598            .update(cx, |assistant, cx| assistant.assist(cx));
599    }
600
601    fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
602        if !self
603            .assistant
604            .update(cx, |assistant, _| assistant.cancel_last_assist())
605        {
606            cx.propagate_action();
607        }
608    }
609
610    fn quote_selection(
611        workspace: &mut Workspace,
612        _: &QuoteSelection,
613        cx: &mut ViewContext<Workspace>,
614    ) {
615        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
616            return;
617        };
618        let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
619            return;
620        };
621
622        let text = editor.read_with(cx, |editor, cx| {
623            let range = editor.selections.newest::<usize>(cx).range();
624            let buffer = editor.buffer().read(cx).snapshot(cx);
625            let start_language = buffer.language_at(range.start);
626            let end_language = buffer.language_at(range.end);
627            let language_name = if start_language == end_language {
628                start_language.map(|language| language.name())
629            } else {
630                None
631            };
632            let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
633
634            let selected_text = buffer.text_for_range(range).collect::<String>();
635            if selected_text.is_empty() {
636                None
637            } else {
638                Some(if language_name == "markdown" {
639                    selected_text
640                        .lines()
641                        .map(|line| format!("> {}", line))
642                        .collect::<Vec<_>>()
643                        .join("\n")
644                } else {
645                    format!("```{language_name}\n{selected_text}\n```")
646                })
647            }
648        });
649
650        // Activate the panel
651        if !panel.read(cx).has_focus(cx) {
652            workspace.toggle_panel_focus::<AssistantPanel>(cx);
653        }
654
655        if let Some(text) = text {
656            panel.update(cx, |panel, cx| {
657                if let Some(assistant) = panel
658                    .pane
659                    .read(cx)
660                    .active_item()
661                    .and_then(|item| item.downcast::<AssistantEditor>())
662                    .ok_or_else(|| anyhow!("no active context"))
663                    .log_err()
664                {
665                    assistant.update(cx, |assistant, cx| {
666                        assistant
667                            .editor
668                            .update(cx, |editor, cx| editor.insert(&text, cx))
669                    });
670                }
671            });
672        }
673    }
674}
675
676impl Entity for AssistantEditor {
677    type Event = ();
678}
679
680impl View for AssistantEditor {
681    fn ui_name() -> &'static str {
682        "ContextEditor"
683    }
684
685    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
686        let theme = &theme::current(cx).assistant;
687
688        ChildView::new(&self.editor, cx)
689            .contained()
690            .with_style(theme.container)
691            .into_any()
692    }
693
694    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
695        if cx.is_self_focused() {
696            cx.focus(&self.editor);
697        }
698    }
699}
700
701impl Item for AssistantEditor {
702    fn tab_content<V: View>(
703        &self,
704        _: Option<usize>,
705        style: &theme::Tab,
706        _: &gpui::AppContext,
707    ) -> AnyElement<V> {
708        Label::new("New Context", style.label.clone()).into_any()
709    }
710}
711
712#[derive(Clone)]
713struct Message {
714    role: Role,
715    content: ModelHandle<Buffer>,
716    sent_at: DateTime<Local>,
717}
718
719async fn stream_completion(
720    api_key: String,
721    executor: Arc<Background>,
722    mut request: OpenAIRequest,
723) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
724    request.stream = true;
725
726    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
727
728    let json_data = serde_json::to_string(&request)?;
729    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
730        .header("Content-Type", "application/json")
731        .header("Authorization", format!("Bearer {}", api_key))
732        .body(json_data)?
733        .send_async()
734        .await?;
735
736    let status = response.status();
737    if status == StatusCode::OK {
738        executor
739            .spawn(async move {
740                let mut lines = BufReader::new(response.body_mut()).lines();
741
742                fn parse_line(
743                    line: Result<String, io::Error>,
744                ) -> Result<Option<OpenAIResponseStreamEvent>> {
745                    if let Some(data) = line?.strip_prefix("data: ") {
746                        let event = serde_json::from_str(&data)?;
747                        Ok(Some(event))
748                    } else {
749                        Ok(None)
750                    }
751                }
752
753                while let Some(line) = lines.next().await {
754                    if let Some(event) = parse_line(line).transpose() {
755                        tx.unbounded_send(event).log_err();
756                    }
757                }
758
759                anyhow::Ok(())
760            })
761            .detach();
762
763        Ok(rx)
764    } else {
765        let mut body = String::new();
766        response.body_mut().read_to_string(&mut body).await?;
767
768        Err(anyhow!(
769            "Failed to connect to OpenAI API: {} {}",
770            response.status(),
771            body,
772        ))
773    }
774}