Remove lead/worker provider (#7989)
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -6113,28 +6113,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"mode",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ModelChange"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -687,10 +687,6 @@ export type MessageEvent = {
|
||||
reason: string;
|
||||
token_state: TokenState;
|
||||
type: 'Finish';
|
||||
} | {
|
||||
mode: string;
|
||||
model: string;
|
||||
type: 'ModelChange';
|
||||
} | {
|
||||
message: {
|
||||
[key: string]: unknown;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AppEvents } from '../constants/events';
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -42,8 +40,7 @@ import { useAutoSubmit } from '../hooks/useAutoSubmit';
|
||||
import { Goose } from './icons';
|
||||
import EnvironmentBadge from './GooseSidebar/EnvironmentBadge';
|
||||
|
||||
const CurrentModelContext = createContext<{ model: string; mode: string } | null>(null);
|
||||
export const useCurrentModelInfo = () => useContext(CurrentModelContext);
|
||||
|
||||
|
||||
interface BaseChatProps {
|
||||
setChat: (chat: ChatType) => void;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Sliders, Bot, Settings } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
|
||||
import { LeadWorkerSettings } from '../subcomponents/LeadWorkerSettings';
|
||||
import { View } from '../../../../utils/navigationUtils';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../../../ui/dropdown-menu';
|
||||
import { useCurrentModelInfo } from '../../../BaseChat';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { getProviderMetadata } from '../modelInterface';
|
||||
import { getModelDisplayName } from '../predefinedModelsUtils';
|
||||
@@ -46,81 +44,17 @@ export default function ModelsBottomBar({
|
||||
const currentModel = sessionModel ?? configModel;
|
||||
const currentProvider = sessionProvider ?? configProvider;
|
||||
|
||||
const currentModelInfo = useCurrentModelInfo();
|
||||
const { read, getProviders } = useConfig();
|
||||
const { getProviders } = useConfig();
|
||||
const [displayProvider, setDisplayProvider] = useState<string | null>(null);
|
||||
const [displayModelName, setDisplayModelName] = useState<string>('Select Model');
|
||||
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
|
||||
const [isLeadWorkerModalOpen, setIsLeadWorkerModalOpen] = useState(false);
|
||||
const [isLocalModelSettingsOpen, setIsLocalModelSettingsOpen] = useState(false);
|
||||
const [isLeadWorkerActive, setIsLeadWorkerActive] = useState(false);
|
||||
const [providerDefaultModel, setProviderDefaultModel] = useState<string | null>(null);
|
||||
|
||||
// Check if lead/worker mode is active
|
||||
useEffect(() => {
|
||||
const checkLeadWorker = async () => {
|
||||
try {
|
||||
const leadModel = await read('GOOSE_LEAD_MODEL', false);
|
||||
setIsLeadWorkerActive(!!leadModel);
|
||||
} catch (error) {
|
||||
console.error('Error checking lead model:', error);
|
||||
setIsLeadWorkerActive(false);
|
||||
}
|
||||
};
|
||||
checkLeadWorker();
|
||||
}, [read]);
|
||||
|
||||
// Refresh lead/worker status when modal closes
|
||||
const handleLeadWorkerModalClose = () => {
|
||||
setIsLeadWorkerModalOpen(false);
|
||||
const checkLeadWorker = async () => {
|
||||
try {
|
||||
const leadModel = await read('GOOSE_LEAD_MODEL', false);
|
||||
const currentModel = await read('GOOSE_MODEL', false);
|
||||
setIsLeadWorkerActive(!!leadModel);
|
||||
setLeadModelName((leadModel as string) || '');
|
||||
setCurrentActiveModel((currentModel as string) || '');
|
||||
} catch (error) {
|
||||
console.error('Error checking lead model after modal close:', error);
|
||||
setIsLeadWorkerActive(false);
|
||||
}
|
||||
};
|
||||
checkLeadWorker();
|
||||
};
|
||||
|
||||
const [leadModelName, setLeadModelName] = useState<string>('');
|
||||
const [currentActiveModel, setCurrentActiveModel] = useState<string>('');
|
||||
|
||||
// Get lead model name and current model for comparison
|
||||
useEffect(() => {
|
||||
const getModelInfo = async () => {
|
||||
try {
|
||||
const leadModel = await read('GOOSE_LEAD_MODEL', false);
|
||||
const currentModel = await read('GOOSE_MODEL', false);
|
||||
setLeadModelName((leadModel as string) || '');
|
||||
setCurrentActiveModel((currentModel as string) || '');
|
||||
} catch (error) {
|
||||
console.error('Error getting model info:', error);
|
||||
}
|
||||
};
|
||||
getModelInfo();
|
||||
}, [read]);
|
||||
|
||||
// Determine the mode based on which model is currently active
|
||||
const modelMode = isLeadWorkerActive
|
||||
? currentActiveModel === leadModelName
|
||||
? 'lead'
|
||||
: 'worker'
|
||||
: undefined;
|
||||
|
||||
// Determine which model to display - activeModel takes priority when lead/worker is active
|
||||
// Hide label while session data is still being fetched (avoids flashing
|
||||
// the config default before the session's actual model arrives).
|
||||
const isModelLoading = sessionId && !sessionLoaded;
|
||||
const displayModel =
|
||||
isLeadWorkerActive && currentModelInfo?.model
|
||||
? currentModelInfo.model
|
||||
: currentModel || providerDefaultModel || displayModelName;
|
||||
const displayModel = currentModel || providerDefaultModel || displayModelName;
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProvider) return;
|
||||
@@ -168,9 +102,6 @@ export default function ModelsBottomBar({
|
||||
<Bot className="mr-1 h-4 w-4 flex-shrink-0" />
|
||||
<span className={`truncate text-xs${isModelLoading ? ' opacity-0' : ''}`}>
|
||||
{displayModel}
|
||||
{isLeadWorkerActive && modelMode && (
|
||||
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -184,10 +115,6 @@ export default function ModelsBottomBar({
|
||||
<span>Change Model</span>
|
||||
<Sliders className="ml-auto h-4 w-4 rotate-90" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsLeadWorkerModalOpen(true)}>
|
||||
<span>Lead/Worker Settings</span>
|
||||
<Sliders className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuItem>
|
||||
{currentProvider === 'local' && currentModel && (
|
||||
<DropdownMenuItem onClick={() => setIsLocalModelSettingsOpen(true)}>
|
||||
<span>Local Model Settings</span>
|
||||
@@ -208,10 +135,6 @@ export default function ModelsBottomBar({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isLeadWorkerModalOpen ? (
|
||||
<LeadWorkerSettings isOpen={isLeadWorkerModalOpen} onClose={handleLeadWorkerModalClose} />
|
||||
) : null}
|
||||
|
||||
{isLocalModelSettingsOpen && currentModel && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-background-default rounded-lg shadow-lg w-[480px] max-h-[80vh] flex flex-col">
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { LeadWorkerSettings } from './LeadWorkerSettings';
|
||||
|
||||
// Mock predefined models utils to force provider-based options (no predefined list)
|
||||
vi.mock('../predefinedModelsUtils', () => ({
|
||||
shouldShowPredefinedModels: () => false,
|
||||
getPredefinedModelsFromEnv: () => [],
|
||||
}));
|
||||
|
||||
// Mocks for useConfig
|
||||
const mockRead = vi.fn();
|
||||
const mockUpsert = vi.fn();
|
||||
const mockRemove = vi.fn();
|
||||
const mockGetProviders = vi.fn();
|
||||
|
||||
vi.mock('../../../ConfigContext', () => ({
|
||||
useConfig: () => ({
|
||||
read: mockRead,
|
||||
upsert: mockUpsert,
|
||||
remove: mockRemove,
|
||||
getProviders: mockGetProviders,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('LeadWorkerSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupHappyPathMocks = () => {
|
||||
// reads
|
||||
mockRead.mockImplementation(async (key: string) => {
|
||||
switch (key) {
|
||||
case 'GOOSE_LEAD_MODEL':
|
||||
return 'my-custom-lead';
|
||||
case 'GOOSE_LEAD_PROVIDER':
|
||||
return 'anthropic';
|
||||
case 'GOOSE_LEAD_TURNS':
|
||||
return 3;
|
||||
case 'GOOSE_LEAD_FAILURE_THRESHOLD':
|
||||
return 2;
|
||||
case 'GOOSE_LEAD_FALLBACK_TURNS':
|
||||
return 2;
|
||||
case 'GOOSE_MODEL':
|
||||
return 'my-custom-worker';
|
||||
case 'GOOSE_PROVIDER':
|
||||
return 'openai';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// providers (options do NOT include the custom models above)
|
||||
mockGetProviders.mockResolvedValue([
|
||||
{
|
||||
is_configured: true,
|
||||
name: 'openai',
|
||||
metadata: {
|
||||
display_name: 'OpenAI',
|
||||
known_models: [{ name: 'gpt-4o' }, { name: 'gpt-4o-mini' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
is_configured: true,
|
||||
name: 'anthropic',
|
||||
metadata: {
|
||||
display_name: 'Anthropic',
|
||||
known_models: [{ name: 'claude-3-5-sonnet' }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// writers
|
||||
mockUpsert.mockResolvedValue(undefined);
|
||||
mockRemove.mockResolvedValue(undefined);
|
||||
};
|
||||
|
||||
it('shows custom inputs for lead/worker when current models are unknown and saves them', async () => {
|
||||
setupHappyPathMocks();
|
||||
|
||||
const onClose = vi.fn();
|
||||
render(<LeadWorkerSettings isOpen={true} onClose={onClose} />);
|
||||
|
||||
// Wait for modal content (not loading)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lead/Worker Mode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Labels should be present with back-to-list controls
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lead Model')).toBeInTheDocument();
|
||||
expect(screen.getByText('Worker Model')).toBeInTheDocument();
|
||||
// Back to model list appears for each section when in custom mode
|
||||
const backLinks = screen.getAllByText('Back to model list');
|
||||
expect(backLinks.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
const inputs = screen.getAllByPlaceholderText('Type model name here') as HTMLInputElement[];
|
||||
expect(inputs.length).toBe(2);
|
||||
const [leadInput, workerInput] = inputs;
|
||||
expect(leadInput.value).toBe('my-custom-lead');
|
||||
expect(workerInput.value).toBe('my-custom-worker');
|
||||
|
||||
// Save settings
|
||||
const saveBtn = screen.getByRole('button', { name: 'Save Settings' });
|
||||
expect(saveBtn).toBeEnabled();
|
||||
fireEvent.click(saveBtn);
|
||||
|
||||
// Assert upserts for models (providers are optional but present in this setup)
|
||||
await waitFor(() => {
|
||||
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_LEAD_MODEL', 'my-custom-lead', false);
|
||||
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_MODEL', 'my-custom-worker', false);
|
||||
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_LEAD_PROVIDER', 'anthropic', false);
|
||||
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_PROVIDER', 'openai', false);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables lead/worker and removes config when toggled off', async () => {
|
||||
setupHappyPathMocks();
|
||||
|
||||
const onClose = vi.fn();
|
||||
render(<LeadWorkerSettings isOpen={true} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lead/Worker Mode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Toggle off
|
||||
const checkbox = screen.getByLabelText('Enable lead/worker mode') as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
fireEvent.click(checkbox);
|
||||
expect(checkbox.checked).toBe(false);
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: 'Save Settings' });
|
||||
expect(saveBtn).toBeEnabled();
|
||||
fireEvent.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRemove).toHaveBeenCalledWith('GOOSE_LEAD_MODEL', false);
|
||||
expect(mockRemove).toHaveBeenCalledWith('GOOSE_LEAD_PROVIDER', false);
|
||||
expect(mockRemove).toHaveBeenCalledWith('GOOSE_LEAD_TURNS', false);
|
||||
expect(mockRemove).toHaveBeenCalledWith('GOOSE_LEAD_FAILURE_THRESHOLD', false);
|
||||
expect(mockRemove).toHaveBeenCalledWith('GOOSE_LEAD_FALLBACK_TURNS', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,419 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { Button } from '../../../ui/button';
|
||||
import { Select } from '../../../ui/Select';
|
||||
import { Input } from '../../../ui/input';
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/dialog';
|
||||
import { fetchModelsForProviders } from '../modelInterface';
|
||||
|
||||
interface LeadWorkerSettingsProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) {
|
||||
const { read, upsert, getProviders, remove } = useConfig();
|
||||
const [leadModel, setLeadModel] = useState<string>('');
|
||||
const [workerModel, setWorkerModel] = useState<string>('');
|
||||
const [leadProvider, setLeadProvider] = useState<string>('');
|
||||
const [workerProvider, setWorkerProvider] = useState<string>('');
|
||||
// Minimal custom model mode toggles
|
||||
const [isLeadCustomModel, setIsLeadCustomModel] = useState<boolean>(false);
|
||||
const [isWorkerCustomModel, setIsWorkerCustomModel] = useState<boolean>(false);
|
||||
const [leadTurns, setLeadTurns] = useState<number>(3);
|
||||
const [failureThreshold, setFailureThreshold] = useState<number>(2);
|
||||
const [fallbackTurns, setFallbackTurns] = useState<number>(2);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [modelOptions, setModelOptions] = useState<
|
||||
{ value: string; label: string; provider: string }[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load current configuration
|
||||
useEffect(() => {
|
||||
if (!isOpen) return; // Only load when modal is open
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [
|
||||
leadModelConfig,
|
||||
leadProviderConfig,
|
||||
leadTurnsConfig,
|
||||
failureThresholdConfig,
|
||||
fallbackTurnsConfig,
|
||||
] = await Promise.all([
|
||||
read('GOOSE_LEAD_MODEL', false),
|
||||
read('GOOSE_LEAD_PROVIDER', false),
|
||||
read('GOOSE_LEAD_TURNS', false),
|
||||
read('GOOSE_LEAD_FAILURE_THRESHOLD', false),
|
||||
read('GOOSE_LEAD_FALLBACK_TURNS', false),
|
||||
]);
|
||||
|
||||
if (leadModelConfig) {
|
||||
setLeadModel(leadModelConfig as string);
|
||||
setIsEnabled(true);
|
||||
} else {
|
||||
setLeadModel('');
|
||||
setIsEnabled(false);
|
||||
}
|
||||
if (leadProviderConfig) setLeadProvider(leadProviderConfig as string);
|
||||
else setLeadProvider('');
|
||||
if (leadTurnsConfig) setLeadTurns(Number(leadTurnsConfig));
|
||||
else setLeadTurns(3);
|
||||
if (failureThresholdConfig) setFailureThreshold(Number(failureThresholdConfig));
|
||||
else setFailureThreshold(2);
|
||||
if (fallbackTurnsConfig) setFallbackTurns(Number(fallbackTurnsConfig));
|
||||
else setFallbackTurns(2);
|
||||
|
||||
// Set worker model from config
|
||||
const workerModelConfig = await read('GOOSE_MODEL', false);
|
||||
if (workerModelConfig) {
|
||||
setWorkerModel(workerModelConfig as string);
|
||||
} else {
|
||||
setWorkerModel('');
|
||||
}
|
||||
|
||||
const workerProviderConfig = await read('GOOSE_PROVIDER', false);
|
||||
if (workerProviderConfig) {
|
||||
setWorkerProvider(workerProviderConfig as string);
|
||||
} else {
|
||||
setWorkerProvider('');
|
||||
}
|
||||
|
||||
// Load available models
|
||||
const options: { value: string; label: string; provider: string }[] = [];
|
||||
|
||||
if (shouldShowPredefinedModels()) {
|
||||
// Use predefined models if available
|
||||
const predefinedModels = getPredefinedModelsFromEnv();
|
||||
predefinedModels.forEach((model) => {
|
||||
options.push({
|
||||
value: model.name, // Use name for switching
|
||||
label: model.alias || model.name, // Use alias for display, fall back to name
|
||||
provider: model.provider,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Fallback to provider-based models
|
||||
const providers = await getProviders(false);
|
||||
const activeProviders = providers.filter((p) => p.is_configured);
|
||||
|
||||
const results = await fetchModelsForProviders(activeProviders);
|
||||
|
||||
results.forEach(({ provider: p, models, error }) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (models && models.length > 0) {
|
||||
models.forEach((modelName) => {
|
||||
options.push({
|
||||
value: modelName,
|
||||
label: `${modelName} (${p.metadata.display_name})`,
|
||||
provider: p.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
// Add custom model option for all non-Custom providers
|
||||
if (p.provider_type !== 'Custom') {
|
||||
options.push({
|
||||
value: `__custom__:${p.name}`,
|
||||
label: 'Enter a model not listed...',
|
||||
provider: p.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setModelOptions(options);
|
||||
} catch (error) {
|
||||
console.error('Error loading configuration:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, [read, getProviders, isOpen]);
|
||||
|
||||
// If current models are not in the list (e.g., previously set to custom), switch to custom mode
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (leadModel && !modelOptions.find((opt) => opt.value === leadModel)) {
|
||||
setIsLeadCustomModel(true);
|
||||
}
|
||||
if (workerModel && !modelOptions.find((opt) => opt.value === workerModel)) {
|
||||
setIsWorkerCustomModel(true);
|
||||
}
|
||||
}
|
||||
}, [isLoading, modelOptions, leadModel, workerModel]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isEnabled && leadModel && workerModel) {
|
||||
// Save lead/worker configuration
|
||||
await Promise.all([
|
||||
upsert('GOOSE_LEAD_MODEL', leadModel, false),
|
||||
leadProvider && upsert('GOOSE_LEAD_PROVIDER', leadProvider, false),
|
||||
upsert('GOOSE_MODEL', workerModel, false),
|
||||
workerProvider && upsert('GOOSE_PROVIDER', workerProvider, false),
|
||||
upsert('GOOSE_LEAD_TURNS', leadTurns, false),
|
||||
upsert('GOOSE_LEAD_FAILURE_THRESHOLD', failureThreshold, false),
|
||||
upsert('GOOSE_LEAD_FALLBACK_TURNS', fallbackTurns, false),
|
||||
]);
|
||||
} else {
|
||||
// Remove lead/worker configuration
|
||||
await Promise.all([
|
||||
remove('GOOSE_LEAD_MODEL', false),
|
||||
remove('GOOSE_LEAD_PROVIDER', false),
|
||||
remove('GOOSE_LEAD_TURNS', false),
|
||||
remove('GOOSE_LEAD_FAILURE_THRESHOLD', false),
|
||||
remove('GOOSE_LEAD_FALLBACK_TURNS', false),
|
||||
]);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving configuration:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lead/Worker Mode</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">Loading...</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lead/Worker Mode</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
Configure a lead model for planning and a worker model for execution
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable-lead-worker"
|
||||
checked={isEnabled}
|
||||
onChange={(e) => setIsEnabled(e.target.checked)}
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="enable-lead-worker" className="text-sm text-text-primary">
|
||||
Enable lead/worker mode
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
className={`text-sm ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Lead Model
|
||||
</label>
|
||||
{isLeadCustomModel && (
|
||||
<button
|
||||
onClick={() => setIsLeadCustomModel(false)}
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'} hover:underline`}
|
||||
type="button"
|
||||
>
|
||||
Back to model list
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isLeadCustomModel ? (
|
||||
<Select
|
||||
options={modelOptions}
|
||||
value={
|
||||
leadModel ? modelOptions.find((opt) => opt.value === leadModel) || null : null
|
||||
}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; provider: string } | null;
|
||||
if (option) {
|
||||
if (option.value.startsWith('__custom__')) {
|
||||
setIsLeadCustomModel(true);
|
||||
setLeadModel('');
|
||||
setLeadProvider(option.provider);
|
||||
return;
|
||||
}
|
||||
setLeadModel(option.value);
|
||||
setLeadProvider(option.provider);
|
||||
}
|
||||
}}
|
||||
placeholder="Select lead model..."
|
||||
isDisabled={!isEnabled}
|
||||
className={!isEnabled ? 'opacity-50' : ''}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="h-[38px] mb-2"
|
||||
placeholder="Type model name here"
|
||||
onChange={(event) => setLeadModel(event.target.value)}
|
||||
value={leadModel}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Strong model for initial planning and fallback recovery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
className={`text-sm ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Worker Model
|
||||
</label>
|
||||
{isWorkerCustomModel && (
|
||||
<button
|
||||
onClick={() => setIsWorkerCustomModel(false)}
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'} hover:underline`}
|
||||
type="button"
|
||||
>
|
||||
Back to model list
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isWorkerCustomModel ? (
|
||||
<Select
|
||||
options={modelOptions}
|
||||
value={
|
||||
workerModel
|
||||
? modelOptions.find((opt) => opt.value === workerModel) || null
|
||||
: null
|
||||
}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; provider: string } | null;
|
||||
if (option) {
|
||||
if (option.value.startsWith('__custom__')) {
|
||||
setIsWorkerCustomModel(true);
|
||||
setWorkerModel('');
|
||||
setWorkerProvider(option.provider);
|
||||
return;
|
||||
}
|
||||
setWorkerModel(option.value);
|
||||
setWorkerProvider(option.provider);
|
||||
}
|
||||
}}
|
||||
placeholder="Select worker model..."
|
||||
isDisabled={!isEnabled}
|
||||
className={!isEnabled ? 'opacity-50' : ''}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="h-[38px] mb-2"
|
||||
placeholder="Type model name here"
|
||||
onChange={(event) => setWorkerModel(event.target.value)}
|
||||
value={workerModel}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Fast model for routine execution tasks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`space-y-4 pt-4 border-t border-border-primary ${!isEnabled ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Initial Lead Turns
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={leadTurns}
|
||||
onChange={(e) => setLeadTurns(Number(e.target.value))}
|
||||
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<p
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Number of turns to use the lead model at the start
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Failure Threshold
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={5}
|
||||
value={failureThreshold}
|
||||
onChange={(e) => setFailureThreshold(Number(e.target.value))}
|
||||
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<p
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Consecutive failures before switching back to lead
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Fallback Turns
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={5}
|
||||
value={fallbackTurns}
|
||||
onChange={(e) => setFallbackTurns(Number(e.target.value))}
|
||||
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<p
|
||||
className={`text-xs ${!isEnabled ? 'text-text-secondary' : 'text-text-secondary'}`}
|
||||
>
|
||||
Turns to use lead model during fallback
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4 border-t border-border-primary">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isEnabled && (!leadModel || !workerModel)}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -318,9 +318,6 @@ function createEventProcessor(
|
||||
onFinish();
|
||||
return true;
|
||||
}
|
||||
case 'ModelChange': {
|
||||
return false;
|
||||
}
|
||||
case 'UpdateConversation': {
|
||||
const conversation = (event as Record<string, unknown>).conversation as Message[];
|
||||
currentMessages = conversation;
|
||||
|
||||
@@ -4,8 +4,6 @@ export const configLabels: Record<string, string> = {
|
||||
GOOSE_MODEL: 'Model',
|
||||
GOOSE_TEMPERATURE: 'Temperature',
|
||||
GOOSE_MODE: 'Mode',
|
||||
GOOSE_LEAD_PROVIDER: 'Lead Provider',
|
||||
GOOSE_LEAD_MODEL: 'Lead Model',
|
||||
GOOSE_PLANNER_PROVIDER: 'Planner Provider',
|
||||
GOOSE_PLANNER_MODEL: 'Planner Model',
|
||||
GOOSE_TOOLSHIM: 'Tool Shim',
|
||||
|
||||
Reference in New Issue
Block a user