[goose-llm] add providerConfig param for exposed LLM functions (#2491)

This commit is contained in:
Salman Mohammed
2025-05-09 13:09:54 -04:00
committed by GitHub
parent ace269dd06
commit 7b81562351
18 changed files with 3589 additions and 219 deletions
+2
View File
@@ -1,3 +1,5 @@
__pycache__
*.pyc
*.jar
run_cli.sh
tokenizer_files/
+18 -8
View File
@@ -66,14 +66,23 @@ fun main() = runBlocking {
printMessages(msgs)
println("---\n")
val sessionName = generateSessionName(msgs)
println("Session Name: $sessionName")
// Setup provider
val providerName = "databricks"
val host = System.getenv("DATABRICKS_HOST") ?: error("DATABRICKS_HOST not set")
val token = System.getenv("DATABRICKS_TOKEN") ?: error("DATABRICKS_TOKEN not set")
val providerConfig = """{"host": "$host", "token": "$token"}"""
val tooltip = generateTooltip(msgs)
println("Tooltip: $tooltip")
println("Provider Name: $providerName")
println("Provider Config: $providerConfig")
val sessionName = generateSessionName(providerName, providerConfig, msgs)
println("\nSession Name: $sessionName")
val tooltip = generateTooltip(providerName, providerConfig, msgs)
println("\nTooltip: $tooltip")
// Completion
val provider = "databricks"
val modelName = "goose-gpt-4-1"
val modelConfig = ModelConfig(
modelName,
@@ -116,8 +125,9 @@ fun main() = runBlocking {
val systemPreamble = "You are a helpful assistant."
val req = CompletionRequest(
provider,
val req = createCompletionRequest(
providerName,
providerConfig,
modelConfig,
systemPreamble,
msgs,
@@ -127,4 +137,4 @@ fun main() = runBlocking {
val response = completion(req)
println("\nCompletion Response:")
println(response.message)
}
}
+78 -53
View File
@@ -769,6 +769,8 @@ internal interface IntegrityCheckingUniffiLib : Library {
// Integrity check functions only
fun uniffi_goose_llm_checksum_func_completion(): Short
fun uniffi_goose_llm_checksum_func_create_completion_request(): Short
fun uniffi_goose_llm_checksum_func_create_tool_config(): Short
fun uniffi_goose_llm_checksum_func_generate_session_name(): Short
@@ -821,6 +823,16 @@ internal interface UniffiLib : Library {
// FFI functions
fun uniffi_goose_llm_fn_func_completion(`req`: RustBuffer.ByValue): Long
fun uniffi_goose_llm_fn_func_create_completion_request(
`providerName`: RustBuffer.ByValue,
`providerConfig`: RustBuffer.ByValue,
`modelConfig`: RustBuffer.ByValue,
`systemPreamble`: RustBuffer.ByValue,
`messages`: RustBuffer.ByValue,
`extensions`: RustBuffer.ByValue,
uniffi_out_err: UniffiRustCallStatus,
): RustBuffer.ByValue
fun uniffi_goose_llm_fn_func_create_tool_config(
`name`: RustBuffer.ByValue,
`description`: RustBuffer.ByValue,
@@ -829,9 +841,17 @@ internal interface UniffiLib : Library {
uniffi_out_err: UniffiRustCallStatus,
): RustBuffer.ByValue
fun uniffi_goose_llm_fn_func_generate_session_name(`messages`: RustBuffer.ByValue): Long
fun uniffi_goose_llm_fn_func_generate_session_name(
`providerName`: RustBuffer.ByValue,
`providerConfig`: RustBuffer.ByValue,
`messages`: RustBuffer.ByValue,
): Long
fun uniffi_goose_llm_fn_func_generate_tooltip(`messages`: RustBuffer.ByValue): Long
fun uniffi_goose_llm_fn_func_generate_tooltip(
`providerName`: RustBuffer.ByValue,
`providerConfig`: RustBuffer.ByValue,
`messages`: RustBuffer.ByValue,
): Long
fun uniffi_goose_llm_fn_func_print_messages(
`messages`: RustBuffer.ByValue,
@@ -1067,16 +1087,19 @@ private fun uniffiCheckContractApiVersion(lib: IntegrityCheckingUniffiLib) {
@Suppress("UNUSED_PARAMETER")
private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
if (lib.uniffi_goose_llm_checksum_func_completion() != 55281.toShort()) {
if (lib.uniffi_goose_llm_checksum_func_completion() != 47457.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_goose_llm_checksum_func_create_completion_request() != 51008.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_goose_llm_checksum_func_create_tool_config() != 22809.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_goose_llm_checksum_func_generate_session_name() != 61290.toShort()) {
if (lib.uniffi_goose_llm_checksum_func_generate_session_name() != 9810.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_goose_llm_checksum_func_generate_tooltip() != 7529.toShort()) {
if (lib.uniffi_goose_llm_checksum_func_generate_tooltip() != 15466.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_goose_llm_checksum_func_print_messages() != 30278.toShort()) {
@@ -1365,50 +1388,6 @@ public object FfiConverterString : FfiConverter<String, RustBuffer.ByValue> {
}
}
data class CompletionRequest(
var `providerName`: kotlin.String,
var `modelConfig`: ModelConfig,
var `systemPreamble`: kotlin.String,
var `messages`: List<Message>,
var `extensions`: List<ExtensionConfig>,
) {
companion object
}
/**
* @suppress
*/
public object FfiConverterTypeCompletionRequest : FfiConverterRustBuffer<CompletionRequest> {
override fun read(buf: ByteBuffer): CompletionRequest =
CompletionRequest(
FfiConverterString.read(buf),
FfiConverterTypeModelConfig.read(buf),
FfiConverterString.read(buf),
FfiConverterSequenceTypeMessage.read(buf),
FfiConverterSequenceTypeExtensionConfig.read(buf),
)
override fun allocationSize(value: CompletionRequest) =
(
FfiConverterString.allocationSize(value.`providerName`) +
FfiConverterTypeModelConfig.allocationSize(value.`modelConfig`) +
FfiConverterString.allocationSize(value.`systemPreamble`) +
FfiConverterSequenceTypeMessage.allocationSize(value.`messages`) +
FfiConverterSequenceTypeExtensionConfig.allocationSize(value.`extensions`)
)
override fun write(
value: CompletionRequest,
buf: ByteBuffer,
) {
FfiConverterString.write(value.`providerName`, buf)
FfiConverterTypeModelConfig.write(value.`modelConfig`, buf)
FfiConverterString.write(value.`systemPreamble`, buf)
FfiConverterSequenceTypeMessage.write(value.`messages`, buf)
FfiConverterSequenceTypeExtensionConfig.write(value.`extensions`, buf)
}
}
data class CompletionResponse(
var `message`: Message,
var `model`: kotlin.String,
@@ -2814,6 +2793,14 @@ public object FfiConverterSequenceTypeToolConfig : FfiConverterRustBuffer<List<T
}
}
/**
* Typealias from the type name used in the UDL file to the builtin type. This
* is needed because the UDL type name is used in function/method signatures.
* It's also what we have an external type that references a custom type.
*/
public typealias CompletionRequest = kotlin.String
public typealias FfiConverterTypeCompletionRequest = FfiConverterString
/**
* Typealias from the type name used in the UDL file to the builtin type. This
* is needed because the UDL type name is used in function/method signatures.
@@ -2871,6 +2858,28 @@ suspend fun `completion`(`req`: CompletionRequest): CompletionResponse =
CompletionException.ErrorHandler,
)
fun `createCompletionRequest`(
`providerName`: kotlin.String,
`providerConfig`: JsonValueFfi,
`modelConfig`: ModelConfig,
`systemPreamble`: kotlin.String,
`messages`: List<Message>,
`extensions`: List<ExtensionConfig>,
): CompletionRequest =
FfiConverterTypeCompletionRequest.lift(
uniffiRustCall { _status ->
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_create_completion_request(
FfiConverterString.lower(`providerName`),
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
FfiConverterTypeModelConfig.lower(`modelConfig`),
FfiConverterString.lower(`systemPreamble`),
FfiConverterSequenceTypeMessage.lower(`messages`),
FfiConverterSequenceTypeExtensionConfig.lower(`extensions`),
_status,
)
},
)
fun `createToolConfig`(
`name`: kotlin.String,
`description`: kotlin.String,
@@ -2894,9 +2903,17 @@ fun `createToolConfig`(
*/
@Throws(ProviderException::class)
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
suspend fun `generateSessionName`(`messages`: List<Message>): kotlin.String =
suspend fun `generateSessionName`(
`providerName`: kotlin.String,
`providerConfig`: JsonValueFfi,
`messages`: List<Message>,
): kotlin.String =
uniffiRustCallAsync(
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_session_name(FfiConverterSequenceTypeMessage.lower(`messages`)),
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_session_name(
FfiConverterString.lower(`providerName`),
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
FfiConverterSequenceTypeMessage.lower(`messages`),
),
{ future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) },
{ future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) },
{ future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) },
@@ -2912,9 +2929,17 @@ suspend fun `generateSessionName`(`messages`: List<Message>): kotlin.String =
*/
@Throws(ProviderException::class)
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
suspend fun `generateTooltip`(`messages`: List<Message>): kotlin.String =
suspend fun `generateTooltip`(
`providerName`: kotlin.String,
`providerConfig`: JsonValueFfi,
`messages`: List<Message>,
): kotlin.String =
uniffiRustCallAsync(
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_tooltip(FfiConverterSequenceTypeMessage.lower(`messages`)),
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_tooltip(
FfiConverterString.lower(`providerName`),
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
FfiConverterSequenceTypeMessage.lower(`messages`),
),
{ future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) },
{ future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) },
{ future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) },
File diff suppressed because it is too large Load Diff
+133
View File
@@ -0,0 +1,133 @@
import asyncio
import os
import time
from goose_llm import (
Message, MessageContent, TextContent, ToolRequest, ToolResponse,
Role, ModelConfig, ToolApprovalMode,
create_tool_config, ExtensionConfig,
generate_session_name, generate_tooltip,
create_completion_request, completion
)
async def main():
now = int(time.time())
# 1) User sends a plain-text prompt
messages = [
Message(
role=Role.USER,
created=now,
content=[MessageContent.TEXT(TextContent(text="What is 7 x 6?"))]
),
# 2) Assistant makes a tool request
Message(
role=Role.ASSISTANT,
created=now + 2,
content=[MessageContent.TOOL_REQ(ToolRequest(
id="calc1",
tool_call="""
{
"status": "success",
"value": {
"name": "calculator_extension__toolname",
"arguments": {
"operation": "multiply",
"numbers": [7, 6]
},
"needsApproval": false
}
}
"""
))]
),
# 3) User sends tool result
Message(
role=Role.USER,
created=now + 3,
content=[MessageContent.TOOL_RESP(ToolResponse(
id="calc1",
tool_result="""
{
"status": "success",
"value": [
{"type": "text", "text": "42"}
]
}
"""
))]
)
]
provider_name = "databricks"
provider_config = f'''{{
"host": "{os.environ.get("DATABRICKS_HOST")}",
"token": "{os.environ.get("DATABRICKS_TOKEN")}"
}}'''
print(f"Provider Name: {provider_name}")
print(f"Provider Config: {provider_config}")
session_name = await generate_session_name(provider_name, provider_config, messages)
print(f"\nSession Name: {session_name}")
tooltip = await generate_tooltip(provider_name, provider_config, messages)
print(f"\nTooltip: {tooltip}")
model_config = ModelConfig(
model_name="goose-gpt-4-1",
max_tokens=500,
temperature=0.1,
context_limit=4096,
)
calculator_tool = create_tool_config(
name="calculator",
description="Perform basic arithmetic operations",
input_schema="""
{
"type": "object",
"required": ["operation", "numbers"],
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform"
},
"numbers": {
"type": "array",
"items": { "type": "number" },
"description": "List of numbers to operate on in order"
}
}
}
""",
approval_mode=ToolApprovalMode.AUTO
)
calculator_extension = ExtensionConfig(
name="calculator_extension",
instructions="This extension provides a calculator tool.",
tools=[calculator_tool]
)
system_preamble = "You are a helpful assistant."
extensions = [calculator_extension]
req = create_completion_request(
provider_name,
provider_config,
model_config,
system_preamble,
messages,
extensions
)
resp = await completion(req)
print(f"\nCompletion Response:\n{resp.message}")
print(f"Msg content: {resp.message.content[0][0]}")
if __name__ == "__main__":
asyncio.run(main())
+21 -12
View File
@@ -31,26 +31,19 @@ Structure:
│ └── goose_llm.kt ← auto-generated bindings
```
Create Kotlin bindings:
```
#### Create Kotlin bindings:
```bash
# run from project root directory
cargo build -p goose-llm
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language kotlin --out-dir bindings/kotlin
```
Download jars in `bindings/kotlin/libs` directory (only need to do this once):
```
pushd bindings/kotlin/libs/
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.jar
curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar
popd
```
#### Kotlin -> Rust: run example
Compile & Run usage example from Kotlin -> Rust:
```
```bash
pushd bindings/kotlin/
kotlinc \
@@ -68,3 +61,19 @@ java \
popd
```
You will have to download jars in `bindings/kotlin/libs` directory (only the first time):
```bash
pushd bindings/kotlin/libs/
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.jar
curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar
popd
```
#### Python -> Rust: generate bindings, run example
```bash
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language python --out-dir bindings/python
DYLD_LIBRARY_PATH=./target/debug python bindings/python/usage.py
```
+23 -10
View File
@@ -3,6 +3,7 @@ use std::vec;
use anyhow::Result;
use goose_llm::{
completion,
extractors::generate_tooltip,
types::completion::{
CompletionRequest, CompletionResponse, ExtensionConfig, ToolApprovalMode, ToolConfig,
},
@@ -13,8 +14,12 @@ use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
let provider = "databricks";
// let model_name = "goose-claude-3-5-sonnet"; // sequential tool calls
let model_name = "goose-gpt-4-1"; // parallel tool calls
let provider_config = json!({
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
});
// let model_name = "goose-gpt-4-1"; // parallel tool calls
let model_name = "claude-3-5-haiku";
let model_config = ModelConfig::new(model_name.to_string());
let calculator_tool = ToolConfig::new(
@@ -92,18 +97,26 @@ async fn main() -> Result<()> {
] {
println!("\n---------------\n");
println!("User Input: {text}");
let messages = vec![Message::user().with_text(text)];
let completion_response: CompletionResponse = completion(CompletionRequest {
provider_name: provider.to_string(),
model_config: model_config.clone(),
system_preamble: system_preamble.to_string(),
messages: messages,
extensions: extensions.clone(),
})
let messages = vec![
Message::user().with_text("Hi there!"),
Message::assistant().with_text("How can I help?"),
Message::user().with_text(text),
];
let completion_response: CompletionResponse = completion(CompletionRequest::new(
provider.to_string(),
provider_config.clone(),
model_config.clone(),
system_preamble.to_string(),
messages.clone(),
extensions.clone(),
))
.await?;
// Print the response
println!("\nCompletion Response:");
println!("{}", serde_json::to_string_pretty(&completion_response)?);
let tooltip = generate_tooltip(provider, provider_config.clone().into(), &messages).await?;
println!("\nTooltip: {}", tooltip);
}
Ok(())
+6 -2
View File
@@ -29,8 +29,12 @@ pub fn print_messages(messages: Vec<Message>) {
pub async fn completion(req: CompletionRequest) -> Result<CompletionResponse, CompletionError> {
let start_total = Instant::now();
let provider = create(&req.provider_name, req.model_config)
.map_err(|_| CompletionError::UnknownProvider(req.provider_name.to_string()))?;
let provider = create(
&req.provider_name,
req.provider_config.clone(),
req.model_config.clone(),
)
.map_err(|_| CompletionError::UnknownProvider(req.provider_name.to_string()))?;
let system_prompt = construct_system_prompt(&req.system_preamble, &req.extensions)?;
let tools = collect_prefixed_tools(&req.extensions);
@@ -1,9 +1,8 @@
use crate::message::Message;
use crate::model::ModelConfig;
use crate::providers::base::Provider;
use crate::providers::databricks::DatabricksProvider;
use crate::providers::create;
use crate::providers::errors::ProviderError;
use crate::types::core::Role;
use crate::{message::Message, types::json_value_ffi::JsonValueFfi};
use anyhow::Result;
use indoc::indoc;
use serde_json::{json, Value};
@@ -50,7 +49,20 @@ fn build_system_prompt() -> String {
/// Generates a short (≤4 words) session name
#[uniffi::export(async_runtime = "tokio")]
pub async fn generate_session_name(messages: &[Message]) -> Result<String, ProviderError> {
pub async fn generate_session_name(
provider_name: &str,
provider_config: JsonValueFfi,
messages: &[Message],
) -> Result<String, ProviderError> {
// Use OpenAI models specifically for this task
let model_name = if provider_name == "databricks" {
"goose-gpt-4-1"
} else {
"gpt-4.1"
};
let model_cfg = ModelConfig::new(model_name.to_string()).with_temperature(Some(0.0));
let provider = create(provider_name, provider_config.into(), model_cfg)?;
// Collect up to the first 3 user messages (truncated to 300 chars each)
let context: Vec<String> = messages
.iter()
@@ -75,10 +87,6 @@ pub async fn generate_session_name(messages: &[Message]) -> Result<String, Provi
let system_prompt = build_system_prompt();
let user_msg_text = format!("Here are the user messages:\n{}", context.join("\n"));
// Instantiate DatabricksProvider with goose-gpt-4-1
let model_cfg = ModelConfig::new("goose-gpt-4-1".to_string()).with_temperature(Some(0.0));
let provider = DatabricksProvider::from_env(model_cfg)?;
// Use `extract` with a simple string schema
let schema = json!({
"type": "object",
+16 -7
View File
@@ -1,9 +1,9 @@
use crate::message::{Message, MessageContent};
use crate::model::ModelConfig;
use crate::providers::base::Provider;
use crate::providers::databricks::DatabricksProvider;
use crate::providers::create;
use crate::providers::errors::ProviderError;
use crate::types::core::{Content, Role};
use crate::types::json_value_ffi::JsonValueFfi;
use anyhow::Result;
use indoc::indoc;
use serde_json::{json, Value};
@@ -54,7 +54,20 @@ fn build_system_prompt() -> String {
/// Generates a tooltip summarizing the last two messages in the session,
/// including any tool calls or results.
#[uniffi::export(async_runtime = "tokio")]
pub async fn generate_tooltip(messages: &[Message]) -> Result<String, ProviderError> {
pub async fn generate_tooltip(
provider_name: &str,
provider_config: JsonValueFfi,
messages: &[Message],
) -> Result<String, ProviderError> {
// Use OpenAI models specifically for this task
let model_name = if provider_name == "databricks" {
"goose-gpt-4-1"
} else {
"gpt-4.1"
};
let model_cfg = ModelConfig::new(model_name.to_string()).with_temperature(Some(0.0));
let provider = create(provider_name, provider_config.into(), model_cfg)?;
// Need at least two messages to summarize
if messages.len() < 2 {
return Err(ProviderError::ExecutionError(
@@ -128,10 +141,6 @@ pub async fn generate_tooltip(messages: &[Message]) -> Result<String, ProviderEr
rendered.join("\n")
);
// Instantiate the provider
let model_cfg = ModelConfig::new("goose-gpt-4-1".to_string()).with_temperature(Some(0.0));
let provider = DatabricksProvider::from_env(model_cfg)?;
// Schema wrapping our tooltip string
let schema = json!({
"type": "object",
+52 -29
View File
@@ -26,66 +26,83 @@ pub const _DATABRICKS_KNOWN_MODELS: &[&str] = &[
"databricks-claude-3-7-sonnet",
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DatabricksAuth {
Token(String),
fn default_timeout() -> u64 {
60
}
impl DatabricksAuth {
pub fn token(token: String) -> Self {
Self::Token(token)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabricksProviderConfig {
pub host: String,
pub token: String,
#[serde(default)]
pub image_format: ImageFormat,
#[serde(default = "default_timeout")]
pub timeout: u64, // timeout in seconds
}
impl DatabricksProviderConfig {
pub fn new(host: String, token: String) -> Self {
Self {
host,
token,
image_format: ImageFormat::OpenAi,
timeout: default_timeout(),
}
}
pub fn from_env() -> Self {
let host = get_env("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST");
let token = get_env("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN");
Self::new(host, token)
}
}
#[derive(Debug)]
pub struct DatabricksProvider {
client: Client,
host: String,
auth: DatabricksAuth,
config: DatabricksProviderConfig,
model: ModelConfig,
image_format: ImageFormat,
client: Client,
}
impl DatabricksProvider {
pub fn from_env(model: ModelConfig) -> Self {
let config = DatabricksProviderConfig::from_env();
DatabricksProvider::from_config(config, model)
.expect("Failed to initialize DatabricksProvider")
}
}
impl Default for DatabricksProvider {
fn default() -> Self {
let config = DatabricksProviderConfig::from_env();
let model = ModelConfig::new(DATABRICKS_DEFAULT_MODEL.to_string());
DatabricksProvider::from_env(model).expect("Failed to initialize Databricks provider")
DatabricksProvider::from_config(config, model)
.expect("Failed to initialize DatabricksProvider")
}
}
impl DatabricksProvider {
pub fn from_env(model: ModelConfig) -> Result<Self> {
let host = get_env("DATABRICKS_HOST")?;
let api_key = get_env("DATABRICKS_TOKEN")?;
pub fn from_config(config: DatabricksProviderConfig, model: ModelConfig) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(600))
.timeout(Duration::from_secs(config.timeout))
.build()?;
Ok(Self {
client,
host,
auth: DatabricksAuth::token(api_key),
config,
model,
image_format: ImageFormat::OpenAi,
client,
})
}
async fn ensure_auth_header(&self) -> Result<String> {
match &self.auth {
DatabricksAuth::Token(token) => Ok(format!("Bearer {}", token)),
}
}
async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
let base_url = Url::parse(&self.host)
let base_url = Url::parse(&self.config.host)
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;
let path = format!("serving-endpoints/{}/invocations", self.model.model_name);
let url = base_url.join(&path).map_err(|e| {
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
})?;
let auth_header = self.ensure_auth_header().await?;
let auth_header = format!("Bearer {}", &self.config.token);
let response = self
.client
.post(url)
@@ -189,7 +206,13 @@ impl Provider for DatabricksProvider {
messages: &[Message],
tools: &[Tool],
) -> Result<ProviderCompleteResponse, ProviderError> {
let mut payload = create_request(&self.model, system, messages, tools, &self.image_format)?;
let mut payload = create_request(
&self.model,
system,
messages,
tools,
&self.config.image_format,
)?;
// Remove the model key which is part of the url with databricks
payload
.as_object_mut()
+18 -4
View File
@@ -2,14 +2,28 @@ use std::sync::Arc;
use anyhow::Result;
use super::{base::Provider, databricks::DatabricksProvider, openai::OpenAiProvider};
use super::{
base::Provider,
databricks::{DatabricksProvider, DatabricksProviderConfig},
openai::{OpenAiProvider, OpenAiProviderConfig},
};
use crate::model::ModelConfig;
pub fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {
pub fn create(
name: &str,
provider_config: serde_json::Value,
model: ModelConfig,
) -> Result<Arc<dyn Provider>> {
// We use Arc instead of Box to be able to clone for multiple async tasks
match name {
"openai" => Ok(Arc::new(OpenAiProvider::from_env(model)?)),
"databricks" => Ok(Arc::new(DatabricksProvider::from_env(model)?)),
"openai" => {
let config: OpenAiProviderConfig = serde_json::from_value(provider_config)?;
Ok(Arc::new(OpenAiProvider::from_config(config, model)?))
}
"databricks" => {
let config: DatabricksProviderConfig = serde_json::from_value(provider_config)?;
Ok(Arc::new(DatabricksProvider::from_config(config, model)?))
}
_ => Err(anyhow::anyhow!("Unknown provider: {}", name)),
}
}
+70 -50
View File
@@ -3,6 +3,7 @@ use std::{collections::HashMap, time::Duration};
use anyhow::Result;
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use super::{
@@ -20,82 +21,112 @@ use crate::{
pub const OPEN_AI_DEFAULT_MODEL: &str = "gpt-4o";
pub const _OPEN_AI_KNOWN_MODELS: &[&str] = &["gpt-4o", "gpt-4.1", "o1", "o3", "o4-mini"];
fn default_timeout() -> u64 {
60
}
fn default_base_path() -> String {
"v1/chat/completions".to_string()
}
fn default_host() -> String {
"https://api.openai.com".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAiProviderConfig {
pub api_key: String,
#[serde(default = "default_host")]
pub host: String,
#[serde(default)]
pub organization: Option<String>,
#[serde(default = "default_base_path")]
pub base_path: String,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub custom_headers: Option<HashMap<String, String>>,
#[serde(default = "default_timeout")]
pub timeout: u64, // timeout in seconds
}
impl OpenAiProviderConfig {
pub fn new(api_key: String) -> Self {
Self {
api_key,
host: default_host(),
organization: None,
base_path: default_base_path(),
project: None,
custom_headers: None,
timeout: 600,
}
}
pub fn from_env() -> Self {
let api_key = get_env("OPENAI_API_KEY").expect("Missing OPENAI_API_KEY");
Self::new(api_key)
}
}
#[derive(Debug)]
pub struct OpenAiProvider {
client: Client,
host: String,
base_path: String,
api_key: String,
organization: Option<String>,
project: Option<String>,
config: OpenAiProviderConfig,
model: ModelConfig,
custom_headers: Option<HashMap<String, String>>,
client: Client,
}
impl OpenAiProvider {
pub fn from_env(model: ModelConfig) -> Self {
let config = OpenAiProviderConfig::from_env();
OpenAiProvider::from_config(config, model).expect("Failed to initialize OpenAiProvider")
}
}
impl Default for OpenAiProvider {
fn default() -> Self {
let config = OpenAiProviderConfig::from_env();
let model = ModelConfig::new(OPEN_AI_DEFAULT_MODEL.to_string());
OpenAiProvider::from_env(model).expect("Failed to initialize OpenAI provider")
OpenAiProvider::from_config(config, model).expect("Failed to initialize OpenAiProvider")
}
}
impl OpenAiProvider {
pub fn from_env(model: ModelConfig) -> Result<Self> {
let api_key: String = get_env("OPENAI_API_KEY")?;
let host: String =
get_env("OPENAI_HOST").unwrap_or_else(|_| "https://api.openai.com".to_string());
let base_path: String =
get_env("OPENAI_BASE_PATH").unwrap_or_else(|_| "v1/chat/completions".to_string());
let organization: Option<String> = get_env("OPENAI_ORGANIZATION").ok();
let project: Option<String> = get_env("OPENAI_PROJECT").ok();
let custom_headers: Option<HashMap<String, String>> = get_env("OPENAI_CUSTOM_HEADERS")
.or_else(|_| get_env("OPENAI_CUSTOM_HEADERS"))
.ok()
.map(parse_custom_headers);
// parse get_env("OPENAI_TIMEOUT") to u64 or set default to 600
let timeout_secs = get_env("OPENAI_TIMEOUT")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(600);
pub fn from_config(config: OpenAiProviderConfig, model: ModelConfig) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.timeout(Duration::from_secs(config.timeout))
.build()?;
Ok(Self {
client,
host,
base_path,
api_key,
organization,
project,
config,
model,
custom_headers,
client,
})
}
async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
let base_url = url::Url::parse(&self.host)
let base_url = url::Url::parse(&self.config.host)
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;
let url = base_url.join(&self.base_path).map_err(|e| {
let url = base_url.join(&self.config.base_path).map_err(|e| {
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
})?;
let mut request = self
.client
.post(url)
.header("Authorization", format!("Bearer {}", self.api_key));
.header("Authorization", format!("Bearer {}", self.config.api_key));
// Add organization header if present
if let Some(org) = &self.organization {
if let Some(org) = &self.config.organization {
request = request.header("OpenAI-Organization", org);
}
// Add project header if present
if let Some(project) = &self.project {
if let Some(project) = &self.config.project {
request = request.header("OpenAI-Project", project);
}
if let Some(custom_headers) = &self.custom_headers {
if let Some(custom_headers) = &self.config.custom_headers {
for (key, value) in custom_headers {
request = request.header(key, value);
}
@@ -198,14 +229,3 @@ impl Provider for OpenAiProvider {
Ok(ProviderExtractResponse::new(data, model, usage))
}
}
fn parse_custom_headers(s: String) -> HashMap<String, String> {
s.split(',')
.filter_map(|header| {
let mut parts = header.splitn(2, '=');
let key = parts.next().map(|s| s.trim().to_string())?;
let value = parts.next().map(|s| s.trim().to_string())?;
Some((key, value))
})
.collect()
}
+11 -1
View File
@@ -19,12 +19,22 @@ struct OpenAIErrorResponse {
error: OpenAIError,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default)]
pub enum ImageFormat {
#[default]
OpenAi,
Anthropic,
}
/// Timeout in seconds.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Timeout(u32);
impl Default for Timeout {
fn default() -> Self {
Timeout(60)
}
}
/// Convert an image content into an image json based on format
pub fn convert_image(image: &ImageContent, image_format: &ImageFormat) -> Value {
match image_format {
+51 -4
View File
@@ -11,17 +11,64 @@ use crate::types::json_value_ffi::JsonValueFfi;
use crate::{message::Message, providers::Usage};
use crate::{model::ModelConfig, providers::errors::ProviderError};
// Lifetimes are not supported in Uniffi, cause other languages don't have them
// https://github.com/mozilla/uniffi-rs/issues/1526#issuecomment-1528851837
#[derive(uniffi::Record)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionRequest {
pub provider_name: String,
pub provider_config: serde_json::Value,
pub model_config: ModelConfig,
pub system_preamble: String,
pub messages: Vec<Message>,
pub extensions: Vec<ExtensionConfig>,
}
impl CompletionRequest {
pub fn new(
provider_name: String,
provider_config: serde_json::Value,
model_config: ModelConfig,
system_preamble: String,
messages: Vec<Message>,
extensions: Vec<ExtensionConfig>,
) -> Self {
Self {
provider_name,
provider_config,
model_config,
system_preamble,
messages,
extensions,
}
}
}
#[uniffi::export]
pub fn create_completion_request(
provider_name: &str,
provider_config: JsonValueFfi,
model_config: ModelConfig,
system_preamble: &str,
messages: Vec<Message>,
extensions: Vec<ExtensionConfig>,
) -> CompletionRequest {
CompletionRequest::new(
provider_name.to_string(),
provider_config.into(),
model_config,
system_preamble.to_string(),
messages,
extensions,
)
}
uniffi::custom_type!(CompletionRequest, String, {
lower: |tc: &CompletionRequest| {
serde_json::to_string(&tc).unwrap()
},
try_lift: |s: String| {
Ok(serde_json::from_str(&s).unwrap())
},
});
// https://mozilla.github.io/uniffi-rs/latest/proc_macro/errors.html
#[derive(Debug, Error, uniffi::Error)]
#[uniffi(flat_error)]
@@ -149,7 +196,7 @@ uniffi::custom_type!(ToolConfig, String, {
// — Register the newtypes with UniFFI, converting via JSON strings —
#[derive(Debug, Clone, Serialize, uniffi::Record)]
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ExtensionConfig {
name: String,
instructions: Option<String>,
+36 -13
View File
@@ -4,15 +4,32 @@ use goose_llm::extractors::generate_session_name;
use goose_llm::message::Message;
use goose_llm::providers::errors::ProviderError;
#[tokio::test]
async fn test_generate_session_name_success() -> Result<(), ProviderError> {
// Load .env for Databricks credentials
fn should_run_test() -> Result<(), String> {
dotenv().ok();
let has_creds =
std::env::var("DATABRICKS_HOST").is_ok() && std::env::var("DATABRICKS_TOKEN").is_ok();
if !has_creds {
println!("Skipping generate_session_name test Databricks creds not set");
return Ok(());
if std::env::var("DATABRICKS_HOST").is_err() {
return Err("Missing DATABRICKS_HOST".to_string());
}
if std::env::var("DATABRICKS_TOKEN").is_err() {
return Err("Missing DATABRICKS_TOKEN".to_string());
}
Ok(())
}
async fn _generate_session_name(messages: &[Message]) -> Result<String, ProviderError> {
let provider_name = "databricks";
let provider_config = serde_json::json!({
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
});
generate_session_name(provider_name, provider_config.into(), messages).await
}
#[tokio::test]
async fn test_generate_session_name_success() {
if should_run_test().is_err() {
println!("Skipping...");
return;
}
// Build a few messages with at least two user messages
@@ -22,7 +39,10 @@ async fn test_generate_session_name_success() -> Result<(), ProviderError> {
Message::user().with_text("Whats the weather in New York tomorrow?"),
];
let name = generate_session_name(&messages).await?;
let name = _generate_session_name(&messages)
.await
.expect("Failed to generate session name");
println!("Generated session name: {:?}", name);
// Should be non-empty and at most 4 words
@@ -34,20 +54,23 @@ async fn test_generate_session_name_success() -> Result<(), ProviderError> {
"Name must be 4 words or less, got {}: {}",
word_count,
name
);
Ok(())
)
}
#[tokio::test]
async fn test_generate_session_name_no_user() {
if should_run_test().is_err() {
println!("Skipping 'test_generate_session_name_no_user'. Databricks creds not set");
return;
}
// No user messages → expect ExecutionError
let messages = vec![
Message::assistant().with_text("System starting…"),
Message::assistant().with_text("All systems go."),
];
let err = generate_session_name(&messages).await;
let err = _generate_session_name(&messages).await;
assert!(
matches!(err, Err(ProviderError::ExecutionError(_))),
"Expected ExecutionError when there are no user messages, got: {:?}",
+35 -16
View File
@@ -6,13 +6,32 @@ use goose_llm::providers::errors::ProviderError;
use goose_llm::types::core::{Content, ToolCall};
use serde_json::json;
#[tokio::test]
async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
// Skip if no Databricks creds
fn should_run_test() -> Result<(), String> {
dotenv().ok();
if std::env::var("DATABRICKS_HOST").is_err() || std::env::var("DATABRICKS_TOKEN").is_err() {
println!("Skipping simple tooltip test Databricks creds not set");
return Ok(());
if std::env::var("DATABRICKS_HOST").is_err() {
return Err("Missing DATABRICKS_HOST".to_string());
}
if std::env::var("DATABRICKS_TOKEN").is_err() {
return Err("Missing DATABRICKS_TOKEN".to_string());
}
Ok(())
}
async fn _generate_tooltip(messages: &[Message]) -> Result<String, ProviderError> {
let provider_name = "databricks";
let provider_config = serde_json::json!({
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
});
generate_tooltip(provider_name, provider_config.into(), messages).await
}
#[tokio::test]
async fn test_generate_tooltip_simple() {
if should_run_test().is_err() {
println!("Skipping...");
return;
}
// Two plain-text messages
@@ -21,7 +40,9 @@ async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
Message::assistant().with_text("I'm fine, thanks! How can I help?"),
];
let tooltip = generate_tooltip(&messages).await?;
let tooltip = _generate_tooltip(&messages)
.await
.expect("Failed to generate tooltip");
println!("Generated tooltip: {:?}", tooltip);
assert!(!tooltip.trim().is_empty(), "Tooltip must not be empty");
@@ -29,16 +50,13 @@ async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
tooltip.len() < 100,
"Tooltip should be reasonably short (<100 chars)"
);
Ok(())
}
#[tokio::test]
async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
// Skip if no Databricks creds
dotenv().ok();
if std::env::var("DATABRICKS_HOST").is_err() || std::env::var("DATABRICKS_TOKEN").is_err() {
println!("Skipping toolbased tooltip test Databricks creds not set");
return Ok(());
async fn test_generate_tooltip_with_tools() {
if should_run_test().is_err() {
println!("Skipping...");
return;
}
// 1) Assistant message with a tool request
@@ -57,7 +75,9 @@ async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
let messages = vec![tool_req_msg, tool_resp_msg];
let tooltip = generate_tooltip(&messages).await?;
let tooltip = _generate_tooltip(&messages)
.await
.expect("Failed to generate tooltip");
println!("Generated tooltip (tools): {:?}", tooltip);
assert!(!tooltip.trim().is_empty(), "Tooltip must not be empty");
@@ -65,5 +85,4 @@ async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
tooltip.len() < 100,
"Tooltip should be reasonably short (<100 chars)"
);
Ok(())
}
+2 -2
View File
@@ -25,8 +25,8 @@ impl ProviderType {
fn create_provider(&self, cfg: ModelConfig) -> Result<Arc<dyn Provider>> {
Ok(match self {
ProviderType::OpenAi => Arc::new(OpenAiProvider::from_env(cfg)?),
ProviderType::Databricks => Arc::new(DatabricksProvider::from_env(cfg)?),
ProviderType::OpenAi => Arc::new(OpenAiProvider::from_env(cfg)),
ProviderType::Databricks => Arc::new(DatabricksProvider::from_env(cfg)),
})
}
}