liter_llm 1.0.0.pre.rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +239 -0
  3. data/ext/liter_llm_rb/extconf.rb +65 -0
  4. data/ext/liter_llm_rb/native/.cargo/config.toml +23 -0
  5. data/ext/liter_llm_rb/native/Cargo.lock +3713 -0
  6. data/ext/liter_llm_rb/native/Cargo.toml +32 -0
  7. data/ext/liter_llm_rb/native/build.rs +15 -0
  8. data/ext/liter_llm_rb/native/src/lib.rs +1079 -0
  9. data/lib/liter_llm.rb +8 -0
  10. data/sig/liter_llm.rbs +416 -0
  11. data/vendor/Cargo.toml +54 -0
  12. data/vendor/liter-llm/Cargo.toml +92 -0
  13. data/vendor/liter-llm/README.md +252 -0
  14. data/vendor/liter-llm/schemas/pricing.json +40 -0
  15. data/vendor/liter-llm/schemas/providers.json +1662 -0
  16. data/vendor/liter-llm/src/auth/azure_ad.rs +264 -0
  17. data/vendor/liter-llm/src/auth/bedrock_sts.rs +353 -0
  18. data/vendor/liter-llm/src/auth/mod.rs +68 -0
  19. data/vendor/liter-llm/src/auth/vertex_oauth.rs +353 -0
  20. data/vendor/liter-llm/src/client/config.rs +351 -0
  21. data/vendor/liter-llm/src/client/managed.rs +622 -0
  22. data/vendor/liter-llm/src/client/mod.rs +864 -0
  23. data/vendor/liter-llm/src/cost.rs +212 -0
  24. data/vendor/liter-llm/src/error.rs +190 -0
  25. data/vendor/liter-llm/src/http/eventstream.rs +860 -0
  26. data/vendor/liter-llm/src/http/mod.rs +12 -0
  27. data/vendor/liter-llm/src/http/request.rs +438 -0
  28. data/vendor/liter-llm/src/http/retry.rs +72 -0
  29. data/vendor/liter-llm/src/http/streaming.rs +289 -0
  30. data/vendor/liter-llm/src/lib.rs +37 -0
  31. data/vendor/liter-llm/src/provider/anthropic.rs +2250 -0
  32. data/vendor/liter-llm/src/provider/azure.rs +579 -0
  33. data/vendor/liter-llm/src/provider/bedrock.rs +1543 -0
  34. data/vendor/liter-llm/src/provider/cohere.rs +654 -0
  35. data/vendor/liter-llm/src/provider/custom.rs +404 -0
  36. data/vendor/liter-llm/src/provider/google_ai.rs +281 -0
  37. data/vendor/liter-llm/src/provider/mistral.rs +188 -0
  38. data/vendor/liter-llm/src/provider/mod.rs +616 -0
  39. data/vendor/liter-llm/src/provider/vertex.rs +1504 -0
  40. data/vendor/liter-llm/src/tests.rs +1425 -0
  41. data/vendor/liter-llm/src/tokenizer.rs +281 -0
  42. data/vendor/liter-llm/src/tower/budget.rs +599 -0
  43. data/vendor/liter-llm/src/tower/cache.rs +502 -0
  44. data/vendor/liter-llm/src/tower/cache_opendal.rs +270 -0
  45. data/vendor/liter-llm/src/tower/cooldown.rs +231 -0
  46. data/vendor/liter-llm/src/tower/cost.rs +404 -0
  47. data/vendor/liter-llm/src/tower/fallback.rs +121 -0
  48. data/vendor/liter-llm/src/tower/health.rs +219 -0
  49. data/vendor/liter-llm/src/tower/hooks.rs +369 -0
  50. data/vendor/liter-llm/src/tower/mod.rs +77 -0
  51. data/vendor/liter-llm/src/tower/rate_limit.rs +300 -0
  52. data/vendor/liter-llm/src/tower/router.rs +436 -0
  53. data/vendor/liter-llm/src/tower/service.rs +181 -0
  54. data/vendor/liter-llm/src/tower/tests.rs +539 -0
  55. data/vendor/liter-llm/src/tower/tests_common.rs +252 -0
  56. data/vendor/liter-llm/src/tower/tracing.rs +209 -0
  57. data/vendor/liter-llm/src/tower/types.rs +170 -0
  58. data/vendor/liter-llm/src/types/audio.rs +52 -0
  59. data/vendor/liter-llm/src/types/batch.rs +77 -0
  60. data/vendor/liter-llm/src/types/chat.rs +214 -0
  61. data/vendor/liter-llm/src/types/common.rs +244 -0
  62. data/vendor/liter-llm/src/types/embedding.rs +84 -0
  63. data/vendor/liter-llm/src/types/files.rs +58 -0
  64. data/vendor/liter-llm/src/types/image.rs +40 -0
  65. data/vendor/liter-llm/src/types/mod.rs +27 -0
  66. data/vendor/liter-llm/src/types/models.rs +21 -0
  67. data/vendor/liter-llm/src/types/moderation.rs +80 -0
  68. data/vendor/liter-llm/src/types/ocr.rs +87 -0
  69. data/vendor/liter-llm/src/types/rerank.rs +46 -0
  70. data/vendor/liter-llm/src/types/responses.rs +55 -0
  71. data/vendor/liter-llm/src/types/search.rs +45 -0
  72. data/vendor/liter-llm/tests/contract.rs +332 -0
  73. data/vendor/liter-llm-ffi/Cargo.toml +30 -0
  74. data/vendor/liter-llm-ffi/build.rs +66 -0
  75. data/vendor/liter-llm-ffi/cbindgen.toml +60 -0
  76. data/vendor/liter-llm-ffi/liter_llm.h +850 -0
  77. data/vendor/liter-llm-ffi/src/lib.rs +2488 -0
  78. metadata +286 -0
