liter_llm 1.2.0 → 1.2.2

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.
@@ -0,0 +1,519 @@
1
+ use std::borrow::Cow;
2
+ use std::collections::hash_map::DefaultHasher;
3
+ use std::hash::{Hash, Hasher};
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ use crate::provider::Provider;
7
+
8
+ /// Default base URL for the GitHub Copilot API.
9
+ pub const DEFAULT_API_BASE: &str = "https://api.githubcopilot.com";
10
+
11
+ /// Environment variable used to override the Copilot API base URL.
12
+ const API_BASE_ENV_VAR: &str = "GITHUB_COPILOT_API_BASE";
13
+
14
+ /// Model name prefix used for routing.
15
+ const MODEL_PREFIX: &str = "github_copilot/";
16
+
17
+ // ── Version constants ────────────────────────────────────────────────────────
18
+
19
+ const COPILOT_VERSION: &str = "0.26.7";
20
+ const EDITOR_VERSION: &str = "vscode/1.95.0";
21
+ const API_VERSION: &str = "2025-04-01";
22
+
23
+ // ── Static headers ───────────────────────────────────────────────────────────
24
+
25
+ /// Static headers required by the GitHub Copilot API on every request.
26
+ ///
27
+ /// These identify the client as VS Code Chat to Copilot's backend and must
28
+ /// appear on all requests regardless of the model or payload.
29
+ static COPILOT_EXTRA_HEADERS: &[(&str, &str)] = &[
30
+ ("copilot-integration-id", "vscode-chat"),
31
+ ("editor-version", EDITOR_VERSION),
32
+ ("editor-plugin-version", "copilot-chat/0.26.7"),
33
+ ("user-agent", "GitHubCopilotChat/0.26.7"),
34
+ ("openai-intent", "conversation-panel"),
35
+ ("x-github-api-version", API_VERSION),
36
+ ("x-vscode-user-agent-library-version", "electron-fetch"),
37
+ ];
38
+
39
+ // Suppress the unused-constant warning: the values are embedded via string
40
+ // interpolation in the static slice above; Rust does not track that reference.
41
+ const _: () = {
42
+ let _ = COPILOT_VERSION;
43
+ };
44
+
45
+ // ── UUID generation (no external deps) ──────────────────────────────────────
46
+
47
+ /// Generate a pseudo-random UUID v4 string without requiring the `uuid` crate.
48
+ ///
49
+ /// Uses the current nanosecond timestamp hashed with the thread ID to produce
50
+ /// 128 bits of pseudo-entropy, then formats them in standard UUID v4 layout
51
+ /// with the version (`4xxx`) and variant (`8xxx`–`Bxxx`) bits set correctly.
52
+ ///
53
+ /// This is not cryptographically random, but it is sufficiently unique for
54
+ /// per-request tracing headers where only collision avoidance matters.
55
+ fn generate_request_id() -> String {
56
+ let nanos = SystemTime::now()
57
+ .duration_since(UNIX_EPOCH)
58
+ .map(|d| d.as_nanos())
59
+ .unwrap_or(0);
60
+
61
+ let thread_id = std::thread::current().id();
62
+
63
+ let mut hasher = DefaultHasher::new();
64
+ nanos.hash(&mut hasher);
65
+ thread_id.hash(&mut hasher);
66
+ let h1 = hasher.finish();
67
+
68
+ // A second hash pass adds entropy independent of the first 64-bit word.
69
+ h1.hash(&mut hasher);
70
+ let h2 = hasher.finish();
71
+
72
+ let b1 = h1.to_le_bytes();
73
+ let b2 = h2.to_le_bytes();
74
+
75
+ // UUID v4 layout:
76
+ // time_low (32 bits) : b1[0..4]
77
+ // time_mid (16 bits) : b1[4..6]
78
+ // version + hi (16 bits) : "4" + b1[6..8] masked to 12 bits
79
+ // variant + clock (16 bits) : 0x8000 | (b2[0..2] & 0x3FFF)
80
+ // node (48 bits) : b2[2..8]
81
+ let time_low = u32::from_le_bytes([b1[0], b1[1], b1[2], b1[3]]);
82
+ let time_mid = u16::from_le_bytes([b1[4], b1[5]]);
83
+ let version_hi = u16::from_le_bytes([b1[6], b1[7]]) & 0x0FFF;
84
+ let variant_clock = (u16::from_le_bytes([b2[0], b2[1]]) & 0x3FFF) | 0x8000;
85
+ let node = u64::from_le_bytes([b2[2], b2[3], b2[4], b2[5], b2[6], b2[7], 0, 0]) & 0x0000_FFFF_FFFF_FFFF;
86
+
87
+ format!("{time_low:08x}-{time_mid:04x}-4{version_hi:03x}-{variant_clock:04x}-{node:012x}")
88
+ }
89
+
90
+ // ── Provider ─────────────────────────────────────────────────────────────────
91
+
92
+ /// GitHub Copilot provider.
93
+ ///
94
+ /// GitHub Copilot exposes an OpenAI-compatible chat completions API at
95
+ /// `https://api.githubcopilot.com` (or a dynamic URL returned during the
96
+ /// OAuth token exchange). The request/response format is identical to
97
+ /// OpenAI's, so no `transform_request` or `transform_response` overrides are
98
+ /// needed.
99
+ ///
100
+ /// Key differences from a vanilla OpenAI provider:
101
+ /// - Several static headers identify the caller as VS Code Chat.
102
+ /// - Each request carries a unique `x-request-id` UUID.
103
+ /// - An `X-Initiator` header signals whether the turn was started by the user
104
+ /// or by an agent (tool/assistant message in the context).
105
+ /// - The base URL can be overridden at construction time (Copilot's OAuth flow
106
+ /// returns a dynamic endpoint URL in the token response).
107
+ ///
108
+ /// # Construction
109
+ ///
110
+ /// ```rust,ignore
111
+ /// // Use the default base URL.
112
+ /// let provider = GithubCopilotProvider::new();
113
+ ///
114
+ /// // Override the base URL (e.g. from the OAuth token exchange).
115
+ /// let provider = GithubCopilotProvider::with_api_base("https://proxy.example.com".to_owned());
116
+ ///
117
+ /// // Read the base URL from the GITHUB_COPILOT_API_BASE environment variable.
118
+ /// let provider = GithubCopilotProvider::from_env();
119
+ /// ```
120
+ pub struct GithubCopilotProvider {
121
+ /// Base URL for the Copilot API. Defaults to [`DEFAULT_API_BASE`].
122
+ api_base: String,
123
+ }
124
+
125
+ impl GithubCopilotProvider {
126
+ /// Create a provider using the default Copilot API base URL.
127
+ #[must_use]
128
+ pub fn new() -> Self {
129
+ Self {
130
+ api_base: DEFAULT_API_BASE.to_owned(),
131
+ }
132
+ }
133
+
134
+ /// Create a provider with a custom base URL.
135
+ ///
136
+ /// Use this when the Copilot OAuth token exchange returns a dynamic
137
+ /// endpoint URL that differs from the default.
138
+ #[must_use]
139
+ #[allow(dead_code)] // public API — used by callers that obtain a dynamic URL from the OAuth flow
140
+ pub fn with_api_base(base: String) -> Self {
141
+ Self { api_base: base }
142
+ }
143
+
144
+ /// Create a provider by reading the base URL from the
145
+ /// `GITHUB_COPILOT_API_BASE` environment variable.
146
+ ///
147
+ /// Falls back to [`DEFAULT_API_BASE`] when the variable is unset or empty.
148
+ #[must_use]
149
+ pub fn from_env() -> Self {
150
+ let api_base = std::env::var(API_BASE_ENV_VAR)
151
+ .ok()
152
+ .filter(|s| !s.is_empty())
153
+ .unwrap_or_else(|| DEFAULT_API_BASE.to_owned());
154
+
155
+ Self { api_base }
156
+ }
157
+ }
158
+
159
+ impl Default for GithubCopilotProvider {
160
+ fn default() -> Self {
161
+ Self::new()
162
+ }
163
+ }
164
+
165
+ impl Provider for GithubCopilotProvider {
166
+ fn name(&self) -> &str {
167
+ "github_copilot"
168
+ }
169
+
170
+ fn base_url(&self) -> &str {
171
+ &self.api_base
172
+ }
173
+
174
+ fn auth_header<'a>(&'a self, api_key: &'a str) -> Option<(Cow<'static, str>, Cow<'a, str>)> {
175
+ Some((Cow::Borrowed("Authorization"), Cow::Owned(format!("Bearer {api_key}"))))
176
+ }
177
+
178
+ /// Static headers required by the GitHub Copilot API on every request.
179
+ ///
180
+ /// These identify the VS Code Chat client to Copilot's backend.
181
+ fn extra_headers(&self) -> &'static [(&'static str, &'static str)] {
182
+ COPILOT_EXTRA_HEADERS
183
+ }
184
+
185
+ /// Compute per-request dynamic headers.
186
+ ///
187
+ /// Two headers are generated here:
188
+ ///
189
+ /// - `x-request-id`: A fresh pseudo-random UUID v4 generated for every
190
+ /// call, used by Copilot's backend for distributed tracing.
191
+ ///
192
+ /// - `X-Initiator`: Either `"agent"` (when the conversation context
193
+ /// contains any message with role `"tool"` or `"assistant"`, indicating
194
+ /// an ongoing agentic turn) or `"user"` (for a fresh human-initiated
195
+ /// turn). Copilot uses this to apply different rate-limiting and
196
+ /// routing policies.
197
+ fn dynamic_headers(&self, body: &serde_json::Value) -> Vec<(String, String)> {
198
+ let request_id = generate_request_id();
199
+
200
+ let initiator = determine_initiator(body);
201
+
202
+ vec![
203
+ ("x-request-id".to_owned(), request_id),
204
+ ("X-Initiator".to_owned(), initiator.to_owned()),
205
+ ]
206
+ }
207
+
208
+ fn matches_model(&self, model: &str) -> bool {
209
+ model.starts_with(MODEL_PREFIX)
210
+ }
211
+
212
+ fn strip_model_prefix<'m>(&self, model: &'m str) -> &'m str {
213
+ model.strip_prefix(MODEL_PREFIX).unwrap_or(model)
214
+ }
215
+
216
+ // All endpoint paths use OpenAI-compatible defaults; no overrides needed.
217
+ }
218
+
219
+ /// Determine the `X-Initiator` value from the request body.
220
+ ///
221
+ /// Returns `"agent"` when any message in `body["messages"]` has a role of
222
+ /// `"tool"` or `"assistant"`, which indicates that the turn is part of an
223
+ /// ongoing multi-step agentic interaction. Returns `"user"` otherwise.
224
+ fn determine_initiator(body: &serde_json::Value) -> &'static str {
225
+ let messages = match body.get("messages").and_then(|m| m.as_array()) {
226
+ Some(msgs) => msgs,
227
+ None => return "user",
228
+ };
229
+
230
+ for message in messages {
231
+ let role = message.get("role").and_then(|r| r.as_str()).unwrap_or("");
232
+ if role == "tool" || role == "assistant" {
233
+ return "agent";
234
+ }
235
+ }
236
+
237
+ "user"
238
+ }
239
+
240
+ // ── Tests ─────────────────────────────────────────────────────────────────────
241
+
242
+ #[cfg(test)]
243
+ mod tests {
244
+ use serde_json::json;
245
+ use serial_test::serial;
246
+
247
+ use super::*;
248
+
249
+ // ── Basic provider metadata ──────────────────────────────────────────────
250
+
251
+ #[test]
252
+ fn test_name() {
253
+ let provider = GithubCopilotProvider::new();
254
+ assert_eq!(provider.name(), "github_copilot");
255
+ }
256
+
257
+ #[test]
258
+ fn test_base_url_default() {
259
+ let provider = GithubCopilotProvider::new();
260
+ assert_eq!(provider.base_url(), "https://api.githubcopilot.com");
261
+ }
262
+
263
+ #[test]
264
+ fn test_base_url_custom() {
265
+ let provider = GithubCopilotProvider::with_api_base("https://proxy.example.com".to_owned());
266
+ assert_eq!(provider.base_url(), "https://proxy.example.com");
267
+ }
268
+
269
+ // ── from_env ─────────────────────────────────────────────────────────────
270
+
271
+ #[test]
272
+ #[serial]
273
+ fn test_from_env_uses_default_when_unset() {
274
+ // Ensure the variable is not set for this test.
275
+ // SAFETY: tests in this group are serialised via #[serial]; no concurrent env access.
276
+ unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
277
+ let provider = GithubCopilotProvider::from_env();
278
+ assert_eq!(provider.base_url(), DEFAULT_API_BASE);
279
+ }
280
+
281
+ #[test]
282
+ #[serial]
283
+ fn test_from_env_reads_custom_base_url() {
284
+ // SAFETY: tests in this group are serialised via #[serial]; no concurrent env access.
285
+ unsafe { std::env::set_var("GITHUB_COPILOT_API_BASE", "https://custom.copilot.test") };
286
+ let provider = GithubCopilotProvider::from_env();
287
+ // SAFETY: tests in this group are serialised via #[serial]; no concurrent env access.
288
+ unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
289
+ assert_eq!(provider.base_url(), "https://custom.copilot.test");
290
+ }
291
+
292
+ #[test]
293
+ #[serial]
294
+ fn test_from_env_falls_back_on_empty_value() {
295
+ // SAFETY: tests in this group are serialised via #[serial]; no concurrent env access.
296
+ unsafe { std::env::set_var("GITHUB_COPILOT_API_BASE", "") };
297
+ let provider = GithubCopilotProvider::from_env();
298
+ // SAFETY: tests in this group are serialised via #[serial]; no concurrent env access.
299
+ unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
300
+ assert_eq!(provider.base_url(), DEFAULT_API_BASE);
301
+ }
302
+
303
+ // ── Model routing ────────────────────────────────────────────────────────
304
+
305
+ #[test]
306
+ fn test_matches_model() {
307
+ let provider = GithubCopilotProvider::new();
308
+ assert!(provider.matches_model("github_copilot/gpt-4o"));
309
+ assert!(provider.matches_model("github_copilot/claude-3.5-sonnet"));
310
+ assert!(provider.matches_model("github_copilot/o3-mini"));
311
+ }
312
+
313
+ #[test]
314
+ fn test_does_not_match_other_providers() {
315
+ let provider = GithubCopilotProvider::new();
316
+ assert!(!provider.matches_model("openai/gpt-4o"));
317
+ assert!(!provider.matches_model("gpt-4o"));
318
+ assert!(!provider.matches_model("claude-3.5-sonnet"));
319
+ assert!(!provider.matches_model("anthropic/claude-3.5-sonnet"));
320
+ }
321
+
322
+ #[test]
323
+ fn test_strip_model_prefix() {
324
+ let provider = GithubCopilotProvider::new();
325
+ assert_eq!(provider.strip_model_prefix("github_copilot/gpt-4o"), "gpt-4o");
326
+ }
327
+
328
+ #[test]
329
+ fn test_strip_model_prefix_no_prefix() {
330
+ let provider = GithubCopilotProvider::new();
331
+ assert_eq!(provider.strip_model_prefix("gpt-4o"), "gpt-4o");
332
+ }
333
+
334
+ // ── Auth header ──────────────────────────────────────────────────────────
335
+
336
+ #[test]
337
+ fn test_auth_header() {
338
+ let provider = GithubCopilotProvider::new();
339
+ let (name, value) = provider
340
+ .auth_header("ghs_test_token_123")
341
+ .expect("should return an auth header");
342
+ assert_eq!(name, "Authorization");
343
+ assert_eq!(value, "Bearer ghs_test_token_123");
344
+ }
345
+
346
+ // ── Static extra headers ─────────────────────────────────────────────────
347
+
348
+ #[test]
349
+ fn test_extra_headers() {
350
+ let provider = GithubCopilotProvider::new();
351
+ let headers = provider.extra_headers();
352
+
353
+ // All 7 required static headers must be present.
354
+ let find = |key: &str| headers.iter().find(|(k, _)| *k == key).map(|(_, v)| *v);
355
+
356
+ assert_eq!(find("copilot-integration-id"), Some("vscode-chat"));
357
+ assert_eq!(find("editor-version"), Some("vscode/1.95.0"));
358
+ assert_eq!(find("editor-plugin-version"), Some("copilot-chat/0.26.7"));
359
+ assert_eq!(find("user-agent"), Some("GitHubCopilotChat/0.26.7"));
360
+ assert_eq!(find("openai-intent"), Some("conversation-panel"));
361
+ assert_eq!(find("x-github-api-version"), Some("2025-04-01"));
362
+ assert_eq!(find("x-vscode-user-agent-library-version"), Some("electron-fetch"));
363
+
364
+ assert_eq!(headers.len(), 7, "expected exactly 7 static headers");
365
+ }
366
+
367
+ // ── Dynamic headers ──────────────────────────────────────────────────────
368
+
369
+ #[test]
370
+ fn test_dynamic_headers_user() {
371
+ let provider = GithubCopilotProvider::new();
372
+ let body = json!({
373
+ "model": "github_copilot/gpt-4o",
374
+ "messages": [
375
+ {"role": "user", "content": "Hello!"}
376
+ ]
377
+ });
378
+
379
+ let headers = provider.dynamic_headers(&body);
380
+ let initiator = headers
381
+ .iter()
382
+ .find(|(k, _)| k == "X-Initiator")
383
+ .map(|(_, v)| v.as_str());
384
+
385
+ assert_eq!(initiator, Some("user"));
386
+ }
387
+
388
+ #[test]
389
+ fn test_dynamic_headers_agent_with_tool_role() {
390
+ let provider = GithubCopilotProvider::new();
391
+ let body = json!({
392
+ "model": "github_copilot/gpt-4o",
393
+ "messages": [
394
+ {"role": "user", "content": "Run the tool."},
395
+ {"role": "assistant", "content": null, "tool_calls": []},
396
+ {"role": "tool", "content": "tool result", "tool_call_id": "abc"}
397
+ ]
398
+ });
399
+
400
+ let headers = provider.dynamic_headers(&body);
401
+ let initiator = headers
402
+ .iter()
403
+ .find(|(k, _)| k == "X-Initiator")
404
+ .map(|(_, v)| v.as_str());
405
+
406
+ assert_eq!(initiator, Some("agent"));
407
+ }
408
+
409
+ #[test]
410
+ fn test_dynamic_headers_agent_with_assistant_role() {
411
+ let provider = GithubCopilotProvider::new();
412
+ let body = json!({
413
+ "messages": [
414
+ {"role": "user", "content": "Hi"},
415
+ {"role": "assistant", "content": "Hello"}
416
+ ]
417
+ });
418
+
419
+ let headers = provider.dynamic_headers(&body);
420
+ let initiator = headers
421
+ .iter()
422
+ .find(|(k, _)| k == "X-Initiator")
423
+ .map(|(_, v)| v.as_str());
424
+
425
+ assert_eq!(initiator, Some("agent"));
426
+ }
427
+
428
+ #[test]
429
+ fn test_dynamic_headers_user_when_no_messages() {
430
+ let provider = GithubCopilotProvider::new();
431
+ let body = json!({ "model": "github_copilot/gpt-4o" });
432
+
433
+ let headers = provider.dynamic_headers(&body);
434
+ let initiator = headers
435
+ .iter()
436
+ .find(|(k, _)| k == "X-Initiator")
437
+ .map(|(_, v)| v.as_str());
438
+
439
+ assert_eq!(initiator, Some("user"));
440
+ }
441
+
442
+ #[test]
443
+ fn test_dynamic_headers_request_id_present_and_valid_uuid() {
444
+ let provider = GithubCopilotProvider::new();
445
+ let body = json!({ "messages": [{"role": "user", "content": "hi"}] });
446
+
447
+ let headers = provider.dynamic_headers(&body);
448
+ let request_id = headers
449
+ .iter()
450
+ .find(|(k, _)| k == "x-request-id")
451
+ .map(|(_, v)| v.as_str())
452
+ .expect("x-request-id header must be present");
453
+
454
+ // UUID v4 canonical form: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
455
+ // Length check: 8-4-4-4-12 = 32 hex + 4 dashes = 36 chars.
456
+ assert_eq!(request_id.len(), 36, "request id must be 36 characters");
457
+
458
+ let parts: Vec<&str> = request_id.split('-').collect();
459
+ assert_eq!(parts.len(), 5, "UUID must have 5 dash-separated groups");
460
+ assert_eq!(parts[0].len(), 8);
461
+ assert_eq!(parts[1].len(), 4);
462
+ assert_eq!(parts[2].len(), 4);
463
+ assert_eq!(parts[3].len(), 4);
464
+ assert_eq!(parts[4].len(), 12);
465
+
466
+ // Version nibble must be '4'.
467
+ assert_eq!(&parts[2][0..1], "4", "third group must start with '4' (UUID version 4)");
468
+
469
+ // Variant bits: first nibble of fourth group must be 8, 9, a, or b.
470
+ let variant_nibble = parts[3].chars().next().expect("fourth group is non-empty");
471
+ assert!(
472
+ matches!(variant_nibble, '8' | '9' | 'a' | 'b'),
473
+ "fourth group must start with 8, 9, a or b (RFC 4122 variant); got '{variant_nibble}'"
474
+ );
475
+
476
+ // All characters must be valid lowercase hex or dashes.
477
+ for ch in request_id.chars() {
478
+ assert!(
479
+ ch.is_ascii_hexdigit() || ch == '-',
480
+ "unexpected character '{ch}' in request id"
481
+ );
482
+ }
483
+ }
484
+
485
+ #[test]
486
+ fn test_dynamic_headers_request_id_unique_per_call() {
487
+ let provider = GithubCopilotProvider::new();
488
+ let body = json!({ "messages": [{"role": "user", "content": "hi"}] });
489
+
490
+ let id1 = provider
491
+ .dynamic_headers(&body)
492
+ .into_iter()
493
+ .find(|(k, _)| k == "x-request-id")
494
+ .map(|(_, v)| v)
495
+ .expect("x-request-id must be present");
496
+
497
+ // Inject a small delay so the nanosecond timestamp differs.
498
+ std::thread::sleep(std::time::Duration::from_nanos(100));
499
+
500
+ let id2 = provider
501
+ .dynamic_headers(&body)
502
+ .into_iter()
503
+ .find(|(k, _)| k == "x-request-id")
504
+ .map(|(_, v)| v)
505
+ .expect("x-request-id must be present");
506
+
507
+ assert_ne!(id1, id2, "consecutive request IDs must differ");
508
+ }
509
+
510
+ // ── Endpoint paths (OpenAI defaults) ─────────────────────────────────────
511
+
512
+ #[test]
513
+ fn test_endpoint_paths_are_openai_compatible() {
514
+ let provider = GithubCopilotProvider::new();
515
+ assert_eq!(provider.chat_completions_path(), "/chat/completions");
516
+ assert_eq!(provider.embeddings_path(), "/embeddings");
517
+ assert_eq!(provider.models_path(), "/models");
518
+ }
519
+ }
@@ -336,6 +336,7 @@ pub mod azure;
336
336
  pub mod bedrock;
