1use std::io::{Read, Write};
2use std::net::{TcpListener, TcpStream};
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use serde::Serialize;
6use serde_json::{json, Value};
7
8use crate::anthropic::{
9 anthropic_message_sse, create_anthropic_message_with_solver_and_memory, AnthropicContentBlock,
10 AnthropicMessagesRequest,
11};
12use crate::context_capacity::ContextCapacity;
13use crate::engine::{knowledge_graph, render_thinking_steps};
14use crate::gemini::{
15 create_gemini_generate_content_response_with_solver_and_memory, gemini_model_list,
16 gemini_model_metadata, gemini_response_sse, vertex_model_list, GeminiGenerateContentRequest,
17};
18use crate::mcp::handle_mcp_request;
19use crate::memory_sync::SyncStore;
20use crate::network_endpoint::{handle_links_query_request, handle_network_request};
21use crate::protocol::{
22 chat_exchange_to_record, chat_tool_executions, create_chat_completion_with_solver_and_memory,
23 create_response_with_solver_and_memory, messages_exchange_to_record,
24 responses_exchange_to_record, ChatCompletion, ChatCompletionRequest, ResponsesRequest,
25};
26use crate::responses_stream::responses_sse_response;
27use crate::seed::{canonical_model_id, merged_bundle, try_resolve_model_id};
28use crate::solver::{ExecutionSurface, SolverConfig, UniversalSolver};
29use crate::telegram::handle_telegram_webhook;
30
31static HTTP_AGENT_MODE_FORCED: AtomicBool = AtomicBool::new(false);
32
33pub const ADVERTISED_MAX_OUTPUT_TOKENS: i64 = 8_192;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ApiHttpResponse {
37 pub status_code: u16,
38 pub content_type: &'static str,
39 pub body: String,
40 pub deprecated: bool,
47}
48
49impl ApiHttpResponse {
50 #[must_use]
55 const fn into_deprecated_alias(mut self) -> Self {
56 self.deprecated = true;
57 self
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Default)]
62pub struct ApiAuthConfig {
63 pub bearer_token: Option<String>,
64}
65
66struct ParsedHttpRequest {
67 method: String,
68 path: String,
69 headers: Vec<(String, String)>,
70 body: String,
71}
72
73impl ApiAuthConfig {
74 #[must_use]
75 pub fn bearer_token(token: impl Into<String>) -> Self {
76 Self {
77 bearer_token: Some(token.into()),
78 }
79 }
80
81 #[must_use]
82 pub fn from_env() -> Self {
83 Self {
84 bearer_token: first_non_empty_env(&[
85 "FORMAL_AI_API_BEARER_TOKEN",
86 "FORMAL_AI_HTTP_BEARER_TOKEN",
87 "FORMAL_AI_API_TOKEN",
88 ]),
89 }
90 }
91
92 #[must_use]
93 pub fn allows(&self, headers: &[(&str, &str)]) -> bool {
94 let Some(expected) = self.bearer_token.as_deref() else {
95 return true;
96 };
97 bearer_token_from_headers(headers).is_some_and(|actual| actual == expected)
98 || api_key_from_headers(headers).is_some_and(|actual| actual == expected)
99 }
100}
101
102#[must_use]
103pub fn handle_api_request(method: &str, path: &str, body: &str) -> ApiHttpResponse {
104 handle_api_request_with_auth(method, path, &[], body, &ApiAuthConfig::from_env())
105}
106
107#[must_use]
108pub fn handle_api_request_with_headers(
109 method: &str,
110 path: &str,
111 headers: &[(&str, &str)],
112 body: &str,
113) -> ApiHttpResponse {
114 handle_api_request_with_auth(method, path, headers, body, &ApiAuthConfig::from_env())
115}
116
117fn record_exchange_best_effort(
121 store: &mut SyncStore,
122 exchange: Option<(String, String)>,
123 tools: &[crate::memory_sync::RecordedToolExecution],
124) {
125 let Some((prompt, answer)) = exchange else {
126 return;
127 };
128 if let Err(error) = store.record_chat_exchange_with_tools(&prompt, &answer, tools) {
129 eprintln!("[memory] failed to record live chat exchange: {error}");
130 }
131}
132
133#[must_use]
134pub fn handle_api_request_with_auth(
135 method: &str,
136 path: &str,
137 headers: &[(&str, &str)],
138 body: &str,
139 auth: &ApiAuthConfig,
140) -> ApiHttpResponse {
141 let normalized_path = path.split('?').next().unwrap_or(path);
142 let authorized = !requires_bearer_auth(method, normalized_path) || auth.allows(headers);
143 let response = dispatch_api_request_with_auth(method, path, headers, body, auth);
144 crate::dialog_log::record_api_exchange_if_enabled(
145 method, path, headers, body, &response, authorized,
146 );
147 response
148}
149
150fn dispatch_api_request_with_auth(
151 method: &str,
152 path: &str,
153 headers: &[(&str, &str)],
154 body: &str,
155 auth: &ApiAuthConfig,
156) -> ApiHttpResponse {
157 let _foreground_activity = crate::dreaming_runtime::ForegroundActivity::begin();
158 let normalized_path = path.split('?').next().unwrap_or(path);
159 let query = path.split_once('?').map_or("", |(_, q)| q);
160
161 if normalized_path == "/mcp" && !mcp_origin_allowed(headers) {
162 return error_response(403, "MCP origin is not allowed");
163 }
164 if requires_bearer_auth(method, normalized_path) && !auth.allows(headers) {
165 return error_response(401, "missing or invalid bearer token");
166 }
167
168 crate::dialog_log::trace_request_if_enabled(method, normalized_path, body);
169
170 if let Some(response) = handle_dynamic_protocol_route(method, normalized_path, body) {
171 return response;
172 }
173
174 match (method, normalized_path) {
175 ("OPTIONS", _) => ApiHttpResponse {
176 status_code: 204,
177 content_type: "application/json",
178 body: String::new(),
179 deprecated: false,
180 },
181 ("GET", "/health") => json_response(
182 200,
183 &json!({
184 "status": "ok",
185 "model": canonical_model_id(),
186 }),
187 ),
188 ("GET", "/v1/models" | "/api/openai/v1/models") => handle_openai_models_request(),
189 ("GET", "/v1/network" | "/api/formal-ai/v1/network") => handle_network_request(query),
190 ("GET", "/v1/graph" | "/api/formal-ai/v1/graph") => {
195 handle_network_request(query).into_deprecated_alias()
196 }
197 ("GET", "/v1/bundle" | "/api/formal-ai/v1/bundle") => {
198 links_notation_response(200, merged_bundle())
199 }
200 ("GET", "/v1/links" | "/api/formal-ai/v1/links") => {
201 links_notation_response(200, knowledge_graph().to_links_notation())
202 }
203 ("POST", "/v1/links/query" | "/api/formal-ai/v1/links/query") => {
204 handle_links_query_request(body)
205 }
206 ("GET", "/v1/memory" | "/api/formal-ai/v1/memory") => {
207 links_notation_response(200, SyncStore::open().to_links_notation())
208 }
209 ("GET", "/v1/memory/since" | "/api/formal-ai/v1/memory/since") => {
210 handle_memory_since_request(query)
211 }
212 ("POST", "/v1/memory/import" | "/api/formal-ai/v1/memory/import") => {
213 handle_memory_import_request(body)
214 }
215 ("POST", "/v1/messages" | "/api/anthropic/v1/messages") => {
216 handle_anthropic_messages_request(body)
217 }
218 ("POST", "/v1/chat/completions" | "/api/openai/v1/chat/completions") => {
219 match serde_json::from_str::<ChatCompletionRequest>(body) {
220 Ok(request) => {
221 if let Some(response) = unsupported_model_response(request.model.as_deref()) {
222 return response;
223 }
224 let solver = http_solver();
225 let mut store = SyncStore::open();
226 let completion = create_chat_completion_with_solver_and_memory(
227 &request,
228 &solver,
229 store.events(),
230 );
231 record_exchange_best_effort(
232 &mut store,
233 chat_exchange_to_record(&request, &completion),
234 &chat_tool_executions(&request.messages),
235 );
236 if request.stream {
237 let include_usage = request
238 .stream_options
239 .is_some_and(|options| options.include_usage);
240 chat_completion_sse_response(&completion, include_usage)
241 } else {
242 json_response(200, &completion)
243 }
244 }
245 Err(error) => error_response(400, &format!("invalid chat request: {error}")),
246 }
247 }
248 ("POST", "/v1/responses" | "/api/openai/v1/responses") => {
249 match serde_json::from_str::<ResponsesRequest>(body) {
250 Ok(request) => {
251 if let Some(response) = unsupported_model_response(request.model.as_deref()) {
252 return response;
253 }
254 let solver = http_solver();
255 let mut store = SyncStore::open();
256 let response =
257 create_response_with_solver_and_memory(&request, &solver, store.events());
258 record_exchange_best_effort(
259 &mut store,
260 responses_exchange_to_record(&request, &response),
261 &chat_tool_executions(&request.to_chat_completion_request().messages),
262 );
263 if request.stream {
264 responses_sse_response(&response)
265 } else {
266 json_response(200, &response)
267 }
268 }
269 Err(error) => error_response(400, &format!("invalid responses request: {error}")),
270 }
271 }
272 ("POST", "/mcp") => handle_mcp_request(body, &http_solver()),
273 ("GET", "/mcp") => error_response(405, "MCP SSE streams are not supported"),
274 ("POST", "/telegram/webhook") => match handle_telegram_webhook(body) {
275 Ok(Some(reply)) => json_response(200, &reply),
276 Ok(None) => ApiHttpResponse {
277 status_code: 200,
278 content_type: "application/json",
279 body: String::new(),
280 deprecated: false,
281 },
282 Err(error) => error_response(400, &error.to_string()),
283 },
284 _ => error_response(404, "route not found"),
285 }
286}
287
288fn requires_bearer_auth(method: &str, normalized_path: &str) -> bool {
289 method != "OPTIONS"
290 && (normalized_path == "/mcp"
291 || normalized_path.starts_with("/v1/")
292 || normalized_path.starts_with("/api/"))
293}
294
295fn mcp_origin_allowed(headers: &[(&str, &str)]) -> bool {
296 let Some(origin) = headers
297 .iter()
298 .find_map(|(name, value)| name.eq_ignore_ascii_case("origin").then_some(*value))
299 else {
300 return true;
301 };
302 let Some(host) = headers
303 .iter()
304 .find_map(|(name, value)| name.eq_ignore_ascii_case("host").then_some(*value))
305 else {
306 return false;
307 };
308 let origin = origin.trim_end_matches('/');
309 origin
310 .strip_prefix("http://")
311 .or_else(|| origin.strip_prefix("https://"))
312 .is_some_and(|authority| authority.eq_ignore_ascii_case(host))
313}
314
315fn handle_dynamic_protocol_route(
316 method: &str,
317 normalized_path: &str,
318 body: &str,
319) -> Option<ApiHttpResponse> {
320 if method == "GET" && normalized_path == "/api/gemini/v1beta/models" {
321 return Some(json_response(200, &gemini_model_list()));
322 }
323 if method == "GET" {
324 if let Some(model) = gemini_model_metadata_path(normalized_path) {
325 return Some(json_response(
326 200,
327 &gemini_model_metadata(&format!("models/{model}")),
328 ));
329 }
330 if let Some((project, location)) = vertex_models_path(normalized_path) {
331 return Some(json_response(200, &vertex_model_list(&project, &location)));
332 }
333 }
334 if method == "POST" {
335 if let Some(model) = gemini_model_action_path(normalized_path, "generateContent") {
336 return Some(handle_gemini_generate_content_request(&model, false, body));
337 }
338 if let Some(model) = gemini_model_action_path(normalized_path, "streamGenerateContent") {
339 return Some(handle_gemini_generate_content_request(&model, true, body));
340 }
341 if let Some(route) = vertex_model_action_path(normalized_path, "generateContent") {
342 return Some(handle_gemini_generate_content_request(
343 &route.model,
344 false,
345 body,
346 ));
347 }
348 if let Some(route) = vertex_model_action_path(normalized_path, "streamGenerateContent") {
349 return Some(handle_gemini_generate_content_request(
350 &route.model,
351 true,
352 body,
353 ));
354 }
355 }
356 None
357}
358
359fn handle_openai_models_request() -> ApiHttpResponse {
360 let model_id = canonical_model_id();
361 let context = match ContextCapacity::current() {
362 Ok(context) => context,
363 Err(error) => return error_response(500, &error.to_string()),
364 };
365 let context_metadata = json!(context);
366 json_response(
367 200,
368 &json!({
369 "object": "list",
370 "data": [{
371 "id": model_id,
372 "slug": model_id,
373 "object": "model",
374 "owned_by": "link-assistant",
375 "context_window": context.context_window_tokens,
376 "context_window_tokens": context.context_window_tokens,
377 "context_used_tokens": context.context_used_tokens,
378 "context_used_fraction": context.context_used_fraction,
379 "disk_free_bytes": context.disk_free_bytes,
380 "memory_used_bytes": context.memory_used_bytes,
381 "avg_utf8_bytes_per_char": context.avg_utf8_bytes_per_char,
382 "context": context_metadata
383 }],
384 "models": [{
385 "id": model_id,
386 "slug": model_id,
387 "name": model_id,
388 "context_window": context.context_window_tokens,
389 "max_output_tokens": ADVERTISED_MAX_OUTPUT_TOKENS,
390 "context_window_tokens": context.context_window_tokens,
391 "context_used_tokens": context.context_used_tokens,
392 "context_used_fraction": context.context_used_fraction,
393 "disk_free_bytes": context.disk_free_bytes,
394 "memory_used_bytes": context.memory_used_bytes,
395 "avg_utf8_bytes_per_char": context.avg_utf8_bytes_per_char,
396 "context": context_metadata
397 }],
398 "rate_limit": {
399 "requests_per_minute": 60,
400 "tokens_per_minute": 60_000
401 }
402 }),
403 )
404}
405
406fn handle_gemini_generate_content_request(
407 model: &str,
408 stream: bool,
409 body: &str,
410) -> ApiHttpResponse {
411 let model = normalize_protocol_model_id(model);
412 if let Some(response) = unsupported_model_response(Some(&model)) {
413 return response;
414 }
415 match serde_json::from_str::<GeminiGenerateContentRequest>(body) {
416 Ok(request) => {
417 let solver = http_solver();
418 let mut store = SyncStore::open();
419 let chat_request = request.to_chat_completion_request(&model);
420 let response = create_gemini_generate_content_response_with_solver_and_memory(
421 &request,
422 &model,
423 &solver,
424 store.events(),
425 );
426 let answer = response["candidates"][0]["content"]["parts"]
427 .as_array()
428 .into_iter()
429 .flatten()
430 .filter_map(|part| part.get("text").and_then(Value::as_str))
431 .collect::<Vec<_>>()
432 .join("\n");
433 record_exchange_best_effort(
434 &mut store,
435 messages_exchange_to_record(&chat_request.messages, &answer),
436 &chat_tool_executions(&chat_request.messages),
437 );
438 if stream {
439 ApiHttpResponse {
440 status_code: 200,
441 content_type: "text/event-stream",
442 body: gemini_response_sse(&response),
443 deprecated: false,
444 }
445 } else {
446 json_response(200, &response)
447 }
448 }
449 Err(error) => error_response(400, &format!("invalid generateContent request: {error}")),
450 }
451}
452
453fn normalize_protocol_model_id(model: &str) -> String {
454 model
455 .strip_prefix("models/")
456 .unwrap_or(model)
457 .trim()
458 .to_owned()
459}
460
461fn gemini_model_metadata_path(path: &str) -> Option<String> {
462 let model = path.strip_prefix("/api/gemini/v1beta/models/")?;
463 (!model.contains(':')).then(|| normalize_protocol_model_id(model))
464}
465
466fn gemini_model_action_path(path: &str, action: &str) -> Option<String> {
467 let model = path.strip_prefix("/api/gemini/v1beta/models/")?;
468 let suffix = format!(":{action}");
469 model.strip_suffix(&suffix).map(normalize_protocol_model_id)
470}
471
472fn vertex_models_path(path: &str) -> Option<(String, String)> {
473 let route = path.strip_prefix("/api/vertex/v1/projects/")?;
474 let (project, route) = route.split_once("/locations/")?;
475 let (location, tail) = route.split_once("/publishers/google/models")?;
476 tail.is_empty()
477 .then(|| (project.to_owned(), location.to_owned()))
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
481struct VertexModelRoute {
482 model: String,
483}
484
485fn vertex_model_action_path(path: &str, action: &str) -> Option<VertexModelRoute> {
486 let route = path.strip_prefix("/api/vertex/v1/projects/")?;
487 let (_project, route) = route.split_once("/locations/")?;
488 let (_location, model) = route.split_once("/publishers/google/models/")?;
489 let suffix = format!(":{action}");
490 let model = model.strip_suffix(&suffix)?;
491 Some(VertexModelRoute {
492 model: normalize_protocol_model_id(model),
493 })
494}
495
496fn first_non_empty_env(names: &[&str]) -> Option<String> {
497 names.iter().find_map(|name| {
498 let value = std::env::var(name).ok()?;
499 let trimmed = value.trim();
500 if trimmed.is_empty() {
501 None
502 } else {
503 Some(trimmed.to_owned())
504 }
505 })
506}
507
508fn bearer_token_from_headers<'a>(headers: &'a [(&str, &str)]) -> Option<&'a str> {
509 headers.iter().find_map(|(name, value)| {
510 if name.eq_ignore_ascii_case("authorization") {
511 parse_bearer_token(value)
512 } else {
513 None
514 }
515 })
516}
517
518fn api_key_from_headers<'a>(headers: &'a [(&str, &str)]) -> Option<&'a str> {
519 headers.iter().find_map(|(name, value)| {
520 (name.eq_ignore_ascii_case("x-api-key")
521 || name.eq_ignore_ascii_case("x-goog-api-key")
522 || name.eq_ignore_ascii_case("anthropic-api-key"))
523 .then(|| value.trim())
524 .filter(|value| !value.is_empty())
525 })
526}
527
528fn parse_bearer_token(value: &str) -> Option<&str> {
529 let mut parts = value.split_whitespace();
530 let scheme = parts.next()?;
531 let token = parts.next()?;
532 if parts.next().is_some() || !scheme.eq_ignore_ascii_case("bearer") {
533 return None;
534 }
535 Some(token)
536}
537
538fn unsupported_model_response(model: Option<&str>) -> Option<ApiHttpResponse> {
539 let model = model.map(str::trim).filter(|model| !model.is_empty())?;
540 if try_resolve_model_id(Some(model)).is_some() {
541 None
542 } else {
543 Some(error_response(
544 400,
545 &format!(
546 "unsupported model `{model}`; use `{}` or a configured alias",
547 canonical_model_id()
548 ),
549 ))
550 }
551}
552
553pub fn enable_http_agent_mode_for_current_process() {
559 HTTP_AGENT_MODE_FORCED.store(true, Ordering::Relaxed);
560}
561
562fn http_solver() -> UniversalSolver {
563 let mut config = SolverConfig::from_env();
564 if HTTP_AGENT_MODE_FORCED.load(Ordering::Relaxed) {
565 config.agent_mode = true;
566 }
567 config.execution_surface = ExecutionSurface::HttpServer;
568 UniversalSolver::new(config)
569}
570
571fn chat_completion_sse_response(
587 completion: &ChatCompletion,
588 include_usage: bool,
589) -> ApiHttpResponse {
590 let mut body = String::new();
591 let base = json!({
592 "id": completion.id,
593 "object": "chat.completion.chunk",
594 "created": completion.created,
595 "model": completion.model,
596 });
597
598 let choice = completion.choices.first();
599
600 let role_delta = json!({
602 "index": 0,
603 "delta": { "role": "assistant" },
604 "finish_reason": null,
605 });
606 body.push_str(&sse_chunk(&base, &role_delta));
607
608 if let Some(choice) = choice {
610 let reasoning = if choice.message.reasoning_content.is_empty() {
611 render_thinking_steps(&choice.message.thinking_steps)
612 } else {
613 choice.message.reasoning_content.clone()
614 };
615 if !reasoning.is_empty() {
616 let delta = json!({
617 "index": 0,
618 "delta": {
619 "reasoning_content": reasoning,
620 "reasoning": reasoning,
621 },
622 "finish_reason": null,
623 });
624 body.push_str(&sse_chunk(&base, &delta));
625 }
626 let text = choice.message.content.plain_text();
627 if !text.is_empty() {
628 let delta = json!({
629 "index": 0,
630 "delta": { "content": text },
631 "finish_reason": null,
632 });
633 body.push_str(&sse_chunk(&base, &delta));
634 }
635 for (index, call) in choice.message.tool_calls.iter().enumerate() {
636 let delta = json!({
637 "index": 0,
638 "delta": {
639 "tool_calls": [{
640 "index": index,
641 "id": call.id,
642 "type": call.kind,
643 "function": {
644 "name": call.function.name,
645 "arguments": call.function.arguments,
646 }
647 }]
648 },
649 "finish_reason": null,
650 });
651 body.push_str(&sse_chunk(&base, &delta));
652 }
653 }
654
655 let finish_reason = choice.map_or_else(
657 || String::from("stop"),
658 |choice| choice.finish_reason.clone(),
659 );
660 let final_chunk = json!({
661 "index": 0,
662 "delta": {},
663 "finish_reason": finish_reason,
664 });
665 body.push_str(&sse_chunk(&base, &final_chunk));
666
667 if include_usage {
670 let usage_payload = json!({
671 "id": completion.id,
672 "object": "chat.completion.chunk",
673 "created": completion.created,
674 "model": completion.model,
675 "choices": [],
676 "usage": {
677 "prompt_tokens": completion.usage.prompt_tokens,
678 "completion_tokens": completion.usage.completion_tokens,
679 "total_tokens": completion.usage.total_tokens,
680 }
681 });
682 body.push_str("data: ");
683 body.push_str(&usage_payload.to_string());
684 body.push_str("\n\n");
685 }
686
687 body.push_str("data: [DONE]\n\n");
688
689 ApiHttpResponse {
690 status_code: 200,
691 content_type: "text/event-stream",
692 body,
693 deprecated: false,
694 }
695}
696
697fn sse_chunk(base: &Value, choice: &Value) -> String {
700 let mut merged = base.clone();
701 if let Value::Object(map) = &mut merged {
702 map.insert(String::from("choices"), Value::Array(vec![choice.clone()]));
703 }
704 format!("data: {merged}\n\n")
705}
706
707fn handle_anthropic_messages_request(body: &str) -> ApiHttpResponse {
712 match serde_json::from_str::<AnthropicMessagesRequest>(body) {
713 Ok(request) => {
714 if let Some(response) = unsupported_model_response(request.model.as_deref()) {
715 return response;
716 }
717 let solver = http_solver();
718 let mut store = SyncStore::open();
719 let chat_request = request.to_chat_completion_request();
720 let message =
721 create_anthropic_message_with_solver_and_memory(&request, &solver, store.events());
722 let answer = message
723 .content
724 .iter()
725 .filter_map(|block| match block {
726 AnthropicContentBlock::Text { text } => Some(text.as_str()),
727 _ => None,
728 })
729 .collect::<Vec<_>>()
730 .join("\n");
731 record_exchange_best_effort(
732 &mut store,
733 messages_exchange_to_record(&chat_request.messages, &answer),
734 &chat_tool_executions(&chat_request.messages),
735 );
736 if request.stream {
737 ApiHttpResponse {
738 status_code: 200,
739 content_type: "text/event-stream",
740 body: anthropic_message_sse(&message),
741 deprecated: false,
742 }
743 } else {
744 json_response(200, &message)
745 }
746 }
747 Err(error) => error_response(400, &format!("invalid messages request: {error}")),
748 }
749}
750
751fn handle_memory_since_request(query: &str) -> ApiHttpResponse {
758 let last_seen = query_param(query, "event");
759 let store = SyncStore::open();
760 links_notation_response(200, store.delta_links_notation(last_seen.as_deref()))
761}
762
763fn handle_memory_import_request(body: &str) -> ApiHttpResponse {
766 let mut store = SyncStore::open();
767 match store.import_links_notation(body) {
768 Ok(added) => json_response(
769 200,
770 &json!({
771 "object": "memory.import",
772 "added": added,
773 "total": store.events().len(),
774 }),
775 ),
776 Err(error) => error_response(500, &format!("failed to persist memory: {error}")),
777 }
778}
779
780fn query_param(query: &str, key: &str) -> Option<String> {
781 query
782 .split('&')
783 .filter(|part| !part.is_empty())
784 .find_map(|pair| {
785 let (name, value) = pair.split_once('=')?;
786 (name == key).then(|| value.to_owned())
787 })
788}
789
790pub(crate) const fn links_notation_response(status_code: u16, body: String) -> ApiHttpResponse {
791 ApiHttpResponse {
792 status_code,
793 content_type: "text/plain",
794 body,
795 deprecated: false,
796 }
797}
798
799pub fn serve(address: &str) -> std::io::Result<()> {
800 crate::dreaming_runtime::start_core_dreaming();
801 eprintln!(
802 "formal-ai shared memory: {}",
803 crate::shared_memory::shared_memory_path().display()
804 );
805 let listener = TcpListener::bind(address)?;
806 eprintln!("formal-ai server listening on http://{address}");
807
808 for stream in listener.incoming() {
809 match stream {
810 Ok(mut stream) => {
811 if let Err(error) = handle_connection(&mut stream) {
812 eprintln!("request failed: {error}");
813 }
814 }
815 Err(error) => eprintln!("connection failed: {error}"),
816 }
817 }
818
819 Ok(())
820}
821
822fn handle_connection(stream: &mut TcpStream) -> std::io::Result<()> {
823 let Some(request) = read_request(stream)? else {
824 return Ok(());
825 };
826 let headers = request
827 .headers
828 .iter()
829 .map(|(name, value)| (name.as_str(), value.as_str()))
830 .collect::<Vec<_>>();
831 let response =
832 handle_api_request_with_headers(&request.method, &request.path, &headers, &request.body);
833 write_response(stream, &response)
834}
835
836fn read_request(stream: &mut TcpStream) -> std::io::Result<Option<ParsedHttpRequest>> {
837 let mut buffer = [0_u8; 8192];
838 let bytes_read = stream.read(&mut buffer)?;
839 if bytes_read == 0 {
840 return Ok(None);
841 }
842
843 let mut request_bytes = buffer[..bytes_read].to_vec();
844 let header_end = loop {
845 if let Some(position) = find_header_end(&request_bytes) {
846 break position;
847 }
848 let bytes_read = stream.read(&mut buffer)?;
849 if bytes_read == 0 {
850 return Ok(None);
851 }
852 request_bytes.extend_from_slice(&buffer[..bytes_read]);
853 };
854
855 let header_text = String::from_utf8_lossy(&request_bytes[..header_end]).to_string();
856 let content_length = content_length(&header_text);
857 let body_start = header_end + 4;
858
859 while request_bytes.len() < body_start.saturating_add(content_length) {
860 let bytes_read = stream.read(&mut buffer)?;
861 if bytes_read == 0 {
862 break;
863 }
864 request_bytes.extend_from_slice(&buffer[..bytes_read]);
865 }
866
867 let request_line = header_text.lines().next().unwrap_or_default();
868 let mut request_parts = request_line.split_whitespace();
869 let method = request_parts.next().unwrap_or_default().to_owned();
870 let path = request_parts.next().unwrap_or_default().to_owned();
871 let headers = request_headers(&header_text);
872 let body_end = body_start
873 .saturating_add(content_length)
874 .min(request_bytes.len());
875 let body = String::from_utf8_lossy(&request_bytes[body_start..body_end]).to_string();
876
877 Ok(Some(ParsedHttpRequest {
878 method,
879 path,
880 headers,
881 body,
882 }))
883}
884
885fn write_response(stream: &mut TcpStream, response: &ApiHttpResponse) -> std::io::Result<()> {
886 let status_text = match response.status_code {
887 200 => "200 OK",
888 204 => "204 No Content",
889 400 => "400 Bad Request",
890 401 => "401 Unauthorized",
891 403 => "403 Forbidden",
892 404 => "404 Not Found",
893 405 => "405 Method Not Allowed",
894 _ => "500 Internal Server Error",
895 };
896
897 let deprecation_header = if response.deprecated {
901 "deprecation: true\r\nlink: </v1/network>; rel=\"successor-version\"\r\n"
902 } else {
903 ""
904 };
905
906 write!(
907 stream,
908 "HTTP/1.1 {status_text}\r\n\
909 content-type: {}\r\n\
910 content-length: {}\r\n\
911 access-control-allow-origin: *\r\n\
912 access-control-allow-methods: GET,POST,OPTIONS\r\n\
913 access-control-allow-headers: content-type,authorization,x-api-key,x-goog-api-key,anthropic-api-key\r\n\
914 {deprecation_header}\
915 connection: close\r\n\
916 \r\n{}",
917 response.content_type,
918 response.body.len(),
919 response.body
920 )
921}
922
923pub(crate) fn json_response<T: Serialize>(status_code: u16, value: &T) -> ApiHttpResponse {
924 match serde_json::to_string_pretty(value) {
925 Ok(body) => ApiHttpResponse {
926 status_code,
927 content_type: "application/json",
928 body,
929 deprecated: false,
930 },
931 Err(error) => error_response(500, &format!("failed to serialize response: {error}")),
932 }
933}
934
935pub(crate) fn error_response(status_code: u16, message: &str) -> ApiHttpResponse {
936 ApiHttpResponse {
937 status_code,
938 content_type: "application/json",
939 body: json!({
940 "error": {
941 "message": message,
942 "type": "formal_ai_error"
943 }
944 })
945 .to_string(),
946 deprecated: false,
947 }
948}
949
950fn find_header_end(bytes: &[u8]) -> Option<usize> {
951 bytes.windows(4).position(|window| window == b"\r\n\r\n")
952}
953
954fn request_headers(headers: &str) -> Vec<(String, String)> {
955 headers
956 .lines()
957 .skip(1)
958 .filter_map(|line| {
959 let (name, value) = line.split_once(':')?;
960 Some((name.trim().to_owned(), value.trim().to_owned()))
961 })
962 .collect()
963}
964
965fn content_length(headers: &str) -> usize {
966 headers
967 .lines()
968 .find_map(|line| {
969 let (name, value) = line.split_once(':')?;
970 if name.eq_ignore_ascii_case("content-length") {
971 value.trim().parse::<usize>().ok()
972 } else {
973 None
974 }
975 })
976 .unwrap_or(0)
977}