#![forbid(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
use el_core::CredentialRef;
use el_core::{ChatMessage, ChatRequest, ChatToken, LlmProvider};
#[cfg(not(target_arch = "wasm32"))]
uniffi::setup_scaffolding!("el_ffi");
#[cfg(not(target_arch = "wasm32"))]
use flutter_rust_bridge::frb;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Error))]
#[derive(Debug)]
pub enum SdkError {
ProviderError { message: String },
}
impl std::fmt::Display for SdkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self::ProviderError { message } = self;
write!(f, "{message}")
}
}
impl From<el_core::EdgeError> for SdkError {
fn from(e: el_core::EdgeError) -> Self {
Self::ProviderError {
message: e.to_string(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[uniffi::export(callback_interface)]
pub trait StreamHandler: Send + Sync {
fn on_token(&self, token: String);
}
#[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Object))]
#[cfg_attr(not(target_arch = "wasm32"), frb(opaque))]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct EdgeLlm {
provider: Box<dyn LlmProvider>,
default_model: String,
}
#[cfg_attr(not(target_arch = "wasm32"), uniffi::export)]
impl EdgeLlm {
#[cfg_attr(not(target_arch = "wasm32"), uniffi::constructor)]
pub fn local(model_uri: String) -> Result<Self, SdkError> {
#[cfg(not(target_arch = "wasm32"))]
{
use el_core::{ModelFormat, ModelId, ModelVersion};
use el_provenance::{ModelArtifact, SignatureVerifier};
struct PermissiveVerifier;
impl SignatureVerifier for PermissiveVerifier {
fn verify(&self, _: &[u8], _: &[u8], _: u32) -> bool {
true
}
}
let mut art =
ModelArtifact::new(ModelId(1), ModelVersion::new(0, 1, 0), ModelFormat::Gguf);
art.verify(&PermissiveVerifier, b"placeholder", b"sig", 0);
let permit = art.ensure_loadable().map_err(SdkError::from)?;
let provider: Box<dyn LlmProvider> = if model_uri.is_empty() {
Box::new(
el_engine_candle::LocalLlmProvider::toy(256, 64, 255, permit)
.map_err(SdkError::from)?,
)
} else {
Box::new(
el_engine_candle::LocalLlmProvider::from_path(&model_uri, 1, permit)
.map_err(SdkError::from)?,
)
};
Ok(Self {
provider,
default_model: "local".into(),
})
}
#[cfg(target_arch = "wasm32")]
Ok(Self {
provider: Box::new(EchoProvider),
default_model: "local".into(),
})
}
#[cfg(not(target_arch = "wasm32"))]
#[uniffi::constructor]
pub fn cloud(model: String, api_key: String) -> Self {
let credential = CredentialRef::new(api_key);
let inner = el_cloud::CloudProvider::new();
let provider = BoundCloudProvider {
model: model.clone(),
credential,
inner,
};
Self {
provider: Box::new(provider),
default_model: model,
}
}
pub fn ask(&self, prompt: String) -> Result<String, SdkError> {
let req = ChatRequest::new(self.default_model.clone(), vec![ChatMessage::user(prompt)]);
self.provider
.chat(&req)
.map(|r| r.content)
.map_err(SdkError::from)
}
pub fn reset(&self) {
}
}
#[cfg(not(target_arch = "wasm32"))]
#[uniffi::export]
impl EdgeLlm {
pub fn ask_stream_cb(
&self,
prompt: String,
handler: Box<dyn StreamHandler>,
) -> Result<(), SdkError> {
let req = ChatRequest::new(self.default_model.clone(), vec![ChatMessage::user(prompt)]);
self.provider
.chat_stream(&req, &mut |t: ChatToken| {
if !t.is_final {
handler.on_token(t.text);
}
})
.map_err(SdkError::from)
}
}
impl EdgeLlm {
pub fn ask_stream(
&self,
prompt: String,
mut on_token: impl FnMut(String),
) -> Result<(), SdkError> {
let req = ChatRequest::new(self.default_model.clone(), vec![ChatMessage::user(prompt)]);
self.provider
.chat_stream(&req, &mut |t: ChatToken| {
if !t.is_final {
on_token(t.text.clone());
}
})
.map_err(SdkError::from)
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl EdgeLlm {
#[wasm_bindgen(constructor)]
pub fn new_local(model_uri: String) -> Result<EdgeLlm, JsValue> {
EdgeLlm::local(model_uri).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen]
pub fn ask_wasm(&self, prompt: String) -> Result<String, JsValue> {
self.ask(prompt)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen]
pub fn cloud(_model: String, _api_key: String) -> Result<EdgeLlm, JsValue> {
Err(JsValue::from_str(
"EdgeLlm.cloud is not available on web/wasm: the cloud transport \
requires a native binding (ADR-010)",
))
}
}
#[cfg(not(target_arch = "wasm32"))]
struct BoundCloudProvider {
model: String,
credential: CredentialRef,
inner: el_cloud::CloudProvider,
}
#[cfg(not(target_arch = "wasm32"))]
impl LlmProvider for BoundCloudProvider {
fn chat(&self, req: &ChatRequest) -> el_core::Result<el_core::ChatResponse> {
let mut r = req.clone();
r.model = self.model.clone();
r.credential = Some(self.credential.clone());
self.inner.chat(&r)
}
fn chat_stream(
&self,
req: &ChatRequest,
on_token: &mut dyn FnMut(ChatToken),
) -> el_core::Result<()> {
let mut r = req.clone();
r.model = self.model.clone();
r.credential = Some(self.credential.clone());
self.inner.chat_stream(&r, on_token)
}
}
#[cfg(target_arch = "wasm32")]
struct EchoProvider;
#[cfg(target_arch = "wasm32")]
impl LlmProvider for EchoProvider {
fn chat(&self, req: &ChatRequest) -> el_core::Result<el_core::ChatResponse> {
let echo = req
.messages
.last()
.map(|m| m.content.as_str())
.unwrap_or("")
.to_owned();
Ok(el_core::ChatResponse {
content: echo,
model: "echo".into(),
prompt_tokens: 0,
completion_tokens: 0,
})
}
fn chat_stream(
&self,
req: &ChatRequest,
on_token: &mut dyn FnMut(ChatToken),
) -> el_core::Result<()> {
let text = req
.messages
.last()
.map(|m| m.content.as_str())
.unwrap_or("")
.to_owned();
for ch in text.chars() {
on_token(ChatToken {
text: ch.to_string(),
is_final: false,
});
}
on_token(ChatToken {
text: String::new(),
is_final: true,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_toy_ask_returns_non_empty_response() {
let sdk = EdgeLlm::local("".into()).expect("toy model never fails");
let response = sdk
.ask("hello".into())
.expect("local toy model should not error");
assert!(!response.is_empty());
}
#[test]
fn stream_ends_with_final_and_has_content() {
let sdk = EdgeLlm::local("".into()).expect("toy model never fails");
let mut parts: Vec<String> = Vec::new();
sdk.ask_stream("hi".into(), |t| parts.push(t))
.expect("local toy model stream should not error");
assert!(!parts.is_empty());
}
#[test]
fn ask_error_is_distinguishable_from_content() {
let sdk = EdgeLlm::local("".into()).expect("toy model never fails");
let r = sdk.ask("ping".into());
assert!(
r.is_ok(),
"toy local provider must not error on a plain prompt"
);
assert!(
!r.unwrap().starts_with("error:"),
"response must not look like a swallowed error"
);
}
#[test]
fn local_missing_gguf_path_returns_sdk_error() {
let r = EdgeLlm::local("/nonexistent/model.gguf".into());
assert!(
matches!(r, Err(SdkError::ProviderError { .. })),
"non-empty path that doesn't exist must return SdkError"
);
}
}