1use simple_agents_core::SimpleAgentsClient;
4use simple_agents_core::SimpleAgentsClientBuilder;
5use simple_agents_providers::anthropic::AnthropicProvider;
6use simple_agents_providers::openai::OpenAIProvider;
7use simple_agents_providers::openrouter::OpenRouterProvider;
8use simple_agent_type::message::Message;
9use simple_agent_type::prelude::{
10 ApiKey, CompletionRequest, Provider, Result, SimpleAgentsError,
11};
12use std::cell::RefCell;
13use std::ffi::{CStr, CString};
14use std::os::raw::c_char;
15use std::panic::{catch_unwind, AssertUnwindSafe};
16use std::sync::{Arc, Mutex};
17
18type Runtime = tokio::runtime::Runtime;
19
20struct FfiClient {
21 runtime: Mutex<Runtime>,
22 client: SimpleAgentsClient,
23}
24
25#[repr(C)]
26pub struct SAClient {
27 inner: FfiClient,
28}
29
30thread_local! {
31 static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
32}
33
34fn set_last_error(message: impl Into<String>) {
35 LAST_ERROR.with(|slot| {
36 *slot.borrow_mut() = Some(message.into());
37 });
38}
39
40fn clear_last_error() {
41 LAST_ERROR.with(|slot| {
42 *slot.borrow_mut() = None;
43 });
44}
45
46fn take_last_error() -> Option<String> {
47 LAST_ERROR.with(|slot| slot.borrow_mut().take())
48}
49
50fn build_runtime() -> Result<Runtime> {
51 Runtime::new().map_err(|e| SimpleAgentsError::Config(format!("Failed to build runtime: {e}")))
52}
53
54fn provider_from_env(provider_name: &str) -> Result<Arc<dyn Provider>> {
55 match provider_name {
56 "openai" => Ok(Arc::new(OpenAIProvider::from_env()?)),
57 "anthropic" => Ok(Arc::new(AnthropicProvider::from_env()?)),
58 "openrouter" => Ok(Arc::new(openrouter_from_env()?)),
59 _ => Err(SimpleAgentsError::Config(format!(
60 "Unknown provider '{provider_name}'"
61 ))),
62 }
63}
64
65fn openrouter_from_env() -> Result<OpenRouterProvider> {
66 let api_key = std::env::var("OPENROUTER_API_KEY").map_err(|_| {
67 SimpleAgentsError::Config("OPENROUTER_API_KEY environment variable is required".to_string())
68 })?;
69 let api_key = ApiKey::new(api_key)?;
70 let base_url = std::env::var("OPENROUTER_API_BASE")
71 .unwrap_or_else(|_| OpenRouterProvider::DEFAULT_BASE_URL.to_string());
72 OpenRouterProvider::with_base_url(api_key, base_url)
73}
74
75unsafe fn cstr_to_string(ptr: *const c_char, field: &str) -> Result<String> {
76 if ptr.is_null() {
77 return Err(SimpleAgentsError::Config(format!("{field} cannot be null")));
78 }
79
80 let c_str = CStr::from_ptr(ptr);
81 let value = c_str
82 .to_str()
83 .map_err(|_| SimpleAgentsError::Config(format!("{field} must be valid UTF-8")))?;
84 if value.is_empty() {
85 return Err(SimpleAgentsError::Config(format!(
86 "{field} cannot be empty"
87 )));
88 }
89
90 Ok(value.to_string())
91}
92
93fn build_client(provider: Arc<dyn Provider>) -> Result<SimpleAgentsClient> {
94 SimpleAgentsClientBuilder::new()
95 .with_provider(provider)
96 .build()
97}
98
99fn build_request(
100 model: &str,
101 prompt: &str,
102 max_tokens: i32,
103 temperature: f32,
104) -> Result<CompletionRequest> {
105 let mut builder = CompletionRequest::builder()
106 .model(model)
107 .message(Message::user(prompt));
108
109 if max_tokens > 0 {
110 builder = builder.max_tokens(max_tokens as u32);
111 }
112
113 if temperature >= 0.0 {
114 builder = builder.temperature(temperature);
115 }
116
117 builder.build()
118}
119
120fn ffi_result_string(result: Result<String>) -> *mut c_char {
121 match result {
122 Ok(value) => match CString::new(value) {
123 Ok(c_string) => {
124 clear_last_error();
125 c_string.into_raw()
126 }
127 Err(_) => {
128 set_last_error("Response contained an interior null byte".to_string());
129 std::ptr::null_mut()
130 }
131 },
132 Err(error) => {
133 set_last_error(error.to_string());
134 std::ptr::null_mut()
135 }
136 }
137}
138
139fn ffi_guard<T>(action: impl FnOnce() -> Result<T>) -> *mut c_char
140where
141 T: Into<String>,
142{
143 let result = catch_unwind(AssertUnwindSafe(action));
144 match result {
145 Ok(inner) => ffi_result_string(inner.map(Into::into)),
146 Err(_) => {
147 set_last_error("Panic occurred in FFI call".to_string());
148 std::ptr::null_mut()
149 }
150 }
151}
152
153#[no_mangle]
162pub unsafe extern "C" fn sa_client_new_from_env(provider_name: *const c_char) -> *mut SAClient {
163 let result = catch_unwind(AssertUnwindSafe(|| -> Result<Box<SAClient>> {
164 let provider = cstr_to_string(provider_name, "provider_name")?;
165 let provider = provider_from_env(&provider)?;
166 let client = build_client(provider)?;
167 let runtime = build_runtime()?;
168
169 Ok(Box::new(SAClient {
170 inner: FfiClient {
171 runtime: Mutex::new(runtime),
172 client,
173 },
174 }))
175 }));
176
177 match result {
178 Ok(Ok(client)) => {
179 clear_last_error();
180 Box::into_raw(client)
181 }
182 Ok(Err(error)) => {
183 set_last_error(error.to_string());
184 std::ptr::null_mut()
185 }
186 Err(_) => {
187 set_last_error("Panic occurred in sa_client_new_from_env".to_string());
188 std::ptr::null_mut()
189 }
190 }
191}
192
193#[no_mangle]
200pub unsafe extern "C" fn sa_client_free(client: *mut SAClient) {
201 if client.is_null() {
202 return;
203 }
204
205 drop(Box::from_raw(client));
206}
207
208#[no_mangle]
218pub unsafe extern "C" fn sa_complete(
219 client: *mut SAClient,
220 model: *const c_char,
221 prompt: *const c_char,
222 max_tokens: i32,
223 temperature: f32,
224) -> *mut c_char {
225 if client.is_null() {
226 set_last_error("client cannot be null".to_string());
227 return std::ptr::null_mut();
228 }
229
230 ffi_guard(|| {
231 let model = cstr_to_string(model, "model")?;
232 let prompt = cstr_to_string(prompt, "prompt")?;
233 let request = build_request(&model, &prompt, max_tokens, temperature)?;
234
235 let client = &(*client).inner;
236 let runtime = client
237 .runtime
238 .lock()
239 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
240 let response = runtime.block_on(client.client.complete(&request))?;
241
242 Ok(response.content().unwrap_or_default().to_string())
243 })
244}
245
246#[no_mangle]
250pub extern "C" fn sa_last_error_message() -> *mut c_char {
251 match take_last_error() {
252 Some(message) => match CString::new(message) {
253 Ok(c_string) => c_string.into_raw(),
254 Err(_) => std::ptr::null_mut(),
255 },
256 None => std::ptr::null_mut(),
257 }
258}
259
260#[no_mangle]
267pub unsafe extern "C" fn sa_string_free(value: *mut c_char) {
268 if value.is_null() {
269 return;
270 }
271
272 drop(CString::from_raw(value));
273}