337
337
  pub mod cohere;
338
338
  pub mod custom;
339
+ pub mod github_copilot;
339
340
  pub mod google_ai;
340
341
  pub mod mistral;
341
342
  pub mod vertex;
@@ -561,6 +562,11 @@ pub fn detect_provider(model: &str) -> Option<Box<dyn Provider>> {
561
562
  return Some(Box::new(mistral::MistralProvider));
562
563
  }
563
564
 
565
+ // 9. GitHub Copilot: "github_copilot/" prefix.
566
+ if model.starts_with("github_copilot/") {
567
+ return Some(Box::new(github_copilot::GithubCopilotProvider::from_env()));
568
+ }
569
+
564
570
  // Grab the registry; if it failed to parse we cannot route.
565
571
  let reg = match REGISTRY.as_ref() {
566
572
  Ok(r) => r,
@@ -8,6 +8,7 @@ pub mod image;
8
8
  pub mod models;
9
9
  pub mod moderation;
10
10
  pub mod ocr;
11
+ pub mod raw;
11
12
  pub mod rerank;
12
13
  pub mod responses;
13
14
  pub mod search;
@@ -22,6 +23,7 @@ pub use image::*;
22
23
  pub use models::*;
23
24
  pub use moderation::*;
24
25
  pub use ocr::*;
26
+ pub use raw::*;
25
27
  pub use rerank::*;
26
28
  pub use responses::*;
27
29
  pub use search::*;
@@ -0,0 +1,29 @@
1
+ /// The raw request and response JSON exchanged with the provider,
2
+ /// paired with the typed (normalized) response.
3
+ ///
4
+ /// Returned by every `_raw` method on [`crate::LlmClientRaw`]. Useful for
5
+ /// debugging provider-specific transformations or implementing custom parsing.
6
+ #[derive(Debug, Clone)]
7
+ pub struct RawExchange<T> {
8
+ /// The typed, normalized response.
9
+ pub data: T,
10
+ /// The final request body sent to the provider (after `transform_request`).
11
+ pub raw_request: serde_json::Value,
12
+ /// The raw response body from the provider, before `transform_response`.
13
+ /// `None` for binary endpoints (speech) or when not applicable.
14
+ pub raw_response: Option<serde_json::Value>,
15
+ }
16
+
17
+ /// Raw exchange data for streaming responses.
18
+ ///
19
+ /// Returned by [`crate::LlmClientRaw::chat_stream_raw`]. The stream itself is
20
+ /// not captured in its entirety — only the request body is available upfront.
21
+ /// `RawStreamExchange` intentionally does not implement `Clone` because streams
22
+ /// cannot be duplicated.
23
+ #[derive(Debug)]
24
+ pub struct RawStreamExchange<S> {
25
+ /// The chunk stream, unchanged.
26
+ pub stream: S,
27
+ /// The final request body sent to the provider.
28
+ pub raw_request: serde_json::Value,
29
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "liter-llm-ffi"
3
- version = "1.2.0"
3
+ version = "1.2.2"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  repository.workspace = true
@@ -20,8 +20,8 @@ default = []
20
20
  base64.workspace = true
21
21
  bytes.workspace = true
22
22
  futures-core.workspace = true
23
- liter-llm = { path = "../liter-llm", version = "1.2.0", features = ["full"] }
24
- liter-llm-bindings-core = { path = "../liter-llm-bindings-core", version = "1.2.0" }
23
+ liter-llm = { path = "../liter-llm", version = "1.2.2", features = ["full"] }
24
+ liter-llm-bindings-core = { path = "../liter-llm-bindings-core", version = "1.2.2" }
25
25
  serde.workspace = true
26
26
  serde_json.workspace = true
27
27
  tokio.workspace = true
@@ -9,8 +9,8 @@
9
9
 
10
10
  #define LITER_LLM_VERSION_MAJOR 1
11
11
  #define LITER_LLM_VERSION_MINOR 2
12
- #define LITER_LLM_VERSION_PATCH 0
13
- #define LITER_LLM_VERSION "1.2.0"
12
+ #define LITER_LLM_VERSION_PATCH 2
13
+ #define LITER_LLM_VERSION "1.2.2"
14
14
 
15
15
 
16
16
  #include <stdarg.h>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liter_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Na'aman Hirschfeld
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-07 00:00:00.000000000 Z
11
+ date: 2026-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -196,6 +196,7 @@ files:
196
196
  - vendor/liter-llm/schemas/providers.json
197
197
  - vendor/liter-llm/src/auth/azure_ad.rs
198
198
  - vendor/liter-llm/src/auth/bedrock_sts.rs
199
+ - vendor/liter-llm/src/auth/github_copilot.rs
199
200
  - vendor/liter-llm/src/auth/mod.rs
200
201
  - vendor/liter-llm/src/auth/vertex_oauth.rs
201
202
  - vendor/liter-llm/src/client/config.rs
@@ -215,6 +216,7 @@ files:
215
216
  - vendor/liter-llm/src/provider/bedrock.rs
216
217
  - vendor/liter-llm/src/provider/cohere.rs
217
218
  - vendor/liter-llm/src/provider/custom.rs
219
+ - vendor/liter-llm/src/provider/github_copilot.rs
218
220
  - vendor/liter-llm/src/provider/google_ai.rs
219
221
  - vendor/liter-llm/src/provider/mistral.rs
220
222
  - vendor/liter-llm/src/provider/mod.rs
@@ -248,6 +250,7 @@ files:
248
250
  - vendor/liter-llm/src/types/models.rs
249
251
  - vendor/liter-llm/src/types/moderation.rs
250
252
  - vendor/liter-llm/src/types/ocr.rs
253
+ - vendor/liter-llm/src/types/raw.rs
251
254
  - vendor/liter-llm/src/types/rerank.rs
252
255
  - vendor/liter-llm/src/types/responses.rs
253
256
  - vendor/liter-llm/src/types/search.rs