Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LM Studio support #1038

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/goose-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ enum CliProviderVariant {
OpenAi,
Databricks,
Ollama,
LmStudio
}

#[tokio::main]
Expand Down
6 changes: 6 additions & 0 deletions crates/goose-server/src/routes/providers_and_keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"models": ["qwen2.5"],
"required_keys": ["OLLAMA_HOST"]
},
"lmstudio": {
"name": "LM Studio",
"description": "Lorem ipsum",
"models": ["llama-3.2-3b-instruct"],
"required_keys": ["LMSTUDIO_HOST"]
},
"openrouter": {
"name": "OpenRouter",
"description": "Lorem ipsum",
Expand Down
3 changes: 3 additions & 0 deletions crates/goose/src/providers/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::{
google::GoogleProvider,
groq::GroqProvider,
ollama::OllamaProvider,
lm_studio::LmStudioProvider,
openai::OpenAiProvider,
openrouter::OpenRouterProvider,
};
Expand All @@ -20,6 +21,7 @@ pub fn providers() -> Vec<ProviderMetadata> {
GoogleProvider::metadata(),
GroqProvider::metadata(),
OllamaProvider::metadata(),
LmStudioProvider::metadata(),
OpenAiProvider::metadata(),
OpenRouterProvider::metadata(),
]
Expand All @@ -33,6 +35,7 @@ pub fn create(name: &str, model: ModelConfig) -> Result<Box<dyn Provider + Send
"databricks" => Ok(Box::new(DatabricksProvider::from_env(model)?)),
"groq" => Ok(Box::new(GroqProvider::from_env(model)?)),
"ollama" => Ok(Box::new(OllamaProvider::from_env(model)?)),
"lmstudio" => Ok(Box::new(LmStudioProvider::from_env(model)?)),
"openrouter" => Ok(Box::new(OpenRouterProvider::from_env(model)?)),
"google" => Ok(Box::new(GoogleProvider::from_env(model)?)),
_ => Err(anyhow::anyhow!("Unknown provider: {}", name)),
Expand Down
190 changes: 190 additions & 0 deletions crates/goose/src/providers/lm_studio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage};
use super::errors::ProviderError;
use super::utils::{get_model, handle_response_openai_compat};
use crate::message::Message;
use crate::model::ModelConfig;
use crate::providers::formats::openai::{create_request, get_usage, response_to_message};
use anyhow::Result;
use async_trait::async_trait;
use mcp_core::tool::Tool;
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
use url::Url;

pub const LMSTUDIO_HOST: &str = "localhost";
pub const LMSTUDIO_DEFAULT_PORT: u16 = 1234;
pub const LMSTUDIO_DEFAULT_MODEL: &str = "default";
pub const LMSTUDIO_DOC_URL: &str = "https://lmstudio.ai/";

// Core supported parameters based on documentation
pub const LMSTUDIO_SUPPORTED_PARAMS: &[&str] = &[
"model",
"messages",
"tools",
"top_p",
"top_k",
"temperature",
"max_tokens",
"stream",
"stop",
"presence_penalty",
"frequency_penalty",
"logit_bias",
"repeat_penalty",
"seed",
];

#[derive(Debug, serde::Serialize)]
pub struct LmStudioProvider {
#[serde(skip)]
client: Client,
host: String,
model: ModelConfig,
}

impl Default for LmStudioProvider {
fn default() -> Self {
let model = ModelConfig::new(LmStudioProvider::metadata().default_model);
LmStudioProvider::from_env(model).expect("Failed to initialize LM Studio provider")
}
}

