mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Move token limits to backend (#2484)
This commit is contained in:
@@ -10,6 +10,7 @@ use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
use goose::config::Config;
|
||||
use goose::config::{extensions::name_to_key, PermissionManager};
|
||||
use goose::config::{ExtensionConfigManager, ExtensionEntry};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::ProviderMetadata;
|
||||
use goose::providers::providers as get_providers;
|
||||
use goose::{agents::ExtensionConfig, config::permission::PermissionLevel};
|
||||
@@ -154,6 +155,14 @@ pub async fn read_config(
|
||||
) -> Result<Json<Value>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
// Special handling for model-limits
|
||||
if query.key == "model-limits" {
|
||||
let limits = ModelConfig::get_all_model_limits();
|
||||
return Ok(Json(
|
||||
serde_json::to_value(limits).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
));
|
||||
}
|
||||
|
||||
let config = Config::global();
|
||||
|
||||
match config.get(&query.key, query.is_secret) {
|
||||
@@ -481,3 +490,45 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/config/permissions", post(upsert_permissions))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_model_limits() {
|
||||
// Create test state and headers
|
||||
let test_state = AppState::new(
|
||||
Arc::new(goose::agents::Agent::default()),
|
||||
"test".to_string(),
|
||||
)
|
||||
.await;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("X-Secret-Key", "test".parse().unwrap());
|
||||
|
||||
// Execute
|
||||
let result = read_config(
|
||||
State(test_state),
|
||||
headers,
|
||||
Json(ConfigKeyQuery {
|
||||
key: "model-limits".to_string(),
|
||||
is_secret: false,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Parse the response and check the contents
|
||||
let limits: Vec<goose::model::ModelLimitConfig> =
|
||||
serde_json::from_value(response.0).unwrap();
|
||||
assert!(!limits.is_empty());
|
||||
|
||||
// Check for some expected patterns
|
||||
let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o");
|
||||
assert!(gpt4_limit.is_some());
|
||||
assert_eq!(gpt4_limit.unwrap().context_limit, 128_000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
use super::utils::verify_secret_key;
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use goose::config::Config;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ConfigResponse {
|
||||
error: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigRequest {
|
||||
key: String,
|
||||
value: String,
|
||||
is_secret: bool,
|
||||
}
|
||||
|
||||
async fn store_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<ConfigRequest>,
|
||||
) -> Result<Json<ConfigResponse>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
let config = Config::global();
|
||||
let result = if request.is_secret {
|
||||
config.set_secret(&request.key, Value::String(request.value))
|
||||
} else {
|
||||
config.set_param(&request.key, Value::String(request.value))
|
||||
};
|
||||
match result {
|
||||
Ok(_) => Ok(Json(ConfigResponse { error: false })),
|
||||
Err(_) => Ok(Json(ConfigResponse { error: true })),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProviderConfigRequest {
|
||||
pub providers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigStatus {
|
||||
pub is_set: bool,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProviderResponse {
|
||||
pub supported: bool,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub models: Option<Vec<String>>,
|
||||
pub config_status: HashMap<String, ConfigStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ProviderConfig {
|
||||
name: String,
|
||||
description: String,
|
||||
models: Vec<String>,
|
||||
required_keys: Vec<String>,
|
||||
}
|
||||
|
||||
static PROVIDER_ENV_REQUIREMENTS: Lazy<HashMap<String, ProviderConfig>> = Lazy::new(|| {
|
||||
let contents = include_str!("providers_and_keys.json");
|
||||
serde_json::from_str(contents).expect("Failed to parse providers_and_keys.json")
|
||||
});
|
||||
|
||||
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.get_param::<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_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 config_status = HashMap::new();
|
||||
|
||||
for key in &provider_config.required_keys {
|
||||
let (key_set, key_location) = check_key_status(config, key);
|
||||
config_status.insert(
|
||||
key.to_string(),
|
||||
ConfigStatus {
|
||||
is_set: key_set,
|
||||
location: key_location,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
response.insert(
|
||||
provider_name,
|
||||
ProviderResponse {
|
||||
supported: true,
|
||||
name: Some(provider_config.name.clone()),
|
||||
description: Some(provider_config.description.clone()),
|
||||
models: Some(provider_config.models.clone()),
|
||||
config_status,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
response.insert(
|
||||
provider_name,
|
||||
ProviderResponse {
|
||||
supported: false,
|
||||
name: None,
|
||||
description: None,
|
||||
models: None,
|
||||
config_status: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetConfigQuery {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetConfigResponse {
|
||||
value: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<GetConfigQuery>,
|
||||
) -> Result<Json<GetConfigResponse>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
// Fetch the configuration value. Right now we don't allow get a secret.
|
||||
let config = Config::global();
|
||||
let value = if let Ok(config_value) = config.get_param::<String>(&query.key) {
|
||||
Some(config_value)
|
||||
} else {
|
||||
std::env::var(&query.key).ok()
|
||||
};
|
||||
|
||||
// Return the value
|
||||
Ok(Json(GetConfigResponse { value }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteConfigRequest {
|
||||
key: String,
|
||||
is_secret: bool,
|
||||
}
|
||||
|
||||
async fn delete_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<DeleteConfigRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
// Attempt to delete the 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: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/configs/providers", post(check_provider_configs))
|
||||
.route("/configs/get", get(get_config))
|
||||
.route("/configs/store", post(store_config))
|
||||
.route("/configs/delete", delete(delete_config))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unsupported_provider() {
|
||||
// Setup
|
||||
let request = ProviderConfigRequest {
|
||||
providers: vec!["unsupported_provider".to_string()],
|
||||
};
|
||||
|
||||
// Execute
|
||||
let result = check_provider_configs(Json(request)).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok());
|
||||
let Json(response) = result.unwrap();
|
||||
|
||||
let provider_status = response
|
||||
.get("unsupported_provider")
|
||||
.expect("Provider should exist");
|
||||
assert!(!provider_status.supported);
|
||||
assert!(provider_status.config_status.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Export route modules
|
||||
pub mod agent;
|
||||
pub mod config_management;
|
||||
pub mod configs;
|
||||
pub mod context;
|
||||
pub mod extension;
|
||||
pub mod health;
|
||||
@@ -21,7 +20,6 @@ pub fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
.merge(agent::routes(state.clone()))
|
||||
.merge(context::routes(state.clone()))
|
||||
.merge(extension::routes(state.clone()))
|
||||
.merge(configs::routes(state.clone()))
|
||||
.merge(config_management::routes(state.clone()))
|
||||
.merge(recipe::routes(state.clone()))
|
||||
.merge(session::routes(state.clone()))
|
||||
|
||||
+62
-22
@@ -1,4 +1,6 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const DEFAULT_CONTEXT_LIMIT: usize = 128_000;
|
||||
|
||||
@@ -6,6 +8,32 @@ const DEFAULT_CONTEXT_LIMIT: usize = 128_000;
|
||||
pub const GPT_4O_TOKENIZER: &str = "Xenova--gpt-4o";
|
||||
pub const CLAUDE_TOKENIZER: &str = "Xenova--claude-tokenizer";
|
||||
|
||||
// Define the model limits as a static HashMap for reuse
|
||||
static MODEL_SPECIFIC_LIMITS: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
// OpenAI models, https://platform.openai.com/docs/models#models-overview
|
||||
map.insert("gpt-4o", 128_000);
|
||||
map.insert("gpt-4-turbo", 128_000);
|
||||
map.insert("o1-mini", 128_000);
|
||||
map.insert("o1-preview", 128_000);
|
||||
map.insert("o1", 200_000);
|
||||
map.insert("o3-mini", 200_000);
|
||||
map.insert("gpt-4.1", 1_000_000);
|
||||
map.insert("gpt-4-1", 1_000_000);
|
||||
|
||||
// Anthropic models, https://docs.anthropic.com/en/docs/about-claude/models
|
||||
map.insert("claude-3", 200_000);
|
||||
|
||||
// Google models, https://ai.google/get-started/our-models/
|
||||
map.insert("gemini-2.5", 1_000_000);
|
||||
map.insert("gemini-2-5", 1_000_000);
|
||||
|
||||
// Meta Llama models, https://github.com/meta-llama/llama-models/tree/main?tab=readme-ov-file#llama-models-1
|
||||
map.insert("llama3.2", 128_000);
|
||||
map.insert("llama3.3", 128_000);
|
||||
map
|
||||
});
|
||||
|
||||
/// Configuration for model-specific settings and limits
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelConfig {
|
||||
@@ -27,6 +55,13 @@ pub struct ModelConfig {
|
||||
pub toolshim_model: Option<String>,
|
||||
}
|
||||
|
||||
/// Struct to represent model pattern matches and their limits
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelLimitConfig {
|
||||
pub pattern: String,
|
||||
pub context_limit: usize,
|
||||
}
|
||||
|
||||
impl ModelConfig {
|
||||
/// Create a new ModelConfig with the specified model name
|
||||
///
|
||||
@@ -70,29 +105,23 @@ impl ModelConfig {
|
||||
|
||||
/// Get model-specific context limit based on model name
|
||||
fn get_model_specific_limit(model_name: &str) -> Option<usize> {
|
||||
// Implement some sensible defaults
|
||||
match model_name {
|
||||
// OpenAI models, https://platform.openai.com/docs/models#models-overview
|
||||
name if name.contains("gpt-4o") => Some(128_000),
|
||||
name if name.contains("gpt-4-turbo") => Some(128_000),
|
||||
name if name.contains("o1-mini") || name.contains("o1-preview") => Some(128_000),
|
||||
name if name.contains("o1") => Some(200_000),
|
||||
name if name.contains("o3-mini") => Some(200_000),
|
||||
name if name.contains("gpt-4.1") => Some(1_000_000),
|
||||
name if name.contains("gpt-4-1") => Some(1_000_000),
|
||||
|
||||
// Anthropic models, https://docs.anthropic.com/en/docs/about-claude/models
|
||||
name if name.contains("claude-3") => Some(200_000),
|
||||
|
||||
// Google models, https://ai.google/get-started/our-models/
|
||||
name if name.contains("gemini-2.5") => Some(1_000_000),
|
||||
name if name.contains("gemini-2-5") => Some(1_000_000),
|
||||
|
||||
// Meta Llama models, https://github.com/meta-llama/llama-models/tree/main?tab=readme-ov-file#llama-models-1
|
||||
name if name.contains("llama3.2") => Some(128_000),
|
||||
name if name.contains("llama3.3") => Some(128_000),
|
||||
_ => None,
|
||||
for (pattern, &limit) in MODEL_SPECIFIC_LIMITS.iter() {
|
||||
if model_name.contains(pattern) {
|
||||
return Some(limit);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get all model pattern matches and their limits
|
||||
pub fn get_all_model_limits() -> Vec<ModelLimitConfig> {
|
||||
MODEL_SPECIFIC_LIMITS
|
||||
.iter()
|
||||
.map(|(&pattern, &context_limit)| ModelLimitConfig {
|
||||
pattern: pattern.to_string(),
|
||||
context_limit,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Set an explicit context limit
|
||||
@@ -215,4 +244,15 @@ mod tests {
|
||||
let config = ModelConfig::new("test-model".to_string());
|
||||
assert_eq!(config.temperature, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_all_model_limits() {
|
||||
let limits = ModelConfig::get_all_model_limits();
|
||||
assert!(!limits.is_empty());
|
||||
|
||||
// Test that we can find specific patterns
|
||||
let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o");
|
||||
assert!(gpt4_limit.is_some());
|
||||
assert_eq!(gpt4_limit.unwrap().context_limit, 128_000);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-309
@@ -1,26 +1,18 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { addExtensionFromDeepLink } from './extensions';
|
||||
import { openSharedSessionFromDeepLink } from './sessionLinks';
|
||||
import { getStoredModel } from './utils/providerUtils';
|
||||
import { getStoredProvider, initializeSystem } from './utils/providerUtils';
|
||||
import { useModel } from './components/settings/models/ModelContext';
|
||||
import { useRecentModels } from './components/settings/models/RecentModels';
|
||||
import { createSelectedModel } from './components/settings/models/utils';
|
||||
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
|
||||
import { initializeSystem } from './utils/providerUtils';
|
||||
import { ErrorUI } from './components/ErrorBoundary';
|
||||
import { ConfirmationModal } from './components/ui/ConfirmationModal';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { toastService } from './toasts';
|
||||
import { settingsV2Enabled } from './flags';
|
||||
import { extractExtensionName } from './components/settings/extensions/utils';
|
||||
import { GoosehintsModal } from './components/GoosehintsModal';
|
||||
import { SessionDetails } from './sessions';
|
||||
|
||||
import WelcomeView from './components/WelcomeView';
|
||||
import ChatView from './components/ChatView';
|
||||
import SuspenseLoader from './suspense-loader';
|
||||
import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||
import { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||
import SettingsViewV2 from './components/settings_v2/SettingsView';
|
||||
import MoreModelsView from './components/settings/models/MoreModelsView';
|
||||
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
||||
@@ -29,7 +21,6 @@ import SharedSessionView from './components/sessions/SharedSessionView';
|
||||
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
|
||||
import RecipeEditor from './components/RecipeEditor';
|
||||
import { useChat } from './hooks/useChat';
|
||||
import { addExtension as addExtensionDirect, FullExtensionConfig } from './extensions';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig, MalformedConfigError } from './components/ConfigContext';
|
||||
@@ -101,7 +92,7 @@ export default function App() {
|
||||
const [extensionConfirmLabel, setExtensionConfirmLabel] = useState<string>('');
|
||||
const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>('');
|
||||
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
|
||||
const { getExtensions, addExtension, disableAllExtensions, read } = useConfig();
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
const initAttemptedRef = useRef(false);
|
||||
|
||||
// Utility function to extract the command from the link
|
||||
@@ -123,158 +114,7 @@ export default function App() {
|
||||
setInternalView({ view, viewOptions });
|
||||
};
|
||||
|
||||
const disableAllStoredExtensions = () => {
|
||||
const userSettingsStr = localStorage.getItem('user_settings');
|
||||
if (!userSettingsStr) return;
|
||||
|
||||
try {
|
||||
const userSettings = JSON.parse(userSettingsStr);
|
||||
// Store original state before modifying
|
||||
localStorage.setItem('user_settings_backup', userSettingsStr);
|
||||
console.log('Backing up user_settings');
|
||||
|
||||
// Disable all extensions
|
||||
userSettings.extensions = userSettings.extensions.map((ext) => ({
|
||||
...ext,
|
||||
enabled: false,
|
||||
}));
|
||||
|
||||
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
||||
console.log('Disabled all stored extensions');
|
||||
window.electron.emit('settings-updated');
|
||||
} catch (error) {
|
||||
console.error('Error disabling stored extensions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to restore original extension states for new non-recipe windows
|
||||
const restoreOriginalExtensionStates = () => {
|
||||
const backupStr = localStorage.getItem('user_settings_backup');
|
||||
if (backupStr) {
|
||||
localStorage.setItem('user_settings', backupStr);
|
||||
console.log('Restored original extension states');
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserSettingsWithConfig = (extensions: FullExtensionConfig[]) => {
|
||||
try {
|
||||
const userSettingsStr = localStorage.getItem('user_settings');
|
||||
const userSettings = userSettingsStr ? JSON.parse(userSettingsStr) : { extensions: [] };
|
||||
|
||||
// For each extension in the passed in config
|
||||
extensions.forEach((newExtension) => {
|
||||
// Find if this extension already exists
|
||||
const existingIndex = userSettings.extensions.findIndex(
|
||||
(ext) => ext.id === newExtension.id
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Extension exists - just set its enabled to true
|
||||
userSettings.extensions[existingIndex].enabled = true;
|
||||
} else {
|
||||
// Extension is new - add it to the array
|
||||
userSettings.extensions.push({
|
||||
...newExtension,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
||||
console.log('Updated user settings with new/enabled extensions:', userSettings.extensions);
|
||||
|
||||
// Notify any listeners (like the settings page) that settings have changed
|
||||
window.electron.emit('settings-updated');
|
||||
} catch (error) {
|
||||
console.error('Error updating user settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const enableRecipeConfigExtensions = async (extensions: FullExtensionConfig[]) => {
|
||||
if (!extensions?.length) {
|
||||
console.log('No extensions to enable from bot config');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Enabling ${extensions.length} extensions from bot config:`, extensions);
|
||||
|
||||
disableAllStoredExtensions();
|
||||
|
||||
// Wait for initial server readiness
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
console.log(`Enabling extension: ${extension.name}`);
|
||||
const extensionConfig = {
|
||||
...extension,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Try to add the extension
|
||||
const response = await addExtensionDirect(extensionConfig, false);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`Failed to enable extension ${extension.name}: Server returned ${response.status}`
|
||||
);
|
||||
// If it's a 428, retry once
|
||||
if (response.status === 428) {
|
||||
console.log('Server not ready, waiting and will retry...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
try {
|
||||
await addExtensionDirect(extensionConfig, true);
|
||||
console.log(`Successfully enabled extension ${extension.name} on retry`);
|
||||
} catch (retryError) {
|
||||
console.error(`Failed to enable extension ${extension.name} on retry:`, retryError);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
updateUserSettingsWithConfig(extensions);
|
||||
|
||||
console.log(`Successfully enabled extension: ${extension.name}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to enable extension ${extension.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Finished enabling bot config extensions');
|
||||
};
|
||||
|
||||
const _enableRecipeConfigExtensionsV2 = useCallback(
|
||||
async (extensions: FullExtensionConfig[]) => {
|
||||
if (!extensions?.length) {
|
||||
console.log('No extensions to enable from bot config');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await disableAllExtensions();
|
||||
console.log('Disabled all existing extensions');
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
console.log(`Enabling extension: ${extension.name}`);
|
||||
await addExtension(extension.name, extension, true);
|
||||
} catch (error) {
|
||||
console.error(`Failed to enable extension ${extension.name}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to enable bot extensions');
|
||||
}
|
||||
console.log('Finished enabling bot config extensions');
|
||||
},
|
||||
[disableAllExtensions, addExtension]
|
||||
);
|
||||
|
||||
// settings v2 initialization
|
||||
useEffect(() => {
|
||||
if (!settingsV2Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against multiple initialization attempts
|
||||
if (initAttemptedRef.current) {
|
||||
console.log('Initialization already attempted, skipping...');
|
||||
@@ -326,12 +166,6 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle bot config extensions first
|
||||
// if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
|
||||
// console.log('Found extensions in bot config:', recipeConfig.extensions);
|
||||
// await enableRecipeConfigExtensionsV2(recipeConfig.extensions);
|
||||
// }
|
||||
|
||||
const config = window.electron.getConfig();
|
||||
|
||||
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
|
||||
@@ -622,11 +456,7 @@ export default function App() {
|
||||
console.log(`Confirming installation of extension from: ${pendingLink}`);
|
||||
setModalVisible(false); // Dismiss modal immediately
|
||||
try {
|
||||
if (settingsV2Enabled) {
|
||||
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
|
||||
} else {
|
||||
await addExtensionFromDeepLink(pendingLink, setView);
|
||||
}
|
||||
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
|
||||
console.log('Extension installation successful');
|
||||
} catch (error) {
|
||||
console.error('Failed to add extension:', error);
|
||||
@@ -648,112 +478,6 @@ export default function App() {
|
||||
setPendingLink(null);
|
||||
};
|
||||
|
||||
// TODO: remove -- careful removal of these and the useEffect below breaks
|
||||
// reloading to chat view using stored provider
|
||||
const { switchModel } = useModel(); // TODO: remove
|
||||
const { addRecentModel } = useRecentModels(); // TODO: remove
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const recipeConfig = window.appConfig.get('recipeConfig');
|
||||
|
||||
if (settingsV2Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Initializing app with settings v1`);
|
||||
|
||||
// Handle bot config extensions first
|
||||
if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
|
||||
console.log('Found extensions in bot config:', recipeConfig.extensions);
|
||||
enableRecipeConfigExtensions(recipeConfig.extensions);
|
||||
}
|
||||
|
||||
// If we have a specific view type in the URL, use that and skip provider detection
|
||||
if (viewType) {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
console.log('Setting view to recipeEditor with config:', recipeConfig);
|
||||
setView('recipeEditor', { config: recipeConfig });
|
||||
} else {
|
||||
setView(viewType as View);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if not in any of the states above (in a regular chat)
|
||||
if (!recipeConfig) {
|
||||
restoreOriginalExtensionStates();
|
||||
}
|
||||
|
||||
console.log(`Initializing app with settings v1`);
|
||||
|
||||
// Attempt to detect config for a stored provider
|
||||
const detectStoredProvider = () => {
|
||||
try {
|
||||
const config = window.electron.getConfig();
|
||||
console.log('Loaded config:', JSON.stringify(config));
|
||||
|
||||
const storedProvider = getStoredProvider(config);
|
||||
console.log('Stored provider:', storedProvider);
|
||||
|
||||
if (storedProvider) {
|
||||
setView('chat');
|
||||
} else {
|
||||
setView('welcome');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DETECTION ERROR:', error);
|
||||
setFatalError(`Config detection error: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize system if we have a stored provider
|
||||
const setupStoredProvider = async () => {
|
||||
try {
|
||||
const config = window.electron.getConfig();
|
||||
|
||||
if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) {
|
||||
console.log('using GOOSE_PROVIDER and GOOSE_MODEL from config');
|
||||
await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL);
|
||||
return;
|
||||
}
|
||||
|
||||
const storedProvider = getStoredProvider(config);
|
||||
const storedModel = getStoredModel();
|
||||
|
||||
if (storedProvider) {
|
||||
try {
|
||||
await initializeSystem(storedProvider, storedModel);
|
||||
console.log('Setup using locally stored provider:', storedProvider);
|
||||
console.log('Setup using locally stored model:', storedModel);
|
||||
|
||||
if (!storedModel) {
|
||||
const modelName = getDefaultModel(storedProvider.toLowerCase());
|
||||
const model = createSelectedModel(storedProvider.toLowerCase(), modelName);
|
||||
switchModel(model);
|
||||
addRecentModel(model);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize with stored provider:', error);
|
||||
setFatalError(`Initialization failed: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SETUP ERROR:', error);
|
||||
setFatalError(`Setup error: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the functions with better error handling
|
||||
detectStoredProvider();
|
||||
setupStoredProvider().catch((error) => {
|
||||
console.error('ASYNC SETUP ERROR:', error);
|
||||
setFatalError(`Async setup error: ${error.message || 'Unknown error'}`);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (fatalError) {
|
||||
return <ErrorUI error={new Error(fatalError)} />;
|
||||
}
|
||||
@@ -796,34 +520,18 @@ export default function App() {
|
||||
<div className="titlebar-drag-region" />
|
||||
<div>
|
||||
{view === 'loading' && <SuspenseLoader />}
|
||||
{view === 'welcome' &&
|
||||
(settingsV2Enabled ? (
|
||||
<ProviderSettings onClose={() => setView('chat')} isOnboarding={true} />
|
||||
) : (
|
||||
<WelcomeView
|
||||
onSubmit={() => {
|
||||
setView('chat');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{view === 'settings' &&
|
||||
(settingsV2Enabled ? (
|
||||
<SettingsViewV2
|
||||
onClose={() => {
|
||||
setView('chat');
|
||||
}}
|
||||
setView={setView}
|
||||
viewOptions={viewOptions as SettingsViewOptions}
|
||||
/>
|
||||
) : (
|
||||
<SettingsView
|
||||
onClose={() => {
|
||||
setView('chat');
|
||||
}}
|
||||
setView={setView}
|
||||
viewOptions={viewOptions as SettingsViewOptions}
|
||||
/>
|
||||
))}
|
||||
{view === 'welcome' && (
|
||||
<ProviderSettings onClose={() => setView('chat')} isOnboarding={true} />
|
||||
)}
|
||||
{view === 'settings' && (
|
||||
<SettingsViewV2
|
||||
onClose={() => {
|
||||
setView('chat');
|
||||
}}
|
||||
setView={setView}
|
||||
viewOptions={viewOptions as SettingsViewOptions}
|
||||
/>
|
||||
)}
|
||||
{view === 'moreModels' && (
|
||||
<MoreModelsView
|
||||
onClose={() => {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useModel } from '../settings/models/ModelContext';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { AlertType, useAlerts } from '../alerts';
|
||||
import { useToolCount } from '../alerts/useToolCount';
|
||||
import BottomMenuAlertPopover from './BottomMenuAlertPopover';
|
||||
import { ModelRadioList } from '../settings/models/ModelRadioList';
|
||||
import { ChevronUp, ChevronDown } from '../icons';
|
||||
import type { View, ViewOptions } from '../../App';
|
||||
import { settingsV2Enabled } from '../../flags';
|
||||
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
|
||||
import ModelsBottomBar from '../settings_v2/models/bottom_bar/ModelsBottomBar';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
@@ -19,6 +15,11 @@ const TOKEN_LIMIT_DEFAULT = 128000; // fallback for custom models that the backe
|
||||
const TOKEN_WARNING_THRESHOLD = 0.8; // warning shows at 80% of the token limit
|
||||
const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning
|
||||
|
||||
interface ModelLimit {
|
||||
pattern: string;
|
||||
context_limit: number;
|
||||
}
|
||||
|
||||
export default function BottomMenu({
|
||||
setView,
|
||||
numTokens = 0,
|
||||
@@ -40,40 +41,28 @@ export default function BottomMenu({
|
||||
const { getProviders, read } = useConfig();
|
||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||
|
||||
// Model-specific token limits that match the backend implementation
|
||||
const MODEL_SPECIFIC_LIMITS: { [key: string]: number } = {
|
||||
// OpenAI models
|
||||
'gpt-4o': 128_000,
|
||||
'gpt-4-turbo': 128_000,
|
||||
'o1-mini': 128_000,
|
||||
'o1-preview': 128_000,
|
||||
o1: 200_000,
|
||||
'o3-mini': 200_000,
|
||||
'gpt-4.1': 1_000_000,
|
||||
'gpt-4-1': 1_000_000,
|
||||
|
||||
// Anthropic models
|
||||
'claude-3': 200_000,
|
||||
|
||||
// Google models
|
||||
'gemini-2.5': 1_000_000,
|
||||
'gemini-2-5': 1_000_000,
|
||||
|
||||
// Meta Llama models
|
||||
'llama3.2': 128_000,
|
||||
'llama3.3': 128_000,
|
||||
// Load model limits from the API
|
||||
const getModelLimits = async () => {
|
||||
try {
|
||||
const response = await read('model-limits', false);
|
||||
if (response) {
|
||||
// The response is already parsed, no need for JSON.parse
|
||||
return response as ModelLimit[];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching model limits:', err);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Helper function to replicate Rust's get_model_specific_limit logic
|
||||
function getModelSpecificLimit(modelName: string): number | null {
|
||||
// Check each pattern against the model name
|
||||
for (const [pattern, limit] of Object.entries(MODEL_SPECIFIC_LIMITS)) {
|
||||
if (modelName.toLowerCase().includes(pattern.toLowerCase())) {
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Helper function to find model limit using pattern matching
|
||||
const findModelLimit = (modelName: string, modelLimits: ModelLimit[]): number | null => {
|
||||
if (!modelName) return null;
|
||||
const matchingLimit = modelLimits.find((limit) =>
|
||||
modelName.toLowerCase().includes(limit.pattern.toLowerCase())
|
||||
);
|
||||
return matchingLimit ? matchingLimit.context_limit : null;
|
||||
};
|
||||
|
||||
// Load providers and get current model's token limit
|
||||
const loadProviderDetails = async () => {
|
||||
@@ -90,7 +79,7 @@ export default function BottomMenu({
|
||||
// Find the provider details for the current provider
|
||||
const currentProvider = providers.find((p) => p.name === provider);
|
||||
if (currentProvider?.metadata?.known_models) {
|
||||
// Find the model's token limit
|
||||
// Find the model's token limit from the backend response
|
||||
const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model);
|
||||
if (modelConfig?.context_limit) {
|
||||
setTokenLimit(modelConfig.context_limit);
|
||||
@@ -98,15 +87,15 @@ export default function BottomMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use the pattern matching logic if no exact match was found
|
||||
const fallbackLimit = getModelSpecificLimit(model);
|
||||
// Fallback: Use pattern matching logic if no exact model match was found
|
||||
const modelLimit = await getModelLimits();
|
||||
const fallbackLimit = findModelLimit(model as string, modelLimit);
|
||||
if (fallbackLimit !== null) {
|
||||
console.log(`Using fallback token limit for model ${model}: ${fallbackLimit}`);
|
||||
setTokenLimit(fallbackLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no match found, use the default
|
||||
// If no match found, use the default model limit
|
||||
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
||||
} catch (err) {
|
||||
console.error('Error loading providers or token limit:', err);
|
||||
@@ -199,76 +188,7 @@ export default function BottomMenu({
|
||||
{<BottomMenuAlertPopover alerts={alerts} />}
|
||||
|
||||
{/* Model Selector Dropdown */}
|
||||
{settingsV2Enabled ? (
|
||||
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
|
||||
) : (
|
||||
<div className="relative flex items-center ml-0 mr-4" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
|
||||
>
|
||||
<span>{(currentModel?.alias ?? currentModel?.name) || 'Select Model'}</span>
|
||||
{isModelMenuOpen ? (
|
||||
<ChevronDown className="w-4 h-4 ml-1" />
|
||||
) : (
|
||||
<ChevronUp className="w-4 h-4 ml-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isModelMenuOpen && (
|
||||
<div className="absolute bottom-[24px] right-0 w-[300px] bg-bgApp rounded-lg border border-borderSubtle">
|
||||
<div className="">
|
||||
<ModelRadioList
|
||||
className="divide-y divide-borderSubtle"
|
||||
renderItem={({ model, isSelected, onSelect }) => (
|
||||
<label key={model.alias ?? model.name} className="block cursor-pointer">
|
||||
<div
|
||||
className="flex items-center justify-between p-2 text-textStandard hover:bg-bgSubtle transition-colors"
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm ">{model.alias ?? model.name}</p>
|
||||
<p className="text-xs text-textSubtle">
|
||||
{model.subtext ?? model.provider}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="flex items-center justify-between text-textStandard p-2 cursor-pointer hover:bg-bgStandard
|
||||
border-t border-borderSubtle mt-2"
|
||||
onClick={() => {
|
||||
setIsModelMenuOpen(false);
|
||||
setView('settings');
|
||||
}}
|
||||
>
|
||||
<span className="text-sm">Tools and Settings</span>
|
||||
<Sliders className="w-4 h-4 ml-2 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-[1px] h-4 bg-borderSubtle mx-2" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { getApiUrl, getSecretKey } from '../../config';
|
||||
import { all_goose_modes, ModeSelectionItem } from '../settings_v2/mode/ModeSelectionItem';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
import { settingsV2Enabled } from '../../flags';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { Orbit } from 'lucide-react';
|
||||
|
||||
@@ -18,23 +16,8 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp
|
||||
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
if (!settingsV2Enabled) {
|
||||
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
if (value) {
|
||||
setGooseMode(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
if (mode) {
|
||||
setGooseMode(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -86,29 +69,12 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settingsV2Enabled) {
|
||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: 'GOOSE_MODE',
|
||||
value: newMode,
|
||||
isSecret: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!storeResponse.ok) {
|
||||
const errorText = await storeResponse.text();
|
||||
console.error('Store response error:', errorText);
|
||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
||||
}
|
||||
setGooseMode(newMode);
|
||||
} else {
|
||||
try {
|
||||
await upsert('GOOSE_MODE', newMode, false);
|
||||
setGooseMode(newMode);
|
||||
} catch (error) {
|
||||
console.error('Error updating goose mode:', error);
|
||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,14 +91,6 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp
|
||||
>
|
||||
<span className="pr-1.5">{getValueByKey(gooseMode).toLowerCase()}</span>
|
||||
<Orbit />
|
||||
{/*<span className="truncate max-w-[170px] md:max-w-[200px] lg:max-w-[380px]">*/}
|
||||
{/* Goose Mode: {getValueByKey(gooseMode)}*/}
|
||||
{/*</span>*/}
|
||||
{/*{isGooseModeMenuOpen ? (*/}
|
||||
{/* <ChevronDown className="w-4 h-4 ml-1" />*/}
|
||||
{/*) : (*/}
|
||||
{/* <ChevronUp className="w-4 h-4 ml-1" />*/}
|
||||
{/*)}*/}
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react';
|
||||
import { ChatSmart, Idea, Refresh, Time, Send, Settings } from '../icons';
|
||||
import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
import { settingsV2Enabled } from '../../flags';
|
||||
import { ViewOptions, View } from '../../App';
|
||||
|
||||
interface MenuButtonProps {
|
||||
@@ -270,40 +269,21 @@ export default function MoreMenu({
|
||||
|
||||
<ThemeSelect themeMode={themeMode} onThemeChange={handleThemeChange} />
|
||||
|
||||
{settingsV2Enabled && (
|
||||
<MenuButton
|
||||
data-testid="reset-provider-button"
|
||||
onClick={async () => {
|
||||
await remove('GOOSE_PROVIDER', false);
|
||||
await remove('GOOSE_MODEL', false);
|
||||
setOpen(false);
|
||||
setView('welcome');
|
||||
}}
|
||||
danger
|
||||
subtitle="Clear selected model and restart (alpha)"
|
||||
icon={<Refresh className="w-4 h-4 text-textStandard" />}
|
||||
className="border-b-0"
|
||||
>
|
||||
Reset provider and model
|
||||
</MenuButton>
|
||||
)}
|
||||
|
||||
{!settingsV2Enabled && (
|
||||
<MenuButton
|
||||
data-testid="reset-provider-button"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('GOOSE_PROVIDER');
|
||||
setOpen(false);
|
||||
window.electron.createChatWindow();
|
||||
}}
|
||||
danger
|
||||
subtitle="Clear selected model and restart"
|
||||
icon={<Refresh className="w-4 h-4 text-textStandard" />}
|
||||
className="border-b-0"
|
||||
>
|
||||
Reset provider and model
|
||||
</MenuButton>
|
||||
)}
|
||||
<MenuButton
|
||||
data-testid="reset-provider-button"
|
||||
onClick={async () => {
|
||||
await remove('GOOSE_PROVIDER', false);
|
||||
await remove('GOOSE_MODEL', false);
|
||||
setOpen(false);
|
||||
setView('welcome');
|
||||
}}
|
||||
danger
|
||||
subtitle="Clear selected model and restart (alpha)"
|
||||
icon={<Refresh className="w-4 h-4 text-textStandard" />}
|
||||
className="border-b-0"
|
||||
>
|
||||
Reset provider and model
|
||||
</MenuButton>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</>
|
||||
|
||||
@@ -1,61 +1,40 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
|
||||
export const ModeSelection = () => {
|
||||
const [currentMode, setCurrentMode] = useState('auto');
|
||||
const [previousApproveModel, setPreviousApproveModel] = useState('');
|
||||
const { read, upsert } = useConfig();
|
||||
|
||||
const handleModeChange = async (newMode: string) => {
|
||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: 'GOOSE_MODE',
|
||||
value: newMode,
|
||||
isSecret: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!storeResponse.ok) {
|
||||
const errorText = await storeResponse.text();
|
||||
console.error('Store response error:', errorText);
|
||||
try {
|
||||
await upsert('GOOSE_MODE', newMode, false);
|
||||
// Only track the previous approve if current mode is approve related but new mode is not.
|
||||
if (currentMode.includes('approve') && !newMode.includes('approve')) {
|
||||
setPreviousApproveModel(currentMode);
|
||||
}
|
||||
setCurrentMode(newMode);
|
||||
} catch (error) {
|
||||
console.error('Error updating goose mode:', error);
|
||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
||||
}
|
||||
// Only track the previous approve if current mode is approve related but new mode is not.
|
||||
if (currentMode.includes('approve') && !newMode.includes('approve')) {
|
||||
setPreviousApproveModel(currentMode);
|
||||
}
|
||||
setCurrentMode(newMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentMode = async () => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
if (value) {
|
||||
setCurrentMode(value);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
if (mode) {
|
||||
setCurrentMode(mode);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
}
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentMode();
|
||||
}, []);
|
||||
}, [fetchCurrentMode]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { ExtensionConfig } from '../../../api/types.gen';
|
||||
import type { ExtensionConfig } from '../../../api';
|
||||
import { toastService } from '../../../toasts';
|
||||
import { activateExtension } from './extension-manager';
|
||||
import { DEFAULT_EXTENSION_TIMEOUT, nameToKey } from './utils';
|
||||
import { settingsV2Enabled } from '../../../flags';
|
||||
import { DEFAULT_EXTENSION_TIMEOUT } from './utils';
|
||||
|
||||
/**
|
||||
* Build an extension config for stdio from the deeplink URL
|
||||
@@ -129,11 +128,7 @@ export async function addExtensionFromDeepLink(
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (config.envs && Object.keys(config.envs).length > 0) {
|
||||
console.log('Environment variables required, redirecting to settings');
|
||||
if (settingsV2Enabled) {
|
||||
setView('settings', { deepLinkConfig: config, showEnvVars: true });
|
||||
} else {
|
||||
setView('settings', { extensionId: nameToKey(name), showEnvVars: true });
|
||||
}
|
||||
setView('settings', { deepLinkConfig: config, showEnvVars: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
||||
import { View, ViewOptions } from '../../../App';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
|
||||
interface ModeSectionProps {
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
@@ -9,53 +9,32 @@ interface ModeSectionProps {
|
||||
|
||||
export const ModeSection = ({ setView }: ModeSectionProps) => {
|
||||
const [currentMode, setCurrentMode] = useState('auto');
|
||||
const { read, upsert } = useConfig();
|
||||
|
||||
const handleModeChange = async (newMode: string) => {
|
||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: 'GOOSE_MODE',
|
||||
value: newMode,
|
||||
isSecret: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!storeResponse.ok) {
|
||||
const errorText = await storeResponse.text();
|
||||
console.error('Store response error:', errorText);
|
||||
try {
|
||||
await upsert('GOOSE_MODE', newMode, false);
|
||||
setCurrentMode(newMode);
|
||||
} catch (error) {
|
||||
console.error('Error updating goose mode:', error);
|
||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
||||
}
|
||||
setCurrentMode(newMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentMode = async () => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
if (value) {
|
||||
setCurrentMode(value);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
if (mode) {
|
||||
setCurrentMode(mode);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
}
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentMode();
|
||||
}, []);
|
||||
}, [fetchCurrentMode]);
|
||||
|
||||
return (
|
||||
<section id="mode" className="px-8">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { getApiUrl, getSecretKey } from './config';
|
||||
import { type View } from './App';
|
||||
import { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import builtInExtensionsData from './built-in-extensions.json';
|
||||
@@ -274,158 +272,3 @@ export async function replaceWithShims(cmd: string) {
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
function envVarsRequired(config: ExtensionConfig) {
|
||||
return config.env_keys?.length > 0;
|
||||
}
|
||||
|
||||
function handleError(message: string, shouldThrow = false): void {
|
||||
toastError({
|
||||
title: 'Failed to install extension',
|
||||
msg: message,
|
||||
traceback: message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
console.error(message);
|
||||
if (shouldThrow) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addExtensionFromDeepLink(
|
||||
url: string,
|
||||
setView: (view: View, options: SettingsViewOptions) => void
|
||||
) {
|
||||
if (!url.startsWith('goose://extension')) {
|
||||
handleError('Invalid URL: URL must use the goose://extension scheme');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (parsedUrl.protocol !== 'goose:') {
|
||||
handleError('Invalid protocol: URL must use the goose:// scheme', true);
|
||||
}
|
||||
|
||||
// Check that all required fields are present and not empty
|
||||
const requiredFields = ['name', 'description'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
const value = parsedUrl.searchParams.get(field);
|
||||
if (!value || value.trim() === '') {
|
||||
handleError(`The link is missing required field '${field}'`, true);
|
||||
}
|
||||
}
|
||||
|
||||
const cmd = parsedUrl.searchParams.get('cmd');
|
||||
const remoteUrl = parsedUrl.searchParams.get('url');
|
||||
|
||||
if (!cmd && !remoteUrl) {
|
||||
handleError(
|
||||
"Failed to install extension: Missing required 'cmd' or 'url' parameter in the URL",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const name = parsedUrl.searchParams.get('name');
|
||||
const description = parsedUrl.searchParams.get('description');
|
||||
const timeout = parsedUrl.searchParams.get('timeout');
|
||||
|
||||
// Create a ExtensionConfig from the URL parameters
|
||||
// Parse timeout if provided, otherwise use default
|
||||
const parsedTimeout = timeout ? parseInt(timeout, 10) : null;
|
||||
|
||||
const config: FullExtensionConfig = cmd
|
||||
? getStdioConfig(cmd, parsedUrl, id, name, description, parsedTimeout)
|
||||
: getSseConfig(remoteUrl, id, name, description, parsedTimeout);
|
||||
|
||||
// Store the extension config regardless of env vars status
|
||||
storeExtensionConfig(config);
|
||||
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (envVarsRequired(config)) {
|
||||
console.log('Environment variables required, redirecting to settings');
|
||||
setView('settings', { extensionId: config.id, showEnvVars: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If no env vars are required, proceed with extending Goosed
|
||||
await addExtension(config);
|
||||
}
|
||||
|
||||
function getStdioConfig(
|
||||
cmd: string,
|
||||
parsedUrl: URL,
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
parsedTimeout: number
|
||||
) {
|
||||
const allowedCommands = ['docker', 'jbang', 'npx', 'uvx', 'goosed'];
|
||||
if (!allowedCommands.includes(cmd)) {
|
||||
handleError(
|
||||
`Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Check for security risk with npx -c command
|
||||
const args = parsedUrl.searchParams.getAll('arg');
|
||||
if (cmd === 'npx' && args.includes('-c')) {
|
||||
handleError('npx with -c argument can lead to code injection', true);
|
||||
}
|
||||
|
||||
const envList = parsedUrl.searchParams.getAll('env');
|
||||
|
||||
// split env based on delimiter to a map
|
||||
const envs = envList.reduce(
|
||||
(acc, env) => {
|
||||
const [key, value] = env.split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const config: FullExtensionConfig = {
|
||||
id,
|
||||
name,
|
||||
type: 'stdio',
|
||||
cmd,
|
||||
args,
|
||||
description,
|
||||
enabled: true,
|
||||
env_keys: Object.keys(envs).length > 0 ? Object.keys(envs) : [],
|
||||
timeout:
|
||||
parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout)
|
||||
? parsedTimeout
|
||||
: DEFAULT_EXTENSION_TIMEOUT,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function getSseConfig(
|
||||
remoteUrl: string,
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
parsedTimeout: number
|
||||
) {
|
||||
const config: FullExtensionConfig = {
|
||||
id,
|
||||
name,
|
||||
type: 'sse',
|
||||
uri: remoteUrl,
|
||||
description,
|
||||
enabled: true,
|
||||
env_keys: [],
|
||||
timeout:
|
||||
parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout)
|
||||
? parsedTimeout
|
||||
: DEFAULT_EXTENSION_TIMEOUT,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const settingsV2Enabled = true;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
import { FullExtensionConfig, loadAndAddStoredExtensions } from '../extensions';
|
||||
import { GOOSE_PROVIDER, GOOSE_MODEL } from '../env_vars';
|
||||
import { Model } from '../components/settings/models/ModelContext';
|
||||
import { gooseModels } from '../components/settings/models/GooseModels';
|
||||
import { FullExtensionConfig } from '../extensions';
|
||||
import { initializeAgent } from '../agent';
|
||||
import { settingsV2Enabled } from '../flags';
|
||||
import {
|
||||
initializeBundledExtensions,
|
||||
syncBundledExtensions,
|
||||
@@ -16,31 +12,6 @@ import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigC
|
||||
import { toastService } from '../toasts';
|
||||
import { ExtensionQuery, addExtension as apiAddExtension } from '../api';
|
||||
|
||||
interface AppConfig {
|
||||
GOOSE_PROVIDER?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function getStoredProvider(config: AppConfig): string | null {
|
||||
return config.GOOSE_PROVIDER || localStorage.getItem(GOOSE_PROVIDER);
|
||||
}
|
||||
|
||||
export function getStoredModel(): string | null {
|
||||
const storedModel = localStorage.getItem('GOOSE_MODEL'); // Adjust key name if necessary
|
||||
|
||||
if (storedModel) {
|
||||
try {
|
||||
const modelInfo: Model = JSON.parse(storedModel);
|
||||
return modelInfo.name || null; // Return name if it exists, otherwise null
|
||||
} catch (error) {
|
||||
console.error('Error parsing GOOSE_MODEL from local storage:', error);
|
||||
return null; // Return null if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Return null if storedModel is not found
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
id: string; // Lowercase key (e.g., "openai")
|
||||
name: string; // Provider name (e.g., "OpenAI")
|
||||
@@ -166,13 +137,6 @@ export const initializeSystem = async (
|
||||
console.log('initializing agent with provider', provider, 'model', model);
|
||||
await initializeAgent({ provider, model });
|
||||
|
||||
// This will go away after the release of settings v2 as this is now handled in config.yaml
|
||||
if (!settingsV2Enabled) {
|
||||
// Sync the model state with React
|
||||
const syncedModel = syncModelWithAgent(provider, model);
|
||||
console.log('Model synced with React state:', syncedModel);
|
||||
}
|
||||
|
||||
// Get recipeConfig directly here
|
||||
const recipeConfig = window.appConfig?.get?.('recipeConfig');
|
||||
const botPrompt = recipeConfig?.instructions;
|
||||
@@ -200,93 +164,47 @@ export const initializeSystem = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsV2Enabled) {
|
||||
if (!options?.getExtensions || !options?.addExtension) {
|
||||
console.warn('Extension helpers not provided in alpha mode');
|
||||
return;
|
||||
}
|
||||
if (!options?.getExtensions || !options?.addExtension) {
|
||||
console.warn('Extension helpers not provided in alpha mode');
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: remove when we want to stop migration logic
|
||||
// Check if we need to migrate extensions from localStorage to config.yaml
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
// NOTE: remove when we want to stop migration logic
|
||||
// Check if we need to migrate extensions from localStorage to config.yaml
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
|
||||
console.log(`shouldMigrateExtensions is ${shouldMigrateExtensions}`);
|
||||
if (shouldMigrateExtensions) {
|
||||
await migrateExtensionsToSettingsV3();
|
||||
}
|
||||
console.log(`shouldMigrateExtensions is ${shouldMigrateExtensions}`);
|
||||
if (shouldMigrateExtensions) {
|
||||
await migrateExtensionsToSettingsV3();
|
||||
}
|
||||
|
||||
/* NOTE:
|
||||
* If we've migrated and this is a version update, refreshedExtensions should be > 0
|
||||
* and we'll want to syncBundledExtensions to ensure any new extensions are added.
|
||||
* Otherwise if the user has never opened goose - refreshedExtensions will be 0
|
||||
* and we want to fall into the case to initializeBundledExtensions.
|
||||
*/
|
||||
/* NOTE:
|
||||
* If we've migrated and this is a version update, refreshedExtensions should be > 0
|
||||
* and we'll want to syncBundledExtensions to ensure any new extensions are added.
|
||||
* Otherwise if the user has never opened goose - refreshedExtensions will be 0
|
||||
* and we want to fall into the case to initializeBundledExtensions.
|
||||
*/
|
||||
|
||||
// Initialize or sync built-in extensions into config.yaml
|
||||
let refreshedExtensions = await options.getExtensions(false);
|
||||
// Initialize or sync built-in extensions into config.yaml
|
||||
let refreshedExtensions = await options.getExtensions(false);
|
||||
|
||||
if (refreshedExtensions.length === 0) {
|
||||
await initializeBundledExtensions(options.addExtension);
|
||||
refreshedExtensions = await options.getExtensions(false);
|
||||
} else {
|
||||
await syncBundledExtensions(refreshedExtensions, options.addExtension);
|
||||
}
|
||||
|
||||
// Add enabled extensions to agent
|
||||
for (const extensionEntry of refreshedExtensions) {
|
||||
if (extensionEntry.enabled) {
|
||||
const extensionConfig = extractExtensionConfig(extensionEntry);
|
||||
await addToAgentOnStartup({ addToConfig: options.addExtension, extensionConfig });
|
||||
}
|
||||
}
|
||||
if (refreshedExtensions.length === 0) {
|
||||
await initializeBundledExtensions(options.addExtension);
|
||||
refreshedExtensions = await options.getExtensions(false);
|
||||
} else {
|
||||
loadAndAddStoredExtensions().catch((error) => {
|
||||
console.error('Failed to load and add stored extension configs:', error);
|
||||
});
|
||||
await syncBundledExtensions(refreshedExtensions, options.addExtension);
|
||||
}
|
||||
|
||||
// Add enabled extensions to agent
|
||||
for (const extensionEntry of refreshedExtensions) {
|
||||
if (extensionEntry.enabled) {
|
||||
const extensionConfig = extractExtensionConfig(extensionEntry);
|
||||
await addToAgentOnStartup({ addToConfig: options.addExtension, extensionConfig });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize agent:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// This function ensures the agent initialization values and React model state stay in sync
|
||||
const syncModelWithAgent = (provider: string, modelName: string): Model | null => {
|
||||
console.log('Syncing model state with agent:', { provider, modelName });
|
||||
|
||||
// First, try to find a matching model in our predefined list
|
||||
let matchingModel = gooseModels.find(
|
||||
(m) => m.name === modelName && m.provider.toLowerCase() === provider.toLowerCase()
|
||||
);
|
||||
|
||||
// If no match by exact name and provider, try just by provider
|
||||
if (!matchingModel) {
|
||||
matchingModel = gooseModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase());
|
||||
|
||||
if (matchingModel) {
|
||||
console.log('Found model by provider only:', matchingModel);
|
||||
}
|
||||
}
|
||||
|
||||
// If still no match, create a custom model
|
||||
if (!matchingModel) {
|
||||
console.log('No matching model found, creating custom model');
|
||||
matchingModel = {
|
||||
id: Date.now(),
|
||||
name: modelName,
|
||||
provider: provider,
|
||||
alias: `${provider} - ${modelName}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Update localStorage with the model
|
||||
if (matchingModel) {
|
||||
localStorage.setItem(GOOSE_PROVIDER, matchingModel.provider.toLowerCase());
|
||||
localStorage.setItem(GOOSE_MODEL, JSON.stringify(matchingModel));
|
||||
|
||||
return matchingModel;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user