Move token limits to backend (#2484)

This commit is contained in:
Zane
2025-05-12 13:52:46 -07:00
committed by GitHub
parent b4aadeb5d4
commit 7027de6238
14 changed files with 260 additions and 1121 deletions
@@ -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);
}
}
-229
View File
@@ -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());
}
}
-2
View File
@@ -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
View File
@@ -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
View File
@@ -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">
-157
View File
@@ -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
View File
@@ -1 +0,0 @@
export const settingsV2Enabled = true;
+33 -115
View File
@@ -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;
};