Show provider failures (#6744)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -17,7 +17,6 @@ use goose::providers::canonical::maybe_get_canonical_model;
|
|||||||
use goose::providers::create_with_default_model;
|
use goose::providers::create_with_default_model;
|
||||||
use goose::providers::errors::ProviderError;
|
use goose::providers::errors::ProviderError;
|
||||||
use goose::providers::providers as get_providers;
|
use goose::providers::providers as get_providers;
|
||||||
use goose::providers::{retry_operation, RetryConfig};
|
|
||||||
use goose::{
|
use goose::{
|
||||||
agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel,
|
agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel,
|
||||||
slash_commands,
|
slash_commands,
|
||||||
@@ -408,10 +407,7 @@ pub async fn get_provider_models(
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
let models_result = retry_operation(&RetryConfig::default(), || async {
|
let models_result = provider.fetch_recommended_models().await;
|
||||||
provider.fetch_recommended_models().await
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match models_result {
|
match models_result {
|
||||||
Ok(Some(models)) => Ok(Json(models)),
|
Ok(Some(models)) => Ok(Json(models)),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
addExtension as apiAddExtension,
|
addExtension as apiAddExtension,
|
||||||
removeExtension as apiRemoveExtension,
|
removeExtension as apiRemoveExtension,
|
||||||
providers,
|
providers,
|
||||||
getProviderModels as apiGetProviderModels,
|
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { syncBundledExtensions } from './settings/extensions';
|
import { syncBundledExtensions } from './settings/extensions';
|
||||||
import type {
|
import type {
|
||||||
@@ -41,7 +40,6 @@ interface ConfigContextType {
|
|||||||
removeExtension: (name: string) => Promise<void>;
|
removeExtension: (name: string) => Promise<void>;
|
||||||
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
|
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
|
||||||
getExtensions: (b: boolean) => Promise<FixedExtensionEntry[]>;
|
getExtensions: (b: boolean) => Promise<FixedExtensionEntry[]>;
|
||||||
getProviderModels: (providerName: string) => Promise<string[]>;
|
|
||||||
disableAllExtensions: () => Promise<void>;
|
disableAllExtensions: () => Promise<void>;
|
||||||
enableBotExtensions: (extensions: ExtensionConfig[]) => Promise<void>;
|
enableBotExtensions: (extensions: ExtensionConfig[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -187,19 +185,6 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
return providersListRef.current;
|
return providersListRef.current;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getProviderModels = useCallback(async (providerName: string): Promise<string[]> => {
|
|
||||||
try {
|
|
||||||
const response = await apiGetProviderModels({
|
|
||||||
path: { name: providerName },
|
|
||||||
throwOnError: true,
|
|
||||||
});
|
|
||||||
return response.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch models for provider ${providerName}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load all configuration data and providers on mount
|
// Load all configuration data and providers on mount
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -284,7 +269,6 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
toggleExtension,
|
toggleExtension,
|
||||||
getProviders,
|
getProviders,
|
||||||
getExtensions,
|
getExtensions,
|
||||||
getProviderModels,
|
|
||||||
disableAllExtensions,
|
disableAllExtensions,
|
||||||
enableBotExtensions,
|
enableBotExtensions,
|
||||||
};
|
};
|
||||||
@@ -301,7 +285,6 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
toggleExtension,
|
toggleExtension,
|
||||||
getProviders,
|
getProviders,
|
||||||
getExtensions,
|
getExtensions,
|
||||||
getProviderModels,
|
|
||||||
reloadConfig,
|
reloadConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ProviderDetails } from '../../../api';
|
import { ProviderDetails, getProviderModels } from '../../../api';
|
||||||
|
|
||||||
export default interface Model {
|
export default interface Model {
|
||||||
id?: number; // Make `id` optional to allow user-defined models
|
id?: number; // Make `id` optional to allow user-defined models
|
||||||
@@ -48,24 +48,19 @@ export interface ProviderModelsResult {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches recommended models for all active providers in parallel.
|
|
||||||
* Falls back to known_models if fetching fails or returns no models.
|
|
||||||
*/
|
|
||||||
export async function fetchModelsForProviders(
|
export async function fetchModelsForProviders(
|
||||||
activeProviders: ProviderDetails[],
|
activeProviders: ProviderDetails[]
|
||||||
getProviderModelsFunc: (providerName: string) => Promise<string[]>
|
|
||||||
): Promise<ProviderModelsResult[]> {
|
): Promise<ProviderModelsResult[]> {
|
||||||
const modelPromises = activeProviders.map(async (p) => {
|
const modelPromises = activeProviders.map(async (p) => {
|
||||||
const providerName = p.name;
|
|
||||||
try {
|
try {
|
||||||
let models = await getProviderModelsFunc(providerName);
|
const response = await getProviderModels({
|
||||||
if ((!models || models.length === 0) && p.metadata.known_models?.length) {
|
path: { name: p.name },
|
||||||
models = p.metadata.known_models.map((m) => m.name);
|
throwOnError: true,
|
||||||
}
|
});
|
||||||
|
const models = response.data || [];
|
||||||
return { provider: p, models, error: null };
|
return { provider: p, models, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = `Failed to fetch models for ${providerName}${e instanceof Error ? `: ${e.message}` : ''}`;
|
const errorMessage = `Failed to fetch models for ${p.name}${e instanceof Error ? `: ${e.message}` : ''}`;
|
||||||
return {
|
return {
|
||||||
provider: p,
|
provider: p,
|
||||||
models: null,
|
models: null,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { Button } from '../../../ui/button';
|
|||||||
import { Select } from '../../../ui/Select';
|
import { Select } from '../../../ui/Select';
|
||||||
import { Input } from '../../../ui/input';
|
import { Input } from '../../../ui/input';
|
||||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||||
import { fetchModelsForProviders } from '../modelInterface';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/dialog';
|
||||||
|
import { fetchModelsForProviders } from '../modelInterface';
|
||||||
|
|
||||||
interface LeadWorkerSettingsProps {
|
interface LeadWorkerSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -14,7 +14,7 @@ interface LeadWorkerSettingsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) {
|
export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) {
|
||||||
const { read, upsert, getProviders, getProviderModels, remove } = useConfig();
|
const { read, upsert, getProviders, remove } = useConfig();
|
||||||
const { currentModel } = useModelAndProvider();
|
const { currentModel } = useModelAndProvider();
|
||||||
const [leadModel, setLeadModel] = useState<string>('');
|
const [leadModel, setLeadModel] = useState<string>('');
|
||||||
const [workerModel, setWorkerModel] = useState<string>('');
|
const [workerModel, setWorkerModel] = useState<string>('');
|
||||||
@@ -104,7 +104,8 @@ export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps)
|
|||||||
const providers = await getProviders(false);
|
const providers = await getProviders(false);
|
||||||
const activeProviders = providers.filter((p) => p.is_configured);
|
const activeProviders = providers.filter((p) => p.is_configured);
|
||||||
|
|
||||||
const results = await fetchModelsForProviders(activeProviders, getProviderModels);
|
const results = await fetchModelsForProviders(activeProviders);
|
||||||
|
|
||||||
results.forEach(({ provider: p, models, error }) => {
|
results.forEach(({ provider: p, models, error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -139,7 +140,7 @@ export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}, [read, getProviders, getProviderModels, currentModel, isOpen]);
|
}, [read, getProviders, currentModel, isOpen]);
|
||||||
|
|
||||||
// If current models are not in the list (e.g., previously set to custom), switch to custom mode
|
// If current models are not in the list (e.g., previously set to custom), switch to custom mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const SwitchModelModal = ({
|
|||||||
initialProvider,
|
initialProvider,
|
||||||
titleOverride,
|
titleOverride,
|
||||||
}: SwitchModelModalProps) => {
|
}: SwitchModelModalProps) => {
|
||||||
const { getProviders, getProviderModels, read } = useConfig();
|
const { getProviders, read } = useConfig();
|
||||||
const { changeModel, currentModel, currentProvider } = useModelAndProvider();
|
const { changeModel, currentModel, currentProvider } = useModelAndProvider();
|
||||||
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
||||||
type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean };
|
type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean };
|
||||||
@@ -96,6 +96,7 @@ export const SwitchModelModal = ({
|
|||||||
const [predefinedModels, setPredefinedModels] = useState<Model[]>([]);
|
const [predefinedModels, setPredefinedModels] = useState<Model[]>([]);
|
||||||
const [loadingModels, setLoadingModels] = useState<boolean>(false);
|
const [loadingModels, setLoadingModels] = useState<boolean>(false);
|
||||||
const [userClearedModel, setUserClearedModel] = useState(false);
|
const [userClearedModel, setUserClearedModel] = useState(false);
|
||||||
|
const [providerErrors, setProviderErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Validate form data
|
// Validate form data
|
||||||
const validateForm = useCallback(() => {
|
const validateForm = useCallback(() => {
|
||||||
@@ -204,24 +205,22 @@ export const SwitchModelModal = ({
|
|||||||
|
|
||||||
setLoadingModels(true);
|
setLoadingModels(true);
|
||||||
|
|
||||||
// Fetching models for all providers (always recommended)
|
const results = await fetchModelsForProviders(activeProviders);
|
||||||
const results = await fetchModelsForProviders(activeProviders, getProviderModels);
|
|
||||||
|
|
||||||
// Process results and build grouped options
|
// Process results and build grouped options
|
||||||
const groupedOptions: {
|
const groupedOptions: {
|
||||||
options: { value: string; label: string; provider: string; providerType: ProviderType }[];
|
options: { value: string; label: string; provider: string; providerType: ProviderType }[];
|
||||||
}[] = [];
|
}[] = [];
|
||||||
const errors: string[] = [];
|
const errorMap: Record<string, string> = {};
|
||||||
|
|
||||||
results.forEach(({ provider: p, models, error }) => {
|
results.forEach(({ provider: p, models, error }) => {
|
||||||
const modelList = error
|
|
||||||
? p.metadata.known_models?.map(({ name }) => name) || []
|
|
||||||
: models || [];
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
errors.push(error);
|
errorMap[p.name] = error;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelList = models || [];
|
||||||
|
|
||||||
const options: {
|
const options: {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -248,10 +247,8 @@ export const SwitchModelModal = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log errors if any providers failed (don't show to user)
|
// Save provider errors to state
|
||||||
if (errors.length > 0) {
|
setProviderErrors(errorMap);
|
||||||
console.error('Provider model fetch errors:', errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
setModelOptions(groupedOptions);
|
setModelOptions(groupedOptions);
|
||||||
setOriginalModelOptions(groupedOptions);
|
setOriginalModelOptions(groupedOptions);
|
||||||
@@ -261,7 +258,7 @@ export const SwitchModelModal = ({
|
|||||||
setLoadingModels(false);
|
setLoadingModels(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [getProviders, getProviderModels, usePredefinedModels, read, initialProvider]);
|
}, [getProviders, usePredefinedModels, read, initialProvider]);
|
||||||
|
|
||||||
const filteredModelOptions = provider
|
const filteredModelOptions = provider
|
||||||
? modelOptions.filter((group) => group.options[0]?.provider === provider)
|
? modelOptions.filter((group) => group.options[0]?.provider === provider)
|
||||||
@@ -454,7 +451,24 @@ export const SwitchModelModal = ({
|
|||||||
|
|
||||||
{provider && (
|
{provider && (
|
||||||
<>
|
<>
|
||||||
{!isCustomModel ? (
|
{providerErrors[provider] ? (
|
||||||
|
/* Show error message when provider failed to connect */
|
||||||
|
<div className="rounded-md bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||||
|
Could not contact provider
|
||||||
|
</h3>
|
||||||
|
<div className="mt-1 text-sm text-red-700 dark:text-red-300">
|
||||||
|
{providerErrors[provider]}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-red-600 dark:text-red-400">
|
||||||
|
Check your provider configuration in Settings → Providers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : !isCustomModel ? (
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
options={
|
options={
|
||||||
@@ -466,10 +480,14 @@ export const SwitchModelModal = ({
|
|||||||
}
|
}
|
||||||
onChange={handleModelChange}
|
onChange={handleModelChange}
|
||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
value={model ? { value: model, label: model } : null}
|
value={
|
||||||
placeholder={
|
loadingModels
|
||||||
loadingModels ? 'Loading models…' : 'Select a model, type to search'
|
? { value: '', label: 'Loading models…', isDisabled: true }
|
||||||
|
: model
|
||||||
|
? { value: model, label: model }
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
|
placeholder="Select a model, type to search"
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={loadingModels}
|
isDisabled={loadingModels}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+40
-5
@@ -1,4 +1,4 @@
|
|||||||
import { checkProvider } from '../../../../../../api';
|
import { getProviderModels, readConfig } from '../../../../../../api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standalone function to submit provider configuration
|
* Standalone function to submit provider configuration
|
||||||
@@ -21,6 +21,30 @@ export const providerConfigSubmitHandler = async (
|
|||||||
) => {
|
) => {
|
||||||
const parameters = provider.metadata.config_keys || [];
|
const parameters = provider.metadata.config_keys || [];
|
||||||
|
|
||||||
|
// Save current NON-SECRET config values for rollback on failure
|
||||||
|
// We skip secrets because readConfig returns masked values for secrets,
|
||||||
|
// and upserting those masked values would corrupt the actual secret
|
||||||
|
const previousConfigValues: Record<string, { value: unknown; isSecret: boolean }> = {};
|
||||||
|
const nonSecretParams = parameters.filter((param) => !param.secret);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nonSecretParams.map(async (param) => {
|
||||||
|
try {
|
||||||
|
const currentValue = await readConfig({
|
||||||
|
body: { key: param.name, is_secret: false },
|
||||||
|
});
|
||||||
|
if (currentValue.data) {
|
||||||
|
previousConfigValues[param.name] = {
|
||||||
|
value: currentValue.data,
|
||||||
|
isSecret: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No previous value exists, that's fine
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const requiredParams = parameters.filter((param) => param.required);
|
const requiredParams = parameters.filter((param) => param.required);
|
||||||
if (requiredParams.length === 0 && parameters.length > 0) {
|
if (requiredParams.length === 0 && parameters.length > 0) {
|
||||||
const allOptionalWithDefaults = parameters.every(
|
const allOptionalWithDefaults = parameters.every(
|
||||||
@@ -70,8 +94,19 @@ export const providerConfigSubmitHandler = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(upsertPromises);
|
await Promise.all(upsertPromises);
|
||||||
await checkProvider({
|
|
||||||
body: { provider: provider.name },
|
try {
|
||||||
throwOnError: true,
|
await getProviderModels({
|
||||||
});
|
path: { name: provider.name },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const rollbackPromises: Promise<void>[] = [];
|
||||||
|
for (const [key, { value, isSecret }] of Object.entries(previousConfigValues)) {
|
||||||
|
rollbackPromises.push(upsertFn(key, value, isSecret));
|
||||||
|
}
|
||||||
|
await Promise.all(rollbackPromises);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user