impl LmStudioProvider {
pub fn from_env(model: ModelConfig) -> Result<Self> {
let config = crate::config::Config::global();
let host: String = config
.get("LMSTUDIO_HOST")
.unwrap_or_else(|_| LMSTUDIO_HOST.to_string());

let client = Client::builder()
.timeout(Duration::from_secs(600))
.build()?;

Ok(Self {
client,
host,
model,
})
}

async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
let base = if self.host.starts_with("http://") || self.host.starts_with("https://") {
self.host.clone()
} else {
format!("http://{}", self.host)
};

let mut base_url = Url::parse(&base)
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;

if base_url.port().is_none() {
base_url.set_port(Some(LMSTUDIO_DEFAULT_PORT)).map_err(|_| {
ProviderError::RequestFailed("Failed to set default port".to_string())
})?;
}

let url = base_url.join("v1/chat/completions").map_err(|e| {
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
})?;

let response = match self.client.post(url).json(&payload).send().await {
Ok(response) => response,
Err(e) if e.is_connect() || e.is_timeout() => {
return Err(ProviderError::RequestFailed(format!(
"Failed to connect to LM Studio server: {}",
e
)))
}
Err(e) => return Err(ProviderError::RequestFailed(e.to_string())),
};

if response.status().is_client_error() {
let error_response = response.text().await.unwrap_or_default();
if error_response.contains("context window") ||
error_response.contains("context length") ||
error_response.contains("token limit") {
return Err(ProviderError::ContextLengthExceeded(error_response));
}
return Err(ProviderError::RequestFailed(error_response));
}

handle_response_openai_compat(response).await
}

fn validate_parameters(&self, payload: &Value) -> Result<(), ProviderError> {
if let Some(obj) = payload.as_object() {
for key in obj.keys() {
if !LMSTUDIO_SUPPORTED_PARAMS.contains(&key.as_str()) {
return Err(ProviderError::RequestFailed(format!(
"Unsupported parameter: {}",
key
)));
}
}
}
Ok(())
}
}

#[async_trait]
impl Provider for LmStudioProvider {
fn metadata() -> ProviderMetadata {
ProviderMetadata::new(
"lmstudio",
"LM Studio",
"Local LM Studio models with OpenAI compatibility API",
LMSTUDIO_DEFAULT_MODEL,
vec![LMSTUDIO_DEFAULT_MODEL.to_string()],
LMSTUDIO_DOC_URL,
vec![ConfigKey::new(
"LMSTUDIO_HOST",
true,
false,
Some(LMSTUDIO_HOST),
)],
)
}

fn get_model_config(&self) -> ModelConfig {
self.model.clone()
}

#[tracing::instrument(
skip(self, system, messages, tools),
fields(model_config, input, output, input_tokens, output_tokens, total_tokens)
)]
async fn complete(
&self,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<(Message, ProviderUsage), ProviderError> {
let payload = create_request(
&self.model,
system,
messages,
tools,
&super::utils::ImageFormat::OpenAi,
)?;

// Validate parameters before sending
self.validate_parameters(&payload)?;

let response = self.post(payload.clone()).await?;

// Parse response
let message = response_to_message(response.clone())?;
let usage = match get_usage(&response) {
Ok(usage) => usage,
Err(ProviderError::UsageError(e)) => {
tracing::warn!("Failed to get usage data: {}", e);
Usage::default()
}
Err(e) => return Err(e),
};
let model = get_model(&response);
super::utils::emit_debug_trace(self, &payload, &response, &usage);
Ok((message, ProviderUsage::new(model, usage)))
}
}
1 change: 1 addition & 0 deletions crates/goose/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod google;
pub mod groq;
pub mod oauth;
pub mod ollama;
pub mod lm_studio;
pub mod openai;
pub mod openrouter;
pub mod utils;
Expand Down
13 changes: 12 additions & 1 deletion crates/goose/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use dotenv::dotenv;
use goose::message::{Message, MessageContent};
use goose::providers::base::Provider;
use goose::providers::errors::ProviderError;
use goose::providers::{anthropic, azure, databricks, google, groq, ollama, openai, openrouter};
use goose::providers::{anthropic, azure, databricks, google, groq, ollama, lm_studio, openai, openrouter};
use mcp_core::content::Content;
use mcp_core::tool::Tool;
use std::collections::HashMap;
Expand Down Expand Up @@ -410,6 +410,17 @@ async fn test_ollama_provider() -> Result<()> {
.await
}

