1use crate::{
8 PluginApi, ProviderRequestFn, ProviderStreamControl, ProviderStreamEmitFn,
9 ProviderStreamRequestFn, StbString, StbStringRef,
10};
11use rpi_ai::{
12 AssistantMessage, AssistantMessageEvent, Context, Model, Provider, SimpleStreamOptions,
13 StopReason,
14};
15use std::{ffi::c_void, sync::Arc, time::Duration};
16
17mod context;
18pub use context::{drive_with_context, register_with_context, ContextFactory, ProviderContext};
19
20pub type Factory = fn(&Model, &SimpleStreamOptions) -> Arc<dyn Provider>;
21
22pub fn completions(
26 model: &Model,
27 options: &SimpleStreamOptions,
28 anonymous: bool,
29) -> Result<Arc<dyn Provider>, String> {
30 let client = rpi_ai::http::build_client(&model.base_url, None, None)?;
31 if anonymous && options.api_key.is_none() {
32 Ok(Arc::new(rpi_ai::providers::openai_completions::OpenAiCompletionsProvider::with_models_without_auth(model.provider.clone(), client, vec![model.clone()])))
33 } else {
34 Ok(Arc::new(rpi_ai::providers::openai_completions::OpenAiCompletionsProvider::with_models_without_env_api_key(model.provider.clone(), options.api_key.clone(), client, vec![model.clone()])))
35 }
36}
37
38pub fn register(
40 api: &PluginApi,
41 id: &str,
42 base_url: &str,
43 style: &str,
44 manifest: serde_json::Value,
45 request: ProviderRequestFn,
46 stream: ProviderStreamRequestFn,
47) -> i32 {
48 if let Some(declare) = api.declare {
49 let json = serde_json::json!({"providers":[manifest]}).to_string();
50 let rc = declare(StbStringRef::from_str(&json));
51 if rc != 0 {
52 return rc;
53 }
54 }
55 let free = crate::callbacks::free_string;
56 if let Some(register) = api.register_streaming_provider {
57 register(
58 StbStringRef::from_str(id),
59 StbStringRef::from_str(base_url),
60 StbStringRef::from_str(style),
61 stream,
62 free,
63 std::ptr::null_mut(),
64 )
65 } else if let Some(register) = api.register_provider {
66 register(
67 StbStringRef::from_str(id),
68 StbStringRef::from_str(base_url),
69 StbStringRef::from_str(style),
70 request,
71 free,
72 std::ptr::null_mut(),
73 )
74 } else {
75 1
76 }
77}
78
79#[derive(serde::Deserialize)]
80struct Request {
81 model: Model,
82 context: Context,
83 options: serde_json::Value,
84}
85
86fn options(value: &serde_json::Value) -> SimpleStreamOptions {
87 let mut options = SimpleStreamOptions::default();
88 options.api_key = value["apiKey"].as_str().map(str::to_string);
89 options.timeout = value["timeoutMs"].as_u64().map(Duration::from_millis);
90 options.stream_idle_timeout = value["streamIdleTimeoutMs"]
91 .as_u64()
92 .map(Duration::from_millis);
93 options.max_retry_delay = value["maxRetryDelayMs"].as_u64().map(Duration::from_millis);
94 options.max_retries = value["maxRetries"].as_u64().and_then(|n| n.try_into().ok());
95 options.max_tokens = value["maxTokens"].as_u64();
96 options.temperature = value["temperature"].as_f64();
97 options.session_id = value["sessionId"].as_str().map(str::to_string);
98 options.headers = serde_json::from_value(value["headers"].clone()).ok();
99 options.metadata = serde_json::from_value(value["metadata"].clone()).ok();
100 options.reasoning = value["reasoning"]
101 .as_str()
102 .and_then(|s| serde_json::from_value(serde_json::json!(s.to_ascii_lowercase())).ok());
103 options.thinking_budgets = serde_json::from_value(value["thinkingBudgets"].clone()).ok();
104 options.cache_retention = match value["cacheRetention"].as_str() {
105 Some("None") => rpi_ai::CacheRetention::None,
106 Some("Long") => rpi_ai::CacheRetention::Long,
107 _ => rpi_ai::CacheRetention::Short,
108 };
109 options
110}
111
112fn reconstructs_text(
114 previous: &AssistantMessage,
115 next: &AssistantMessage,
116 index: usize,
117 delta: &str,
118) -> bool {
119 let AssistantMessage {
120 role,
121 content,
122 api,
123 provider,
124 model,
125 response_model,
126 response_id,
127 usage,
128 stop_reason,
129 deferred,
130 error_message,
131 raw_stop_reason,
132 end_turn,
133 timestamp,
134 } = previous;
135 if role != &next.role
136 || api != &next.api
137 || provider != &next.provider
138 || model != &next.model
139 || response_model != &next.response_model
140 || response_id != &next.response_id
141 || usage != &next.usage
142 || stop_reason != &next.stop_reason
143 || deferred != &next.deferred
144 || error_message != &next.error_message
145 || raw_stop_reason != &next.raw_stop_reason
146 || end_turn != &next.end_turn
147 || timestamp != &next.timestamp
148 || content.len() != next.content.len()
149 {
150 return false;
151 }
152 let (Some(rpi_ai::Content::Text(before)), Some(rpi_ai::Content::Text(after))) =
153 (content.get(index), next.content.get(index))
154 else {
155 return false;
156 };
157 let rpi_ai::TextContent {
158 kind,
159 text,
160 text_signature,
161 } = before;
162 kind == &after.kind
163 && text_signature == &after.text_signature
164 && after.text.strip_prefix(text) == Some(delta)
165 && content
166 .iter()
167 .zip(&next.content)
168 .enumerate()
169 .all(|(i, (a, b))| i == index || a == b)
170}
171
172fn event_json(
173 event: &AssistantMessageEvent,
174 previous: &mut Option<(usize, Arc<AssistantMessage>)>,
175) -> serde_json::Value {
176 let compact = match (event, previous.as_ref()) {
177 (
178 AssistantMessageEvent::TextDelta {
179 content_index,
180 delta,
181 partial,
182 },
183 Some((index, before)),
184 ) => index == content_index && reconstructs_text(before, partial, *index, delta),
185 _ => false,
186 };
187 *previous = match event {
188 AssistantMessageEvent::TextDelta {
189 content_index,
190 partial,
191 ..
192 } => Some((*content_index, Arc::clone(partial))),
193 _ => None,
194 };
195 if compact {
196 let AssistantMessageEvent::TextDelta {
197 content_index,
198 delta,
199 ..
200 } = event
201 else {
202 unreachable!()
203 };
204 return serde_json::json!({"type":"text_delta","contentIndex":content_index,"delta":delta});
205 }
206 let mut chunk = serde_json::to_value(event).expect("provider event JSON");
207 let tool = chunk.get("toolCall").cloned().or_else(|| {
209 if event.type_tag() != "toolcall_start" {
210 return None;
211 }
212 let index = chunk["contentIndex"].as_u64()? as usize;
213 chunk.get("partial")?.get("content")?.get(index).cloned()
214 });
215 if let Some(tool) = tool {
216 for field in ["id", "name", "arguments"] {
217 if let Some(value) = tool.get(field) {
218 chunk[field] = value.clone();
219 }
220 }
221 }
222 chunk
223}
224
225async fn drive_stream(
226 provider: Arc<dyn Provider>,
227 request: &Request,
228 options: &SimpleStreamOptions,
229 emit: Option<ProviderStreamEmitFn>,
230 emit_data: *mut c_void,
231 control: Option<&ProviderStreamControl>,
232) -> AssistantMessage {
233 let mut stream = provider
234 .stream_simple(&request.model, &request.context, options)
235 .await;
236 let mut partial = Arc::new(AssistantMessage::empty(
237 request.model.api.clone(),
238 request.model.provider.clone(),
239 request.model.id.clone(),
240 0,
241 ));
242 let mut previous_text = None;
243 let mut poll = tokio::time::interval(Duration::from_millis(10));
244 loop {
245 if options.signal.is_cancelled()
248 || control.is_some_and(|control| (control.is_cancelled)(control.user_data, 0) != 0)
249 {
250 options.signal.cancel();
251 let mut partial = (*partial).clone();
252 partial.stop_reason = StopReason::Aborted;
253 partial.error_message = Some("provider request was cancelled".into());
254 break partial;
255 }
256 tokio::select! {
257 biased;
258 _ = poll.tick() => {}
259 event = stream.next() => {
260 let Some(event) = event else {
261 break stream.result().await.unwrap_or_else(|error| AssistantMessage::terminal(
262 request.model.api.clone(), request.model.provider.clone(), request.model.id.clone(), StopReason::Error, error.to_string(), 0));
263 };
264 match &event {
265 AssistantMessageEvent::Done { message, .. } => break message.clone(),
266 AssistantMessageEvent::Error { error, .. } => break error.clone(),
267 _ => {},
268 }
269 partial = match &event {
270 AssistantMessageEvent::Start { partial }
271 | AssistantMessageEvent::TextStart { partial, .. }
272 | AssistantMessageEvent::TextDelta { partial, .. }
273 | AssistantMessageEvent::TextEnd { partial, .. }
274 | AssistantMessageEvent::ThinkingStart { partial, .. }
275 | AssistantMessageEvent::ThinkingDelta { partial, .. }
276 | AssistantMessageEvent::ThinkingEnd { partial, .. }
277 | AssistantMessageEvent::ToolCallStart { partial, .. }
278 | AssistantMessageEvent::ToolCallDelta { partial, .. }
279 | AssistantMessageEvent::ToolCallEnd { partial, .. } => Arc::clone(partial),
280 AssistantMessageEvent::Done { .. } | AssistantMessageEvent::Error { .. } => unreachable!(),
281 };
282 let Some(emit) = emit else { continue; };
283 let chunk = event_json(&event, &mut previous_text);
284 if emit(StbString::from_string(chunk.to_string()), emit_data) != 0 {
285 options.signal.cancel();
286 let mut partial = (*partial).clone();
287 partial.stop_reason = StopReason::Aborted;
288 break partial;
289 }
290 }
291 }
292 }
293}
294
295pub unsafe fn drive(
301 factory: Factory,
302 request: StbStringRef,
303 emit: Option<ProviderStreamEmitFn>,
304 emit_data: *mut c_void,
305 control: *const ProviderStreamControl,
306 out: *mut StbString,
307) -> i32 {
308 crate::guard_or(1, || {
309 if out.is_null() {
310 return 2;
311 }
312 let Ok(request) = serde_json::from_str::<Request>(unsafe { request.as_str() }) else {
313 return 2;
314 };
315 let options = options(&request.options);
316 let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
317 .enable_all()
318 .build()
319 else {
320 return 1;
321 };
322 let control = unsafe { control.as_ref() };
323 let message = runtime.block_on(async {
324 let provider = factory(&request.model, &options);
325 drive_stream(provider, &request, &options, emit, emit_data, control).await
326 });
327 let json = serde_json::to_value(message)
329 .expect("assistant message JSON")
330 .to_string();
331 unsafe {
332 *out = StbString::from_string(json);
333 }
334 0
335 })
336}
337
338#[macro_export]
344macro_rules! export_provider {
345 ($id:expr, $base:expr, $api:expr, $manifest:expr, $factory:path) => {
346 extern "C" fn request(
347 req: $crate::StbStringRef,
348 out: *mut $crate::StbString,
349 _: *mut std::ffi::c_void,
350 ) -> i32 {
351 unsafe {
352 $crate::provider::drive(
353 $factory,
354 req,
355 None,
356 std::ptr::null_mut(),
357 std::ptr::null(),
358 out,
359 )
360 }
361 }
362 extern "C" fn stream(
363 req: $crate::StbStringRef,
364 emit: Option<$crate::ProviderStreamEmitFn>,
365 data: *mut std::ffi::c_void,
366 control: *const $crate::ProviderStreamControl,
367 _: Option<$crate::ProviderStreamCompletionFn>,
368 _: *mut std::ffi::c_void,
369 out: *mut $crate::StbString,
370 _: *mut std::ffi::c_void,
371 ) -> i32 {
372 unsafe { $crate::provider::drive($factory, req, emit, data, control, out) }
373 }
374 $crate::export_plugin!(|api| $crate::provider::register(
375 api, $id, $base, $api, $manifest, request, stream
376 ));
377 };
378}
379
380#[cfg(test)]
381mod bridge_tests;
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 fn factory(_: &Model, _: &SimpleStreamOptions) -> Arc<dyn Provider> {
387 rpi_ai::providers::faux::FauxProvider::new(
388 rpi_ai::providers::faux::FauxScript::new()
389 .with_tool_call("echo", serde_json::json!({"value":"hello"})),
390 )
391 }
392 extern "C" fn collect(raw: StbString, data: *mut c_void) -> i32 {
393 let string = unsafe {
394 crate::callbacks::OwnedString::from_raw(raw, Some(crate::callbacks::free_string))
395 };
396 let value = serde_json::from_str(&string.to_string_lossy()).unwrap();
397 unsafe { &mut *data.cast::<Vec<serde_json::Value>>() }.push(value);
399 0
400 }
401 fn request() -> String {
402 serde_json::json!({"model":Model::new("m","M",rpi_ai::Api::Faux,"probe","http://localhost"),
403 "context":Context::new(vec![]),"options":{}})
404 .to_string()
405 }
406 #[test]
407 fn transport_construction_errors_return_terminal_output_with_or_without_emit() {
408 fn factory(
409 _: &ProviderContext,
410 model: &Model,
411 options: &SimpleStreamOptions,
412 ) -> Result<Arc<dyn Provider>, String> {
413 completions(model, options, false)
414 }
415 let model = Model::new(
416 "m",
417 "M",
418 rpi_ai::Api::OpenaiCompletions,
419 "probe",
420 ":invalid",
421 );
422 assert!(completions(&model, &SimpleStreamOptions::default(), false).is_err());
423 let request =
424 serde_json::json!({"model":model, "context":Context::new(vec![]), "options":{}})
425 .to_string();
426 for with_emit in [false, true] {
427 let mut chunks = Vec::<serde_json::Value>::new();
428 let mut out = StbString::empty();
429 assert_eq!(
430 unsafe {
431 drive_with_context(
432 factory,
433 std::ptr::null_mut(),
434 StbStringRef::from_str(&request),
435 with_emit.then_some(collect as ProviderStreamEmitFn),
436 (&mut chunks as *mut Vec<serde_json::Value>).cast(),
437 std::ptr::null(),
438 &mut out,
439 )
440 },
441 0
442 );
443 let output = unsafe {
444 crate::callbacks::OwnedString::from_raw(out, Some(crate::callbacks::free_string))
445 };
446 let message: AssistantMessage =
447 serde_json::from_str(&output.to_string_lossy()).unwrap();
448 assert_eq!(message.stop_reason, StopReason::Error);
449 assert!(message
450 .error_message
451 .as_deref()
452 .unwrap()
453 .contains("invalid target URL"));
454 assert!(chunks.is_empty(), "terminal errors use the output slot");
455 }
456 }
457
458 #[test]
459 fn bridge_emits_tool_identity_and_valid_terminal_json() {
460 let mut chunks = Vec::<serde_json::Value>::new();
461 let mut out = StbString::empty();
462 let rc = unsafe {
463 drive(
464 factory,
465 StbStringRef::from_str(&request()),
466 Some(collect),
467 (&mut chunks as *mut Vec<serde_json::Value>).cast(),
468 std::ptr::null(),
469 &mut out,
470 )
471 };
472 assert_eq!(rc, 0);
473 let output = unsafe {
474 crate::callbacks::OwnedString::from_raw(out, Some(crate::callbacks::free_string))
475 };
476 let message: AssistantMessage = serde_json::from_str(&output.to_string_lossy()).unwrap();
477 assert_eq!(message.stop_reason, StopReason::ToolUse);
478 for kind in ["toolcall_start", "toolcall_end"] {
479 let chunk = chunks
480 .iter()
481 .find(|chunk| chunk["type"] == kind)
482 .expect(kind);
483 assert_eq!(chunk["name"], "echo");
484 assert!(!chunk["id"].as_str().unwrap().is_empty());
485 }
486 let end = chunks
487 .iter()
488 .find(|chunk| chunk["type"] == "toolcall_end")
489 .unwrap();
490 assert_eq!(end["arguments"]["value"], "hello");
491 assert!(!chunks
492 .iter()
493 .any(|chunk| chunk["type"] == "done" || chunk["type"] == "error"));
494 }
495 #[test]
496 fn bridge_honors_the_host_control_surface() {
497 extern "C" fn cancelled(_: *mut c_void, _: u32) -> i32 {
498 1
499 }
500 let control = ProviderStreamControl {
501 is_cancelled: cancelled,
502 wait_cancelled_ms: cancelled,
503 user_data: std::ptr::null_mut(),
504 };
505 let mut out = StbString::empty();
506 assert_eq!(
507 unsafe {
508 drive(
509 factory,
510 StbStringRef::from_str(&request()),
511 None,
512 std::ptr::null_mut(),
513 &control,
514 &mut out,
515 )
516 },
517 0
518 );
519 let output = unsafe {
520 crate::callbacks::OwnedString::from_raw(out, Some(crate::callbacks::free_string))
521 };
522 let message: AssistantMessage = serde_json::from_str(&output.to_string_lossy()).unwrap();
523 assert_eq!(message.stop_reason, StopReason::Aborted);
524 }
525 #[test]
526 fn options_preserve_provider_controls() {
527 let options = options(
528 &serde_json::json!({"apiKey":"key","timeoutMs":42,"streamIdleTimeoutMs":11,
529 "maxRetries":3,"maxRetryDelayMs":17,"reasoning":"Xhigh","maxTokens":80,
530 "headers":{"x-custom":"value"},"cacheRetention":"Long","sessionId":"s"}),
531 );
532 assert_eq!(options.api_key.as_deref(), Some("key"));
533 assert_eq!(options.timeout, Some(Duration::from_millis(42)));
534 assert_eq!(options.stream_idle_timeout, Some(Duration::from_millis(11)));
535 assert_eq!(options.max_retries, Some(3));
536 assert_eq!(options.max_retry_delay, Some(Duration::from_millis(17)));
537 assert_eq!(options.reasoning, Some(rpi_ai::ThinkingLevel::Xhigh));
538 assert_eq!(options.max_tokens, Some(80));
539 assert_eq!(options.session_id.as_deref(), Some("s"));
540 assert_eq!(options.headers.unwrap()["x-custom"], "value");
541 }
542}