Skip to content

Commit

Permalink
feat: update ui for ollama host (#912)
Browse files Browse the repository at this point in the history
  • Loading branch information
yingjiehe-xyz authored Jan 30, 2025
1 parent aea45df commit 654e5c6
Show file tree
Hide file tree
Showing 14 changed files with 125 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ use serde_json::Value;
use std::collections::HashMap;

#[derive(Serialize)]
struct SecretResponse {
struct ConfigResponse {
error: bool,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SecretRequest {
struct ConfigRequest {
key: String,
value: String,
is_secret: bool,
}

async fn store_secret(
async fn store_config(
State(state): State<AppState>,
headers: HeaderMap,
Json(request): Json<SecretRequest>,
) -> Result<Json<SecretResponse>, StatusCode> {
Json(request): Json<ConfigRequest>,
) -> Result<Json<ConfigResponse>, StatusCode> {
// Verify secret key
let secret_key = headers
.get("X-Secret-Key")
Expand All @@ -42,18 +42,18 @@ async fn store_secret(
config.set(&request.key, Value::String(request.value))
};
match result {
Ok(_) => Ok(Json(SecretResponse { error: false })),
Err(_) => Ok(Json(SecretResponse { error: true })),
Ok(_) => Ok(Json(ConfigResponse { error: false })),
Err(_) => Ok(Json(ConfigResponse { error: true })),
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProviderSecretRequest {
pub struct ProviderConfigRequest {
pub providers: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SecretStatus {
pub struct ConfigStatus {
pub is_set: bool,
pub location: Option<String>,
}
Expand All @@ -64,7 +64,7 @@ pub struct ProviderResponse {
pub name: Option<String>,
pub description: Option<String>,
pub models: Option<Vec<String>>,
pub secret_status: HashMap<String, SecretStatus>,
pub config_status: HashMap<String, ConfigStatus>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -80,30 +80,33 @@ static PROVIDER_ENV_REQUIREMENTS: Lazy<HashMap<String, ProviderConfig>> = Lazy::
serde_json::from_str(contents).expect("Failed to parse providers_and_keys.json")
});

fn check_key_status(key: &str) -> (bool, Option<String>) {
fn check_key_status(config: &Config, key: &str) -> (bool, Option<String>) {
if let Ok(_value) = std::env::var(key) {
(true, Some("env".to_string()))
} else if Config::global().get_secret::<String>(key).is_ok() {
} else if config.get::<String>(key).is_ok() {
(true, Some("yaml".to_string()))
} else if config.get_secret::<String>(key).is_ok() {
(true, Some("keyring".to_string()))
} else {
(false, None)
}
}

async fn check_provider_secrets(
Json(request): Json<ProviderSecretRequest>,
async fn check_provider_configs(
Json(request): Json<ProviderConfigRequest>,
) -> Result<Json<HashMap<String, ProviderResponse>>, StatusCode> {
let mut response = HashMap::new();
let config = Config::global();

for provider_name in request.providers {
if let Some(provider_config) = PROVIDER_ENV_REQUIREMENTS.get(&provider_name) {
let mut secret_status = HashMap::new();
let mut config_status = HashMap::new();

for key in &provider_config.required_keys {
let (key_set, key_location) = check_key_status(key);
secret_status.insert(
let (key_set, key_location) = check_key_status(config, key);
config_status.insert(
key.to_string(),
SecretStatus {
ConfigStatus {
is_set: key_set,
location: key_location,
},
Expand All @@ -117,7 +120,7 @@ async fn check_provider_secrets(
name: Some(provider_config.name.clone()),
description: Some(provider_config.description.clone()),
models: Some(provider_config.models.clone()),
secret_status,
config_status,
},
);
} else {
Expand All @@ -128,7 +131,7 @@ async fn check_provider_secrets(
name: None,
description: None,
models: None,
secret_status: HashMap::new(),
config_status: HashMap::new(),
},
);
}
Expand All @@ -138,14 +141,16 @@ async fn check_provider_secrets(
}

#[derive(Deserialize)]
struct DeleteSecretRequest {
#[serde(rename_all = "camelCase")]
struct DeleteConfigRequest {
key: String,
is_secret: bool,
}

async fn delete_secret(
async fn delete_config(
State(state): State<AppState>,
headers: HeaderMap,
Json(request): Json<DeleteSecretRequest>,
Json(request): Json<DeleteConfigRequest>,
) -> Result<StatusCode, StatusCode> {
// Verify secret key
let secret_key = headers
Expand All @@ -158,17 +163,23 @@ async fn delete_secret(
}

// Attempt to delete the key
match Config::global().delete_secret(&request.key) {
let config = Config::global();
let result = if request.is_secret {
config.delete_secret(&request.key)
} else {
config.delete(&request.key)
};
match result {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}

pub fn routes(state: AppState) -> Router {
Router::new()
.route("/secrets/providers", post(check_provider_secrets))
.route("/secrets/store", post(store_secret))
.route("/secrets/delete", delete(delete_secret))
.route("/configs/providers", post(check_provider_configs))
.route("/configs/store", post(store_config))
.route("/configs/delete", delete(delete_config))
.with_state(state)
}

Expand All @@ -179,12 +190,12 @@ mod tests {
#[tokio::test]
async fn test_unsupported_provider() {
// Setup
let request = ProviderSecretRequest {
let request = ProviderConfigRequest {
providers: vec!["unsupported_provider".to_string()],
};

// Execute
let result = check_provider_secrets(Json(request)).await;
let result = check_provider_configs(Json(request)).await;

// Assert
assert!(result.is_ok());
Expand All @@ -194,6 +205,6 @@ mod tests {
.get("unsupported_provider")
.expect("Provider should exist");
assert!(!provider_status.supported);
assert!(provider_status.secret_status.is_empty());
assert!(provider_status.config_status.is_empty());
}
}
4 changes: 2 additions & 2 deletions crates/goose-server/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Export route modules
pub mod agent;
pub mod configs;
pub mod extension;
pub mod health;
pub mod reply;
pub mod secrets;

use axum::Router;

Expand All @@ -14,5 +14,5 @@ pub fn configure(state: crate::state::AppState) -> Router {
.merge(reply::routes(state.clone()))
.merge(agent::routes(state.clone()))
.merge(extension::routes(state.clone()))
.merge(secrets::routes(state))
.merge(configs::routes(state))
}
2 changes: 1 addition & 1 deletion crates/goose-server/src/routes/providers_and_keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"name": "Ollama",
"description": "Lorem ipsum",
"models": ["qwen2.5"],
"required_keys": []
"required_keys": ["OLLAMA_HOST"]
},
"openrouter": {
"name": "OpenRouter",
Expand Down
61 changes: 52 additions & 9 deletions crates/goose/src/config/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,21 @@ impl Config {
}
}

// Save current values to the config file
fn save_values(&self, values: HashMap<String, Value>) -> Result<(), ConfigError> {
// Convert to YAML for storage
let yaml_value = serde_yaml::to_string(&values)?;

// Ensure the directory exists
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::DirectoryError(e.to_string()))?;
}

std::fs::write(&self.config_path, yaml_value)?;
Ok(())
}

// Load current secrets from the keyring
fn load_secrets(&self) -> Result<HashMap<String, Value>, ConfigError> {
let entry = Entry::new(&self.keyring_service, KEYRING_USERNAME)?;
Expand Down Expand Up @@ -231,17 +246,27 @@ impl Config {
let mut values = self.load_values()?;
values.insert(key.to_string(), value);

// Convert to YAML for storage
let yaml_value = serde_yaml::to_string(&values)?;
self.save_values(values)
}

// Ensure the directory exists
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::DirectoryError(e.to_string()))?;
}
/// Delete a configuration value in the config file.
///
/// This will immediately write the value to the config file. The value
/// can be any type that can be serialized to JSON/YAML.
///
/// Note that this does not affect environment variables - those can only
/// be set through the system environment.
///
/// # Errors
///
/// Returns a ConfigError if:
/// - There is an error reading or writing the config file
/// - There is an error serializing the value
pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
let mut values = self.load_values()?;
values.remove(key);

std::fs::write(&self.config_path, yaml_value)?;
Ok(())
self.save_values(values)
}

/// Get a secret value.
Expand Down Expand Up @@ -408,6 +433,24 @@ mod tests {
Ok(())
}

#[test]
fn test_value_management() -> Result<(), ConfigError> {
let temp_file = NamedTempFile::new().unwrap();
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;

config.set("key", Value::String("value".to_string()))?;

let value: String = config.get("key")?;
assert_eq!(value, "value");

config.delete("key")?;

let result: Result<String, ConfigError> = config.get("key");
assert!(matches!(result, Err(ConfigError::NotFound(_))));

Ok(())
}

#[test]
#[serial]
fn test_secret_management() -> Result<(), ConfigError> {
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/src/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export default function ChatWindow() {
}, []);

const storeSecret = async (key: string, value: string) => {
const response = await fetch(getApiUrl('/secrets/store'), {
const response = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
4 changes: 2 additions & 2 deletions ui/desktop/src/components/settings/api_keys/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ export interface ProviderResponse {
name?: string;
description?: string;
models?: string[];
secret_status: Record<string, SecretDetails>;
config_status: Record<string, ConfigDetails>;
}

export interface SecretDetails {
export interface ConfigDetails {
key: string;
is_set: boolean;
location?: string;
Expand Down
29 changes: 7 additions & 22 deletions ui/desktop/src/components/settings/api_keys/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Provider, ProviderResponse } from './types';
import { getApiUrl, getSecretKey } from '../../../config';
import { special_provider_cases } from '../providers/utils';

export function isSecretKey(keyName: string): boolean {
// Ollama and Databricks use host name right now and it should not be stored as secret.
Expand All @@ -10,47 +9,33 @@ export function isSecretKey(keyName: string): boolean {
export async function getActiveProviders(): Promise<string[]> {
try {
// Fetch the secrets settings
const secretsSettings = await getSecretsSettings();

// Check for special provider cases (e.g. ollama needs to be installed in Applications folder)
const specialCasesResults = await Promise.all(
Object.entries(special_provider_cases).map(async ([providerName, checkFunction]) => {
const isActive = await checkFunction(); // Dynamically re-check status
console.log(`Special case result for ${providerName}:`, isActive);
return isActive ? providerName : null;
})
);
const configSettings = await getConfigSettings();

// Extract active providers based on `is_set` in `secret_status` or providers with no keys
const activeProviders = Object.values(secretsSettings) // Convert object to array
const activeProviders = Object.values(configSettings) // Convert object to array
.filter((provider) => {
const apiKeyStatus = Object.values(provider.secret_status || {}); // Get all key statuses
const apiKeyStatus = Object.values(provider.config_status || {}); // Get all key statuses

// Include providers if:
// - They have at least one key set (`is_set: true`)
return apiKeyStatus.some((key) => key.is_set);
})
.map((provider) => provider.name || 'Unknown Provider'); // Extract provider name

// Combine active providers from secrets settings and special cases
const allActiveProviders = [
...activeProviders,
...specialCasesResults.filter((provider) => provider !== null), // Filter out null results
];
return allActiveProviders;
return activeProviders;
} catch (error) {
console.error('Failed to get active providers:', error);
return [];
}
}

export async function getSecretsSettings(): Promise<Record<string, ProviderResponse>> {
export async function getConfigSettings(): Promise<Record<string, ProviderResponse>> {
const providerList = await getProvidersList();
// Extract the list of IDs
const providerIds = providerList.map((provider) => provider.id);

// Fetch secrets state (set/unset) using the provider IDs
const response = await fetch(getApiUrl('/secrets/providers'), {
// Fetch configs state (set/unset) using the provider IDs
const response = await fetch(getApiUrl('/configs/providers'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function ConfigureBuiltInExtensionModal({
const value = envValues[envKey];
if (!value) continue;

const storeResponse = await fetch(getApiUrl('/secrets/store'), {
const storeResponse = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Loading

0 comments on commit 654e5c6

Please sign in to comment.