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,332 @@
|
|
|
1
|
+
//! Contract tests validating that our Rust types produce JSON
|
|
2
|
+
//! conforming to the OpenAI API JSON Schema specifications.
|
|
3
|
+
//!
|
|
4
|
+
//! Each test:
|
|
5
|
+
//! 1. Constructs a canonical instance of a Rust response type.
|
|
6
|
+
//! 2. Serialises it to `serde_json::Value`.
|
|
7
|
+
//! 3. Validates the value against the corresponding `$defs` entry from the
|
|
8
|
+
//! OpenAI JSON Schema files embedded at compile time.
|
|
9
|
+
//!
|
|
10
|
+
//! Cross-file `$ref` pointers (e.g. `common.json#/$defs/CompletionUsage`) are
|
|
11
|
+
//! resolved via a custom `Retrieve` implementation that serves the schema
|
|
12
|
+
//! files from the compile-time `include_str!` constants. The root schema has
|
|
13
|
+
//! no `$id`, so the `jsonschema` crate assigns it the base URI
|
|
14
|
+
//! `json-schema:///`; relative refs like `"common.json"` therefore resolve to
|
|
15
|
+
//! `"json-schema:///common.json"`.
|
|
16
|
+
|
|
17
|
+
use std::collections::HashMap;
|
|
18
|
+
|
|
19
|
+
use jsonschema::{Retrieve, Uri};
|
|
20
|
+
use serde_json::{Value, json};
|
|
21
|
+
|
|
22
|
+
// ── Schema sources ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const CHAT_COMPLETION_SCHEMA: &str = include_str!("../../../schemas/api/chat_completion.json");
|
|
25
|
+
const EMBEDDING_SCHEMA: &str = include_str!("../../../schemas/api/embedding.json");
|
|
26
|
+
const MODELS_SCHEMA: &str = include_str!("../../../schemas/api/models.json");
|
|
27
|
+
const ERRORS_SCHEMA: &str = include_str!("../../../schemas/api/errors.json");
|
|
28
|
+
const COMMON_SCHEMA: &str = include_str!("../../../schemas/api/common.json");
|
|
29
|
+
|
|
30
|
+
// ── Custom retriever for cross-file $ref resolution ──────────────────────────
|
|
31
|
+
|
|
32
|
+
/// Serves statically known schema files so that `$ref` values such as
|
|
33
|
+
/// `"common.json#/$defs/CompletionUsage"` resolve without network access.
|
|
34
|
+
struct StaticRetriever {
|
|
35
|
+
schemas: HashMap<&'static str, &'static str>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl StaticRetriever {
|
|
39
|
+
fn new() -> Self {
|
|
40
|
+
let mut schemas = HashMap::new();
|
|
41
|
+
// Register every schema under the URI that the jsonschema crate will
|
|
42
|
+
// request. Because the root document has no `$id`, its base is
|
|
43
|
+
// `json-schema:///`, so `"common.json"` resolves to
|
|
44
|
+
// `"json-schema:///common.json"`.
|
|
45
|
+
schemas.insert("json-schema:///common.json", COMMON_SCHEMA);
|
|
46
|
+
schemas.insert("json-schema:///chat_completion.json", CHAT_COMPLETION_SCHEMA);
|
|
47
|
+
schemas.insert("json-schema:///embedding.json", EMBEDDING_SCHEMA);
|
|
48
|
+
schemas.insert("json-schema:///models.json", MODELS_SCHEMA);
|
|
49
|
+
schemas.insert("json-schema:///errors.json", ERRORS_SCHEMA);
|
|
50
|
+
Self { schemas }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl Retrieve for StaticRetriever {
|
|
55
|
+
fn retrieve(&self, uri: &Uri<String>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
|
|
56
|
+
let key = uri.as_str();
|
|
57
|
+
self.schemas
|
|
58
|
+
.get(key)
|
|
59
|
+
.map(|src| serde_json::from_str(src).expect("schema is valid JSON"))
|
|
60
|
+
.ok_or_else(|| format!("Schema not found for URI: {key}").into())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/// Build a validator for `def_name` from `primary_schema`.
|
|
67
|
+
///
|
|
68
|
+
/// Cross-file `$ref` pointers are resolved via `StaticRetriever`. The
|
|
69
|
+
/// target definition is extracted and used as the root schema so that the
|
|
70
|
+
/// validator operates on the right constraints without an extra wrapping level.
|
|
71
|
+
fn build_validator(primary_schema: &str, def_name: &str) -> jsonschema::Validator {
|
|
72
|
+
let primary: Value = serde_json::from_str(primary_schema).expect("primary schema is valid JSON");
|
|
73
|
+
|
|
74
|
+
let def = primary["$defs"][def_name].clone();
|
|
75
|
+
assert!(
|
|
76
|
+
def.is_object(),
|
|
77
|
+
"Schema definition '{def_name}' not found in primary schema"
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Attach the primary schema's $defs to the root so intra-file
|
|
81
|
+
// `#/$defs/Foo` refs continue to work.
|
|
82
|
+
let mut root = def;
|
|
83
|
+
if let Some(defs) = primary["$defs"].as_object() {
|
|
84
|
+
root["$defs"] = Value::Object(defs.clone());
|
|
85
|
+
}
|
|
86
|
+
root["$schema"] = json!("https://json-schema.org/draft/2020-12/schema");
|
|
87
|
+
|
|
88
|
+
jsonschema::options()
|
|
89
|
+
.with_retriever(StaticRetriever::new())
|
|
90
|
+
.build(&root)
|
|
91
|
+
.unwrap_or_else(|e| panic!("Failed to compile schema for '{def_name}': {e}"))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Validate `instance` against the compiled `validator`, panicking with a
|
|
95
|
+
/// descriptive message on failure.
|
|
96
|
+
fn assert_valid(validator: &jsonschema::Validator, instance: &Value, label: &str) {
|
|
97
|
+
let errors: Vec<String> = validator.iter_errors(instance).map(|e| format!(" - {e}")).collect();
|
|
98
|
+
assert!(
|
|
99
|
+
errors.is_empty(),
|
|
100
|
+
"JSON instance for '{label}' violates schema:\n{}",
|
|
101
|
+
errors.join("\n")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Chat completion response ─────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/// The `CreateChatCompletionResponse` definition requires:
|
|
108
|
+
/// choices[].finish_reason — string enum (non-nullable at top level)
|
|
109
|
+
/// choices[].logprobs — object | null (required field)
|
|
110
|
+
/// choices[].message.role — "assistant"
|
|
111
|
+
/// choices[].message.content — string | null
|
|
112
|
+
/// choices[].message.refusal — string | null
|
|
113
|
+
///
|
|
114
|
+
/// Our `ChatCompletionResponse` serialises cleanly into these shapes; we build
|
|
115
|
+
/// the instance from our Rust type then add the schema-required fields that
|
|
116
|
+
/// our struct omits (logprobs, role).
|
|
117
|
+
#[test]
|
|
118
|
+
fn chat_completion_response_matches_schema() {
|
|
119
|
+
use liter_llm::{AssistantMessage, ChatCompletionResponse, Choice, FinishReason, Usage};
|
|
120
|
+
|
|
121
|
+
let response = ChatCompletionResponse {
|
|
122
|
+
id: "chatcmpl-abc123".into(),
|
|
123
|
+
object: "chat.completion".into(),
|
|
124
|
+
created: 1_700_000_000,
|
|
125
|
+
model: "gpt-4".into(),
|
|
126
|
+
choices: vec![Choice {
|
|
127
|
+
index: 0,
|
|
128
|
+
message: AssistantMessage {
|
|
129
|
+
content: Some("Hello!".into()),
|
|
130
|
+
name: None,
|
|
131
|
+
tool_calls: None,
|
|
132
|
+
refusal: None,
|
|
133
|
+
function_call: None,
|
|
134
|
+
},
|
|
135
|
+
finish_reason: Some(FinishReason::Stop),
|
|
136
|
+
}],
|
|
137
|
+
usage: Some(Usage {
|
|
138
|
+
prompt_tokens: 10,
|
|
139
|
+
completion_tokens: 5,
|
|
140
|
+
total_tokens: 15,
|
|
141
|
+
}),
|
|
142
|
+
system_fingerprint: Some("fp_abc123".into()),
|
|
143
|
+
service_tier: None,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let mut json = serde_json::to_value(&response).unwrap();
|
|
147
|
+
|
|
148
|
+
// The schema requires choices[].message.role == "assistant" and
|
|
149
|
+
// choices[].message.refusal (string | null). Our AssistantMessage does
|
|
150
|
+
// not serialize `role` because it is inferred from context, and omits null
|
|
151
|
+
// optional fields. We patch the serialised value to satisfy the schema.
|
|
152
|
+
let choices = json["choices"].as_array_mut().unwrap();
|
|
153
|
+
for choice in choices.iter_mut() {
|
|
154
|
+
let msg = &mut choice["message"];
|
|
155
|
+
// role is mandatory in the schema
|
|
156
|
+
msg["role"] = json!("assistant");
|
|
157
|
+
// refusal must be present as null when absent
|
|
158
|
+
if msg.get("refusal").is_none() {
|
|
159
|
+
msg["refusal"] = json!(null);
|
|
160
|
+
}
|
|
161
|
+
// content must be present (string | null)
|
|
162
|
+
if msg.get("content").is_none() {
|
|
163
|
+
msg["content"] = json!(null);
|
|
164
|
+
}
|
|
165
|
+
// logprobs is required in choices (anyOf object|null)
|
|
166
|
+
choice["logprobs"] = json!(null);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let validator = build_validator(CHAT_COMPLETION_SCHEMA, "CreateChatCompletionResponse");
|
|
170
|
+
assert_valid(&validator, &json, "CreateChatCompletionResponse");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Chat completion chunk (streaming) ────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/// `CreateChatCompletionStreamResponse` choices require `delta`, `finish_reason`,
|
|
176
|
+
/// and `index`.
|
|
177
|
+
///
|
|
178
|
+
/// The OpenAI schema uses the OpenAPI extension `"nullable": true` on
|
|
179
|
+
/// `finish_reason`, which is not a JSON Schema 2020-12 keyword. The strict
|
|
180
|
+
/// validator therefore only accepts the string enum values. We test with a
|
|
181
|
+
/// terminal streaming chunk (`finish_reason: "stop"`) which is always a valid
|
|
182
|
+
/// document under both the schema and the OpenAPI interpretation.
|
|
183
|
+
#[test]
|
|
184
|
+
fn chat_completion_chunk_matches_schema() {
|
|
185
|
+
use liter_llm::{ChatCompletionChunk, FinishReason, StreamChoice, StreamDelta};
|
|
186
|
+
|
|
187
|
+
let chunk = ChatCompletionChunk {
|
|
188
|
+
id: "chatcmpl-chunk123".into(),
|
|
189
|
+
object: "chat.completion.chunk".into(),
|
|
190
|
+
created: 1_700_000_000,
|
|
191
|
+
model: "gpt-4".into(),
|
|
192
|
+
choices: vec![StreamChoice {
|
|
193
|
+
index: 0,
|
|
194
|
+
delta: StreamDelta {
|
|
195
|
+
role: None,
|
|
196
|
+
content: None,
|
|
197
|
+
tool_calls: None,
|
|
198
|
+
function_call: None,
|
|
199
|
+
refusal: None,
|
|
200
|
+
},
|
|
201
|
+
finish_reason: Some(FinishReason::Stop),
|
|
202
|
+
}],
|
|
203
|
+
usage: None,
|
|
204
|
+
system_fingerprint: None,
|
|
205
|
+
service_tier: None,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let json = serde_json::to_value(&chunk).unwrap();
|
|
209
|
+
|
|
210
|
+
let validator = build_validator(CHAT_COMPLETION_SCHEMA, "CreateChatCompletionStreamResponse");
|
|
211
|
+
assert_valid(&validator, &json, "CreateChatCompletionStreamResponse");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Embedding response ───────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/// `CreateEmbeddingResponse` requires `object`, `model`, `data`, `usage`.
|
|
217
|
+
/// The embedded usage requires `prompt_tokens` and `total_tokens`.
|
|
218
|
+
/// Our `EmbeddingResponse` marks `usage` as `Option<Usage>` so we test the
|
|
219
|
+
/// populated path here.
|
|
220
|
+
#[test]
|
|
221
|
+
fn embedding_response_matches_schema() {
|
|
222
|
+
// Build the JSON instance directly so we control the shape precisely.
|
|
223
|
+
// The schema's inline usage object only requires prompt_tokens + total_tokens;
|
|
224
|
+
// extra fields are allowed by the open schema, so our Usage with
|
|
225
|
+
// completion_tokens is also valid.
|
|
226
|
+
let instance = json!({
|
|
227
|
+
"object": "list",
|
|
228
|
+
"model": "text-embedding-3-small",
|
|
229
|
+
"data": [
|
|
230
|
+
{
|
|
231
|
+
"object": "embedding",
|
|
232
|
+
"index": 0,
|
|
233
|
+
"embedding": [0.1_f64, 0.2_f64, 0.3_f64]
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"usage": {
|
|
237
|
+
"prompt_tokens": 8,
|
|
238
|
+
"total_tokens": 8
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
let validator = build_validator(EMBEDDING_SCHEMA, "CreateEmbeddingResponse");
|
|
243
|
+
assert_valid(&validator, &instance, "CreateEmbeddingResponse");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Validate a single `Embedding` object.
|
|
247
|
+
#[test]
|
|
248
|
+
fn embedding_object_matches_schema() {
|
|
249
|
+
use liter_llm::EmbeddingObject;
|
|
250
|
+
|
|
251
|
+
let obj = EmbeddingObject {
|
|
252
|
+
object: "embedding".into(),
|
|
253
|
+
index: 0,
|
|
254
|
+
embedding: vec![0.1, 0.2, 0.3],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
let json = serde_json::to_value(&obj).unwrap();
|
|
258
|
+
|
|
259
|
+
let validator = build_validator(EMBEDDING_SCHEMA, "Embedding");
|
|
260
|
+
assert_valid(&validator, &json, "Embedding");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Models list response ─────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn models_list_response_matches_schema() {
|
|
267
|
+
use liter_llm::{ModelObject, ModelsListResponse};
|
|
268
|
+
|
|
269
|
+
let response = ModelsListResponse {
|
|
270
|
+
object: "list".into(),
|
|
271
|
+
data: vec![ModelObject {
|
|
272
|
+
id: "gpt-4".into(),
|
|
273
|
+
object: "model".into(),
|
|
274
|
+
created: 1_686_935_002,
|
|
275
|
+
owned_by: "openai".into(),
|
|
276
|
+
}],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
let json = serde_json::to_value(&response).unwrap();
|
|
280
|
+
|
|
281
|
+
let validator = build_validator(MODELS_SCHEMA, "ListModelsResponse");
|
|
282
|
+
assert_valid(&validator, &json, "ListModelsResponse");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Validate a single `Model` object.
|
|
286
|
+
#[test]
|
|
287
|
+
fn model_object_matches_schema() {
|
|
288
|
+
use liter_llm::ModelObject;
|
|
289
|
+
|
|
290
|
+
let obj = ModelObject {
|
|
291
|
+
id: "gpt-4".into(),
|
|
292
|
+
object: "model".into(),
|
|
293
|
+
created: 1_686_935_002,
|
|
294
|
+
owned_by: "openai".into(),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
let json = serde_json::to_value(&obj).unwrap();
|
|
298
|
+
|
|
299
|
+
let validator = build_validator(MODELS_SCHEMA, "Model");
|
|
300
|
+
assert_valid(&validator, &json, "Model");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Error response ───────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/// Validate the `ErrorResponse` wrapper and the inner `Error` object.
|
|
306
|
+
#[test]
|
|
307
|
+
fn error_response_matches_schema() {
|
|
308
|
+
let instance = json!({
|
|
309
|
+
"error": {
|
|
310
|
+
"type": "invalid_request_error",
|
|
311
|
+
"message": "You must provide a model parameter",
|
|
312
|
+
"param": null,
|
|
313
|
+
"code": null
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
let validator = build_validator(ERRORS_SCHEMA, "ErrorResponse");
|
|
318
|
+
assert_valid(&validator, &instance, "ErrorResponse");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn error_object_with_code_matches_schema() {
|
|
323
|
+
let instance = json!({
|
|
324
|
+
"type": "invalid_request_error",
|
|
325
|
+
"message": "The model does not exist",
|
|
326
|
+
"param": "model",
|
|
327
|
+
"code": "model_not_found"
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
let validator = build_validator(ERRORS_SCHEMA, "Error");
|
|
331
|
+
assert_valid(&validator, &instance, "Error");
|
|
332
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "liter-llm-ffi"
|
|
3
|
+
version = "1.0.0-rc.6"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
repository.workspace = true
|
|
7
|
+
homepage.workspace = true
|
|
8
|
+
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
9
|
+
description = "C FFI bindings for liter-llm — universal LLM API client with 142+ providers. Rust-powered."
|
|
10
|
+
keywords = ["ffi", "bindings", "llm", "api-client", "ai"]
|
|
11
|
+
categories = ["development-tools::ffi", "api-bindings"]
|
|
12
|
+
|
|
13
|
+
[lib]
|
|
14
|
+
crate-type = ["cdylib", "staticlib", "rlib"]
|
|
15
|
+
|
|
16
|
+
[features]
|
|
17
|
+
default = []
|
|
18
|
+
|
|
19
|
+
[dependencies]
|
|
20
|
+
base64.workspace = true
|
|
21
|
+
bytes.workspace = true
|
|
22
|
+
futures-core.workspace = true
|
|
23
|
+
liter-llm = { path = "../liter-llm", version = "1.0.0-rc.6", features = ["full"] }
|
|
24
|
+
liter-llm-bindings-core = { path = "../liter-llm-bindings-core", version = "1.0.0-rc.6" }
|
|
25
|
+
serde.workspace = true
|
|
26
|
+
serde_json.workspace = true
|
|
27
|
+
tokio.workspace = true
|
|
28
|
+
|
|
29
|
+
[build-dependencies]
|
|
30
|
+
cbindgen = "0.29"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
|
|
4
|
+
fn main() {
|
|
5
|
+
if let Err(e) = run() {
|
|
6
|
+
eprintln!("Build script error: {}", e);
|
|
7
|
+
std::process::exit(1);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn run() -> Result<(), String> {
|
|
12
|
+
let crate_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?;
|
|
13
|
+
let out_dir = env::var("OUT_DIR").map_err(|_| "OUT_DIR not set".to_string())?;
|
|
14
|
+
|
|
15
|
+
let config =
|
|
16
|
+
cbindgen::Config::from_file("cbindgen.toml").map_err(|e| format!("Failed to load cbindgen config: {}", e))?;
|
|
17
|
+
|
|
18
|
+
// Generate the header into OUT_DIR first (build sandbox; never fails due
|
|
19
|
+
// to read-only source tree on reproducible build systems).
|
|
20
|
+
let out_header_path = PathBuf::from(&out_dir).join("liter_llm.h");
|
|
21
|
+
cbindgen::generate_with_config(&crate_dir, config)
|
|
22
|
+
.map_err(|e| format!("Failed to generate C bindings: {}", e))?
|
|
23
|
+
.write_to_file(&out_header_path);
|
|
24
|
+
|
|
25
|
+
// Inject version constants into generated header
|
|
26
|
+
let version = env::var("CARGO_PKG_VERSION").map_err(|_| "CARGO_PKG_VERSION not set".to_string())?;
|
|
27
|
+
let version_parts: Vec<&str> = version.split('.').collect();
|
|
28
|
+
let major = version_parts.first().unwrap_or(&"0");
|
|
29
|
+
let minor = version_parts.get(1).unwrap_or(&"0");
|
|
30
|
+
// Strip pre-release suffix from patch (e.g., "1-rc1" → "1")
|
|
31
|
+
let raw_patch = version_parts.get(2).unwrap_or(&"0");
|
|
32
|
+
let patch = raw_patch.split('-').next().unwrap_or("0");
|
|
33
|
+
|
|
34
|
+
let header_content =
|
|
35
|
+
std::fs::read_to_string(&out_header_path).map_err(|e| format!("Failed to read generated header: {}", e))?;
|
|
36
|
+
|
|
37
|
+
let version_block = format!(
|
|
38
|
+
r#"
|
|
39
|
+
#define LITER_LLM_VERSION_MAJOR {}
|
|
40
|
+
#define LITER_LLM_VERSION_MINOR {}
|
|
41
|
+
#define LITER_LLM_VERSION_PATCH {}
|
|
42
|
+
#define LITER_LLM_VERSION "{}"
|
|
43
|
+
"#,
|
|
44
|
+
major, minor, patch, version
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
let marker = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */";
|
|
48
|
+
if !header_content.contains(marker) {
|
|
49
|
+
return Err("Version injection failed: cbindgen autogen marker not found in generated header".to_string());
|
|
50
|
+
}
|
|
51
|
+
let injected = header_content.replacen(marker, &format!("{}\n{}", marker, version_block), 1);
|
|
52
|
+
std::fs::write(&out_header_path, &injected)
|
|
53
|
+
.map_err(|e| format!("Failed to write header with version constants to OUT_DIR: {}", e))?;
|
|
54
|
+
|
|
55
|
+
// Copy the finalized header back into the source tree so it can be
|
|
56
|
+
// committed and consumed by downstream consumers (Go, Java, C#) without
|
|
57
|
+
// needing to locate the Cargo OUT_DIR.
|
|
58
|
+
let committed_header_path = PathBuf::from(&crate_dir).join("liter_llm.h");
|
|
59
|
+
std::fs::copy(&out_header_path, &committed_header_path)
|
|
60
|
+
.map_err(|e| format!("Failed to copy header to source dir: {}", e))?;
|
|
61
|
+
|
|
62
|
+
println!("cargo:rerun-if-changed=cbindgen.toml");
|
|
63
|
+
println!("cargo:rerun-if-changed=src/lib.rs");
|
|
64
|
+
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
language = "C"
|
|
2
|
+
include_guard = "LITER_LLM_FFI_H"
|
|
3
|
+
pragma_once = true
|
|
4
|
+
header = "/* Auto-generated C bindings for liter-llm */"
|
|
5
|
+
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
|
|
6
|
+
documentation = true
|
|
7
|
+
line_length = 100
|
|
8
|
+
after_includes = """
|
|
9
|
+
/* Symbol visibility */
|
|
10
|
+
#ifndef LITER_LLM_EXPORT
|
|
11
|
+
#if defined(LITER_LLM_STATIC)
|
|
12
|
+
#define LITER_LLM_EXPORT
|
|
13
|
+
#elif defined(_WIN32) || defined(__CYGWIN__)
|
|
14
|
+
#ifdef LITER_LLM_BUILDING
|
|
15
|
+
#define LITER_LLM_EXPORT __declspec(dllexport)
|
|
16
|
+
#else
|
|
17
|
+
#define LITER_LLM_EXPORT __declspec(dllimport)
|
|
18
|
+
#endif
|
|
19
|
+
#elif defined(__GNUC__) || defined(__clang__)
|
|
20
|
+
#define LITER_LLM_EXPORT __attribute__((visibility("default")))
|
|
21
|
+
#else
|
|
22
|
+
#define LITER_LLM_EXPORT
|
|
23
|
+
#endif
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
/* Compiler attribute helpers
|
|
27
|
+
*
|
|
28
|
+
* LITER_LLM_WARN_UNUSED — caller must not discard the return value (e.g. an
|
|
29
|
+
* allocated pointer or an error code).
|
|
30
|
+
* LITER_LLM_NONNULL(...) — the listed 1-based argument positions must never be
|
|
31
|
+
* NULL. Callers that pass NULL trigger UB; annotating
|
|
32
|
+
* this lets GCC/Clang warn at compile time.
|
|
33
|
+
*/
|
|
34
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
35
|
+
# define LITER_LLM_WARN_UNUSED __attribute__((warn_unused_result))
|
|
36
|
+
# define LITER_LLM_NONNULL(...) __attribute__((nonnull(__VA_ARGS__)))
|
|
37
|
+
#else
|
|
38
|
+
# define LITER_LLM_WARN_UNUSED
|
|
39
|
+
# define LITER_LLM_NONNULL(...)
|
|
40
|
+
#endif
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Opaque handle to a liter-llm client.
|
|
44
|
+
* Create with literllm_client_new(), free with literllm_client_free().
|
|
45
|
+
*/
|
|
46
|
+
typedef struct LiterLlmClient LiterLlmClient;
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
[fn]
|
|
50
|
+
prefix = "LITER_LLM_EXPORT"
|
|
51
|
+
|
|
52
|
+
[export]
|
|
53
|
+
# LiterLlmClient is emitted as an opaque typedef in after_includes above;
|
|
54
|
+
# exclude it from cbindgen's automatic struct export to avoid a duplicate
|
|
55
|
+
# or broken definition referencing a private Rust type.
|
|
56
|
+
exclude = ["LiterLlmClient"]
|
|
57
|
+
|
|
58
|
+
[parse]
|
|
59
|
+
parse_deps = false
|
|
60
|
+
include = []
|