orchestral_runtime/planner/llm/
http.rs1use async_trait::async_trait;
2use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
3use serde::{Deserialize, Serialize};
4
5use super::{LlmClient, LlmError, LlmRequest};
6
7#[derive(Debug, Clone)]
8pub struct HttpLlmClientConfig {
9 pub endpoint: String,
10 pub api_key: Option<String>,
11 pub model: String,
12 pub temperature: f32,
13 pub timeout_secs: u64,
14 pub extra_headers: HeaderMap,
15}
16
17impl Default for HttpLlmClientConfig {
18 fn default() -> Self {
19 Self {
20 endpoint: "https://api.openai.com/v1/chat/completions".to_string(),
21 api_key: None,
22 model: "gpt-4o-mini".to_string(),
23 temperature: 0.2,
24 timeout_secs: 30,
25 extra_headers: HeaderMap::new(),
26 }
27 }
28}
29
30pub struct HttpLlmClient {
31 client: reqwest::Client,
32 config: HttpLlmClientConfig,
33}
34
35impl HttpLlmClient {
36 pub fn new(config: HttpLlmClientConfig) -> Result<Self, LlmError> {
37 let client = reqwest::Client::builder()
38 .timeout(std::time::Duration::from_secs(config.timeout_secs))
39 .build()
40 .map_err(|e| LlmError::Http(e.to_string()))?;
41 Ok(Self { client, config })
42 }
43}
44
45#[derive(Debug, Serialize)]
46struct ChatMessage {
47 role: String,
48 content: String,
49}
50
51#[derive(Debug, Serialize)]
52struct ChatRequest {
53 model: String,
54 messages: Vec<ChatMessage>,
55 temperature: f32,
56}
57
58#[derive(Debug, Deserialize)]
59struct ChatResponse {
60 choices: Vec<ChatChoice>,
61}
62
63#[derive(Debug, Deserialize)]
64struct ChatChoice {
65 message: ChatMessageResponse,
66}
67
68#[derive(Debug, Deserialize)]
69struct ChatMessageResponse {
70 content: String,
71}
72
73#[async_trait]
74impl LlmClient for HttpLlmClient {
75 async fn complete(&self, request: LlmRequest) -> Result<String, LlmError> {
76 let mut headers = self.config.extra_headers.clone();
77 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
78 if let Some(key) = &self.config.api_key {
79 let value = format!("Bearer {}", key);
80 headers.insert(
81 AUTHORIZATION,
82 HeaderValue::from_str(&value).map_err(|e| LlmError::Http(e.to_string()))?,
83 );
84 }
85
86 let body = ChatRequest {
87 model: request.model,
88 messages: vec![
89 ChatMessage {
90 role: "system".to_string(),
91 content: request.system,
92 },
93 ChatMessage {
94 role: "user".to_string(),
95 content: request.user,
96 },
97 ],
98 temperature: request.temperature,
99 };
100
101 let response = self
102 .client
103 .post(&self.config.endpoint)
104 .headers(headers)
105 .json(&body)
106 .send()
107 .await
108 .map_err(|e| LlmError::Http(e.to_string()))?;
109
110 if !response.status().is_success() {
111 let status = response.status();
112 let text = response.text().await.unwrap_or_default();
113 return Err(LlmError::Response(format!("HTTP {}: {}", status, text)));
114 }
115
116 let text = response
117 .text()
118 .await
119 .map_err(|e| LlmError::Http(e.to_string()))?;
120 let parsed: ChatResponse =
121 serde_json::from_str(&text).map_err(|e| LlmError::Serialization(e.to_string()))?;
122
123 let content = parsed
124 .choices
125 .first()
126 .map(|c| c.message.content.clone())
127 .ok_or_else(|| LlmError::Response("Missing choices".to_string()))?;
128
129 Ok(content)
130 }
131}