Skip to main content

offline_intelligence/api/
online_api.rs

1//! Online mode API endpoints
2//!
3//! Handles online mode requests that connect directly to external APIs like OpenRouter.
4
5use axum::{
6    extract::State,
7    response::{
8        sse::{Event, Sse},
9        IntoResponse, Response,
10    },
11    http::StatusCode,
12    Json,
13};
14use futures_util::StreamExt;
15use serde::Deserialize;
16use std::convert::Infallible;
17use tracing::{info, error, debug};
18use reqwest;
19use serde_json::Value;
20
21use crate::memory::Message;
22use crate::shared_state::UnifiedAppState;
23
24/// Request body for online mode streaming
25#[derive(Debug, Deserialize)]
26pub struct OnlineStreamRequest {
27    pub model: String,
28    pub messages: Vec<Message>,
29    pub session_id: String,
30    #[serde(default = "default_max_tokens")]
31    pub max_tokens: u32,
32    #[serde(default = "default_temperature")]
33    pub temperature: f32,
34    #[serde(default = "default_stream")]
35    pub stream: bool,
36    pub api_key: Option<String>, // API key passed from frontend
37}
38
39fn default_max_tokens() -> u32 { 2000 }
40fn default_temperature() -> f32 { 0.7 }
41fn default_stream() -> bool { true }
42
43/// POST /online/stream — Online mode streaming endpoint
44/// Connects directly to OpenRouter API
45pub async fn online_stream(
46    State(state): State<UnifiedAppState>,
47    Json(req): Json<OnlineStreamRequest>,
48) -> Response {
49    info!("Online stream request for session: {}", req.session_id);
50
51    // Debug: Log what we received
52    debug!("Request api_key present: {}", 
53        req.api_key.is_some()
54    );
55
56    // Get OpenRouter API key - priority order:
57    // 1. Frontend-passed key (for backward compatibility)
58    // 2. Database stored keys (persisted, synced)
59    // 3. Environment variables
60    // 4. Config file
61    let api_key = if let Some(key) = &req.api_key {
62        if !key.is_empty() {
63            key.clone()
64        } else {
65            state.get_openrouter_api_key().await.unwrap_or_default()
66        }
67    } else {
68        state.get_openrouter_api_key().await.unwrap_or_default()
69    };
70
71    debug!("Final API key length: {}", api_key.len());
72
73    if api_key.is_empty() {
74        error!("OpenRouter API key is empty");
75        return (StatusCode::UNAUTHORIZED, "OpenRouter API key not configured. Please add your API key in Settings.").into_response();
76    }
77
78    // Prepare messages in OpenRouter format
79    let openrouter_messages = req.messages.iter().map(|m| {
80        serde_json::json!({
81            "role": m.role,
82            "content": m.content
83        })
84    }).collect::<Vec<_>>();
85
86    // Prepare OpenRouter request
87    let openrouter_request = serde_json::json!({
88        "model": req.model,
89        "messages": openrouter_messages,
90        "max_tokens": req.max_tokens,
91        "temperature": req.temperature,
92        "stream": req.stream,
93    });
94
95    // Make request to OpenRouter API
96    let client = reqwest::Client::new();
97    let response = client
98        .post("https://openrouter.ai/api/v1/chat/completions")
99        .header("Authorization", format!("Bearer {}", api_key))
100        .header("Content-Type", "application/json")
101        .header("HTTP-Referer", "https://aud.io")
102        .header("X-Title", "Aud.io")
103        .json(&openrouter_request)
104        .send()
105        .await;
106
107    match response {
108        Ok(resp) => {
109            if !resp.status().is_success() {
110                let status = resp.status();
111                let body = resp.text().await.unwrap_or_default();
112                error!("OpenRouter API error ({}): {}", status, body);
113                return (StatusCode::BAD_GATEWAY, format!("OpenRouter API error: {}", body)).into_response();
114            }
115
116            let byte_stream = resp.bytes_stream();
117
118            let sse_stream = async_stream::stream! {
119                let mut buffer = String::new();
120
121                futures_util::pin_mut!(byte_stream);
122
123                while let Some(chunk_result) = byte_stream.next().await {
124                    match chunk_result {
125                        Ok(chunk) => {
126                            buffer.push_str(&String::from_utf8_lossy(&chunk));
127
128                            while let Some(newline_pos) = buffer.find('\n') {
129                                let line = buffer[..newline_pos].trim().to_string();
130                                buffer = buffer[newline_pos + 1..].to_string();
131
132                                if line.is_empty() {
133                                    continue;
134                                }
135
136                                if line.starts_with("data: ") {
137                                    let data = &line[6..];
138
139                                    if data == "[DONE]" {
140                                        yield Ok::<_, Infallible>(Event::default().data("[DONE]"));
141                                        return;
142                                    }
143
144                                    // Forward the data as-is to the client
145                                    yield Ok(Event::default().data(data));
146                                }
147                            }
148                        }
149                        Err(e) => {
150                            error!("Stream error: {}", e);
151                            yield Ok(Event::default().data(
152                                format!("{{\"error\": \"{}\"}}", e)
153                            ));
154                            break;
155                        }
156                    }
157                }
158            };
159
160            Sse::new(sse_stream)
161                .keep_alive(
162                    axum::response::sse::KeepAlive::new()
163                        .interval(std::time::Duration::from_secs(15))
164                )
165                .into_response()
166        }
167        Err(e) => {
168            error!("Failed to connect to OpenRouter: {}", e);
169            (StatusCode::BAD_GATEWAY, format!("Failed to connect to OpenRouter: {}", e)).into_response()
170        }
171    }
172}