#[tokio::test]
async fn test_lm_studio_provider() -> Result<()> {
test_provider(
"LMStudio",
&["LMSTUDIO_HOST"],
None,
lm_studio::LmStudioProvider::default,
)
.await
}

#[tokio::test]
async fn test_groq_provider() -> Result<()> {
test_provider("Groq", &["GROQ_API_KEY"], None, groq::GroqProvider::default).await
Expand Down
17 changes: 15 additions & 2 deletions crates/goose/tests/truncate_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use goose::providers::base::Provider;
use goose::providers::{anthropic::AnthropicProvider, databricks::DatabricksProvider};
use goose::providers::{
azure::AzureProvider, ollama::OllamaProvider, openai::OpenAiProvider,
openrouter::OpenRouterProvider,
openrouter::OpenRouterProvider, lm_studio::LmStudioProvider,
google::GoogleProvider, groq::GroqProvider
};
use goose::providers::{google::GoogleProvider, groq::GroqProvider};

#[derive(Debug)]
enum ProviderType {
Expand All @@ -22,6 +22,7 @@ enum ProviderType {
Google,
Groq,
Ollama,
LmStudio,
OpenRouter,
}

Expand All @@ -39,6 +40,7 @@ impl ProviderType {
ProviderType::Google => &["GOOGLE_API_KEY"],
ProviderType::Groq => &["GROQ_API_KEY"],
ProviderType::Ollama => &[],
ProviderType::LmStudio => &[],
ProviderType::OpenRouter => &["OPENROUTER_API_KEY"],
}
}
Expand Down Expand Up @@ -70,6 +72,7 @@ impl ProviderType {
ProviderType::Google => Box::new(GoogleProvider::from_env(model_config)?),
ProviderType::Groq => Box::new(GroqProvider::from_env(model_config)?),
ProviderType::Ollama => Box::new(OllamaProvider::from_env(model_config)?),
ProviderType::LmStudio => Box::new(LmStudioProvider::from_env(model_config)?),
ProviderType::OpenRouter => Box::new(OpenRouterProvider::from_env(model_config)?),
})
}
Expand Down Expand Up @@ -269,4 +272,14 @@ mod tests {
})
.await
}

#[tokio::test]
async fn test_truncate_agent_with_lm_studio() -> Result<()> {
run_test_with_config(TestConfig {
provider_type: ProviderType::LmStudio,
model: "llama-3.2-3b-instruct",
context_window: 9_000,
})
.await
}
}
19 changes: 10 additions & 9 deletions documentation/docs/getting-started/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ Goose relies heavily on tool calling capabilities and currently works best with

## Available Providers

