* feat: prevent duplicate model names under same provider Implement case-insensitive validation to prevent users from creating duplicate model names under the same provider. This validation is implemented both in the backend API and the frontend UI. Changes: - Backend: Add duplicate check in create_model endpoint (case-insensitive) - Frontend: Add client-side validation in AddModelForm - Frontend: Improve error message display from backend - Tests: Add unit tests for duplicate model validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: optimize duplicate model validation and improve error handling - Replace O(n) model iteration with efficient SurrealDB query for duplicate check - Improve error message to include model name and provider for better UX - Remove frontend duplicate validation (backend-only enforcement) - Fix test authentication by setting OPEN_NOTEBOOK_PASSWORD before imports - Update test mocking to use repo_query instead of Model.get_all() - Add pytest fixture for TestClient to ensure proper test isolation All 11 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * remove unnecessary package * fix: replace any with unknown type in error handler - Change error type from 'any' to 'unknown' to satisfy ESLint - Add proper type assertion for error object structure - Maintains same runtime behavior with better type safety --------- Co-authored-by: Claude <noreply@anthropic.com>
136 lines
No EOL
4.5 KiB
TypeScript
136 lines
No EOL
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { CreateModelRequest, ProviderAvailability } from '@/lib/types/models'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
import { useCreateModel } from '@/lib/hooks/use-models'
|
|
import { Plus } from 'lucide-react'
|
|
|
|
interface AddModelFormProps {
|
|
modelType: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text'
|
|
providers: ProviderAvailability
|
|
}
|
|
|
|
export function AddModelForm({ modelType, providers }: AddModelFormProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const createModel = useCreateModel()
|
|
const { register, handleSubmit, formState: { errors }, reset, setValue, watch } = useForm<CreateModelRequest>({
|
|
defaultValues: {
|
|
type: modelType
|
|
}
|
|
})
|
|
|
|
// Get available providers that support this model type
|
|
const availableProviders = providers.available.filter(provider =>
|
|
providers.supported_types[provider]?.includes(modelType)
|
|
)
|
|
|
|
const onSubmit = async (data: CreateModelRequest) => {
|
|
await createModel.mutateAsync(data)
|
|
reset()
|
|
setOpen(false)
|
|
}
|
|
|
|
const getModelTypeName = () => {
|
|
return modelType.replace(/_/g, ' ')
|
|
}
|
|
|
|
const getModelPlaceholder = () => {
|
|
switch (modelType) {
|
|
case 'language':
|
|
return 'e.g., gpt-5-mini, claude, gemini'
|
|
case 'embedding':
|
|
return 'e.g., text-embedding-3-small'
|
|
case 'text_to_speech':
|
|
return 'e.g., tts-gpt-4o-mini-tts, tts-1-hd'
|
|
case 'speech_to_text':
|
|
return 'e.g., whisper-1'
|
|
default:
|
|
return 'Enter model name'
|
|
}
|
|
}
|
|
|
|
if (availableProviders.length === 0) {
|
|
return (
|
|
<div className="text-sm text-muted-foreground">
|
|
No providers available for {getModelTypeName()} models
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const handleOpenChange = (isOpen: boolean) => {
|
|
setOpen(isOpen)
|
|
if (!isOpen) {
|
|
reset()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm">
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Model
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Add {getModelTypeName()} Model</DialogTitle>
|
|
<DialogDescription>
|
|
Configure a new {getModelTypeName()} model from available providers.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="provider">Provider</Label>
|
|
<Select onValueChange={(value) => setValue('provider', value)} required>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a provider" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableProviders.map((provider) => (
|
|
<SelectItem key={provider} value={provider}>
|
|
<span className="capitalize">{provider}</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.provider && (
|
|
<p className="text-sm text-destructive mt-1">Provider is required</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="name">Model Name</Label>
|
|
<Input
|
|
id="name"
|
|
{...register('name', { required: 'Model name is required' })}
|
|
placeholder={getModelPlaceholder()}
|
|
/>
|
|
{errors.name && (
|
|
<p className="text-sm text-destructive mt-1">{errors.name.message}</p>
|
|
)}
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{modelType === 'language' && watch('provider') === 'azure' &&
|
|
'For Azure, use the deployment name as the model name'}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={createModel.isPending}>
|
|
{createModel.isPending ? 'Adding...' : 'Add Model'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
} |