//! `llm.chat` — generic Ollama-compatible LLM chat adapter. //! //! Sends a single user prompt (with optional system prompt) to an //! Ollama `/api/chat` endpoint and returns the assistant text. //! Reports `model_endpoint`, `model_name`, and `model_digest` as //! separate outputs for audit consumers. //! //! v0.1.0 targets Ollama only. OpenAI / Anthropic adapters land //! when a flow needs them. mod llm; use fai_module_sdk::prelude::*; #[fai_module] pub fn invoke(_ctx: Context, inputs: Inputs) -> Result { let prompt = inputs.require_text("prompt")?.to_string(); let endpoint = inputs.require_text("endpoint")?.to_string(); let model = inputs.require_text("model")?.to_string(); let api_key = inputs .get("api_key") .and_then(payload_text) .unwrap_or_default(); let system_prompt = inputs .get("system_prompt") .and_then(payload_text) .unwrap_or_default(); let client = make_client(); let params = crate::llm::ChatParams { endpoint: &endpoint, model: &model, api_key: &api_key, system_prompt: &system_prompt, prompt: &prompt, }; let result = crate::llm::chat_with_identity(&client, ¶ms) .map_err(|e| ModuleError::internal(e.to_string()))?; Ok(Outputs::new() .with_text("response", result.response) .with_text("model_endpoint", endpoint) .with_text("model_name", model) .with_text("model_digest", result.model_digest.unwrap_or_default())) } fn payload_text(p: &Payload) -> Option { match p { Payload::Text(s) => Some(s.clone()), _ => None, } } #[cfg(target_arch = "wasm32")] fn make_client() -> WakiClient { WakiClient } #[cfg(not(target_arch = "wasm32"))] fn make_client() -> HostStubClient { HostStubClient } #[cfg(not(target_arch = "wasm32"))] struct HostStubClient; #[cfg(not(target_arch = "wasm32"))] #[allow(dead_code)] impl crate::llm::LlmClient for HostStubClient { fn post_json( &self, _url: &str, _body: &str, _api_key: &str, ) -> Result { Err(crate::llm::LlmError::Http( "LLM HTTP path is unavailable on the host build; only wasm32 supports outbound HTTP" .to_string(), )) } } #[cfg(target_arch = "wasm32")] struct WakiClient; #[cfg(target_arch = "wasm32")] impl crate::llm::LlmClient for WakiClient { fn post_json( &self, url: &str, body: &str, api_key: &str, ) -> Result { let mut request = waki::Client::new() .post(url) .header("Content-Type", "application/json") .body(body.to_string()); if !api_key.is_empty() { request = request.header("Authorization", &format!("Bearer {api_key}")); } let response = request .send() .map_err(|e| crate::llm::LlmError::Http(e.to_string()))?; let status = response.status_code(); if !(200..300).contains(&status) { return Err(crate::llm::LlmError::Status(status)); } let bytes = response .body() .map_err(|e| crate::llm::LlmError::Http(e.to_string()))?; String::from_utf8(bytes).map_err(|e| crate::llm::LlmError::Decode(e.to_string())) } }