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.
- checksums.yaml +7 -0
- data/README.md +239 -0
- data/ext/liter_llm_rb/extconf.rb +65 -0
- data/ext/liter_llm_rb/native/.cargo/config.toml +23 -0
- data/ext/liter_llm_rb/native/Cargo.lock +3713 -0
- data/ext/liter_llm_rb/native/Cargo.toml +32 -0
- data/ext/liter_llm_rb/native/build.rs +15 -0
- data/ext/liter_llm_rb/native/src/lib.rs +1079 -0
- data/lib/liter_llm.rb +8 -0
- data/sig/liter_llm.rbs +416 -0
- data/vendor/Cargo.toml +54 -0
- data/vendor/liter-llm/Cargo.toml +92 -0
- data/vendor/liter-llm/README.md +252 -0
- data/vendor/liter-llm/schemas/pricing.json +40 -0
- data/vendor/liter-llm/schemas/providers.json +1662 -0
- data/vendor/liter-llm/src/auth/azure_ad.rs +264 -0
- data/vendor/liter-llm/src/auth/bedrock_sts.rs +353 -0
- data/vendor/liter-llm/src/auth/mod.rs +68 -0
- data/vendor/liter-llm/src/auth/vertex_oauth.rs +353 -0
- data/vendor/liter-llm/src/client/config.rs +351 -0
- data/vendor/liter-llm/src/client/managed.rs +622 -0
- data/vendor/liter-llm/src/client/mod.rs +864 -0
- data/vendor/liter-llm/src/cost.rs +212 -0
- data/vendor/liter-llm/src/error.rs +190 -0
- data/vendor/liter-llm/src/http/eventstream.rs +860 -0
- data/vendor/liter-llm/src/http/mod.rs +12 -0
- data/vendor/liter-llm/src/http/request.rs +438 -0
- data/vendor/liter-llm/src/http/retry.rs +72 -0
- data/vendor/liter-llm/src/http/streaming.rs +289 -0
- data/vendor/liter-llm/src/lib.rs +37 -0
- data/vendor/liter-llm/src/provider/anthropic.rs +2250 -0
- data/vendor/liter-llm/src/provider/azure.rs +579 -0
- data/vendor/liter-llm/src/provider/bedrock.rs +1543 -0
- data/vendor/liter-llm/src/provider/cohere.rs +654 -0
- data/vendor/liter-llm/src/provider/custom.rs +404 -0
- data/vendor/liter-llm/src/provider/google_ai.rs +281 -0
- data/vendor/liter-llm/src/provider/mistral.rs +188 -0
- data/vendor/liter-llm/src/provider/mod.rs +616 -0
- data/vendor/liter-llm/src/provider/vertex.rs +1504 -0
- data/vendor/liter-llm/src/tests.rs +1425 -0
- data/vendor/liter-llm/src/tokenizer.rs +281 -0
- data/vendor/liter-llm/src/tower/budget.rs +599 -0
- data/vendor/liter-llm/src/tower/cache.rs +502 -0
- data/vendor/liter-llm/src/tower/cache_opendal.rs +270 -0
- data/vendor/liter-llm/src/tower/cooldown.rs +231 -0
- data/vendor/liter-llm/src/tower/cost.rs +404 -0
- data/vendor/liter-llm/src/tower/fallback.rs +121 -0
- data/vendor/liter-llm/src/tower/health.rs +219 -0
- data/vendor/liter-llm/src/tower/hooks.rs +369 -0
- data/vendor/liter-llm/src/tower/mod.rs +77 -0
- data/vendor/liter-llm/src/tower/rate_limit.rs +300 -0
- data/vendor/liter-llm/src/tower/router.rs +436 -0
- data/vendor/liter-llm/src/tower/service.rs +181 -0
- data/vendor/liter-llm/src/tower/tests.rs +539 -0
- data/vendor/liter-llm/src/tower/tests_common.rs +252 -0
- data/vendor/liter-llm/src/tower/tracing.rs +209 -0
- data/vendor/liter-llm/src/tower/types.rs +170 -0
- data/vendor/liter-llm/src/types/audio.rs +52 -0
- data/vendor/liter-llm/src/types/batch.rs +77 -0
- data/vendor/liter-llm/src/types/chat.rs +214 -0
- data/vendor/liter-llm/src/types/common.rs +244 -0
- data/vendor/liter-llm/src/types/embedding.rs +84 -0
- data/vendor/liter-llm/src/types/files.rs +58 -0
- data/vendor/liter-llm/src/types/image.rs +40 -0
- data/vendor/liter-llm/src/types/mod.rs +27 -0
- data/vendor/liter-llm/src/types/models.rs +21 -0
- data/vendor/liter-llm/src/types/moderation.rs +80 -0
- data/vendor/liter-llm/src/types/ocr.rs +87 -0
- data/vendor/liter-llm/src/types/rerank.rs +46 -0
- data/vendor/liter-llm/src/types/responses.rs +55 -0
- data/vendor/liter-llm/src/types/search.rs +45 -0
- data/vendor/liter-llm/tests/contract.rs +332 -0
- data/vendor/liter-llm-ffi/Cargo.toml +30 -0
- data/vendor/liter-llm-ffi/build.rs +66 -0
- data/vendor/liter-llm-ffi/cbindgen.toml +60 -0
- data/vendor/liter-llm-ffi/liter_llm.h +850 -0
- data/vendor/liter-llm-ffi/src/lib.rs +2488 -0
- 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
|
+
}
|