better settings form validation, grouped model selection (#383)

* better form validation, grouped model selection

* bump version

* fix fe build issue

* fix test

* change linting error

* Fixed integration tests

* fixed tests

* sample commit

---------

Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
Cole Goldsmith 2025-11-11 19:39:59 -06:00 committed by GitHub
parent a553fb7d9b
commit 1385fd5d5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1906 additions and 1798 deletions

View file

@ -17,8 +17,22 @@ import {
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export type ModelOption = {
value: string;
label: string;
default?: boolean;
provider?: string;
};
export type GroupedModelOption = {
group: string;
options: ModelOption[];
icon?: React.ReactNode;
};
export function ModelSelector({ export function ModelSelector({
options, options,
groupedOptions,
value = "", value = "",
onValueChange, onValueChange,
icon, icon,
@ -28,33 +42,40 @@ export function ModelSelector({
custom = false, custom = false,
hasError = false, hasError = false,
}: { }: {
options: { options?: ModelOption[];
value: string; groupedOptions?: GroupedModelOption[];
label: string;
default?: boolean;
}[];
value: string; value: string;
icon?: React.ReactNode; icon?: React.ReactNode;
placeholder?: string; placeholder?: string;
searchPlaceholder?: string; searchPlaceholder?: string;
noOptionsPlaceholder?: string; noOptionsPlaceholder?: string;
custom?: boolean; custom?: boolean;
onValueChange: (value: string) => void; onValueChange: (value: string, provider?: string) => void;
hasError?: boolean; hasError?: boolean;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
// Flatten grouped options or use regular options
const allOptions =
groupedOptions?.flatMap((group) => group.options) || options || [];
// Find the group icon for the selected value
const selectedOptionGroup = groupedOptions?.find((group) =>
group.options.some((opt) => opt.value === value)
);
const selectedIcon = selectedOptionGroup?.icon || icon;
useEffect(() => { useEffect(() => {
if ( if (
value && value &&
value !== "" && value !== "" &&
!options.find((option) => option.value === value) && !allOptions.find((option) => option.value === value) &&
!custom !custom
) { ) {
onValueChange(""); onValueChange("");
} }
}, [options, value, custom, onValueChange]); }, [allOptions, value, custom, onValueChange]);
return ( return (
<Popover open={open} onOpenChange={setOpen} modal={false}> <Popover open={open} onOpenChange={setOpen} modal={false}>
@ -63,7 +84,7 @@ export function ModelSelector({
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"
disabled={options.length === 0} disabled={allOptions.length === 0}
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"w-full gap-2 justify-between font-normal text-sm", "w-full gap-2 justify-between font-normal text-sm",
@ -72,24 +93,18 @@ export function ModelSelector({
> >
{value ? ( {value ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{icon && <div className="w-4 h-4">{icon}</div>} {selectedIcon && <div className="w-4 h-4">{selectedIcon}</div>}
{options.find((framework) => framework.value === value)?.label || {allOptions.find((framework) => framework.value === value)
value} ?.label || value}
{/* {options.find((framework) => framework.value === value)
?.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)} */}
{custom && {custom &&
value && value &&
!options.find((framework) => framework.value === value) && ( !allOptions.find((framework) => framework.value === value) && (
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
CUSTOM CUSTOM
</Badge> </Badge>
)} )}
</div> </div>
) : options.length === 0 ? ( ) : allOptions.length === 0 ? (
noOptionsPlaceholder noOptionsPlaceholder
) : ( ) : (
placeholder placeholder
@ -113,14 +128,52 @@ export function ModelSelector({
onWheel={(e) => e.stopPropagation()} onWheel={(e) => e.stopPropagation()}
> >
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty> <CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
<CommandGroup> {groupedOptions ? (
{options.map((option) => ( groupedOptions.map((group) => (
<CommandGroup
key={group.group}
heading={
<div className="flex items-center gap-2">
{group.icon && (
<div className="w-4 h-4">{group.icon}</div>
)}
<span>{group.group}</span>
</div>
}
>
{group.options.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.value} value={option.value}
onSelect={(currentValue) => { onSelect={(currentValue) => {
if (currentValue !== value) { if (currentValue !== value) {
onValueChange(currentValue); onValueChange(currentValue, option.provider);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex items-center gap-2">
{option.label}
</div>
</CommandItem>
))}
</CommandGroup>
))
) : (
<CommandGroup>
{allOptions.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue, option.provider);
} }
setOpen(false); setOpen(false);
}} }}
@ -133,17 +186,14 @@ export function ModelSelector({
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{option.label} {option.label}
{/* {option.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted"> // DISABLING DEFAULT TAG FOR NOW
Default
</span>
)} */}
</div> </div>
</CommandItem> </CommandItem>
))} ))}
{custom && {custom &&
searchValue && searchValue &&
!options.find((option) => option.value === searchValue) && ( !allOptions.find(
(option) => option.value === searchValue
) && (
<CommandItem <CommandItem
value={searchValue} value={searchValue}
onSelect={(currentValue) => { onSelect={(currentValue) => {
@ -168,6 +218,7 @@ export function ModelSelector({
</CommandItem> </CommandItem>
)} )}
</CommandGroup> </CommandGroup>
)}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View file

@ -15,7 +15,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [currentStep, setCurrentStep] = useState<number | null>(null); const [currentStep, setCurrentStep] = useState<number | null>(null);
const STEP_LIST = ["Uploading your document", "Generating embeddings", "Ingesting document", "Processing your document"]; const STEP_LIST = ["Uploading your document", "Processing your document"];
// Query tasks to track completion // Query tasks to track completion
const { data: tasks } = useGetTasksQuery({ const { data: tasks } = useGetTasksQuery({

View file

@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation"; import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
@ -14,7 +15,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useDebouncedValue } from "@/lib/debounce";
import { import {
AnthropicSettingsForm, AnthropicSettingsForm,
type AnthropicSettingsFormData, type AnthropicSettingsFormData,
@ -28,6 +28,8 @@ const AnthropicSettingsDialog = ({
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<Error | null>(null);
const methods = useForm<AnthropicSettingsFormData>({ const methods = useForm<AnthropicSettingsFormData>({
mode: "onSubmit", mode: "onSubmit",
@ -36,24 +38,18 @@ const AnthropicSettingsDialog = ({
}, },
}); });
const { handleSubmit, watch, formState } = methods; const { handleSubmit, watch } = methods;
const apiKey = watch("apiKey"); const apiKey = watch("apiKey");
const debouncedApiKey = useDebouncedValue(apiKey, 500);
const { const { refetch: validateCredentials } = useGetAnthropicModelsQuery(
isLoading: isLoadingModels,
error: modelsError,
} = useGetAnthropicModelsQuery(
{ {
apiKey: debouncedApiKey, apiKey: apiKey,
}, },
{ {
enabled: !!debouncedApiKey && open, enabled: false,
} }
); );
const hasValidationError = !!modelsError || !!formState.errors.apiKey;
const settingsMutation = useUpdateSettingsMutation({ const settingsMutation = useUpdateSettingsMutation({
onSuccess: () => { onSuccess: () => {
// Update provider health cache to healthy since backend validated the setup // Update provider health cache to healthy since backend validated the setup
@ -64,12 +60,29 @@ const AnthropicSettingsDialog = ({
}; };
queryClient.setQueryData(["provider", "health"], healthData); queryClient.setQueryData(["provider", "health"], healthData);
toast.success("Anthropic credentials saved. Configure models in the Settings page."); toast.success(
"Anthropic credentials saved. Configure models in the Settings page."
);
setOpen(false); setOpen(false);
}, },
}); });
const onSubmit = (data: AnthropicSettingsFormData) => { const onSubmit = async (data: AnthropicSettingsFormData) => {
// Clear any previous validation errors
setValidationError(null);
// Only validate if a new API key was entered
if (data.apiKey) {
setIsValidating(true);
const result = await validateCredentials();
setIsValidating(false);
if (result.isError) {
setValidationError(result.error);
return;
}
}
const payload: { const payload: {
anthropic_api_key?: string; anthropic_api_key?: string;
} = {}; } = {};
@ -98,8 +111,8 @@ const AnthropicSettingsDialog = ({
</DialogHeader> </DialogHeader>
<AnthropicSettingsForm <AnthropicSettingsForm
modelsError={modelsError} modelsError={validationError}
isLoadingModels={isLoadingModels} isLoadingModels={isValidating}
/> />
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -126,9 +139,13 @@ const AnthropicSettingsDialog = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels} disabled={settingsMutation.isPending || isValidating}
> >
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"} {settingsMutation.isPending
? "Saving..."
: isValidating
? "Validating..."
: "Save"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View file

@ -7,6 +7,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@ -20,7 +21,6 @@ import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettings
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery"; import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useDebouncedValue } from "@/lib/debounce";
const OllamaSettingsDialog = ({ const OllamaSettingsDialog = ({
open, open,
@ -31,6 +31,8 @@ const OllamaSettingsDialog = ({
}) => { }) => {
const { isAuthenticated, isNoAuthMode } = useAuth(); const { isAuthenticated, isNoAuthMode } = useAuth();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<Error | null>(null);
const { data: settings = {} } = useGetSettingsQuery({ const { data: settings = {} } = useGetSettingsQuery({
enabled: isAuthenticated || isNoAuthMode, enabled: isAuthenticated || isNoAuthMode,
@ -47,24 +49,18 @@ const OllamaSettingsDialog = ({
}, },
}); });
const { handleSubmit, watch, formState } = methods; const { handleSubmit, watch } = methods;
const endpoint = watch("endpoint"); const endpoint = watch("endpoint");
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
const { const { refetch: validateCredentials } = useGetOllamaModelsQuery(
isLoading: isLoadingModels,
error: modelsError,
} = useGetOllamaModelsQuery(
{ {
endpoint: debouncedEndpoint, endpoint: endpoint,
}, },
{ {
enabled: formState.isDirty && !!debouncedEndpoint && open, enabled: false,
} }
); );
const hasValidationError = !!modelsError || !!formState.errors.endpoint;
const settingsMutation = useUpdateSettingsMutation({ const settingsMutation = useUpdateSettingsMutation({
onSuccess: () => { onSuccess: () => {
// Update provider health cache to healthy since backend validated the setup // Update provider health cache to healthy since backend validated the setup
@ -75,12 +71,27 @@ const OllamaSettingsDialog = ({
}; };
queryClient.setQueryData(["provider", "health"], healthData); queryClient.setQueryData(["provider", "health"], healthData);
toast.success("Ollama endpoint saved. Configure models in the Settings page."); toast.success(
"Ollama endpoint saved. Configure models in the Settings page."
);
setOpen(false); setOpen(false);
}, },
}); });
const onSubmit = (data: OllamaSettingsFormData) => { const onSubmit = async (data: OllamaSettingsFormData) => {
// Clear any previous validation errors
setValidationError(null);
// Validate endpoint by fetching models
setIsValidating(true);
const result = await validateCredentials();
setIsValidating(false);
if (result.isError) {
setValidationError(result.error);
return;
}
settingsMutation.mutate({ settingsMutation.mutate({
ollama_endpoint: data.endpoint, ollama_endpoint: data.endpoint,
}); });
@ -101,8 +112,8 @@ const OllamaSettingsDialog = ({
</DialogHeader> </DialogHeader>
<OllamaSettingsForm <OllamaSettingsForm
modelsError={modelsError} modelsError={validationError}
isLoadingModels={isLoadingModels} isLoadingModels={isValidating}
/> />
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -129,9 +140,13 @@ const OllamaSettingsDialog = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels} disabled={settingsMutation.isPending || isValidating}
> >
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"} {settingsMutation.isPending
? "Saving..."
: isValidating
? "Validating..."
: "Save"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View file

@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation"; import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
@ -14,7 +15,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useDebouncedValue } from "@/lib/debounce";
import { import {
OpenAISettingsForm, OpenAISettingsForm,
type OpenAISettingsFormData, type OpenAISettingsFormData,
@ -28,6 +28,8 @@ const OpenAISettingsDialog = ({
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<Error | null>(null);
const methods = useForm<OpenAISettingsFormData>({ const methods = useForm<OpenAISettingsFormData>({
mode: "onSubmit", mode: "onSubmit",
@ -36,24 +38,18 @@ const OpenAISettingsDialog = ({
}, },
}); });
const { handleSubmit, watch, formState } = methods; const { handleSubmit, watch } = methods;
const apiKey = watch("apiKey"); const apiKey = watch("apiKey");
const debouncedApiKey = useDebouncedValue(apiKey, 500);
const { const { refetch: validateCredentials } = useGetOpenAIModelsQuery(
isLoading: isLoadingModels,
error: modelsError,
} = useGetOpenAIModelsQuery(
{ {
apiKey: debouncedApiKey, apiKey: apiKey,
}, },
{ {
enabled: !!debouncedApiKey && open, enabled: false,
} }
); );
const hasValidationError = !!modelsError || !!formState.errors.apiKey;
const settingsMutation = useUpdateSettingsMutation({ const settingsMutation = useUpdateSettingsMutation({
onSuccess: () => { onSuccess: () => {
// Update provider health cache to healthy since backend validated the setup // Update provider health cache to healthy since backend validated the setup
@ -64,12 +60,29 @@ const OpenAISettingsDialog = ({
}; };
queryClient.setQueryData(["provider", "health"], healthData); queryClient.setQueryData(["provider", "health"], healthData);
toast.success("OpenAI credentials saved. Configure models in the Settings page."); toast.success(
"OpenAI credentials saved. Configure models in the Settings page."
);
setOpen(false); setOpen(false);
}, },
}); });
const onSubmit = (data: OpenAISettingsFormData) => { const onSubmit = async (data: OpenAISettingsFormData) => {
// Clear any previous validation errors
setValidationError(null);
// Only validate if a new API key was entered
if (data.apiKey) {
setIsValidating(true);
const result = await validateCredentials();
setIsValidating(false);
if (result.isError) {
setValidationError(result.error);
return;
}
}
const payload: { const payload: {
openai_api_key?: string; openai_api_key?: string;
} = {}; } = {};
@ -98,8 +111,8 @@ const OpenAISettingsDialog = ({
</DialogHeader> </DialogHeader>
<OpenAISettingsForm <OpenAISettingsForm
modelsError={modelsError} modelsError={validationError}
isLoadingModels={isLoadingModels} isLoadingModels={isValidating}
/> />
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -126,9 +139,13 @@ const OpenAISettingsDialog = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels} disabled={settingsMutation.isPending || isValidating}
> >
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"} {settingsMutation.isPending
? "Saving..."
: isValidating
? "Validating..."
: "Save"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View file

@ -7,6 +7,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@ -18,7 +19,6 @@ import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettings
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery"; import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useDebouncedValue } from "@/lib/debounce";
const WatsonxSettingsDialog = ({ const WatsonxSettingsDialog = ({
open, open,
@ -28,6 +28,8 @@ const WatsonxSettingsDialog = ({
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<Error | null>(null);
const methods = useForm<WatsonxSettingsFormData>({ const methods = useForm<WatsonxSettingsFormData>({
mode: "onSubmit", mode: "onSubmit",
@ -38,31 +40,22 @@ const WatsonxSettingsDialog = ({
}, },
}); });
const { handleSubmit, watch, formState } = methods; const { handleSubmit, watch } = methods;
const endpoint = watch("endpoint"); const endpoint = watch("endpoint");
const apiKey = watch("apiKey"); const apiKey = watch("apiKey");
const projectId = watch("projectId"); const projectId = watch("projectId");
const debouncedEndpoint = useDebouncedValue(endpoint, 500); const { refetch: validateCredentials } = useGetIBMModelsQuery(
const debouncedApiKey = useDebouncedValue(apiKey, 500);
const debouncedProjectId = useDebouncedValue(projectId, 500);
const {
isLoading: isLoadingModels,
error: modelsError,
} = useGetIBMModelsQuery(
{ {
endpoint: debouncedEndpoint, endpoint: endpoint,
apiKey: debouncedApiKey, apiKey: apiKey,
projectId: debouncedProjectId, projectId: projectId,
}, },
{ {
enabled: !!debouncedEndpoint && !!debouncedApiKey && !!debouncedProjectId && open, enabled: false,
} }
); );
const hasValidationError = !!modelsError || !!formState.errors.endpoint || !!formState.errors.apiKey || !!formState.errors.projectId;
const settingsMutation = useUpdateSettingsMutation({ const settingsMutation = useUpdateSettingsMutation({
onSuccess: () => { onSuccess: () => {
// Update provider health cache to healthy since backend validated the setup // Update provider health cache to healthy since backend validated the setup
@ -72,12 +65,27 @@ const WatsonxSettingsDialog = ({
provider: "watsonx", provider: "watsonx",
}; };
queryClient.setQueryData(["provider", "health"], healthData); queryClient.setQueryData(["provider", "health"], healthData);
toast.success("watsonx credentials saved. Configure models in the Settings page."); toast.success(
"watsonx credentials saved. Configure models in the Settings page."
);
setOpen(false); setOpen(false);
}, },
}); });
const onSubmit = (data: WatsonxSettingsFormData) => { const onSubmit = async (data: WatsonxSettingsFormData) => {
// Clear any previous validation errors
setValidationError(null);
// Validate credentials by fetching models
setIsValidating(true);
const result = await validateCredentials();
setIsValidating(false);
if (result.isError) {
setValidationError(result.error);
return;
}
const payload: { const payload: {
watsonx_endpoint: string; watsonx_endpoint: string;
watsonx_api_key?: string; watsonx_api_key?: string;
@ -111,8 +119,8 @@ const WatsonxSettingsDialog = ({
</DialogHeader> </DialogHeader>
<WatsonxSettingsForm <WatsonxSettingsForm
modelsError={modelsError} modelsError={validationError}
isLoadingModels={isLoadingModels} isLoadingModels={isValidating}
/> />
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -139,9 +147,13 @@ const WatsonxSettingsDialog = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels} disabled={settingsMutation.isPending || isValidating}
> >
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"} {settingsMutation.isPending
? "Saving..."
: isValidating
? "Validating..."
: "Save"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View file

@ -5,7 +5,12 @@ import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useState } from "react"; import { Suspense, useCallback, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useGetOpenAIModelsQuery, useGetAnthropicModelsQuery, useGetOllamaModelsQuery, useGetIBMModelsQuery } from "@/app/api/queries/useGetModelsQuery"; import {
useGetOpenAIModelsQuery,
useGetAnthropicModelsQuery,
useGetOllamaModelsQuery,
useGetIBMModelsQuery,
} from "@/app/api/queries/useGetModelsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { ConfirmationDialog } from "@/components/confirmation-dialog"; import { ConfirmationDialog } from "@/components/confirmation-dialog";
import { LabelWrapper } from "@/components/label-wrapper"; import { LabelWrapper } from "@/components/label-wrapper";
@ -116,90 +121,125 @@ function KnowledgeSourcesPage() {
enabled: isAuthenticated || isNoAuthMode, enabled: isAuthenticated || isNoAuthMode,
}); });
// Get the current providers from settings
const currentLlmProvider = (settings.agent?.llm_provider ||
"openai") as ModelProvider;
const currentEmbeddingProvider = (settings.knowledge?.embedding_provider ||
"openai") as ModelProvider;
// State for selected providers (for changing provider on the fly)
const [selectedLlmProvider, setSelectedLlmProvider] = useState<ModelProvider>(currentLlmProvider);
const [selectedEmbeddingProvider, setSelectedEmbeddingProvider] = useState<ModelProvider>(currentEmbeddingProvider);
// Sync state with settings when they change
useEffect(() => {
if (settings.agent?.llm_provider) {
setSelectedLlmProvider(settings.agent.llm_provider as ModelProvider);
}
}, [settings.agent?.llm_provider]);
useEffect(() => {
if (settings.knowledge?.embedding_provider) {
setSelectedEmbeddingProvider(settings.knowledge.embedding_provider as ModelProvider);
}
}, [settings.knowledge?.embedding_provider]);
// Fetch models for each provider // Fetch models for each provider
const { data: openaiModels, isLoading: openaiLoading } = useGetOpenAIModelsQuery( const { data: openaiModels, isLoading: openaiLoading } =
useGetOpenAIModelsQuery(
{ apiKey: "" }, { apiKey: "" },
{ enabled: settings?.providers?.openai?.configured === true } { enabled: settings?.providers?.openai?.configured === true }
); );
const { data: anthropicModels, isLoading: anthropicLoading } = useGetAnthropicModelsQuery( const { data: anthropicModels, isLoading: anthropicLoading } =
useGetAnthropicModelsQuery(
{ apiKey: "" }, { apiKey: "" },
{ enabled: settings?.providers?.anthropic?.configured === true } { enabled: settings?.providers?.anthropic?.configured === true }
); );
const { data: ollamaModels, isLoading: ollamaLoading } = useGetOllamaModelsQuery( const { data: ollamaModels, isLoading: ollamaLoading } =
useGetOllamaModelsQuery(
{ endpoint: settings?.providers?.ollama?.endpoint }, { endpoint: settings?.providers?.ollama?.endpoint },
{ enabled: settings?.providers?.ollama?.configured === true && !!settings?.providers?.ollama?.endpoint } {
enabled:
settings?.providers?.ollama?.configured === true &&
!!settings?.providers?.ollama?.endpoint,
}
); );
const { data: watsonxModels, isLoading: watsonxLoading } = useGetIBMModelsQuery( const { data: watsonxModels, isLoading: watsonxLoading } =
useGetIBMModelsQuery(
{ {
endpoint: settings?.providers?.watsonx?.endpoint, endpoint: settings?.providers?.watsonx?.endpoint,
apiKey: "", apiKey: "",
projectId: settings?.providers?.watsonx?.project_id, projectId: settings?.providers?.watsonx?.project_id,
}, },
{ {
enabled: settings?.providers?.watsonx?.configured === true && enabled:
settings?.providers?.watsonx?.configured === true &&
!!settings?.providers?.watsonx?.endpoint && !!settings?.providers?.watsonx?.endpoint &&
!!settings?.providers?.watsonx?.project_id !!settings?.providers?.watsonx?.project_id,
} }
); );
// Get models for selected LLM provider // Build grouped LLM model options from all configured providers
const getModelsForProvider = (provider: ModelProvider) => { const groupedLlmModels = [
switch (provider) { {
case "openai": group: "OpenAI",
return { data: openaiModels, isLoading: openaiLoading }; provider: "openai",
case "anthropic": icon: getModelLogo("", "openai"),
return { data: anthropicModels, isLoading: anthropicLoading }; models: openaiModels?.language_models || [],
case "ollama": configured: settings.providers?.openai?.configured === true,
return { data: ollamaModels, isLoading: ollamaLoading }; },
case "watsonx": {
return { data: watsonxModels, isLoading: watsonxLoading }; group: "Anthropic",
default: provider: "anthropic",
return { data: undefined, isLoading: false }; icon: getModelLogo("", "anthropic"),
} models: anthropicModels?.language_models || [],
}; configured: settings.providers?.anthropic?.configured === true,
},
{
group: "Ollama",
provider: "ollama",
icon: getModelLogo("", "ollama"),
models: ollamaModels?.language_models || [],
configured: settings.providers?.ollama?.configured === true,
},
{
group: "IBM watsonx.ai",
provider: "watsonx",
icon: getModelLogo("", "watsonx"),
models: watsonxModels?.language_models || [],
configured: settings.providers?.watsonx?.configured === true,
},
]
.filter((provider) => provider.configured)
.map((provider) => ({
group: provider.group,
icon: provider.icon,
options: provider.models.map((model) => ({
...model,
provider: provider.provider,
})),
}))
.filter((provider) => provider.options.length > 0);
const llmModelsQuery = getModelsForProvider(selectedLlmProvider); // Build grouped embedding model options from all configured providers (excluding Anthropic)
const embeddingModelsQuery = getModelsForProvider(selectedEmbeddingProvider); const groupedEmbeddingModels = [
{
group: "OpenAI",
provider: "openai",
icon: getModelLogo("", "openai"),
models: openaiModels?.embedding_models || [],
configured: settings.providers?.openai?.configured === true,
},
{
group: "Ollama",
provider: "ollama",
icon: getModelLogo("", "ollama"),
models: ollamaModels?.embedding_models || [],
configured: settings.providers?.ollama?.configured === true,
},
{
group: "IBM watsonx.ai",
provider: "watsonx",
icon: getModelLogo("", "watsonx"),
models: watsonxModels?.embedding_models || [],
configured: settings.providers?.watsonx?.configured === true,
},
]
.filter((provider) => provider.configured)
.map((provider) => ({
group: provider.group,
icon: provider.icon,
options: provider.models.map((model) => ({
...model,
provider: provider.provider,
})),
}))
.filter((provider) => provider.options.length > 0);
// Filter provider options to only show configured ones const isLoadingAnyLlmModels =
const configuredLlmProviders = [ openaiLoading || anthropicLoading || ollamaLoading || watsonxLoading;
{ value: "openai", label: "OpenAI", default: selectedLlmProvider === "openai" }, const isLoadingAnyEmbeddingModels =
{ value: "anthropic", label: "Anthropic", default: selectedLlmProvider === "anthropic" }, openaiLoading || ollamaLoading || watsonxLoading;
{ value: "ollama", label: "Ollama", default: selectedLlmProvider === "ollama" },
{ value: "watsonx", label: "IBM watsonx.ai", default: selectedLlmProvider === "watsonx" },
].filter((option) => settings.providers?.[option.value as ModelProvider]?.configured === true);
const configuredEmbeddingProviders = [
{ value: "openai", label: "OpenAI", default: selectedEmbeddingProvider === "openai" },
{ value: "ollama", label: "Ollama", default: selectedEmbeddingProvider === "ollama" },
{ value: "watsonx", label: "IBM watsonx.ai", default: selectedEmbeddingProvider === "watsonx" },
].filter((option) => settings.providers?.[option.value as ModelProvider]?.configured === true);
// Mutations // Mutations
const updateSettingsMutation = useUpdateSettingsMutation({ const updateSettingsMutation = useUpdateSettingsMutation({
@ -218,7 +258,7 @@ function KnowledgeSourcesPage() {
(variables: Parameters<typeof updateSettingsMutation.mutate>[0]) => { (variables: Parameters<typeof updateSettingsMutation.mutate>[0]) => {
updateSettingsMutation.mutate(variables); updateSettingsMutation.mutate(variables);
}, },
500, 500
); );
// Sync system prompt state with settings data // Sync system prompt state with settings data
@ -260,28 +300,15 @@ function KnowledgeSourcesPage() {
} }
}, [settings.knowledge?.picture_descriptions]); }, [settings.knowledge?.picture_descriptions]);
// Update model selection immediately // Update model selection immediately (also updates provider)
const handleModelChange = (newModel: string) => { const handleModelChange = (newModel: string, provider?: string) => {
if (newModel) updateSettingsMutation.mutate({ llm_model: newModel }); if (newModel && provider) {
};
// Update LLM provider selection
const handleLlmProviderChange = (newProvider: string) => {
setSelectedLlmProvider(newProvider as ModelProvider);
// Get models for the new provider
const modelsForProvider = getModelsForProvider(newProvider as ModelProvider);
const models = modelsForProvider.data?.language_models;
// If models are available, select the first one along with the provider
if (models && models.length > 0 && models[0].value) {
updateSettingsMutation.mutate({ updateSettingsMutation.mutate({
llm_provider: newProvider, llm_model: newModel,
llm_model: models[0].value llm_provider: provider,
}); });
} else { } else if (newModel) {
// If models aren't loaded yet, just update the provider updateSettingsMutation.mutate({ llm_model: newModel });
updateSettingsMutation.mutate({ llm_provider: newProvider });
} }
}; };
@ -290,33 +317,18 @@ function KnowledgeSourcesPage() {
updateSettingsMutation.mutate({ system_prompt: systemPrompt }); updateSettingsMutation.mutate({ system_prompt: systemPrompt });
}; };
// Update embedding model selection immediately // Update embedding model selection immediately (also updates provider)
const handleEmbeddingModelChange = (newModel: string) => { const handleEmbeddingModelChange = (newModel: string, provider?: string) => {
if (newModel) updateSettingsMutation.mutate({ embedding_model: newModel }); if (newModel && provider) {
};
// Update embedding provider selection
const handleEmbeddingProviderChange = (newProvider: string) => {
setSelectedEmbeddingProvider(newProvider as ModelProvider);
// Get models for the new provider
const modelsForProvider = getModelsForProvider(newProvider as ModelProvider);
const models = modelsForProvider.data?.embedding_models;
// If models are available, select the first one along with the provider
if (models && models.length > 0 && models[0].value) {
updateSettingsMutation.mutate({ updateSettingsMutation.mutate({
embedding_provider: newProvider, embedding_model: newModel,
embedding_model: models[0].value embedding_provider: provider,
}); });
} else { } else if (newModel) {
// If models aren't loaded yet, just update the provider updateSettingsMutation.mutate({ embedding_model: newModel });
updateSettingsMutation.mutate({ embedding_provider: newProvider });
} }
}; };
const isEmbeddingModelSelectDisabled = updateSettingsMutation.isPending;
// Update chunk size setting with debounce // Update chunk size setting with debounce
const handleChunkSizeChange = (value: string) => { const handleChunkSizeChange = (value: string) => {
const numValue = Math.max(0, parseInt(value) || 0); const numValue = Math.max(0, parseInt(value) || 0);
@ -398,7 +410,7 @@ function KnowledgeSourcesPage() {
const data = await response.json(); const data = await response.json();
const connections = data.connections || []; const connections = data.connections || [];
const activeConnection = connections.find( const activeConnection = connections.find(
(conn: Connection) => conn.is_active, (conn: Connection) => conn.is_active
); );
const isConnected = activeConnection !== undefined; const isConnected = activeConnection !== undefined;
@ -410,8 +422,8 @@ function KnowledgeSourcesPage() {
status: isConnected ? "connected" : "not_connected", status: isConnected ? "connected" : "not_connected",
connectionId: activeConnection?.connection_id, connectionId: activeConnection?.connection_id,
} }
: c, : c
), )
); );
} }
} }
@ -454,7 +466,7 @@ function KnowledgeSourcesPage() {
`response_type=code&` + `response_type=code&` +
`scope=${result.oauth_config.scopes.join(" ")}&` + `scope=${result.oauth_config.scopes.join(" ")}&` +
`redirect_uri=${encodeURIComponent( `redirect_uri=${encodeURIComponent(
result.oauth_config.redirect_uri, result.oauth_config.redirect_uri
)}&` + )}&` +
`access_type=offline&` + `access_type=offline&` +
`prompt=consent&` + `prompt=consent&` +
@ -593,7 +605,7 @@ function KnowledgeSourcesPage() {
const handleEditInLangflow = ( const handleEditInLangflow = (
flowType: "chat" | "ingest", flowType: "chat" | "ingest",
closeDialog: () => void, closeDialog: () => void
) => { ) => {
// Select the appropriate flow ID and edit URL based on flow type // Select the appropriate flow ID and edit URL based on flow type
const targetFlowId = const targetFlowId =
@ -968,22 +980,6 @@ function KnowledgeSourcesPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2">
<LabelWrapper
label="Language model provider"
helperText="Choose which provider to use for chat"
id="llm-provider"
required={true}
>
<ModelSelector
options={configuredLlmProviders}
noOptionsPlaceholder="No providers available"
icon={getModelLogo("", selectedLlmProvider)}
value={selectedLlmProvider}
onValueChange={handleLlmProviderChange}
/>
</LabelWrapper>
</div>
<div className="space-y-2"> <div className="space-y-2">
<LabelWrapper <LabelWrapper
label="Language model" label="Language model"
@ -992,13 +988,12 @@ function KnowledgeSourcesPage() {
required={true} required={true}
> >
<ModelSelector <ModelSelector
options={llmModelsQuery.data?.language_models || []} groupedOptions={groupedLlmModels}
noOptionsPlaceholder={ noOptionsPlaceholder={
llmModelsQuery.isLoading isLoadingAnyLlmModels
? "Loading models..." ? "Loading models..."
: "No language models detected. Configure this provider first." : "No language models detected. Configure a provider first."
} }
icon={getModelLogo("", selectedLlmProvider)}
value={settings.agent?.llm_model || ""} value={settings.agent?.llm_model || ""}
onValueChange={handleModelChange} onValueChange={handleModelChange}
/> />
@ -1130,22 +1125,6 @@ function KnowledgeSourcesPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2">
<LabelWrapper
label="Embedding model provider"
helperText="Choose which provider to use for embeddings"
id="embedding-provider"
required={true}
>
<ModelSelector
options={configuredEmbeddingProviders}
noOptionsPlaceholder="No providers available"
icon={getModelLogo("", selectedEmbeddingProvider)}
value={selectedEmbeddingProvider}
onValueChange={handleEmbeddingProviderChange}
/>
</LabelWrapper>
</div>
<div className="space-y-2"> <div className="space-y-2">
<LabelWrapper <LabelWrapper
helperText="Model used for knowledge ingest and retrieval" helperText="Model used for knowledge ingest and retrieval"
@ -1154,13 +1133,12 @@ function KnowledgeSourcesPage() {
required={true} required={true}
> >
<ModelSelector <ModelSelector
options={embeddingModelsQuery.data?.embedding_models || []} groupedOptions={groupedEmbeddingModels}
noOptionsPlaceholder={ noOptionsPlaceholder={
embeddingModelsQuery.isLoading isLoadingAnyEmbeddingModels
? "Loading models..." ? "Loading models..."
: "No embedding models detected. Configure this provider first." : "No embedding models detected. Configure a provider first."
} }
icon={getModelLogo("", selectedEmbeddingProvider)}
value={settings.knowledge?.embedding_model || ""} value={settings.knowledge?.embedding_model || ""}
onValueChange={handleEmbeddingModelChange} onValueChange={handleEmbeddingModelChange}
/> />
@ -1233,7 +1211,7 @@ function KnowledgeSourcesPage() {
size="iconSm" size="iconSm"
onClick={() => onClick={() =>
handleChunkOverlapChange( handleChunkOverlapChange(
(chunkOverlap + 1).toString(), (chunkOverlap + 1).toString()
) )
} }
> >
@ -1246,7 +1224,7 @@ function KnowledgeSourcesPage() {
size="iconSm" size="iconSm"
onClick={() => onClick={() =>
handleChunkOverlapChange( handleChunkOverlapChange(
(chunkOverlap - 1).toString(), (chunkOverlap - 1).toString()
) )
} }
> >

View file

@ -1,36 +1,40 @@
"use client"; "use client";
import { Switch } from "@/components/ui/switch"; import { ChevronRight } from "lucide-react";
import { useEffect } from "react";
import {
useGetIBMModelsQuery,
useGetOllamaModelsQuery,
useGetOpenAIModelsQuery,
} from "@/app/api/queries/useGetModelsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import type { ModelOption } from "@/app/onboarding/components/model-selector";
import {
getFallbackModels,
type ModelProvider,
} from "@/app/settings/helpers/model-helpers";
import { ModelSelectItems } from "@/app/settings/helpers/model-select-item";
import { LabelWrapper } from "@/components/label-wrapper";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { ChevronRight } from "lucide-react"; import { NumberInput } from "@/components/ui/inputs/number-input";
import { IngestSettings as IngestSettingsType } from "./types";
import { LabelWrapper } from "@/components/label-wrapper";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { ModelSelectItems } from "@/app/settings/helpers/model-select-item";
import { getFallbackModels, type ModelProvider } from "@/app/settings/helpers/model-helpers";
import { NumberInput } from "@/components/ui/inputs/number-input";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import {
useGetOpenAIModelsQuery,
useGetOllamaModelsQuery,
useGetIBMModelsQuery,
} from "@/app/api/queries/useGetModelsQuery";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { useEffect } from "react"; import type { IngestSettings as IngestSettingsType } from "./types";
interface IngestSettingsProps { interface IngestSettingsProps {
isOpen: boolean; isOpen: boolean;
@ -53,7 +57,7 @@ export const IngestSettings = ({
}); });
// Get the current provider from API settings // Get the current provider from API settings
const currentProvider = (apiSettings.providers?.model_provider || const currentProvider = (apiSettings.knowledge?.embedding_provider ||
"openai") as ModelProvider; "openai") as ModelProvider;
// Fetch available models based on provider // Fetch available models based on provider
@ -99,10 +103,16 @@ export const IngestSettings = ({
// Update settings when API embedding model changes // Update settings when API embedding model changes
useEffect(() => { useEffect(() => {
if (apiEmbeddingModel && (!settings || settings.embeddingModel !== apiEmbeddingModel)) { if (
onSettingsChange?.({ ...currentSettings, embeddingModel: apiEmbeddingModel }); apiEmbeddingModel &&
(!settings || settings.embeddingModel !== apiEmbeddingModel)
) {
onSettingsChange?.({
...currentSettings,
embeddingModel: apiEmbeddingModel,
});
} }
}, [apiEmbeddingModel]); }, [apiEmbeddingModel, settings, onSettingsChange, currentSettings]);
const handleSettingsChange = (newSettings: Partial<IngestSettingsType>) => { const handleSettingsChange = (newSettings: Partial<IngestSettingsType>) => {
const updatedSettings = { ...currentSettings, ...newSettings }; const updatedSettings = { ...currentSettings, ...newSettings };
@ -137,7 +147,9 @@ export const IngestSettings = ({
<Select <Select
disabled={false} disabled={false}
value={currentSettings.embeddingModel} value={currentSettings.embeddingModel}
onValueChange={(value) => handleSettingsChange({ embeddingModel: value })} onValueChange={(value) =>
handleSettingsChange({ embeddingModel: value })
}
> >
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -152,7 +164,10 @@ export const IngestSettings = ({
<SelectContent> <SelectContent>
<ModelSelectItems <ModelSelectItems
models={modelsData?.embedding_models} models={modelsData?.embedding_models}
fallbackModels={getFallbackModels(currentProvider).embedding} fallbackModels={
getFallbackModels(currentProvider)
.embedding as ModelOption[]
}
provider={currentProvider} provider={currentProvider}
/> />
</SelectContent> </SelectContent>

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "openrag" name = "openrag"
version = "0.1.32" version = "0.1.33"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"

View file

@ -750,4 +750,4 @@ def get_agent_config():
def get_embedding_model() -> str: def get_embedding_model() -> str:
"""Return the currently configured embedding model.""" """Return the currently configured embedding model."""
return get_openrag_config().knowledge.embedding_model return get_openrag_config().knowledge.embedding_model or EMBED_MODEL if DISABLE_INGEST_WITH_LANGFLOW else ""

View file

@ -1,7 +1,7 @@
import copy import copy
from typing import Any, Dict from typing import Any, Dict
from agentd.tool_decorator import tool from agentd.tool_decorator import tool
from config.settings import clients, INDEX_NAME, get_embedding_model from config.settings import EMBED_MODEL, clients, INDEX_NAME, get_embedding_model
from auth_context import get_auth_context from auth_context import get_auth_context
from utils.logging_config import get_logger from utils.logging_config import get_logger
@ -34,8 +34,8 @@ class SearchService:
# Strategy: Use provided model, or default to the configured embedding # Strategy: Use provided model, or default to the configured embedding
# model. This assumes documents are embedded with that model by default. # model. This assumes documents are embedded with that model by default.
# Future enhancement: Could auto-detect available models in corpus # Future enhancement: Could auto-detect available models in corpus.
embedding_model = embedding_model or get_embedding_model() embedding_model = embedding_model or get_embedding_model() or EMBED_MODEL
embedding_field_name = get_embedding_field_name(embedding_model) embedding_field_name = get_embedding_field_name(embedding_model)
logger.info( logger.info(

View file

@ -47,7 +47,8 @@ async def onboard_system():
transport = httpx.ASGITransport(app=app) transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
onboarding_payload = { onboarding_payload = {
"model_provider": "openai", "llm_provider": "openai",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small", "embedding_model": "text-embedding-3-small",
"llm_model": "gpt-4o-mini", "llm_model": "gpt-4o-mini",
"sample_data": False, "sample_data": False,

View file

@ -114,6 +114,8 @@ async def test_upload_and_search_endpoint(tmp_path: Path, disable_langflow_inges
# Ensure we route uploads to traditional processor and disable startup ingest # Ensure we route uploads to traditional processor and disable startup ingest
os.environ["DISABLE_INGEST_WITH_LANGFLOW"] = "true" if disable_langflow_ingest else "false" os.environ["DISABLE_INGEST_WITH_LANGFLOW"] = "true" if disable_langflow_ingest else "false"
os.environ["DISABLE_STARTUP_INGEST"] = "true" os.environ["DISABLE_STARTUP_INGEST"] = "true"
os.environ["EMBEDDING_MODEL"] = "text-embedding-3-small"
os.environ["EMBEDDING_PROVIDER"] = "openai"
# Force no-auth mode so endpoints bypass authentication # Force no-auth mode so endpoints bypass authentication
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = "" os.environ["GOOGLE_OAUTH_CLIENT_ID"] = ""
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = "" os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = ""

2
uv.lock generated
View file

@ -2352,7 +2352,7 @@ wheels = [
[[package]] [[package]]
name = "openrag" name = "openrag"
version = "0.1.32" version = "0.1.33"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "agentd" }, { name = "agentd" },