Files
tkmind_go/crates/goose-sdk/src/bindings.rs
T
2026-07-14 16:55:57 +00:00

329 lines
9.6 KiB
Rust

//! In-process uniffi bindings for the Goose SDK.
//!
//! This is the API surface exposed to Python and Kotlin. It currently focuses
//! on declarative providers: consumers can construct a provider from JSON and
//! stream completions from it.
use std::{future::Future, sync::Arc, sync::OnceLock};
use futures::StreamExt;
use goose_providers::{
api_client::{ApiClient, AuthMethod},
base::{MessageStream, Provider as GooseProvider},
conversation::message::Message,
databricks::DatabricksProvider as GooseDatabricksProvider,
databricks_auth::DatabricksAuth,
declarative::EnvKeyResolver,
model::ModelConfig,
openai::OpenAiProviderBuilder,
};
/// Errors surfaced across the uniffi boundary.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum GooseError {
#[error("{0}")]
Generic(String),
}
impl From<anyhow::Error> for GooseError {
fn from(error: anyhow::Error) -> Self {
Self::Generic(error.to_string())
}
}
impl From<goose_providers::errors::ProviderError> for GooseError {
fn from(error: goose_providers::errors::ProviderError) -> Self {
Self::Generic(error.to_string())
}
}
impl From<serde_json::Error> for GooseError {
fn from(error: serde_json::Error) -> Self {
Self::Generic(error.to_string())
}
}
/// A text message passed to a provider.
#[derive(Debug, Clone, uniffi::Record)]
pub struct ProviderMessage {
pub role: MessageRole,
pub text: String,
}
/// Supported message roles for provider requests and streamed responses.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum MessageRole {
User,
Assistant,
}
impl ProviderMessage {
fn to_goose_message(&self) -> Message {
match self.role {
MessageRole::User => Message::user().with_text(&self.text),
MessageRole::Assistant => Message::assistant().with_text(&self.text),
}
}
}
/// Model selection and optional generation settings for a provider request.
#[derive(Debug, Clone, uniffi::Record)]
pub struct ProviderModelConfig {
pub model_name: String,
#[uniffi(default = None)]
pub context_limit: Option<u64>,
#[uniffi(default = None)]
pub temperature: Option<f32>,
#[uniffi(default = None)]
pub max_tokens: Option<i32>,
#[uniffi(default = false)]
pub toolshim: bool,
#[uniffi(default = None)]
pub toolshim_model: Option<String>,
/// Provider-specific request parameters as a JSON object string.
#[uniffi(default = None)]
pub request_params_json: Option<String>,
#[uniffi(default = None)]
pub reasoning: Option<bool>,
}
impl ProviderModelConfig {
fn to_goose_model_config(&self) -> Result<ModelConfig, GooseError> {
let mut config = ModelConfig::new(&self.model_name)
.with_context_limit(self.context_limit.map(|limit| limit as usize))
.with_temperature(self.temperature)
.with_max_tokens(self.max_tokens)
.with_toolshim(self.toolshim)
.with_toolshim_model(self.toolshim_model.clone());
if let Some(request_params_json) = &self.request_params_json {
let request_params = serde_json::from_str(request_params_json)?;
config = config.with_merged_request_params(request_params);
}
config.reasoning = self.reasoning;
Ok(config)
}
}
/// One item yielded by a provider stream.
#[derive(Debug, Clone, uniffi::Record)]
pub struct ProviderStreamChunk {
/// The concatenated text content in this message chunk, if one was emitted.
pub text: Option<String>,
/// Full Goose message JSON for callers that need non-text content such as tool requests.
pub message_json: Option<String>,
/// Provider usage JSON when the provider emits usage metadata.
pub usage_json: Option<String>,
}
static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
fn runtime() -> Result<&'static tokio::runtime::Runtime, GooseError> {
if let Some(runtime) = RUNTIME.get() {
return Ok(runtime);
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| GooseError::Generic(error.to_string()))?;
let _ = RUNTIME.set(runtime);
Ok(RUNTIME.get().expect("runtime was initialized"))
}
async fn run_on_runtime<T>(
future: impl Future<Output = T> + Send + 'static,
) -> Result<T, GooseError>
where
T: Send + 'static,
{
let (sender, receiver) = tokio::sync::oneshot::channel();
runtime()?.spawn(async move {
let _ = sender.send(future.await);
});
receiver
.await
.map_err(|_| GooseError::Generic("runtime task was cancelled".to_string()))
}
struct ProviderHandle {
provider: Arc<dyn GooseProvider>,
}
impl ProviderHandle {
fn new(provider: Box<dyn GooseProvider>) -> Self {
Self {
provider: Arc::from(provider),
}
}
fn name(&self) -> String {
self.provider.get_name().to_string()
}
async fn stream(
&self,
model: ProviderModelConfig,
system: String,
messages: Vec<ProviderMessage>,
) -> Result<Arc<ProviderStream>, GooseError> {
let model = model.to_goose_model_config()?;
let messages = messages
.iter()
.map(ProviderMessage::to_goose_message)
.collect::<Vec<_>>();
let provider = Arc::clone(&self.provider);
let stream =
run_on_runtime(async move { provider.stream(&model, &system, &messages, &[]).await })
.await??;
Ok(Arc::new(ProviderStream {
stream: Arc::new(tokio::sync::Mutex::new(stream)),
}))
}
}
/// A Goose provider backed by one of Goose's native provider implementations.
#[derive(uniffi::Object)]
pub struct Provider {
handle: ProviderHandle,
}
impl Provider {
fn new(provider: Box<dyn GooseProvider>) -> Arc<Self> {
Arc::new(Self {
handle: ProviderHandle::new(provider),
})
}
}
#[uniffi::export]
impl Provider {
pub fn name(&self) -> String {
self.handle.name()
}
/// Start a streaming completion request. Tools are not yet exposed over the
/// uniffi boundary, so this calls providers with an empty tool list.
pub async fn stream(
&self,
model: ProviderModelConfig,
system: String,
messages: Vec<ProviderMessage>,
) -> Result<Arc<ProviderStream>, GooseError> {
self.handle.stream(model, system, messages).await
}
}
#[uniffi::export]
pub fn declarative_provider_from_json(json: String) -> Result<Arc<Provider>, GooseError> {
let provider = goose_providers::declarative::from_json(&json, None, EnvKeyResolver {})?;
Ok(Provider::new(provider))
}
#[uniffi::export]
pub fn openai_default_model() -> String {
goose_providers::openai::OPEN_AI_DEFAULT_MODEL.to_string()
}
#[uniffi::export]
pub fn openai_provider(api_key: String) -> Result<Arc<Provider>, GooseError> {
let api_client = ApiClient::new_with_tls(
"https://api.openai.com".to_string(),
AuthMethod::BearerToken(api_key),
None,
)?;
let provider = OpenAiProviderBuilder::new(api_client).build();
Ok(Provider::new(Box::new(provider)))
}
#[uniffi::export]
pub fn databricks_default_model() -> String {
goose_providers::databricks::DATABRICKS_DEFAULT_MODEL.to_string()
}
#[uniffi::export]
pub fn databricks_provider(host: String, token: String) -> Result<Arc<Provider>, GooseError> {
let retry_config = GooseDatabricksProvider::load_retry_config(|key| std::env::var(key).ok());
let provider = GooseDatabricksProvider::new(
host,
DatabricksAuth::token(token),
retry_config,
None,
None,
None,
None,
None,
None,
None,
)?;
Ok(Provider::new(Box::new(provider)))
}
/// An async iterator over provider stream chunks.
#[derive(uniffi::Object)]
pub struct ProviderStream {
stream: Arc<tokio::sync::Mutex<MessageStream>>,
}
#[uniffi::export]
impl ProviderStream {
/// Return the next stream chunk, or `None` when the stream is exhausted.
pub async fn next(&self) -> Result<Option<ProviderStreamChunk>, GooseError> {
let stream = Arc::clone(&self.stream);
run_on_runtime(async move {
let mut stream = stream.lock().await;
let Some((message, usage)) = stream.next().await.transpose()? else {
return Ok(None);
};
let text = message.as_ref().map(Message::as_concat_text);
let message_json = message.as_ref().map(serde_json::to_string).transpose()?;
let usage_json = usage.as_ref().map(serde_json::to_string).transpose()?;
Ok(Some(ProviderStreamChunk {
text,
message_json,
usage_json,
}))
})
.await?
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_config_rejects_invalid_request_params_json() {
let config = ProviderModelConfig {
model_name: "test".to_string(),
context_limit: None,
temperature: None,
max_tokens: None,
toolshim: false,
toolshim_model: None,
request_params_json: Some("not json".to_string()),
reasoning: None,
};
assert!(config.to_goose_model_config().is_err());
}
#[test]
fn provider_message_converts_user_text() {
let message = ProviderMessage {
role: MessageRole::User,
text: "what is the capital of France?".to_string(),
}
.to_goose_message();
assert_eq!(message.as_concat_text(), "what is the capital of France?");
}
}