lmjtfy.git / apps / lmjtfy / src / ai.rs
ai.rsannotatedai.rssource43 lines · 1.7 KB · raw
1//! The Workers AI binding, called with JSON text and answered with JSON text.
2//!
3//! workers-rs's own `Ai::run` converts through serde-wasm-bindgen, which
4//! turns a JSON object into a JS `Map` unless every level is a Rust struct.
5//! The request here is built as JSON (the same text the page shows and the
6//! eval sends over REST), so it goes in through `JSON.parse` and the reply
7//! comes back through `JSON.stringify`, untouched either way.
8
9use js_sys::{Function, JSON, Promise, Reflect};
10use wasm_bindgen::{JsCast, JsValue};
11use wasm_bindgen_futures::JsFuture;
12use worker::Env;
13
14/// The binding's name in `wrangler.toml`.
15const BINDING: &str = "AI";
16
17fn message(error: JsValue) -> String {
18    error
19        .dyn_ref::<js_sys::Error>()
20        .map(|error| String::from(error.message()))
21        .or_else(|| error.as_string())
22        .unwrap_or_else(|| format!("{error:?}"))
23}
24
25/// Runs `model` on `request` and returns the reply as it came.
26pub async fn run(env: &Env, model: &str, request: &str) -> Result<String, String> {
27    let ai = Reflect::get(env.as_ref(), &BINDING.into()).map_err(message)?;
28    if ai.is_undefined() {
29        return Err(format!("the Worker has no {BINDING} binding"));
30    }
31    let run: Function = Reflect::get(&ai, &"run".into())
32        .map_err(message)?
33        .dyn_into()
34        .map_err(|_| format!("{BINDING}.run is not a function"))?;
35    let input = JSON::parse(request).map_err(message)?;
36    let promise: Promise = run
37        .call2(&ai, &model.into(), &input)
38        .map_err(message)?
39        .dyn_into()
40        .map_err(|_| format!("{BINDING}.run did not return a promise"))?;
41    let reply = JsFuture::from(promise).await.map_err(message)?;
42    JSON::stringify(&reply).map(String::from).map_err(message)
43}