| Provider | Description | Parameters |
|-----------------------------------------------|-----------------------------------------------------|---------------------------------------|
| [Anthropic](https://www.anthropic.com/) | Offers Claude, an advanced AI model for natural language tasks. | `ANTHROPIC_API_KEY` |
| [Databricks](https://www.databricks.com/) | Unified data analytics and AI platform for building and deploying models. | `DATABRICKS_HOST`, `DATABRICKS_TOKEN` |
| [Gemini](https://ai.google.dev/gemini-api/docs) | Advanced LLMs by Google with multimodal capabilities (text, images). | `GOOGLE_API_KEY` |
| [Groq](https://groq.com/) | High-performance inference hardware and tools for LLMs. | `GROQ_API_KEY` |
| [Ollama](https://ollama.com/) | Local model runner supporting Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](/docs/getting-started/providers#local-llms-ollama).** | `OLLAMA_HOST` |
| [OpenAI](https://platform.openai.com/api-keys) | Provides gpt-4o, o1, and other advanced language models. **o1-mini and o1-preview are not supported because Goose uses tool calling.** | `OPENAI_API_KEY` |
| [OpenRouter](https://openrouter.ai/) | API gateway for unified access to various models with features like rate-limiting management. | `OPENROUTER_API_KEY` |
| Provider | Description | Parameters |
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|
| [Anthropic](https://www.anthropic.com/) | Offers Claude, an advanced AI model for natural language tasks. | `ANTHROPIC_API_KEY` |
| [Databricks](https://www.databricks.com/) | Unified data analytics and AI platform for building and deploying models. | `DATABRICKS_HOST`, `DATABRICKS_TOKEN` |
| [Gemini](https://ai.google.dev/gemini-api/docs) | Advanced LLMs by Google with multimodal capabilities (text, images). | `GOOGLE_API_KEY` |
| [Groq](https://groq.com/) | High-performance inference hardware and tools for LLMs. | `GROQ_API_KEY` |
| [Ollama](https://ollama.com/) | Local model runner supporting Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](/docs/getting-started/providers#local-llms-ollama).** | `OLLAMA_HOST` |
| [LM Studio](https://lmstudio.ai/) | Local model runner supporting Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](/docs/getting-started/providers#local-llms-lm-studio).** | `LMSTUDIO_HOST` |
| [OpenAI](https://platform.openai.com/api-keys) | Provides gpt-4o, o1, and other advanced language models. **o1-mini and o1-preview are not supported because Goose uses tool calling.** | `OPENAI_API_KEY` |
| [OpenRouter](https://openrouter.ai/) | API gateway for unified access to various models with features like rate-limiting management. | `OPENROUTER_API_KEY` |



Expand Down
6 changes: 6 additions & 0 deletions documentation/docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ Ollama provides local LLMs, which means you must first [download Ollama and run

> ExecutionError("error sending request for url (http://localhost:11434/v1/chat/completions)")

### Using LM Studio Provider

LM Studio provides local LLMs, which means you must first download LM Studio and run a model before attempting to use this provider with Goose. If you do not have the model downloaded and server running, you'll run into the following error:

> ExecutionError("error sending request for url (http://localhost:1234/v1/chat/completions)")


Another thing to note is that the DeepSeek models do not support tool calling, so all Goose [extensions must be disabled](/docs/getting-started/using-extensions#enablingdisabling-extensions) to use one of these models. Unfortunately, without the use of tools, there is not much Goose will be able to do autonomously if using DeepSeek. However, Ollama's other models such as `qwen2.5` do support tool calling and can be used with Goose extensions.

Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/components/settings/api_keys/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function isSecretKey(keyName: string): boolean {
const nonSecretKeys = [
'DATABRICKS_HOST',
'OLLAMA_HOST',
'LMSTUDIO_HOST',
'AZURE_OPENAI_ENDPOINT',
'AZURE_OPENAI_DEPLOYMENT_NAME',
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const goose_models: Model[] = [
{ id: 14, name: 'gemma-2-2b-it', provider: 'Google' },
{ id: 15, name: 'llama-3.3-70b-versatile', provider: 'Groq' },
{ id: 16, name: 'qwen2.5', provider: 'Ollama' },
{ id: 16, name: 'qwen2.5', provider: 'LMStudio' },
{ id: 17, name: 'anthropic/claude-3.5-sonnet', provider: 'OpenRouter' },
{ id: 18, name: 'gpt-4o', provider: 'Azure OpenAI' },
];
Expand All @@ -39,7 +40,8 @@ export const google_models = [

export const groq_models = ['llama-3.3-70b-versatile'];

export const ollama_mdoels = ['qwen2.5'];
export const ollama_models = ['qwen2.5'];
export const lm_studio_models = ['qwen2.5'];

export const openrouter_models = ['anthropic/claude-3.5-sonnet'];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getProviderDescription(provider) {
Databricks: 'Access models hosted on your Databricks instance',
OpenRouter: 'Access a variety of AI models through OpenRouter',
Ollama: 'Run and use open-source models locally',
LMStudio: 'Run and use open-source models locally',
};
return descriptions[provider] || `Access ${provider} models`;
}
Expand Down
Loading
Loading