@@ -0,0 +1,2488 @@
1
+ //! C FFI bindings for liter-llm.
2
+ //!
3
+ //! Provides an opaque-handle C API consumed by Go (cgo), Java (Panama FFM),
4
+ //! C# (P/Invoke), and any other language with C FFI support.
5
+ //!
6
+ //! ## Ownership model
7
+ //!
8
+ //! - [`literllm_client_new`] returns a heap-allocated `*mut LiterLlmClient`.
9
+ //! The caller **owns** it and must eventually call [`literllm_client_free`].
10
+ //! - [`literllm_chat`], [`literllm_embed`], [`literllm_list_models`] return
11
+ //! heap-allocated `*mut c_char` JSON strings.
12
+ //! The caller **owns** them and must call [`literllm_free_string`].
13
+ //! - [`literllm_last_error`] returns a thread-local `*const c_char`.
14
+ //! The caller must **not** free it; it is valid until the next call on the
15
+ //! same thread.
16
+
17
+ use std::ffi::{CStr, CString, c_char};
18
+
19
+ use liter_llm::client::managed::ManagedClient;
20
+ use liter_llm::client::{BatchClient, ClientConfig, FileClient, LlmClient, ResponseClient};
21
+ use liter_llm_bindings_core::error::format_error;
22
+ use liter_llm_bindings_core::runtime::current_thread_runtime;
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Thread-local last-error storage
26
+ // ---------------------------------------------------------------------------
27
+
28
+ thread_local! {
29
+ /// Holds the last error message for the current thread.
30
+ /// Stored as a `CString` so the pointer stays valid until next error.
31
+ static LAST_ERROR: std::cell::RefCell<Option<CString>> =
32
+ const { std::cell::RefCell::new(None) };
33
+ }
34
+
35
+ /// Store a new last-error string for this thread.
36
+ fn set_last_error(msg: String) {
37
+ LAST_ERROR.with(|cell| {
38
+ // Silently fall back to a truncated message if the string contains
39
+ // interior NUL bytes (should never happen in practice).
40
+ let c_str = CString::new(msg).unwrap_or_else(|_| c"<error message contained NUL byte>".into());
41
+ *cell.borrow_mut() = Some(c_str);
42
+ });
43
+ }
44
+
45
+ /// Clear the last-error for this thread.
46
+ fn clear_last_error() {
47
+ LAST_ERROR.with(|cell| {
48
+ *cell.borrow_mut() = None;
49
+ });
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Hook invocation helpers
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /// Invoke the `on_request` hook if registered. Returns the hook's return
57
+ /// code: `0` to proceed, non-zero to reject the request (guardrail).
58
+ ///
59
+ /// # Safety
60
+ ///
61
+ /// The `hooks` field must contain valid function pointers for the lifetime
62
+ /// of the client, as guaranteed by the `literllm_set_hooks` contract.
63
+ fn invoke_on_request(hooks: &Option<LiterLlmHookCallbacks>, request_json_c: &CString) -> i32 {
64
+ if let Some(cb) = hooks
65
+ && let Some(on_request) = cb.on_request
66
+ {
67
+ // SAFETY: `on_request` is a valid function pointer provided by
68
+ // the caller of `literllm_set_hooks`. `request_json_c.as_ptr()`
69
+ // is valid for this call scope. `cb.user_data` is forwarded as-is.
70
+ return unsafe { on_request(request_json_c.as_ptr(), cb.user_data) };
71
+ }
72
+ 0
73
+ }
74
+
75
+ /// Invoke the `on_response` hook if registered.
76
+ ///
77
+ /// # Safety
78
+ ///
79
+ /// Same safety requirements as `invoke_on_request`.
80
+ fn invoke_on_response(hooks: &Option<LiterLlmHookCallbacks>, request_json_c: &CString, response_json_c: &CString) {
81
+ if let Some(cb) = hooks
82
+ && let Some(on_response) = cb.on_response
83
+ {
84
+ // SAFETY: both CString pointers are valid for this call scope.
85
+ unsafe { on_response(request_json_c.as_ptr(), response_json_c.as_ptr(), cb.user_data) };
86
+ }
87
+ }
88
+
89
+ /// Invoke the `on_error` hook if registered.
90
+ ///
91
+ /// # Safety
92
+ ///
93
+ /// Same safety requirements as `invoke_on_request`.
94
+ fn invoke_on_error(hooks: &Option<LiterLlmHookCallbacks>, request_json_c: &CString, error_msg: &str) {
95
+ if let Some(cb) = hooks
96
+ && let Some(on_error) = cb.on_error
97
+ && let Ok(err_c) = CString::new(error_msg)
98
+ {
99
+ // SAFETY: both CString pointers are valid for this call scope.
100
+ unsafe { on_error(request_json_c.as_ptr(), err_c.as_ptr(), cb.user_data) };
101
+ }
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Opaque client handle
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /// Opaque handle to a liter-llm client.
109
+ ///
110
+ /// Create with [`literllm_client_new`], destroy with [`literllm_client_free`].
111
+ /// All fields are private; callers interact only through the public functions.
112
+ ///
113
+ /// cbindgen:no-export — we emit the opaque declaration manually in the header
114
+ /// preamble so C callers only ever hold a `LiterLlmClient*`.
115
+ pub struct LiterLlmClient {
116
+ inner: ManagedClient,
117
+ /// Stored lifecycle hook callbacks, set via `literllm_set_hooks`.
118
+ hooks: Option<LiterLlmHookCallbacks>,
119
+ }
120
+
121
+ /// Tokio runtime used for blocking on async operations from synchronous C callers.
122
+ ///
123
+ /// A single runtime is created on first use and shared across all threads.
124
+ ///
125
+ /// # Thread safety
126
+ ///
127
+ /// `LiterLlmClient` holds a `ManagedClient`, which is `Send + Sync`. The
128
+ /// shared runtime is likewise `Send + Sync`. All calls into this crate are
129
+ /// therefore safe to make from multiple threads concurrently.
130
+ // Compile-time assertion: ManagedClient must be Send + Sync so that the
131
+ // opaque handle can be used from multiple C threads without data races.
132
+ const _: () = {
133
+ const fn _assert_send_sync<T: Send + Sync>() {}
134
+ // Called at compile time — zero run-time cost.
135
+ let _ = _assert_send_sync::<ManagedClient>;
136
+ };
137
+
138
+ /// Get the shared current-thread Tokio runtime from `liter-llm-bindings-core`.
139
+ ///
140
+ /// Uses `current_thread` so that `block_on` drives all work on the calling
141
+ /// thread. This guarantees that `LAST_ERROR` TLS writes happen on the
142
+ /// same thread that called the public API function.
143
+ fn runtime() -> Result<&'static tokio::runtime::Runtime, String> {
144
+ Ok(current_thread_runtime())
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Public C API
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /// Create a new liter-llm client.
152
+ ///
153
+ /// # Parameters
154
+ ///
155
+ /// - `api_key`: NUL-terminated API key string. Pass an empty string (`""`)
156
+ /// when using a provider that does not require authentication.
157
+ /// - `base_url`: NUL-terminated base URL override. Pass `NULL` to use the
158
+ /// default provider routing based on model-name prefix.
159
+ /// - `model_hint`: NUL-terminated model name hint for provider auto-detection
160
+ /// (e.g. `"groq/llama3-70b"`). Pass `NULL` to default to OpenAI. Used
161
+ /// only when `base_url` is also `NULL`.
162
+ ///
163
+ /// # Return value
164
+ ///
165
+ /// Returns a heap-allocated `LiterLlmClient*` on success, or `NULL` on failure.
166
+ /// Check [`literllm_last_error`] for the error message when `NULL` is returned.
167
+ ///
168
+ /// The returned pointer must be freed with [`literllm_client_free`].
169
+ ///
170
+ /// # Safety
171
+ ///
172
+ /// - `api_key` must be a valid, non-null, NUL-terminated C string.
173
+ /// - `base_url` may be `NULL` (treated as no override) or a valid NUL-terminated C string.
174
+ /// - `model_hint` may be `NULL` (treated as no hint) or a valid NUL-terminated C string.
175
+ /// - The caller owns the returned pointer and must call `literllm_client_free` exactly once.
176
+ #[unsafe(no_mangle)]
177
+ pub unsafe extern "C" fn literllm_client_new(
178
+ api_key: *const c_char,
179
+ base_url: *const c_char,
180
+ model_hint: *const c_char,
181
+ ) -> *mut LiterLlmClient {
182
+ clear_last_error();
183
+
184
+ // SAFETY: caller guarantees `api_key` is non-null and NUL-terminated.
185
+ if api_key.is_null() {
186
+ set_last_error("literllm_client_new: api_key must not be NULL".into());
187
+ return std::ptr::null_mut();
188
+ }
189
+
190
+ let api_key_str = match unsafe { CStr::from_ptr(api_key) }.to_str() {
191
+ Ok(s) => s.to_owned(),
192
+ Err(e) => {
193
+ set_last_error(format!("literllm_client_new: api_key is not valid UTF-8: {e}"));
194
+ return std::ptr::null_mut();
195
+ }
196
+ };
197
+
198
+ let mut config_builder = liter_llm::client::ClientConfigBuilder::new(api_key_str);
199
+
200
+ // SAFETY: `base_url` is either NULL (skip) or a valid NUL-terminated C string.
201
+ if !base_url.is_null() {
202
+ match unsafe { CStr::from_ptr(base_url) }.to_str() {
203
+ Ok(url) if !url.is_empty() => {
204
+ config_builder = config_builder.base_url(url);
205
+ }
206
+ Ok(_) => {} // empty string — treat as no override
207
+ Err(e) => {
208
+ set_last_error(format!("literllm_client_new: base_url is not valid UTF-8: {e}"));
209
+ return std::ptr::null_mut();
210
+ }
211
+ }
212
+ }
213
+
214
+ // Parse model_hint: NULL or empty string → None; otherwise Some(&str).
215
+ // SAFETY: `model_hint` is either NULL (skip) or a valid NUL-terminated C string.
216
+ let model_hint_str: Option<String> = if model_hint.is_null() {
217
+ None
218
+ } else {
219
+ match unsafe { CStr::from_ptr(model_hint) }.to_str() {
220
+ Ok(s) if !s.is_empty() => Some(s.to_owned()),
221
+ Ok(_) => None, // empty string — treat as no hint
222
+ Err(e) => {
223
+ set_last_error(format!("literllm_client_new: model_hint is not valid UTF-8: {e}"));
224
+ return std::ptr::null_mut();
225
+ }
226
+ }
227
+ };
228
+
229
+ let config: ClientConfig = config_builder.build();
230
+
231
+ match ManagedClient::new(config, model_hint_str.as_deref()) {
232
+ Ok(client) => {
233
+ let handle = Box::new(LiterLlmClient {
234
+ inner: client,
235
+ hooks: None,
236
+ });
237
+ Box::into_raw(handle)
238
+ }
239
+ Err(e) => {
240
+ set_last_error(format!("literllm_client_new: {}", format_error(&e)));
241
+ std::ptr::null_mut()
242
+ }
243
+ }
244
+ }
245
+
246
+ /// Free a client created by [`literllm_client_new`].
247
+ ///
248
+ /// # Safety
249
+ ///
250
+ /// - `client` must be a valid pointer returned by `literllm_client_new`.
251
+ /// - `client` must not be used after this call (use-after-free is UB).
252
+ /// - Passing `NULL` is safe and is a no-op.
253
+ #[unsafe(no_mangle)]
254
+ pub unsafe extern "C" fn literllm_client_free(client: *mut LiterLlmClient) {
255
+ // SAFETY: `client` is either NULL (safe to skip) or was returned by
256
+ // `literllm_client_new`, which heap-allocates a `Box<LiterLlmClient>` via
257
+ // `Box::into_raw`. Reconstructing the `Box` here transfers ownership back
258
+ // to Rust, which drops it at the end of this scope.
259
+ if !client.is_null() {
260
+ drop(unsafe { Box::from_raw(client) });
261
+ }
262
+ }
263
+
264
+ /// Send a chat completion request.
265
+ ///
266
+ /// # Parameters
267
+ ///
268
+ /// - `client`: A valid client pointer.
269
+ /// - `request_json`: NUL-terminated JSON string conforming to the
270
+ /// `ChatCompletionRequest` schema.
271
+ ///
272
+ /// # Return value
273
+ ///
274
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
275
+ /// `ChatCompletionResponse` on success, or `NULL` on failure.
276
+ /// Check [`literllm_last_error`] on failure.
277
+ ///
278
+ /// The caller must free the returned string with [`literllm_free_string`].
279
+ ///
280
+ /// # Safety
281
+ ///
282
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
283
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
284
+ #[unsafe(no_mangle)]
285
+ pub unsafe extern "C" fn literllm_chat(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
286
+ clear_last_error();
287
+
288
+ if client.is_null() {
289
+ set_last_error("literllm_chat: client must not be NULL".into());
290
+ return std::ptr::null_mut();
291
+ }
292
+ if request_json.is_null() {
293
+ set_last_error("literllm_chat: request_json must not be NULL".into());
294
+ return std::ptr::null_mut();
295
+ }
296
+
297
+ // SAFETY: caller guarantees `client` and `request_json` are non-null and valid.
298
+ let client_handle = unsafe { &(*client) };
299
+ let client_ref = &client_handle.inner;
300
+
301
+ let json_str = match unsafe { CStr::from_ptr(request_json) }.to_str() {
302
+ Ok(s) => s,
303
+ Err(e) => {
304
+ set_last_error(format!("literllm_chat: request_json is not valid UTF-8: {e}"));
305
+ return std::ptr::null_mut();
306
+ }
307
+ };
308
+
309
+ // Build a CString copy of the request for hook invocation.
310
+ let req_c = match CString::new(json_str) {
311
+ Ok(c) => c,
312
+ Err(e) => {
313
+ set_last_error(format!("literllm_chat: request_json contained NUL byte: {e}"));
314
+ return std::ptr::null_mut();
315
+ }
316
+ };
317
+
318
+ // Invoke on_request hook; non-zero return rejects the request.
319
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
320
+ if hook_rc != 0 {
321
+ set_last_error("literllm_chat: request rejected by on_request hook".into());
322
+ invoke_on_error(&client_handle.hooks, &req_c, "request rejected by on_request hook");
323
+ return std::ptr::null_mut();
324
+ }
325
+
326
+ let request = match serde_json::from_str(json_str) {
327
+ Ok(r) => r,
328
+ Err(e) => {
329
+ set_last_error(format!("literllm_chat: failed to parse request JSON: {e}"));
330
+ return std::ptr::null_mut();
331
+ }
332
+ };
333
+
334
+ let rt = match runtime() {
335
+ Ok(rt) => rt,
336
+ Err(e) => {
337
+ set_last_error(format!("literllm_chat: {e}"));
338
+ return std::ptr::null_mut();
339
+ }
340
+ };
341
+ let result = rt.block_on(client_ref.chat(request));
342
+
343
+ match result {
344
+ Ok(response) => match serde_json::to_string(&response) {
345
+ Ok(json) => match CString::new(json) {
346
+ Ok(c_str) => {
347
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
348
+ c_str.into_raw()
349
+ }
350
+ Err(e) => {
351
+ set_last_error(format!("literllm_chat: response JSON contained NUL byte: {e}"));
352
+ std::ptr::null_mut()
353
+ }
354
+ },
355
+ Err(e) => {
356
+ set_last_error(format!("literllm_chat: failed to serialize response: {e}"));
357
+ std::ptr::null_mut()
358
+ }
359
+ },
360
+ Err(e) => {
361
+ let msg = format!("literllm_chat: {}", format_error(&e));
362
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
363
+ set_last_error(msg);
364
+ std::ptr::null_mut()
365
+ }
366
+ }
367
+ }
368
+
369
+ /// Callback invoked for each SSE chunk during a streaming chat completion.
370
+ ///
371
+ /// - `chunk_json`: NUL-terminated JSON string for one `ChatCompletionChunk`.
372
+ /// The pointer is valid only for the duration of the callback invocation.
373
+ /// The callee must **not** free it.
374
+ /// - `user_data`: The opaque pointer passed to [`literllm_chat_stream`].
375
+ ///
376
+ /// This callback returns void; there is no return value.
377
+ pub type LiterLlmStreamCallback = unsafe extern "C" fn(chunk_json: *const c_char, user_data: *mut std::ffi::c_void);
378
+
379
+ /// Send a streaming chat completion request, invoking a callback for each chunk.
380
+ ///
381
+ /// # Parameters
382
+ ///
383
+ /// - `client`: A valid client pointer.
384
+ /// - `request_json`: NUL-terminated JSON string conforming to the
385
+ /// `ChatCompletionRequest` schema.
386
+ /// - `callback`: Function called once per SSE chunk with the JSON-serialised
387
+ /// `ChatCompletionChunk`. The `chunk_json` pointer is valid only for the
388
+ /// duration of each callback invocation and must **not** be freed.
389
+ /// - `user_data`: Opaque pointer forwarded unchanged to each `callback` call.
390
+ /// May be `NULL`.
391
+ ///
392
+ /// # Return value
393
+ ///
394
+ /// Returns `0` on success (all chunks delivered) or `-1` on failure.
395
+ /// Check [`literllm_last_error`] on failure.
396
+ ///
397
+ /// # Safety
398
+ ///
399
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
400
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
401
+ /// - `callback` must be a valid function pointer; it is invoked from the calling
402
+ /// thread with the Tokio runtime blocked.
403
+ /// - `user_data` is forwarded as-is; the caller is responsible for its lifetime.
404
+ #[unsafe(no_mangle)]
405
+ pub unsafe extern "C" fn literllm_chat_stream(
406
+ client: *const LiterLlmClient,
407
+ request_json: *const c_char,
408
+ callback: LiterLlmStreamCallback,
409
+ user_data: *mut std::ffi::c_void,
410
+ ) -> i32 {
411
+ clear_last_error();
412
+
413
+ if client.is_null() {
414
+ set_last_error("literllm_chat_stream: client must not be NULL".into());
415
+ return -1;
416
+ }
417
+ if request_json.is_null() {
418
+ set_last_error("literllm_chat_stream: request_json must not be NULL".into());
419
+ return -1;
420
+ }
421
+
422
+ // SAFETY: caller guarantees `client` and `request_json` are non-null and valid.
423
+ let client_handle = unsafe { &(*client) };
424
+ let client_ref = &client_handle.inner;
425
+
426
+ let json_str = match unsafe { CStr::from_ptr(request_json) }.to_str() {
427
+ Ok(s) => s,
428
+ Err(e) => {
429
+ set_last_error(format!("literllm_chat_stream: request_json is not valid UTF-8: {e}"));
430
+ return -1;
431
+ }
432
+ };
433
+
434
+ let req_c = match CString::new(json_str) {
435
+ Ok(c) => c,
436
+ Err(e) => {
437
+ set_last_error(format!("literllm_chat_stream: request_json contained NUL byte: {e}"));
438
+ return -1;
439
+ }
440
+ };
441
+
442
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
443
+ if hook_rc != 0 {
444
+ set_last_error("literllm_chat_stream: request rejected by on_request hook".into());
445
+ invoke_on_error(&client_handle.hooks, &req_c, "request rejected by on_request hook");
446
+ return -1;
447
+ }
448
+
449
+ let request = match serde_json::from_str(json_str) {
450
+ Ok(r) => r,
451
+ Err(e) => {
452
+ set_last_error(format!("literllm_chat_stream: failed to parse request JSON: {e}"));
453
+ return -1;
454
+ }
455
+ };
456
+
457
+ let rt = match runtime() {
458
+ Ok(rt) => rt,
459
+ Err(e) => {
460
+ set_last_error(format!("literllm_chat_stream: {e}"));
461
+ return -1;
462
+ }
463
+ };
464
+
465
+ // Block on obtaining the stream, then iterate every chunk synchronously,
466
+ // invoking the callback for each one. C FFI callers cannot model async
467
+ // iterators natively, so a blocking callback pattern is the correct API.
468
+ let result = rt.block_on(async {
469
+ use futures_core::Stream;
470
+ use std::pin::Pin;
471
+
472
+ let mut stream = match client_ref.chat_stream(request).await {
473
+ Ok(s) => s,
474
+ Err(e) => return Err(format!("literllm_chat_stream: failed to open stream: {e}")),
475
+ };
476
+
477
+ loop {
478
+ let next = std::future::poll_fn(|cx| Pin::new(&mut stream).poll_next(cx)).await;
479
+ match next {
480
+ None => break,
481
+ Some(Err(e)) => return Err(format!("literllm_chat_stream: stream error: {e}")),
482
+ Some(Ok(chunk)) => {
483
+ let chunk_json = match serde_json::to_string(&chunk) {
484
+ Ok(s) => s,
485
+ Err(e) => return Err(format!("literllm_chat_stream: failed to serialise chunk: {e}")),
486
+ };
487
+ match CString::new(chunk_json) {
488
+ Ok(c_str) => {
489
+ // SAFETY: `callback` is a valid function pointer supplied
490
+ // by the caller. `c_str.as_ptr()` is valid for this block
491
+ // scope and must not be stored or freed by the callee.
492
+ // `user_data` is forwarded as-is; ownership stays with the caller.
493
+ unsafe { callback(c_str.as_ptr(), user_data) };
494
+ }
495
+ Err(e) => return Err(format!("literllm_chat_stream: chunk JSON contained NUL byte: {e}")),
496
+ }
497
+ }
498
+ }
499
+ }
500
+ Ok(())
501
+ });
502
+
503
+ match result {
504
+ Ok(()) => {
505
+ // Notify on_response with a synthetic "stream complete" marker.
506
+ let done_c = CString::new(r#"{"stream":"complete"}"#).unwrap_or_default();
507
+ invoke_on_response(&client_handle.hooks, &req_c, &done_c);
508
+ 0
509
+ }
510
+ Err(e) => {
511
+ invoke_on_error(&client_handle.hooks, &req_c, &e);
512
+ set_last_error(e);
513
+ -1
514
+ }
515
+ }
516
+ }
517
+
518
+ /// Send an embedding request.
519
+ ///
520
+ /// # Parameters
521
+ ///
522
+ /// - `client`: A valid client pointer.
523
+ /// - `request_json`: NUL-terminated JSON string conforming to the
524
+ /// `EmbeddingRequest` schema.
525
+ ///
526
+ /// # Return value
527
+ ///
528
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
529
+ /// `EmbeddingResponse` on success, or `NULL` on failure.
530
+ /// Check [`literllm_last_error`] on failure.
531
+ ///
532
+ /// The caller must free the returned string with [`literllm_free_string`].
533
+ ///
534
+ /// # Safety
535
+ ///
536
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
537
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
538
+ #[unsafe(no_mangle)]
539
+ pub unsafe extern "C" fn literllm_embed(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
540
+ clear_last_error();
541
+
542
+ if client.is_null() {
543
+ set_last_error("literllm_embed: client must not be NULL".into());
544
+ return std::ptr::null_mut();
545
+ }
546
+ if request_json.is_null() {
547
+ set_last_error("literllm_embed: request_json must not be NULL".into());
548
+ return std::ptr::null_mut();
549
+ }
550
+
551
+ // SAFETY: caller guarantees `client` and `request_json` are non-null and valid.
552
+ let client_handle = unsafe { &(*client) };
553
+ let client_ref = &client_handle.inner;
554
+
555
+ let json_str = match unsafe { CStr::from_ptr(request_json) }.to_str() {
556
+ Ok(s) => s,
557
+ Err(e) => {
558
+ set_last_error(format!("literllm_embed: request_json is not valid UTF-8: {e}"));
559
+ return std::ptr::null_mut();
560
+ }
561
+ };
562
+
563
+ let req_c = match CString::new(json_str) {
564
+ Ok(c) => c,
565
+ Err(e) => {
566
+ set_last_error(format!("literllm_embed: request_json contained NUL byte: {e}"));
567
+ return std::ptr::null_mut();
568
+ }
569
+ };
570
+
571
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
572
+ if hook_rc != 0 {
573
+ set_last_error("literllm_embed: request rejected by on_request hook".into());
574
+ invoke_on_error(&client_handle.hooks, &req_c, "request rejected by on_request hook");
575
+ return std::ptr::null_mut();
576
+ }
577
+
578
+ let request = match serde_json::from_str(json_str) {
579
+ Ok(r) => r,
580
+ Err(e) => {
581
+ set_last_error(format!("literllm_embed: failed to parse request JSON: {e}"));
582
+ return std::ptr::null_mut();
583
+ }
584
+ };
585
+
586
+ let rt = match runtime() {
587
+ Ok(rt) => rt,
588
+ Err(e) => {
589
+ set_last_error(format!("literllm_embed: {e}"));
590
+ return std::ptr::null_mut();
591
+ }
592
+ };
593
+ let result = rt.block_on(client_ref.embed(request));
594
+
595
+ match result {
596
+ Ok(response) => match serde_json::to_string(&response) {
597
+ Ok(json) => match CString::new(json) {
598
+ Ok(c_str) => {
599
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
600
+ c_str.into_raw()
601
+ }
602
+ Err(e) => {
603
+ set_last_error(format!("literllm_embed: response JSON contained NUL byte: {e}"));
604
+ std::ptr::null_mut()
605
+ }
606
+ },
607
+ Err(e) => {
608
+ set_last_error(format!("literllm_embed: failed to serialize response: {e}"));
609
+ std::ptr::null_mut()
610
+ }
611
+ },
612
+ Err(e) => {
613
+ let msg = format!("literllm_embed: {}", format_error(&e));
614
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
615
+ set_last_error(msg);
616
+ std::ptr::null_mut()
617
+ }
618
+ }
619
+ }
620
+
621
+ /// List available models.
622
+ ///
623
+ /// # Parameters
624
+ ///
625
+ /// - `client`: A valid client pointer.
626
+ ///
627
+ /// # Return value
628
+ ///
629
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
630
+ /// `ModelsListResponse` on success, or `NULL` on failure.
631
+ /// Check [`literllm_last_error`] on failure.
632
+ ///
633
+ /// The caller must free the returned string with [`literllm_free_string`].
634
+ ///
635
+ /// # Safety
636
+ ///
637
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
638
+ #[unsafe(no_mangle)]
639
+ pub unsafe extern "C" fn literllm_list_models(client: *const LiterLlmClient) -> *mut c_char {
640
+ clear_last_error();
641
+
642
+ if client.is_null() {
643
+ set_last_error("literllm_list_models: client must not be NULL".into());
644
+ return std::ptr::null_mut();
645
+ }
646
+
647
+ // SAFETY: caller guarantees `client` is non-null and was returned by
648
+ // `literllm_client_new`. The shared reference is valid for the duration
649
+ // of this call.
650
+ let client_handle = unsafe { &(*client) };
651
+ let client_ref = &client_handle.inner;
652
+
653
+ // Use a synthetic request marker for hook invocation since list_models
654
+ // does not take a request body.
655
+ let req_c = CString::new(r#"{"action":"list_models"}"#).unwrap_or_default();
656
+
657
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
658
+ if hook_rc != 0 {
659
+ set_last_error("literllm_list_models: request rejected by on_request hook".into());
660
+ invoke_on_error(&client_handle.hooks, &req_c, "request rejected by on_request hook");
661
+ return std::ptr::null_mut();
662
+ }
663
+
664
+ let rt = match runtime() {
665
+ Ok(rt) => rt,
666
+ Err(e) => {
667
+ set_last_error(format!("literllm_list_models: {e}"));
668
+ return std::ptr::null_mut();
669
+ }
670
+ };
671
+ let result = rt.block_on(client_ref.list_models());
672
+
673
+ match result {
674
+ Ok(response) => match serde_json::to_string(&response) {
675
+ Ok(json) => match CString::new(json) {
676
+ Ok(c_str) => {
677
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
678
+ c_str.into_raw()
679
+ }
680
+ Err(e) => {
681
+ set_last_error(format!("literllm_list_models: response JSON contained NUL byte: {e}"));
682
+ std::ptr::null_mut()
683
+ }
684
+ },
685
+ Err(e) => {
686
+ set_last_error(format!("literllm_list_models: failed to serialize response: {e}"));
687
+ std::ptr::null_mut()
688
+ }
689
+ },
690
+ Err(e) => {
691
+ let msg = format!("literllm_list_models: {}", format_error(&e));
692
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
693
+ set_last_error(msg);
694
+ std::ptr::null_mut()
695
+ }
696
+ }
697
+ }
698
+
699
+ // ---------------------------------------------------------------------------
700
+ // Helper: JSON-in / JSON-out for request-based endpoints
701
+ // ---------------------------------------------------------------------------
702
+
703
+ /// Internal helper shared by all JSON-in/JSON-out FFI functions that take a
704
+ /// `(client, request_json)` pair. Validates inputs, deserialises the JSON,
705
+ /// calls `op` inside the Tokio runtime, serialises the response, and returns
706
+ /// an owned `*mut c_char` (or `NULL` on error, with `LAST_ERROR` set).
707
+ ///
708
+ /// `name` is used only for error messages.
709
+ fn json_request_response<Req, Resp>(
710
+ name: &str,
711
+ client: *const LiterLlmClient,
712
+ request_json: *const c_char,
713
+ op: impl for<'a> FnOnce(
714
+ &'a ManagedClient,
715
+ Req,
716
+ ) -> std::pin::Pin<
717
+ Box<dyn std::future::Future<Output = liter_llm::error::Result<Resp>> + Send + 'a>,
718
+ >,
719
+ ) -> *mut c_char
720
+ where
721
+ Req: serde::de::DeserializeOwned,
722
+ Resp: serde::Serialize,
723
+ {
724
+ clear_last_error();
725
+
726
+ if client.is_null() {
727
+ set_last_error(format!("{name}: client must not be NULL"));
728
+ return std::ptr::null_mut();
729
+ }
730
+ if request_json.is_null() {
731
+ set_last_error(format!("{name}: request_json must not be NULL"));
732
+ return std::ptr::null_mut();
733
+ }
734
+
735
+ // SAFETY: caller guarantees `client` and `request_json` are non-null and valid.
736
+ let client_handle = unsafe { &(*client) };
737
+ let client_ref = &client_handle.inner;
738
+
739
+ let json_str = match unsafe { CStr::from_ptr(request_json) }.to_str() {
740
+ Ok(s) => s,
741
+ Err(e) => {
742
+ set_last_error(format!("{name}: request_json is not valid UTF-8: {e}"));
743
+ return std::ptr::null_mut();
744
+ }
745
+ };
746
+
747
+ let req_c = match CString::new(json_str) {
748
+ Ok(c) => c,
749
+ Err(e) => {
750
+ set_last_error(format!("{name}: request_json contained NUL byte: {e}"));
751
+ return std::ptr::null_mut();
752
+ }
753
+ };
754
+
755
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
756
+ if hook_rc != 0 {
757
+ set_last_error(format!("{name}: request rejected by on_request hook"));
758
+ invoke_on_error(
759
+ &client_handle.hooks,
760
+ &req_c,
761
+ &format!("{name}: request rejected by on_request hook"),
762
+ );
763
+ return std::ptr::null_mut();
764
+ }
765
+
766
+ let request: Req = match serde_json::from_str(json_str) {
767
+ Ok(r) => r,
768
+ Err(e) => {
769
+ set_last_error(format!("{name}: failed to parse request JSON: {e}"));
770
+ return std::ptr::null_mut();
771
+ }
772
+ };
773
+
774
+ let rt = match runtime() {
775
+ Ok(rt) => rt,
776
+ Err(e) => {
777
+ set_last_error(format!("{name}: {e}"));
778
+ return std::ptr::null_mut();
779
+ }
780
+ };
781
+ let result = rt.block_on(op(client_ref, request));
782
+
783
+ match result {
784
+ Ok(response) => match serde_json::to_string(&response) {
785
+ Ok(json) => match CString::new(json) {
786
+ Ok(c_str) => {
787
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
788
+ c_str.into_raw()
789
+ }
790
+ Err(e) => {
791
+ set_last_error(format!("{name}: response JSON contained NUL byte: {e}"));
792
+ std::ptr::null_mut()
793
+ }
794
+ },
795
+ Err(e) => {
796
+ set_last_error(format!("{name}: failed to serialize response: {e}"));
797
+ std::ptr::null_mut()
798
+ }
799
+ },
800
+ Err(e) => {
801
+ let msg = format!("{name}: {}", format_error(&e));
802
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
803
+ set_last_error(msg);
804
+ std::ptr::null_mut()
805
+ }
806
+ }
807
+ }
808
+
809
+ /// Internal helper for endpoints that take `(client, id_string)` and return JSON.
810
+ fn id_request_response<Resp>(
811
+ name: &str,
812
+ client: *const LiterLlmClient,
813
+ id_ptr: *const c_char,
814
+ id_label: &str,
815
+ op: impl for<'a> FnOnce(
816
+ &'a ManagedClient,
817
+ &'a str,
818
+ ) -> std::pin::Pin<
819
+ Box<dyn std::future::Future<Output = liter_llm::error::Result<Resp>> + Send + 'a>,
820
+ >,
821
+ ) -> *mut c_char
822
+ where
823
+ Resp: serde::Serialize,
824
+ {
825
+ clear_last_error();
826
+
827
+ if client.is_null() {
828
+ set_last_error(format!("{name}: client must not be NULL"));
829
+ return std::ptr::null_mut();
830
+ }
831
+ if id_ptr.is_null() {
832
+ set_last_error(format!("{name}: {id_label} must not be NULL"));
833
+ return std::ptr::null_mut();
834
+ }
835
+
836
+ // SAFETY: caller guarantees `client` and `id_ptr` are non-null and valid.
837
+ let client_handle = unsafe { &(*client) };
838
+ let client_ref = &client_handle.inner;
839
+
840
+ let id_str = match unsafe { CStr::from_ptr(id_ptr) }.to_str() {
841
+ Ok(s) => s,
842
+ Err(e) => {
843
+ set_last_error(format!("{name}: {id_label} is not valid UTF-8: {e}"));
844
+ return std::ptr::null_mut();
845
+ }
846
+ };
847
+
848
+ // Synthetic request JSON for hook invocation.
849
+ let req_c = CString::new(format!(r#"{{"action":"{name}","{id_label}":"{id_str}"}}"#)).unwrap_or_default();
850
+
851
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
852
+ if hook_rc != 0 {
853
+ set_last_error(format!("{name}: request rejected by on_request hook"));
854
+ invoke_on_error(
855
+ &client_handle.hooks,
856
+ &req_c,
857
+ &format!("{name}: request rejected by on_request hook"),
858
+ );
859
+ return std::ptr::null_mut();
860
+ }
861
+
862
+ let rt = match runtime() {
863
+ Ok(rt) => rt,
864
+ Err(e) => {
865
+ set_last_error(format!("{name}: {e}"));
866
+ return std::ptr::null_mut();
867
+ }
868
+ };
869
+ let result = rt.block_on(op(client_ref, id_str));
870
+
871
+ match result {
872
+ Ok(response) => match serde_json::to_string(&response) {
873
+ Ok(json) => match CString::new(json) {
874
+ Ok(c_str) => {
875
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
876
+ c_str.into_raw()
877
+ }
878
+ Err(e) => {
879
+ set_last_error(format!("{name}: response JSON contained NUL byte: {e}"));
880
+ std::ptr::null_mut()
881
+ }
882
+ },
883
+ Err(e) => {
884
+ set_last_error(format!("{name}: failed to serialize response: {e}"));
885
+ std::ptr::null_mut()
886
+ }
887
+ },
888
+ Err(e) => {
889
+ let msg = format!("{name}: {}", format_error(&e));
890
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
891
+ set_last_error(msg);
892
+ std::ptr::null_mut()
893
+ }
894
+ }
895
+ }
896
+
897
+ /// Internal helper for endpoints that return raw bytes (encoded as base64 JSON).
898
+ fn id_request_bytes(
899
+ name: &str,
900
+ client: *const LiterLlmClient,
901
+ id_ptr: *const c_char,
902
+ id_label: &str,
903
+ op: impl for<'a> FnOnce(
904
+ &'a ManagedClient,
905
+ &'a str,
906
+ ) -> std::pin::Pin<
907
+ Box<dyn std::future::Future<Output = liter_llm::error::Result<bytes::Bytes>> + Send + 'a>,
908
+ >,
909
+ ) -> *mut c_char {
910
+ clear_last_error();
911
+
912
+ if client.is_null() {
913
+ set_last_error(format!("{name}: client must not be NULL"));
914
+ return std::ptr::null_mut();
915
+ }
916
+ if id_ptr.is_null() {
917
+ set_last_error(format!("{name}: {id_label} must not be NULL"));
918
+ return std::ptr::null_mut();
919
+ }
920
+
921
+ // SAFETY: caller guarantees `client` and `id_ptr` are non-null and valid.
922
+ let client_handle = unsafe { &(*client) };
923
+ let client_ref = &client_handle.inner;
924
+
925
+ let id_str = match unsafe { CStr::from_ptr(id_ptr) }.to_str() {
926
+ Ok(s) => s,
927
+ Err(e) => {
928
+ set_last_error(format!("{name}: {id_label} is not valid UTF-8: {e}"));
929
+ return std::ptr::null_mut();
930
+ }
931
+ };
932
+
933
+ let req_c = CString::new(format!(r#"{{"action":"{name}","{id_label}":"{id_str}"}}"#)).unwrap_or_default();
934
+
935
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
936
+ if hook_rc != 0 {
937
+ set_last_error(format!("{name}: request rejected by on_request hook"));
938
+ invoke_on_error(
939
+ &client_handle.hooks,
940
+ &req_c,
941
+ &format!("{name}: request rejected by on_request hook"),
942
+ );
943
+ return std::ptr::null_mut();
944
+ }
945
+
946
+ let rt = match runtime() {
947
+ Ok(rt) => rt,
948
+ Err(e) => {
949
+ set_last_error(format!("{name}: {e}"));
950
+ return std::ptr::null_mut();
951
+ }
952
+ };
953
+ let result = rt.block_on(op(client_ref, id_str));
954
+
955
+ match result {
956
+ Ok(data) => {
957
+ use base64::Engine;
958
+ let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
959
+ match CString::new(encoded) {
960
+ Ok(c_str) => {
961
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
962
+ c_str.into_raw()
963
+ }
964
+ Err(e) => {
965
+ set_last_error(format!("{name}: base64 output contained NUL byte: {e}"));
966
+ std::ptr::null_mut()
967
+ }
968
+ }
969
+ }
970
+ Err(e) => {
971
+ let msg = format!("{name}: {}", format_error(&e));
972
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
973
+ set_last_error(msg);
974
+ std::ptr::null_mut()
975
+ }
976
+ }
977
+ }
978
+
979
+ // ---------------------------------------------------------------------------
980
+ // Inference API: image_generate, speech, transcribe, moderate, rerank
981
+ // ---------------------------------------------------------------------------
982
+
983
+ /// Generate an image from a text prompt.
984
+ ///
985
+ /// # Parameters
986
+ ///
987
+ /// - `client`: A valid client pointer.
988
+ /// - `request_json`: NUL-terminated JSON string conforming to the
989
+ /// `CreateImageRequest` schema.
990
+ ///
991
+ /// # Return value
992
+ ///
993
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
994
+ /// `ImagesResponse` on success, or `NULL` on failure.
995
+ /// The caller must free the returned string with [`literllm_free_string`].
996
+ ///
997
+ /// # Safety
998
+ ///
999
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1000
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1001
+ #[unsafe(no_mangle)]
1002
+ pub unsafe extern "C" fn literllm_image_generate(
1003
+ client: *const LiterLlmClient,
1004
+ request_json: *const c_char,
1005
+ ) -> *mut c_char {
1006
+ json_request_response("literllm_image_generate", client, request_json, |c, req| {
1007
+ Box::pin(c.image_generate(req))
1008
+ })
1009
+ }
1010
+
1011
+ /// Generate speech audio from text.
1012
+ ///
1013
+ /// # Parameters
1014
+ ///
1015
+ /// - `client`: A valid client pointer.
1016
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1017
+ /// `CreateSpeechRequest` schema.
1018
+ ///
1019
+ /// # Return value
1020
+ ///
1021
+ /// Returns a heap-allocated NUL-terminated base64-encoded string of the audio
1022
+ /// bytes on success, or `NULL` on failure.
1023
+ /// The caller must free the returned string with [`literllm_free_string`].
1024
+ ///
1025
+ /// # Safety
1026
+ ///
1027
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1028
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1029
+ #[unsafe(no_mangle)]
1030
+ pub unsafe extern "C" fn literllm_speech(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
1031
+ clear_last_error();
1032
+
1033
+ if client.is_null() {
1034
+ set_last_error("literllm_speech: client must not be NULL".into());
1035
+ return std::ptr::null_mut();
1036
+ }
1037
+ if request_json.is_null() {
1038
+ set_last_error("literllm_speech: request_json must not be NULL".into());
1039
+ return std::ptr::null_mut();
1040
+ }
1041
+
1042
+ // SAFETY: caller guarantees both pointers are non-null and valid.
1043
+ let client_handle = unsafe { &(*client) };
1044
+ let client_ref = &client_handle.inner;
1045
+
1046
+ let json_str = match unsafe { CStr::from_ptr(request_json) }.to_str() {
1047
+ Ok(s) => s,
1048
+ Err(e) => {
1049
+ set_last_error(format!("literllm_speech: request_json is not valid UTF-8: {e}"));
1050
+ return std::ptr::null_mut();
1051
+ }
1052
+ };
1053
+
1054
+ let req_c = match CString::new(json_str) {
1055
+ Ok(c) => c,
1056
+ Err(e) => {
1057
+ set_last_error(format!("literllm_speech: request_json contained NUL byte: {e}"));
1058
+ return std::ptr::null_mut();
1059
+ }
1060
+ };
1061
+
1062
+ let hook_rc = invoke_on_request(&client_handle.hooks, &req_c);
1063
+ if hook_rc != 0 {
1064
+ set_last_error("literllm_speech: request rejected by on_request hook".into());
1065
+ invoke_on_error(&client_handle.hooks, &req_c, "request rejected by on_request hook");
1066
+ return std::ptr::null_mut();
1067
+ }
1068
+
1069
+ let request = match serde_json::from_str(json_str) {
1070
+ Ok(r) => r,
1071
+ Err(e) => {
1072
+ set_last_error(format!("literllm_speech: failed to parse request JSON: {e}"));
1073
+ return std::ptr::null_mut();
1074
+ }
1075
+ };
1076
+
1077
+ let rt = match runtime() {
1078
+ Ok(rt) => rt,
1079
+ Err(e) => {
1080
+ set_last_error(format!("literllm_speech: {e}"));
1081
+ return std::ptr::null_mut();
1082
+ }
1083
+ };
1084
+ let result = rt.block_on(client_ref.speech(request));
1085
+
1086
+ match result {
1087
+ Ok(data) => {
1088
+ use base64::Engine;
1089
+ let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
1090
+ match CString::new(encoded) {
1091
+ Ok(c_str) => {
1092
+ invoke_on_response(&client_handle.hooks, &req_c, &c_str);
1093
+ c_str.into_raw()
1094
+ }
1095
+ Err(e) => {
1096
+ set_last_error(format!("literllm_speech: base64 output contained NUL byte: {e}"));
1097
+ std::ptr::null_mut()
1098
+ }
1099
+ }
1100
+ }
1101
+ Err(e) => {
1102
+ let msg = format!("literllm_speech: {}", format_error(&e));
1103
+ invoke_on_error(&client_handle.hooks, &req_c, &msg);
1104
+ set_last_error(msg);
1105
+ std::ptr::null_mut()
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ /// Transcribe audio to text.
1111
+ ///
1112
+ /// # Parameters
1113
+ ///
1114
+ /// - `client`: A valid client pointer.
1115
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1116
+ /// `CreateTranscriptionRequest` schema.
1117
+ ///
1118
+ /// # Return value
1119
+ ///
1120
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1121
+ /// `TranscriptionResponse` on success, or `NULL` on failure.
1122
+ /// The caller must free the returned string with [`literllm_free_string`].
1123
+ ///
1124
+ /// # Safety
1125
+ ///
1126
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1127
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1128
+ #[unsafe(no_mangle)]
1129
+ pub unsafe extern "C" fn literllm_transcribe(
1130
+ client: *const LiterLlmClient,
1131
+ request_json: *const c_char,
1132
+ ) -> *mut c_char {
1133
+ json_request_response("literllm_transcribe", client, request_json, |c, req| {
1134
+ Box::pin(c.transcribe(req))
1135
+ })
1136
+ }
1137
+
1138
+ /// Check content against moderation policies.
1139
+ ///
1140
+ /// # Parameters
1141
+ ///
1142
+ /// - `client`: A valid client pointer.
1143
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1144
+ /// `ModerationRequest` schema.
1145
+ ///
1146
+ /// # Return value
1147
+ ///
1148
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1149
+ /// `ModerationResponse` on success, or `NULL` on failure.
1150
+ /// The caller must free the returned string with [`literllm_free_string`].
1151
+ ///
1152
+ /// # Safety
1153
+ ///
1154
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1155
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1156
+ #[unsafe(no_mangle)]
1157
+ pub unsafe extern "C" fn literllm_moderate(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
1158
+ json_request_response("literllm_moderate", client, request_json, |c, req| {
1159
+ Box::pin(c.moderate(req))
1160
+ })
1161
+ }
1162
+
1163
+ /// Rerank documents by relevance to a query.
1164
+ ///
1165
+ /// # Parameters
1166
+ ///
1167
+ /// - `client`: A valid client pointer.
1168
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1169
+ /// `RerankRequest` schema.
1170
+ ///
1171
+ /// # Return value
1172
+ ///
1173
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1174
+ /// `RerankResponse` on success, or `NULL` on failure.
1175
+ /// The caller must free the returned string with [`literllm_free_string`].
1176
+ ///
1177
+ /// # Safety
1178
+ ///
1179
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1180
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1181
+ #[unsafe(no_mangle)]
1182
+ pub unsafe extern "C" fn literllm_rerank(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
1183
+ json_request_response("literllm_rerank", client, request_json, |c, req| {
1184
+ Box::pin(c.rerank(req))
1185
+ })
1186
+ }
1187
+
1188
+ /// Perform a web/document search.
1189
+ ///
1190
+ /// # Parameters
1191
+ ///
1192
+ /// - `client`: A valid client pointer.
1193
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1194
+ /// `SearchRequest` schema.
1195
+ ///
1196
+ /// # Return value
1197
+ ///
1198
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1199
+ /// `SearchResponse` on success, or `NULL` on failure.
1200
+ /// The caller must free the returned string with [`literllm_free_string`].
1201
+ ///
1202
+ /// # Safety
1203
+ ///
1204
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1205
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1206
+ #[unsafe(no_mangle)]
1207
+ pub unsafe extern "C" fn literllm_search(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
1208
+ json_request_response("literllm_search", client, request_json, |c, req| {
1209
+ Box::pin(c.search(req))
1210
+ })
1211
+ }
1212
+
1213
+ /// Extract text from a document via OCR.
1214
+ ///
1215
+ /// # Parameters
1216
+ ///
1217
+ /// - `client`: A valid client pointer.
1218
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1219
+ /// `OcrRequest` schema.
1220
+ ///
1221
+ /// # Return value
1222
+ ///
1223
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1224
+ /// `OcrResponse` on success, or `NULL` on failure.
1225
+ /// The caller must free the returned string with [`literllm_free_string`].
1226
+ ///
1227
+ /// # Safety
1228
+ ///
1229
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1230
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1231
+ #[unsafe(no_mangle)]
1232
+ pub unsafe extern "C" fn literllm_ocr(client: *const LiterLlmClient, request_json: *const c_char) -> *mut c_char {
1233
+ json_request_response("literllm_ocr", client, request_json, |c, req| Box::pin(c.ocr(req)))
1234
+ }
1235
+
1236
+ // ---------------------------------------------------------------------------
1237
+ // File management API
1238
+ // ---------------------------------------------------------------------------
1239
+
1240
+ /// Upload a file.
1241
+ ///
1242
+ /// # Parameters
1243
+ ///
1244
+ /// - `client`: A valid client pointer.
1245
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1246
+ /// `CreateFileRequest` schema. The `file` field must be base64-encoded.
1247
+ ///
1248
+ /// # Return value
1249
+ ///
1250
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1251
+ /// `FileObject` on success, or `NULL` on failure.
1252
+ /// The caller must free the returned string with [`literllm_free_string`].
1253
+ ///
1254
+ /// # Safety
1255
+ ///
1256
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1257
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1258
+ #[unsafe(no_mangle)]
1259
+ pub unsafe extern "C" fn literllm_create_file(
1260
+ client: *const LiterLlmClient,
1261
+ request_json: *const c_char,
1262
+ ) -> *mut c_char {
1263
+ json_request_response("literllm_create_file", client, request_json, |c, req| {
1264
+ Box::pin(c.create_file(req))
1265
+ })
1266
+ }
1267
+
1268
+ /// Retrieve metadata for a file by ID.
1269
+ ///
1270
+ /// # Parameters
1271
+ ///
1272
+ /// - `client`: A valid client pointer.
1273
+ /// - `file_id`: NUL-terminated file ID string.
1274
+ ///
1275
+ /// # Return value
1276
+ ///
1277
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1278
+ /// `FileObject` on success, or `NULL` on failure.
1279
+ /// The caller must free the returned string with [`literllm_free_string`].
1280
+ ///
1281
+ /// # Safety
1282
+ ///
1283
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1284
+ /// - `file_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1285
+ #[unsafe(no_mangle)]
1286
+ pub unsafe extern "C" fn literllm_retrieve_file(client: *const LiterLlmClient, file_id: *const c_char) -> *mut c_char {
1287
+ id_request_response("literllm_retrieve_file", client, file_id, "file_id", |c, id| {
1288
+ Box::pin(c.retrieve_file(id))
1289
+ })
1290
+ }
1291
+
1292
+ /// Delete a file by ID.
1293
+ ///
1294
+ /// # Parameters
1295
+ ///
1296
+ /// - `client`: A valid client pointer.
1297
+ /// - `file_id`: NUL-terminated file ID string.
1298
+ ///
1299
+ /// # Return value
1300
+ ///
1301
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1302
+ /// `DeleteResponse` on success, or `NULL` on failure.
1303
+ /// The caller must free the returned string with [`literllm_free_string`].
1304
+ ///
1305
+ /// # Safety
1306
+ ///
1307
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1308
+ /// - `file_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1309
+ #[unsafe(no_mangle)]
1310
+ pub unsafe extern "C" fn literllm_delete_file(client: *const LiterLlmClient, file_id: *const c_char) -> *mut c_char {
1311
+ id_request_response("literllm_delete_file", client, file_id, "file_id", |c, id| {
1312
+ Box::pin(c.delete_file(id))
1313
+ })
1314
+ }
1315
+
1316
+ /// List files, optionally filtered by query parameters.
1317
+ ///
1318
+ /// # Parameters
1319
+ ///
1320
+ /// - `client`: A valid client pointer.
1321
+ /// - `query_json`: NUL-terminated JSON string conforming to the
1322
+ /// `FileListQuery` schema. May be `NULL` to list all files.
1323
+ ///
1324
+ /// # Return value
1325
+ ///
1326
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1327
+ /// `FileListResponse` on success, or `NULL` on failure.
1328
+ /// The caller must free the returned string with [`literllm_free_string`].
1329
+ ///
1330
+ /// # Safety
1331
+ ///
1332
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1333
+ /// - `query_json` may be `NULL` or a valid NUL-terminated UTF-8 JSON string.
1334
+ #[unsafe(no_mangle)]
1335
+ pub unsafe extern "C" fn literllm_list_files(client: *const LiterLlmClient, query_json: *const c_char) -> *mut c_char {
1336
+ clear_last_error();
1337
+
1338
+ if client.is_null() {
1339
+ set_last_error("literllm_list_files: client must not be NULL".into());
1340
+ return std::ptr::null_mut();
1341
+ }
1342
+
1343
+ // SAFETY: caller guarantees `client` is non-null and valid.
1344
+ let client_ref = unsafe { &(*client).inner };
1345
+
1346
+ let query: Option<liter_llm::types::files::FileListQuery> = if query_json.is_null() {
1347
+ None
1348
+ } else {
1349
+ // SAFETY: caller guarantees `query_json` is a valid NUL-terminated string.
1350
+ let json_str = match unsafe { CStr::from_ptr(query_json) }.to_str() {
1351
+ Ok(s) => s,
1352
+ Err(e) => {
1353
+ set_last_error(format!("literllm_list_files: query_json is not valid UTF-8: {e}"));
1354
+ return std::ptr::null_mut();
1355
+ }
1356
+ };
1357
+ match serde_json::from_str(json_str) {
1358
+ Ok(q) => Some(q),
1359
+ Err(e) => {
1360
+ set_last_error(format!("literllm_list_files: failed to parse query JSON: {e}"));
1361
+ return std::ptr::null_mut();
1362
+ }
1363
+ }
1364
+ };
1365
+
1366
+ let rt = match runtime() {
1367
+ Ok(rt) => rt,
1368
+ Err(e) => {
1369
+ set_last_error(format!("literllm_list_files: {e}"));
1370
+ return std::ptr::null_mut();
1371
+ }
1372
+ };
1373
+ let result = rt.block_on(client_ref.list_files(query));
1374
+
1375
+ match result {
1376
+ Ok(response) => match serde_json::to_string(&response) {
1377
+ Ok(json) => match CString::new(json) {
1378
+ Ok(c_str) => c_str.into_raw(),
1379
+ Err(e) => {
1380
+ set_last_error(format!("literllm_list_files: response JSON contained NUL byte: {e}"));
1381
+ std::ptr::null_mut()
1382
+ }
1383
+ },
1384
+ Err(e) => {
1385
+ set_last_error(format!("literllm_list_files: failed to serialize response: {e}"));
1386
+ std::ptr::null_mut()
1387
+ }
1388
+ },
1389
+ Err(e) => {
1390
+ set_last_error(format!("literllm_list_files: {e}"));
1391
+ std::ptr::null_mut()
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ /// Retrieve the raw content of a file (returned as base64-encoded string).
1397
+ ///
1398
+ /// # Parameters
1399
+ ///
1400
+ /// - `client`: A valid client pointer.
1401
+ /// - `file_id`: NUL-terminated file ID string.
1402
+ ///
1403
+ /// # Return value
1404
+ ///
1405
+ /// Returns a heap-allocated NUL-terminated base64-encoded string of the file
1406
+ /// content on success, or `NULL` on failure.
1407
+ /// The caller must free the returned string with [`literllm_free_string`].
1408
+ ///
1409
+ /// # Safety
1410
+ ///
1411
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1412
+ /// - `file_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1413
+ #[unsafe(no_mangle)]
1414
+ pub unsafe extern "C" fn literllm_file_content(client: *const LiterLlmClient, file_id: *const c_char) -> *mut c_char {
1415
+ id_request_bytes("literllm_file_content", client, file_id, "file_id", |c, id| {
1416
+ Box::pin(c.file_content(id))
1417
+ })
1418
+ }
1419
+
1420
+ // ---------------------------------------------------------------------------
1421
+ // Batch API
1422
+ // ---------------------------------------------------------------------------
1423
+
1424
+ /// Create a new batch job.
1425
+ ///
1426
+ /// # Parameters
1427
+ ///
1428
+ /// - `client`: A valid client pointer.
1429
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1430
+ /// `CreateBatchRequest` schema.
1431
+ ///
1432
+ /// # Return value
1433
+ ///
1434
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1435
+ /// `BatchObject` on success, or `NULL` on failure.
1436
+ /// The caller must free the returned string with [`literllm_free_string`].
1437
+ ///
1438
+ /// # Safety
1439
+ ///
1440
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1441
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1442
+ #[unsafe(no_mangle)]
1443
+ pub unsafe extern "C" fn literllm_create_batch(
1444
+ client: *const LiterLlmClient,
1445
+ request_json: *const c_char,
1446
+ ) -> *mut c_char {
1447
+ json_request_response("literllm_create_batch", client, request_json, |c, req| {
1448
+ Box::pin(c.create_batch(req))
1449
+ })
1450
+ }
1451
+
1452
+ /// Retrieve a batch by ID.
1453
+ ///
1454
+ /// # Parameters
1455
+ ///
1456
+ /// - `client`: A valid client pointer.
1457
+ /// - `batch_id`: NUL-terminated batch ID string.
1458
+ ///
1459
+ /// # Return value
1460
+ ///
1461
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1462
+ /// `BatchObject` on success, or `NULL` on failure.
1463
+ /// The caller must free the returned string with [`literllm_free_string`].
1464
+ ///
1465
+ /// # Safety
1466
+ ///
1467
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1468
+ /// - `batch_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1469
+ #[unsafe(no_mangle)]
1470
+ pub unsafe extern "C" fn literllm_retrieve_batch(
1471
+ client: *const LiterLlmClient,
1472
+ batch_id: *const c_char,
1473
+ ) -> *mut c_char {
1474
+ id_request_response("literllm_retrieve_batch", client, batch_id, "batch_id", |c, id| {
1475
+ Box::pin(c.retrieve_batch(id))
1476
+ })
1477
+ }
1478
+
1479
+ /// List batches, optionally filtered by query parameters.
1480
+ ///
1481
+ /// # Parameters
1482
+ ///
1483
+ /// - `client`: A valid client pointer.
1484
+ /// - `query_json`: NUL-terminated JSON string conforming to the
1485
+ /// `BatchListQuery` schema. May be `NULL` to list all batches.
1486
+ ///
1487
+ /// # Return value
1488
+ ///
1489
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1490
+ /// `BatchListResponse` on success, or `NULL` on failure.
1491
+ /// The caller must free the returned string with [`literllm_free_string`].
1492
+ ///
1493
+ /// # Safety
1494
+ ///
1495
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1496
+ /// - `query_json` may be `NULL` or a valid NUL-terminated UTF-8 JSON string.
1497
+ #[unsafe(no_mangle)]
1498
+ pub unsafe extern "C" fn literllm_list_batches(
1499
+ client: *const LiterLlmClient,
1500
+ query_json: *const c_char,
1501
+ ) -> *mut c_char {
1502
+ clear_last_error();
1503
+
1504
+ if client.is_null() {
1505
+ set_last_error("literllm_list_batches: client must not be NULL".into());
1506
+ return std::ptr::null_mut();
1507
+ }
1508
+
1509
+ // SAFETY: caller guarantees `client` is non-null and valid.
1510
+ let client_ref = unsafe { &(*client).inner };
1511
+
1512
+ let query: Option<liter_llm::types::batch::BatchListQuery> = if query_json.is_null() {
1513
+ None
1514
+ } else {
1515
+ // SAFETY: caller guarantees `query_json` is a valid NUL-terminated string.
1516
+ let json_str = match unsafe { CStr::from_ptr(query_json) }.to_str() {
1517
+ Ok(s) => s,
1518
+ Err(e) => {
1519
+ set_last_error(format!("literllm_list_batches: query_json is not valid UTF-8: {e}"));
1520
+ return std::ptr::null_mut();
1521
+ }
1522
+ };
1523
+ match serde_json::from_str(json_str) {
1524
+ Ok(q) => Some(q),
1525
+ Err(e) => {
1526
+ set_last_error(format!("literllm_list_batches: failed to parse query JSON: {e}"));
1527
+ return std::ptr::null_mut();
1528
+ }
1529
+ }
1530
+ };
1531
+
1532
+ let rt = match runtime() {
1533
+ Ok(rt) => rt,
1534
+ Err(e) => {
1535
+ set_last_error(format!("literllm_list_batches: {e}"));
1536
+ return std::ptr::null_mut();
1537
+ }
1538
+ };
1539
+ let result = rt.block_on(client_ref.list_batches(query));
1540
+
1541
+ match result {
1542
+ Ok(response) => match serde_json::to_string(&response) {
1543
+ Ok(json) => match CString::new(json) {
1544
+ Ok(c_str) => c_str.into_raw(),
1545
+ Err(e) => {
1546
+ set_last_error(format!("literllm_list_batches: response JSON contained NUL byte: {e}"));
1547
+ std::ptr::null_mut()
1548
+ }
1549
+ },
1550
+ Err(e) => {
1551
+ set_last_error(format!("literllm_list_batches: failed to serialize response: {e}"));
1552
+ std::ptr::null_mut()
1553
+ }
1554
+ },
1555
+ Err(e) => {
1556
+ set_last_error(format!("literllm_list_batches: {e}"));
1557
+ std::ptr::null_mut()
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ /// Cancel an in-progress batch.
1563
+ ///
1564
+ /// # Parameters
1565
+ ///
1566
+ /// - `client`: A valid client pointer.
1567
+ /// - `batch_id`: NUL-terminated batch ID string.
1568
+ ///
1569
+ /// # Return value
1570
+ ///
1571
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1572
+ /// `BatchObject` on success, or `NULL` on failure.
1573
+ /// The caller must free the returned string with [`literllm_free_string`].
1574
+ ///
1575
+ /// # Safety
1576
+ ///
1577
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1578
+ /// - `batch_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1579
+ #[unsafe(no_mangle)]
1580
+ pub unsafe extern "C" fn literllm_cancel_batch(client: *const LiterLlmClient, batch_id: *const c_char) -> *mut c_char {
1581
+ id_request_response("literllm_cancel_batch", client, batch_id, "batch_id", |c, id| {
1582
+ Box::pin(c.cancel_batch(id))
1583
+ })
1584
+ }
1585
+
1586
+ // ---------------------------------------------------------------------------
1587
+ // Responses API
1588
+ // ---------------------------------------------------------------------------
1589
+
1590
+ /// Create a new response.
1591
+ ///
1592
+ /// # Parameters
1593
+ ///
1594
+ /// - `client`: A valid client pointer.
1595
+ /// - `request_json`: NUL-terminated JSON string conforming to the
1596
+ /// `CreateResponseRequest` schema.
1597
+ ///
1598
+ /// # Return value
1599
+ ///
1600
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1601
+ /// `ResponseObject` on success, or `NULL` on failure.
1602
+ /// The caller must free the returned string with [`literllm_free_string`].
1603
+ ///
1604
+ /// # Safety
1605
+ ///
1606
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1607
+ /// - `request_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1608
+ #[unsafe(no_mangle)]
1609
+ pub unsafe extern "C" fn literllm_create_response(
1610
+ client: *const LiterLlmClient,
1611
+ request_json: *const c_char,
1612
+ ) -> *mut c_char {
1613
+ json_request_response("literllm_create_response", client, request_json, |c, req| {
1614
+ Box::pin(c.create_response(req))
1615
+ })
1616
+ }
1617
+
1618
+ /// Retrieve a response by ID.
1619
+ ///
1620
+ /// # Parameters
1621
+ ///
1622
+ /// - `client`: A valid client pointer.
1623
+ /// - `response_id`: NUL-terminated response ID string.
1624
+ ///
1625
+ /// # Return value
1626
+ ///
1627
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1628
+ /// `ResponseObject` on success, or `NULL` on failure.
1629
+ /// The caller must free the returned string with [`literllm_free_string`].
1630
+ ///
1631
+ /// # Safety
1632
+ ///
1633
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1634
+ /// - `response_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1635
+ #[unsafe(no_mangle)]
1636
+ pub unsafe extern "C" fn literllm_retrieve_response(
1637
+ client: *const LiterLlmClient,
1638
+ response_id: *const c_char,
1639
+ ) -> *mut c_char {
1640
+ id_request_response(
1641
+ "literllm_retrieve_response",
1642
+ client,
1643
+ response_id,
1644
+ "response_id",
1645
+ |c, id| Box::pin(c.retrieve_response(id)),
1646
+ )
1647
+ }
1648
+
1649
+ /// Cancel an in-progress response.
1650
+ ///
1651
+ /// # Parameters
1652
+ ///
1653
+ /// - `client`: A valid client pointer.
1654
+ /// - `response_id`: NUL-terminated response ID string.
1655
+ ///
1656
+ /// # Return value
1657
+ ///
1658
+ /// Returns a heap-allocated NUL-terminated JSON string containing the
1659
+ /// `ResponseObject` on success, or `NULL` on failure.
1660
+ /// The caller must free the returned string with [`literllm_free_string`].
1661
+ ///
1662
+ /// # Safety
1663
+ ///
1664
+ /// - `client` must be a valid, non-null pointer returned by `literllm_client_new`.
1665
+ /// - `response_id` must be a valid, non-null, NUL-terminated UTF-8 string.
1666
+ #[unsafe(no_mangle)]
1667
+ pub unsafe extern "C" fn literllm_cancel_response(
1668
+ client: *const LiterLlmClient,
1669
+ response_id: *const c_char,
1670
+ ) -> *mut c_char {
1671
+ id_request_response(
1672
+ "literllm_cancel_response",
1673
+ client,
1674
+ response_id,
1675
+ "response_id",
1676
+ |c, id| Box::pin(c.cancel_response(id)),
1677
+ )
1678
+ }
1679
+
1680
+ // ---------------------------------------------------------------------------
1681
+ // Budget
1682
+ // ---------------------------------------------------------------------------
1683
+
1684
+ /// Read the cumulative global spend tracked by the budget layer.
1685
+ ///
1686
+ /// Returns the spend in USD. If no budget is configured on the client,
1687
+ /// returns `0.0`.
1688
+ ///
1689
+ /// # Safety
1690
+ ///
1691
+ /// - `client` must be a valid, non-null pointer returned by
1692
+ /// `literllm_client_new` or `literllm_client_new_with_config`.
1693
+ #[unsafe(no_mangle)]
1694
+ pub unsafe extern "C" fn literllm_budget_usage(client: *const LiterLlmClient) -> f64 {
1695
+ if client.is_null() {
1696
+ return 0.0;
1697
+ }
1698
+ // SAFETY: caller guarantees `client` is non-null and valid.
1699
+ let client_handle = unsafe { &(*client) };
1700
+ client_handle
1701
+ .inner
1702
+ .budget_state()
1703
+ .map(|s| s.global_spend())
1704
+ .unwrap_or(0.0)
1705
+ }
1706
+
1707
+ // ---------------------------------------------------------------------------
1708
+ // Utility
1709
+ // ---------------------------------------------------------------------------
1710
+
1711
+ /// Retrieve the last error message for the current thread.
1712
+ ///
1713
+ /// Returns a `const char*` pointer to the NUL-terminated error string, or
1714
+ /// `NULL` if no error has occurred since the last successful call.
1715
+ ///
1716
+ /// The returned pointer is valid only until the **next** liter-llm function
1717
+ /// call on the **same thread**. The caller must **not** free this pointer.
1718
+ ///
1719
+ /// # Safety
1720
+ ///
1721
+ /// Always safe to call. No preconditions.
1722
+ #[unsafe(no_mangle)]
1723
+ pub extern "C" fn literllm_last_error() -> *const c_char {
1724
+ LAST_ERROR.with(|cell| match &*cell.borrow() {
1725
+ Some(c_str) => c_str.as_ptr(),
1726
+ None => std::ptr::null(),
1727
+ })
1728
+ }
1729
+
1730
+ /// Free a string returned by [`literllm_chat`], [`literllm_embed`], or
1731
+ /// [`literllm_list_models`].
1732
+ ///
1733
+ /// # Safety
1734
+ ///
1735
+ /// - `s` must be a pointer returned by one of the functions listed above.
1736
+ /// - `s` must not be used after this call (use-after-free is UB).
1737
+ /// - Passing `NULL` is safe and is a no-op.
1738
+ /// - Do **not** pass the pointer returned by [`literllm_last_error`]; that
1739
+ /// pointer must not be freed.
1740
+ #[unsafe(no_mangle)]
1741
+ pub unsafe extern "C" fn literllm_free_string(s: *mut c_char) {
1742
+ // SAFETY: `s` is either NULL (no-op) or was returned by `CString::into_raw`
1743
+ // inside this crate. Reconstructing the `CString` transfers ownership back
1744
+ // to Rust, which drops and deallocates the allocation at end of scope.
1745
+ if !s.is_null() {
1746
+ drop(unsafe { CString::from_raw(s) });
1747
+ }
1748
+ }
1749
+
1750
+ /// Returns the version string of the liter-llm library.
1751
+ ///
1752
+ /// The returned pointer is valid for the lifetime of the program and must
1753
+ /// **not** be freed.
1754
+ ///
1755
+ /// # Safety
1756
+ ///
1757
+ /// Always safe to call.
1758
+ #[unsafe(no_mangle)]
1759
+ pub extern "C" fn literllm_version() -> *const c_char {
1760
+ // SAFETY: VERSION is 'static, NUL-terminated, and lives for the duration
1761
+ // of the program. It is initialised exactly once via OnceLock on first
1762
+ // call. The raw pointer is never freed by the caller (documented above).
1763
+ //
1764
+ // `CARGO_PKG_VERSION` is set by Cargo at compile time and never contains
1765
+ // interior NUL bytes (semver syntax does not include NUL).
1766
+ static VERSION: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
1767
+ VERSION
1768
+ .get_or_init(|| {
1769
+ // SAFETY: semver strings (e.g. "1.0.0") never contain NUL bytes,
1770
+ // so `CString::new` will always succeed here.
1771
+ CString::new(env!("CARGO_PKG_VERSION")).unwrap_or_else(|_| c"unknown".to_owned())
1772
+ })
1773
+ .as_ptr()
1774
+ }
1775
+
1776
+ // ---------------------------------------------------------------------------
1777
+ // Custom provider registration
1778
+ // ---------------------------------------------------------------------------
1779
+
1780
+ /// Register a custom LLM provider at runtime.
1781
+ ///
1782
+ /// # Parameters
1783
+ ///
1784
+ /// - `config_json`: NUL-terminated JSON string conforming to the
1785
+ /// [`CustomProviderConfig`](liter_llm::CustomProviderConfig) schema.
1786
+ ///
1787
+ /// # Return value
1788
+ ///
1789
+ /// Returns `0` on success, `-1` on failure.
1790
+ /// Check [`literllm_last_error`] for the error message when `-1` is returned.
1791
+ ///
1792
+ /// # Safety
1793
+ ///
1794
+ /// - `config_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1795
+ #[unsafe(no_mangle)]
1796
+ pub unsafe extern "C" fn literllm_register_provider(config_json: *const c_char) -> i32 {
1797
+ clear_last_error();
1798
+
1799
+ if config_json.is_null() {
1800
+ set_last_error("literllm_register_provider: config_json must not be NULL".into());
1801
+ return -1;
1802
+ }
1803
+
1804
+ // SAFETY: caller guarantees `config_json` is non-null and NUL-terminated.
1805
+ let json_str = match unsafe { CStr::from_ptr(config_json) }.to_str() {
1806
+ Ok(s) => s,
1807
+ Err(e) => {
1808
+ set_last_error(format!(
1809
+ "literllm_register_provider: config_json is not valid UTF-8: {e}"
1810
+ ));
1811
+ return -1;
1812
+ }
1813
+ };
1814
+
1815
+ let config: liter_llm::CustomProviderConfig = match serde_json::from_str(json_str) {
1816
+ Ok(c) => c,
1817
+ Err(e) => {
1818
+ set_last_error(format!("literllm_register_provider: failed to parse config JSON: {e}"));
1819
+ return -1;
1820
+ }
1821
+ };
1822
+
1823
+ match liter_llm::register_custom_provider(config) {
1824
+ Ok(()) => 0,
1825
+ Err(e) => {
1826
+ set_last_error(format!("literllm_register_provider: {e}"));
1827
+ -1
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ /// Unregister a previously registered custom provider by name.
1833
+ ///
1834
+ /// # Parameters
1835
+ ///
1836
+ /// - `name`: NUL-terminated provider name string.
1837
+ ///
1838
+ /// # Return value
1839
+ ///
1840
+ /// Returns `0` if the provider was found and removed, `1` if no provider with
1841
+ /// that name existed, or `-1` on failure.
1842
+ /// Check [`literllm_last_error`] for the error message when `-1` is returned.
1843
+ ///
1844
+ /// # Safety
1845
+ ///
1846
+ /// - `name` must be a valid, non-null, NUL-terminated UTF-8 string.
1847
+ #[unsafe(no_mangle)]
1848
+ pub unsafe extern "C" fn literllm_unregister_provider(name: *const c_char) -> i32 {
1849
+ clear_last_error();
1850
+
1851
+ if name.is_null() {
1852
+ set_last_error("literllm_unregister_provider: name must not be NULL".into());
1853
+ return -1;
1854
+ }
1855
+
1856
+ // SAFETY: caller guarantees `name` is non-null and NUL-terminated.
1857
+ let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
1858
+ Ok(s) => s,
1859
+ Err(e) => {
1860
+ set_last_error(format!("literllm_unregister_provider: name is not valid UTF-8: {e}"));
1861
+ return -1;
1862
+ }
1863
+ };
1864
+
1865
+ match liter_llm::unregister_custom_provider(name_str) {
1866
+ Ok(true) => 0,
1867
+ Ok(false) => 1,
1868
+ Err(e) => {
1869
+ set_last_error(format!("literllm_unregister_provider: {e}"));
1870
+ -1
1871
+ }
1872
+ }
1873
+ }
1874
+
1875
+ // ---------------------------------------------------------------------------
1876
+ // Extended client construction with full JSON config
1877
+ // ---------------------------------------------------------------------------
1878
+
1879
+ /// Create a new liter-llm client from a full JSON configuration object.
1880
+ ///
1881
+ /// This is an extended version of [`literllm_client_new`] that accepts a
1882
+ /// single JSON string containing all configuration options, including
1883
+ /// cache, budget, extra headers, and model hint.
1884
+ ///
1885
+ /// # JSON Schema
1886
+ ///
1887
+ /// ```json
1888
+ /// {
1889
+ /// "api_key": "sk-...",
1890
+ /// "base_url": "https://...", // optional
1891
+ /// "model_hint": "groq/llama3-70b", // optional
1892
+ /// "max_retries": 3, // optional, default 3
1893
+ /// "timeout_secs": 60, // optional, default 60
1894
+ /// "extra_headers": {"X-Custom": "v"}, // optional
1895
+ /// "cache": { // optional
1896
+ /// "max_entries": 256,
1897
+ /// "ttl_secs": 300
1898
+ /// },
1899
+ /// "budget": { // optional
1900
+ /// "global_limit": 10.0,
1901
+ /// "model_limits": {"gpt-4": 5.0},
1902
+ /// "enforcement": "hard"
1903
+ /// }
1904
+ /// }
1905
+ /// ```
1906
+ ///
1907
+ /// # Return value
1908
+ ///
1909
+ /// Returns a heap-allocated `LiterLlmClient*` on success, or `NULL` on
1910
+ /// failure. Check [`literllm_last_error`] for the error message when
1911
+ /// `NULL` is returned.
1912
+ ///
1913
+ /// The returned pointer must be freed with [`literllm_client_free`].
1914
+ ///
1915
+ /// # Safety
1916
+ ///
1917
+ /// - `config_json` must be a valid, non-null, NUL-terminated UTF-8 JSON string.
1918
+ /// - The caller owns the returned pointer and must call `literllm_client_free`
1919
+ /// exactly once.
1920
+ #[unsafe(no_mangle)]
1921
+ pub unsafe extern "C" fn literllm_client_new_with_config(config_json: *const c_char) -> *mut LiterLlmClient {
1922
+ clear_last_error();
1923
+
1924
+ if config_json.is_null() {
1925
+ set_last_error("literllm_client_new_with_config: config_json must not be NULL".into());
1926
+ return std::ptr::null_mut();
1927
+ }
1928
+
1929
+ // SAFETY: caller guarantees `config_json` is non-null and NUL-terminated.
1930
+ let json_str = match unsafe { CStr::from_ptr(config_json) }.to_str() {
1931
+ Ok(s) => s,
1932
+ Err(e) => {
1933
+ set_last_error(format!(
1934
+ "literllm_client_new_with_config: config_json is not valid UTF-8: {e}"
1935
+ ));
1936
+ return std::ptr::null_mut();
1937
+ }
1938
+ };
1939
+
1940
+ let parsed: FfiClientConfig = match serde_json::from_str(json_str) {
1941
+ Ok(c) => c,
1942
+ Err(e) => {
1943
+ set_last_error(format!(
1944
+ "literllm_client_new_with_config: failed to parse config JSON: {e}"
1945
+ ));
1946
+ return std::ptr::null_mut();
1947
+ }
1948
+ };
1949
+
1950
+ let mut builder = liter_llm::client::ClientConfigBuilder::new(parsed.api_key);
1951
+
1952
+ if let Some(url) = parsed.base_url
1953
+ && !url.is_empty()
1954
+ {
1955
+ builder = builder.base_url(url);
1956
+ }
1957
+ if let Some(retries) = parsed.max_retries {
1958
+ builder = builder.max_retries(retries);
1959
+ }
1960
+ if let Some(secs) = parsed.timeout_secs {
1961
+ builder = builder.timeout(std::time::Duration::from_secs(secs));
1962
+ }
1963
+
1964
+ // Extra headers.
1965
+ if let Some(headers) = parsed.extra_headers {
1966
+ for (key, value) in headers {
1967
+ match builder.header(key, value) {
1968
+ Ok(b) => builder = b,
1969
+ Err(e) => {
1970
+ set_last_error(format!("literllm_client_new_with_config: invalid header: {e}"));
1971
+ return std::ptr::null_mut();
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+
1977
+ // Cache configuration.
1978
+ if let Some(cache) = parsed.cache {
1979
+ let cache_config = liter_llm::tower::CacheConfig {
1980
+ max_entries: cache.max_entries.unwrap_or(256),
1981
+ ttl: std::time::Duration::from_secs(cache.ttl_secs.unwrap_or(300)),
1982
+ backend: Default::default(),
1983
+ };
1984
+ builder = builder.cache(cache_config);
1985
+ }
1986
+
1987
+ // Budget configuration.
1988
+ if let Some(budget) = parsed.budget {
1989
+ let enforcement = match budget.enforcement.as_deref() {
1990
+ Some("soft") => liter_llm::tower::Enforcement::Soft,
1991
+ _ => liter_llm::tower::Enforcement::Hard,
1992
+ };
1993
+ let budget_config = liter_llm::tower::BudgetConfig {
1994
+ global_limit: budget.global_limit,
1995
+ model_limits: budget.model_limits.unwrap_or_default(),
1996
+ enforcement,
1997
+ };
1998
+ builder = builder.budget(budget_config);
1999
+ }
2000
+
2001
+ // Cooldown configuration.
2002
+ if let Some(secs) = parsed.cooldown_secs {
2003
+ builder = builder.cooldown(std::time::Duration::from_secs(secs));
2004
+ }
2005
+
2006
+ // Rate limit configuration.
2007
+ if let Some(rl) = parsed.rate_limit {
2008
+ let rl_config = liter_llm::tower::RateLimitConfig {
2009
+ rpm: rl.rpm,
2010
+ tpm: rl.tpm,
2011
+ window: std::time::Duration::from_secs(rl.window_seconds.unwrap_or(60)),
2012
+ };
2013
+ builder = builder.rate_limit(rl_config);
2014
+ }
2015
+
2016
+ // Health check configuration.
2017
+ if let Some(secs) = parsed.health_check_secs {
2018
+ builder = builder.health_check(std::time::Duration::from_secs(secs));
2019
+ }
2020
+
2021
+ // Cost tracking.
2022
+ if parsed.cost_tracking.unwrap_or(false) {
2023
+ builder = builder.cost_tracking(true);
2024
+ }
2025
+
2026
+ // Tracing.
2027
+ if parsed.tracing.unwrap_or(false) {
2028
+ builder = builder.tracing(true);
2029
+ }
2030
+
2031
+ let config: ClientConfig = builder.build();
2032
+
2033
+ match ManagedClient::new(config, parsed.model_hint.as_deref()) {
2034
+ Ok(client) => {
2035
+ let handle = Box::new(LiterLlmClient {
2036
+ inner: client,
2037
+ hooks: None,
2038
+ });
2039
+ Box::into_raw(handle)
2040
+ }
2041
+ Err(e) => {
2042
+ set_last_error(format!("literllm_client_new_with_config: {e}"));
2043
+ std::ptr::null_mut()
2044
+ }
2045
+ }
2046
+ }
2047
+
2048
+ /// Deserialized JSON config for `literllm_client_new_with_config`.
2049
+ #[derive(serde::Deserialize)]
2050
+ #[serde(deny_unknown_fields)]
2051
+ struct FfiClientConfig {
2052
+ api_key: String,
2053
+ #[serde(default)]
2054
+ base_url: Option<String>,
2055
+ #[serde(default)]
2056
+ model_hint: Option<String>,
2057
+ #[serde(default)]
2058
+ max_retries: Option<u32>,
2059
+ #[serde(default)]
2060
+ timeout_secs: Option<u64>,
2061
+ #[serde(default)]
2062
+ extra_headers: Option<std::collections::HashMap<String, String>>,
2063
+ #[serde(default)]
2064
+ cache: Option<FfiCacheConfig>,
2065
+ #[serde(default)]
2066
+ budget: Option<FfiBudgetConfig>,
2067
+ #[serde(default)]
2068
+ cooldown_secs: Option<u64>,
2069
+ #[serde(default)]
2070
+ rate_limit: Option<FfiRateLimitConfig>,
2071
+ #[serde(default)]
2072
+ health_check_secs: Option<u64>,
2073
+ #[serde(default)]
2074
+ cost_tracking: Option<bool>,
2075
+ #[serde(default)]
2076
+ tracing: Option<bool>,
2077
+ }
2078
+
2079
+ #[derive(serde::Deserialize)]
2080
+ #[serde(deny_unknown_fields)]
2081
+ struct FfiCacheConfig {
2082
+ max_entries: Option<usize>,
2083
+ ttl_secs: Option<u64>,
2084
+ }
2085
+
2086
+ #[derive(serde::Deserialize)]
2087
+ #[serde(deny_unknown_fields)]
2088
+ struct FfiBudgetConfig {
2089
+ global_limit: Option<f64>,
2090
+ model_limits: Option<std::collections::HashMap<String, f64>>,
2091
+ enforcement: Option<String>,
2092
+ }
2093
+
2094
+ #[derive(serde::Deserialize)]
2095
+ #[serde(deny_unknown_fields)]
2096
+ struct FfiRateLimitConfig {
2097
+ rpm: Option<u32>,
2098
+ tpm: Option<u64>,
2099
+ window_seconds: Option<u64>,
2100
+ }
2101
+
2102
+ // ---------------------------------------------------------------------------
2103
+ // Hook callback registration
2104
+ // ---------------------------------------------------------------------------
2105
+
2106
+ /// Function pointer struct for lifecycle hook callbacks.
2107
+ ///
2108
+ /// All function pointers are optional (may be NULL). When non-NULL, the
2109
+ /// corresponding callback is invoked at the appropriate lifecycle point.
2110
+ ///
2111
+ /// # Memory ownership
2112
+ ///
2113
+ /// - `request_json` passed to callbacks is a NUL-terminated JSON string owned
2114
+ /// by the caller (liter-llm). The hook must **not** free it; it is valid
2115
+ /// only for the duration of the callback invocation.
2116
+ /// - `response_json` and `error_message` follow the same ownership rules.
2117
+ /// - `user_data` is forwarded as-is to each callback; the caller is
2118
+ /// responsible for its lifetime and thread safety.
2119
+ #[repr(C)]
2120
+ pub struct LiterLlmHookCallbacks {
2121
+ /// Called before the request is sent.
2122
+ ///
2123
+ /// Return `0` to proceed, or non-zero to reject the request (guardrail).
2124
+ /// When non-zero is returned, `literllm_last_error` will contain the
2125
+ /// rejection message if set by the hook.
2126
+ pub on_request: Option<unsafe extern "C" fn(request_json: *const c_char, user_data: *mut std::ffi::c_void) -> i32>,
2127
+
2128
+ /// Called after a successful response.
2129
+ pub on_response: Option<
2130
+ unsafe extern "C" fn(
2131
+ request_json: *const c_char,
2132
+ response_json: *const c_char,
2133
+ user_data: *mut std::ffi::c_void,
2134
+ ),
2135
+ >,
2136
+
2137
+ /// Called when the request fails with an error.
2138
+ pub on_error: Option<
2139
+ unsafe extern "C" fn(
2140
+ request_json: *const c_char,
2141
+ error_message: *const c_char,
2142
+ user_data: *mut std::ffi::c_void,
2143
+ ),
2144
+ >,
2145
+
2146
+ /// Opaque user data pointer forwarded to all callbacks.
2147
+ pub user_data: *mut std::ffi::c_void,
2148
+ }
2149
+
2150
+ /// Register lifecycle hook callbacks for a client.
2151
+ ///
2152
+ /// The callbacks are stored for the lifetime of the client and invoked
2153
+ /// around each API call (chat, embed, etc.).
2154
+ ///
2155
+ /// **Note:** In the current implementation, hooks are advisory metadata
2156
+ /// stored on the client handle. Full Tower-integrated hook invocation
2157
+ /// requires the client to be wrapped in a `HooksLayer` service stack,
2158
+ /// which is an internal architecture detail. C FFI callers should use
2159
+ /// these callbacks as a notification mechanism; the Rust core handles
2160
+ /// the actual request lifecycle.
2161
+ ///
2162
+ /// # Parameters
2163
+ ///
2164
+ /// - `client`: A valid client pointer.
2165
+ /// - `callbacks`: Pointer to a `LiterLlmHookCallbacks` struct. The struct
2166
+ /// is copied; the caller may free it after this call returns.
2167
+ ///
2168
+ /// # Return value
2169
+ ///
2170
+ /// Returns `0` on success, `-1` on failure.
2171
+ ///
2172
+ /// # Safety
2173
+ ///
2174
+ /// - `client` must be a valid, non-null pointer returned by
2175
+ /// `literllm_client_new` or `literllm_client_new_with_config`.
2176
+ /// - `callbacks` must be a valid, non-null pointer to a
2177
+ /// `LiterLlmHookCallbacks` struct.
2178
+ /// - Function pointers in `callbacks` must remain valid for the lifetime
2179
+ /// of the client.
2180
+ /// - `user_data` must be valid for the lifetime of the client if non-NULL.
2181
+ #[unsafe(no_mangle)]
2182
+ pub unsafe extern "C" fn literllm_set_hooks(
2183
+ client: *mut LiterLlmClient,
2184
+ callbacks: *const LiterLlmHookCallbacks,
2185
+ ) -> i32 {
2186
+ clear_last_error();
2187
+
2188
+ if client.is_null() {
2189
+ set_last_error("literllm_set_hooks: client must not be NULL".into());
2190
+ return -1;
2191
+ }
2192
+ if callbacks.is_null() {
2193
+ set_last_error("literllm_set_hooks: callbacks must not be NULL".into());
2194
+ return -1;
2195
+ }
2196
+
2197
+ // SAFETY: caller guarantees both pointers are non-null and valid.
2198
+ // We copy the callbacks struct so the caller can free theirs.
2199
+ let cb = unsafe { std::ptr::read(callbacks) };
2200
+
2201
+ // SAFETY: caller guarantees `client` is a valid, non-null pointer returned
2202
+ // by `literllm_client_new`. We store the callbacks on the client handle
2203
+ // so they can be invoked during each API call's lifecycle.
2204
+ let client_ref = unsafe { &mut *client };
2205
+ client_ref.hooks = Some(cb);
2206
+
2207
+ 0
2208
+ }
2209
+
2210
+ // ---------------------------------------------------------------------------
2211
+ // Tests
2212
+ // ---------------------------------------------------------------------------
2213
+
2214
+ #[cfg(test)]
2215
+ mod tests {
2216
+ use super::*;
2217
+ use std::ffi::{CStr, CString};
2218
+
2219
+ #[test]
2220
+ fn version_is_non_null() {
2221
+ let ptr = literllm_version();
2222
+ assert!(!ptr.is_null());
2223
+ // SAFETY: `ptr` points to a static NUL-terminated string.
2224
+ let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
2225
+ assert!(s.contains('.'), "version should contain a dot: {s}");
2226
+ }
2227
+
2228
+ #[test]
2229
+ fn last_error_null_initially() {
2230
+ clear_last_error();
2231
+ let ptr = literllm_last_error();
2232
+ assert!(ptr.is_null(), "last error should be null when none set");
2233
+ }
2234
+
2235
+ #[test]
2236
+ fn last_error_returns_message_after_set() {
2237
+ set_last_error("something went wrong".into());
2238
+ let ptr = literllm_last_error();
2239
+ assert!(!ptr.is_null());
2240
+ // SAFETY: `ptr` is valid until the next liter-llm call on this thread.
2241
+ let msg = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
2242
+ assert_eq!(msg, "something went wrong");
2243
+ clear_last_error();
2244
+ }
2245
+
2246
+ #[test]
2247
+ fn client_new_null_api_key_returns_null() {
2248
+ // SAFETY: passing NULL api_key is documented to return NULL + set error.
2249
+ let client = unsafe { literllm_client_new(std::ptr::null(), std::ptr::null(), std::ptr::null()) };
2250
+ assert!(client.is_null());
2251
+ let err = literllm_last_error();
2252
+ assert!(!err.is_null());
2253
+ // SAFETY: err is valid until next call on this thread.
2254
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2255
+ assert!(msg.contains("NULL"));
2256
+ }
2257
+
2258
+ #[test]
2259
+ fn client_new_and_free_empty_key() {
2260
+ let api_key = CString::new("test-key").unwrap();
2261
+ // SAFETY: api_key is a valid NUL-terminated string; base_url and model_hint are NULL.
2262
+ let client = unsafe { literllm_client_new(api_key.as_ptr(), std::ptr::null(), std::ptr::null()) };
2263
+ // Construction may fail if reqwest internals fail, but on CI it should succeed.
2264
+ if !client.is_null() {
2265
+ // SAFETY: client was returned by literllm_client_new.
2266
+ unsafe { literllm_client_free(client) };
2267
+ }
2268
+ }
2269
+
2270
+ #[test]
2271
+ fn client_free_null_is_noop() {
2272
+ // SAFETY: NULL is documented to be safe.
2273
+ unsafe { literllm_client_free(std::ptr::null_mut()) };
2274
+ }
2275
+
2276
+ #[test]
2277
+ fn free_string_null_is_noop() {
2278
+ // SAFETY: NULL is documented to be safe.
2279
+ unsafe { literllm_free_string(std::ptr::null_mut()) };
2280
+ }
2281
+
2282
+ #[test]
2283
+ fn chat_null_client_returns_null() {
2284
+ let req = CString::new("{}").unwrap();
2285
+ // SAFETY: NULL client is documented to return NULL + set error.
2286
+ let result = unsafe { literllm_chat(std::ptr::null(), req.as_ptr()) };
2287
+ assert!(result.is_null());
2288
+ let err = literllm_last_error();
2289
+ assert!(!err.is_null());
2290
+ }
2291
+
2292
+ #[test]
2293
+ fn chat_null_request_returns_null() {
2294
+ let api_key = CString::new("test-key").unwrap();
2295
+ // SAFETY: api_key is valid; base_url and model_hint are NULL.
2296
+ let client = unsafe { literllm_client_new(api_key.as_ptr(), std::ptr::null(), std::ptr::null()) };
2297
+ if client.is_null() {
2298
+ return; // skip if construction failed
2299
+ }
2300
+ // SAFETY: client is valid; request_json is NULL (should return NULL + error).
2301
+ let result = unsafe { literllm_chat(client, std::ptr::null()) };
2302
+ assert!(result.is_null());
2303
+ let err = literllm_last_error();
2304
+ assert!(!err.is_null());
2305
+ // SAFETY: client was returned by literllm_client_new.
2306
+ unsafe { literllm_client_free(client) };
2307
+ }
2308
+
2309
+ #[test]
2310
+ fn embed_null_client_returns_null() {
2311
+ let req = CString::new("{}").unwrap();
2312
+ // SAFETY: NULL client is documented to return NULL + set error.
2313
+ let result = unsafe { literllm_embed(std::ptr::null(), req.as_ptr()) };
2314
+ assert!(result.is_null());
2315
+ }
2316
+
2317
+ #[test]
2318
+ fn list_models_null_client_returns_null() {
2319
+ // SAFETY: NULL client is documented to return NULL + set error.
2320
+ let result = unsafe { literllm_list_models(std::ptr::null()) };
2321
+ assert!(result.is_null());
2322
+ let err = literllm_last_error();
2323
+ assert!(!err.is_null());
2324
+ }
2325
+
2326
+ #[test]
2327
+ fn register_provider_null_json_returns_error() {
2328
+ // SAFETY: NULL is documented to return -1 + set error.
2329
+ let result = unsafe { literllm_register_provider(std::ptr::null()) };
2330
+ assert_eq!(result, -1);
2331
+ let err = literllm_last_error();
2332
+ assert!(!err.is_null());
2333
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2334
+ assert!(msg.contains("NULL"));
2335
+ }
2336
+
2337
+ #[test]
2338
+ fn register_provider_invalid_json_returns_error() {
2339
+ let json = CString::new("not valid json").unwrap();
2340
+ // SAFETY: json is a valid NUL-terminated string.
2341
+ let result = unsafe { literllm_register_provider(json.as_ptr()) };
2342
+ assert_eq!(result, -1);
2343
+ let err = literllm_last_error();
2344
+ assert!(!err.is_null());
2345
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2346
+ assert!(msg.contains("parse"));
2347
+ }
2348
+
2349
+ #[test]
2350
+ fn register_and_unregister_provider_via_ffi() {
2351
+ let json = CString::new(
2352
+ r#"{"name":"ffi-test","base_url":"https://example.com/v1","auth_header":"Bearer","model_prefixes":["ffi-test/"]}"#,
2353
+ )
2354
+ .unwrap();
2355
+ // SAFETY: json is a valid NUL-terminated string.
2356
+ let result = unsafe { literllm_register_provider(json.as_ptr()) };
2357
+ assert_eq!(result, 0, "registration should succeed");
2358
+
2359
+ let name = CString::new("ffi-test").unwrap();
2360
+ // SAFETY: name is a valid NUL-terminated string.
2361
+ let result = unsafe { literllm_unregister_provider(name.as_ptr()) };
2362
+ assert_eq!(result, 0, "unregister should return 0 (found and removed)");
2363
+
2364
+ // Unregister again — should return 1 (not found).
2365
+ let result = unsafe { literllm_unregister_provider(name.as_ptr()) };
2366
+ assert_eq!(result, 1, "unregister should return 1 (not found)");
2367
+ }
2368
+
2369
+ #[test]
2370
+ fn unregister_provider_null_name_returns_error() {
2371
+ // SAFETY: NULL is documented to return -1 + set error.
2372
+ let result = unsafe { literllm_unregister_provider(std::ptr::null()) };
2373
+ assert_eq!(result, -1);
2374
+ let err = literllm_last_error();
2375
+ assert!(!err.is_null());
2376
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2377
+ assert!(msg.contains("NULL"));
2378
+ }
2379
+
2380
+ #[test]
2381
+ fn client_new_with_config_null_returns_null() {
2382
+ // SAFETY: NULL config_json is documented to return NULL + set error.
2383
+ let client = unsafe { literllm_client_new_with_config(std::ptr::null()) };
2384
+ assert!(client.is_null());
2385
+ let err = literllm_last_error();
2386
+ assert!(!err.is_null());
2387
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2388
+ assert!(msg.contains("NULL"));
2389
+ }
2390
+
2391
+ #[test]
2392
+ fn client_new_with_config_invalid_json_returns_null() {
2393
+ let json = CString::new("not valid json").unwrap();
2394
+ // SAFETY: json is a valid NUL-terminated string.
2395
+ let client = unsafe { literllm_client_new_with_config(json.as_ptr()) };
2396
+ assert!(client.is_null());
2397
+ let err = literllm_last_error();
2398
+ assert!(!err.is_null());
2399
+ let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
2400
+ assert!(msg.contains("parse"));
2401
+ }
2402
+
2403
+ #[test]
2404
+ fn client_new_with_config_minimal() {
2405
+ let json = CString::new(r#"{"api_key":"test-key"}"#).unwrap();
2406
+ // SAFETY: json is a valid NUL-terminated JSON string.
2407
+ let client = unsafe { literllm_client_new_with_config(json.as_ptr()) };
2408
+ // Construction may succeed or fail depending on reqwest availability.
2409
+ if !client.is_null() {
2410
+ // SAFETY: client was returned by literllm_client_new_with_config.
2411
+ unsafe { literllm_client_free(client) };
2412
+ }
2413
+ }
2414
+
2415
+ #[test]
2416
+ fn client_new_with_config_full() {
2417
+ let json = CString::new(
2418
+ r#"{
2419
+ "api_key": "test-key",
2420
+ "base_url": "https://example.com/v1",
2421
+ "model_hint": "custom/model",
2422
+ "max_retries": 5,
2423
+ "timeout_secs": 120,
2424
+ "extra_headers": {"X-Custom": "value"},
2425
+ "cache": {"max_entries": 100, "ttl_secs": 60},
2426
+ "budget": {"global_limit": 10.0, "enforcement": "soft"}
2427
+ }"#,
2428
+ )
2429
+ .unwrap();
2430
+ // SAFETY: json is a valid NUL-terminated JSON string.
2431
+ let client = unsafe { literllm_client_new_with_config(json.as_ptr()) };
2432
+ if !client.is_null() {
2433
+ // SAFETY: client was returned by literllm_client_new_with_config.
2434
+ unsafe { literllm_client_free(client) };
2435
+ }
2436
+ }
2437
+
2438
+ #[test]
2439
+ fn set_hooks_null_client_returns_error() {
2440
+ let callbacks = LiterLlmHookCallbacks {
2441
+ on_request: None,
2442
+ on_response: None,
2443
+ on_error: None,
2444
+ user_data: std::ptr::null_mut(),
2445
+ };
2446
+ // SAFETY: NULL client is documented to return -1 + set error.
2447
+ let result = unsafe { literllm_set_hooks(std::ptr::null_mut(), &callbacks) };
2448
+ assert_eq!(result, -1);
2449
+ let err = literllm_last_error();
2450
+ assert!(!err.is_null());
2451
+ }
2452
+
2453
+ #[test]
2454
+ fn set_hooks_null_callbacks_returns_error() {
2455
+ let api_key = CString::new("test-key").unwrap();
2456
+ // SAFETY: api_key is a valid NUL-terminated string.
2457
+ let client = unsafe { literllm_client_new(api_key.as_ptr(), std::ptr::null(), std::ptr::null()) };
2458
+ if client.is_null() {
2459
+ return; // skip if construction failed
2460
+ }
2461
+ // SAFETY: client is valid; callbacks is NULL (should return -1).
2462
+ let result = unsafe { literllm_set_hooks(client, std::ptr::null()) };
2463
+ assert_eq!(result, -1);
2464
+ // SAFETY: client was returned by literllm_client_new.
2465
+ unsafe { literllm_client_free(client) };
2466
+ }
2467
+
2468
+ #[test]
2469
+ fn set_hooks_with_valid_client_succeeds() {
2470
+ let api_key = CString::new("test-key").unwrap();
2471
+ // SAFETY: api_key is a valid NUL-terminated string.
2472
+ let client = unsafe { literllm_client_new(api_key.as_ptr(), std::ptr::null(), std::ptr::null()) };
2473
+ if client.is_null() {
2474
+ return; // skip if construction failed
2475
+ }
2476
+ let callbacks = LiterLlmHookCallbacks {
2477
+ on_request: None,
2478
+ on_response: None,
2479
+ on_error: None,
2480
+ user_data: std::ptr::null_mut(),
2481
+ };
2482
+ // SAFETY: both pointers are valid.
2483
+ let result = unsafe { literllm_set_hooks(client, &callbacks) };
2484
+ assert_eq!(result, 0, "set_hooks should succeed with valid client");
2485
+ // SAFETY: client was returned by literllm_client_new.
2486
+ unsafe { literllm_client_free(client) };
2487
+ }
2488
+ }