Declarative providers (#5084)
Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import type { View } from '../../../../utils/navigationUtils';
|
||||
import Model, { getProviderMetadata } from '../modelInterface';
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
import { ProviderType } from '../../../../api';
|
||||
|
||||
type SwitchModelModalProps = {
|
||||
sessionId: string | null;
|
||||
@@ -165,8 +166,9 @@ export const SwitchModelModal = ({ sessionId, onClose, setView }: SwitchModelMod
|
||||
const results = await Promise.all(modelPromises);
|
||||
|
||||
// Process results and build grouped options
|
||||
const groupedOptions: { options: { value: string; label: string; provider: string }[] }[] =
|
||||
[];
|
||||
const groupedOptions: {
|
||||
options: { value: string; label: string; provider: string; providerType: ProviderType }[];
|
||||
}[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
results.forEach(({ provider: p, models, error }) => {
|
||||
@@ -178,13 +180,19 @@ export const SwitchModelModal = ({ sessionId, onClose, setView }: SwitchModelMod
|
||||
options: p.metadata.known_models.map(({ name }) => ({
|
||||
value: name,
|
||||
label: name,
|
||||
providerType: p.provider_type,
|
||||
provider: p.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
} else if (models && models.length > 0) {
|
||||
groupedOptions.push({
|
||||
options: models.map((m) => ({ value: m, label: m, provider: p.name })),
|
||||
options: models.map((m) => ({
|
||||
value: m,
|
||||
label: m,
|
||||
provider: p.name,
|
||||
providerType: p.provider_type,
|
||||
})),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -196,12 +204,14 @@ export const SwitchModelModal = ({ sessionId, onClose, setView }: SwitchModelMod
|
||||
|
||||
// Add the "Custom model" option to each provider group
|
||||
groupedOptions.forEach((group) => {
|
||||
const providerName = group.options[0]?.provider;
|
||||
if (providerName && !providerName.startsWith('custom_')) {
|
||||
const option = group.options[0];
|
||||
const providerName = option?.provider;
|
||||
if (providerName && option?.providerType !== 'Custom') {
|
||||
group.options.push({
|
||||
value: 'custom',
|
||||
label: 'Use custom model',
|
||||
provider: providerName,
|
||||
providerType: option?.providerType,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,11 @@ import { ProviderCard } from './subcomponents/ProviderCard';
|
||||
import CardContainer from './subcomponents/CardContainer';
|
||||
import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider';
|
||||
import ProviderConfigurationModal from './modal/ProviderConfiguationModal';
|
||||
import { ProviderDetails, CreateCustomProviderRequest } from '../../../api';
|
||||
import {
|
||||
DeclarativeProviderConfig,
|
||||
ProviderDetails,
|
||||
UpdateCustomProviderRequest,
|
||||
} from '../../../api';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
|
||||
@@ -43,7 +47,6 @@ const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onCli
|
||||
);
|
||||
});
|
||||
|
||||
// Memoize the ProviderCards component
|
||||
const ProviderCards = memo(function ProviderCards({
|
||||
providers,
|
||||
isOnboarding,
|
||||
@@ -57,28 +60,69 @@ const ProviderCards = memo(function ProviderCards({
|
||||
}) {
|
||||
const { openModal } = useProviderModal();
|
||||
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<{
|
||||
id: string;
|
||||
config: DeclarativeProviderConfig;
|
||||
isEditable: boolean;
|
||||
} | null>(null);
|
||||
|
||||
// Memoize these functions so they don't get recreated on every render
|
||||
const configureProviderViaModal = useCallback(
|
||||
(provider: ProviderDetails) => {
|
||||
openModal(provider, {
|
||||
onSubmit: () => {
|
||||
// Only refresh if the function is provided
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
onDelete: (_values: unknown) => {
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
formProps: {},
|
||||
});
|
||||
async (provider: ProviderDetails) => {
|
||||
if (provider.provider_type === 'Custom' || provider.provider_type === 'Declarative') {
|
||||
const { getCustomProvider } = await import('../../../api');
|
||||
const result = await getCustomProvider({ path: { id: provider.name }, throwOnError: true });
|
||||
|
||||
if (result.data) {
|
||||
setEditingProvider({
|
||||
id: provider.name,
|
||||
config: result.data.config,
|
||||
isEditable: result.data.is_editable,
|
||||
});
|
||||
setShowCustomProviderModal(true);
|
||||
}
|
||||
} else {
|
||||
openModal(provider, {
|
||||
onSubmit: () => {
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
onDelete: (_values: unknown) => {
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
formProps: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[openModal, refreshProviders]
|
||||
);
|
||||
|
||||
const handleUpdateCustomProvider = useCallback(
|
||||
async (data: UpdateCustomProviderRequest) => {
|
||||
if (!editingProvider) return;
|
||||
|
||||
const { updateCustomProvider } = await import('../../../api');
|
||||
await updateCustomProvider({
|
||||
path: { id: editingProvider.id },
|
||||
body: data,
|
||||
throwOnError: true,
|
||||
});
|
||||
setShowCustomProviderModal(false);
|
||||
setEditingProvider(null);
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
[editingProvider, refreshProviders]
|
||||
);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setShowCustomProviderModal(false);
|
||||
setEditingProvider(null);
|
||||
}, []);
|
||||
|
||||
const deleteProviderConfigViaModal = useCallback(
|
||||
(provider: ProviderDetails) => {
|
||||
openModal(provider, {
|
||||
@@ -95,22 +139,17 @@ const ProviderCards = memo(function ProviderCards({
|
||||
);
|
||||
|
||||
const handleCreateCustomProvider = useCallback(
|
||||
async (data: CreateCustomProviderRequest) => {
|
||||
try {
|
||||
const { createCustomProvider } = await import('../../../api');
|
||||
await createCustomProvider({ body: data });
|
||||
setShowCustomProviderModal(false);
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create custom provider:', error);
|
||||
async (data: UpdateCustomProviderRequest) => {
|
||||
const { createCustomProvider } = await import('../../../api');
|
||||
await createCustomProvider({ body: data, throwOnError: true });
|
||||
setShowCustomProviderModal(false);
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
[refreshProviders]
|
||||
);
|
||||
|
||||
// Use useMemo to memoize the cards array
|
||||
const providerCards = useMemo(() => {
|
||||
// providers needs to be an array
|
||||
const providersArray = Array.isArray(providers) ? providers : [];
|
||||
@@ -138,21 +177,33 @@ const ProviderCards = memo(function ProviderCards({
|
||||
onProviderLaunch,
|
||||
]);
|
||||
|
||||
const initialData = editingProvider && {
|
||||
engine: editingProvider.config.engine.toLowerCase() + '_compatible',
|
||||
display_name: editingProvider.config.display_name,
|
||||
api_url: editingProvider.config.base_url,
|
||||
api_key: '',
|
||||
models: editingProvider.config.models.map((m) => m.name),
|
||||
supports_streaming: editingProvider.config.supports_streaming ?? true,
|
||||
};
|
||||
|
||||
const editable = editingProvider ? editingProvider.isEditable : true;
|
||||
const title = (editingProvider ? (editable ? 'Edit' : 'Configure') : 'Add') + ' Provider';
|
||||
return (
|
||||
<>
|
||||
{providerCards}
|
||||
|
||||
<Dialog open={showCustomProviderModal} onOpenChange={setShowCustomProviderModal}>
|
||||
<Dialog open={showCustomProviderModal} onOpenChange={handleCloseModal}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Custom Provider</DialogTitle>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CustomProviderForm
|
||||
onSubmit={handleCreateCustomProvider}
|
||||
onCancel={() => setShowCustomProviderModal(false)}
|
||||
initialData={initialData}
|
||||
isEditable={editable}
|
||||
onSubmit={editingProvider ? handleUpdateCustomProvider : handleCreateCustomProvider}
|
||||
onCancel={handleCloseModal}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>{' '}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function ProviderConfigurationModal() {
|
||||
}
|
||||
|
||||
try {
|
||||
const isCustomProvider = currentProvider.name.startsWith('custom_');
|
||||
const isCustomProvider = currentProvider.provider_type === 'Custom';
|
||||
|
||||
if (isCustomProvider) {
|
||||
await removeCustomProvider({
|
||||
|
||||
+171
-150
@@ -1,24 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import { Select } from '../../../../../ui/Select';
|
||||
import { Button } from '../../../../../ui/button';
|
||||
import { SecureStorageNotice } from '../SecureStorageNotice';
|
||||
import { Checkbox } from '@radix-ui/themes';
|
||||
import { UpdateCustomProviderRequest } from '../../../../../../api';
|
||||
|
||||
interface CustomProviderFormProps {
|
||||
onSubmit: (data: {
|
||||
provider_type: string;
|
||||
display_name: string;
|
||||
api_url: string;
|
||||
api_key: string;
|
||||
models: string[];
|
||||
supports_streaming: boolean;
|
||||
}) => void;
|
||||
onSubmit: (data: UpdateCustomProviderRequest) => void;
|
||||
onCancel: () => void;
|
||||
initialData: UpdateCustomProviderRequest | null;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
export default function CustomProviderForm({ onSubmit, onCancel }: CustomProviderFormProps) {
|
||||
const [providerType, setProviderType] = useState('openai_compatible');
|
||||
export default function CustomProviderForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
initialData,
|
||||
isEditable,
|
||||
}: CustomProviderFormProps) {
|
||||
const [engine, setEngine] = useState('openai_compatible');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [apiUrl, setApiUrl] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
@@ -27,6 +28,22 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
const [supportsStreaming, setSupportsStreaming] = useState(true);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const engineMap: Record<string, string> = {
|
||||
openai: 'openai_compatible',
|
||||
anthropic: 'anthropic_compatible',
|
||||
ollama: 'ollama_compatible',
|
||||
};
|
||||
|
||||
setEngine(engineMap[initialData.engine.toLowerCase()] || 'openai_compatible');
|
||||
setDisplayName(initialData.display_name);
|
||||
setApiUrl(initialData.api_url);
|
||||
setModels(initialData.models.join(', '));
|
||||
setSupportsStreaming(initialData.supports_streaming ?? true);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleLocalModels = (checked: boolean) => {
|
||||
setIsLocalModel(checked);
|
||||
if (checked) {
|
||||
@@ -42,7 +59,7 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
const errors: Record<string, string> = {};
|
||||
if (!displayName) errors.displayName = 'Display name is required';
|
||||
if (!apiUrl) errors.apiUrl = 'API URL is required';
|
||||
if (!isLocalModel && !apiKey) errors.apiKey = 'API key is required';
|
||||
if (!isLocalModel && !apiKey && !initialData) errors.apiKey = 'API key is required';
|
||||
if (!models) errors.models = 'At least one model is required';
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
@@ -56,7 +73,7 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
.filter((m) => m);
|
||||
|
||||
onSubmit({
|
||||
provider_type: providerType,
|
||||
engine,
|
||||
display_name: displayName,
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey,
|
||||
@@ -67,92 +84,94 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="provider-select"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Provider Type
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="provider-select"
|
||||
aria-invalid={!!validationErrors.providerType}
|
||||
aria-describedby={validationErrors.providerType ? 'provider-select-error' : undefined}
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
]}
|
||||
value={{
|
||||
value: providerType,
|
||||
label:
|
||||
providerType === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
: providerType === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption) setProviderType(selectedOption.value);
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
{validationErrors.providerType && (
|
||||
<p id="provider-select-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.providerType}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="display-name"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Display Name
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="display-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
aria-invalid={!!validationErrors.displayName}
|
||||
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.displayName && (
|
||||
<p id="display-name-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="api-url"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
API URL
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="api-url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1/messages"
|
||||
aria-invalid={!!validationErrors.apiUrl}
|
||||
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiUrl && (
|
||||
<p id="api-url-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiUrl}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isEditable && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="provider-select"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Provider Type
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="provider-select"
|
||||
aria-invalid={!!validationErrors.providerType}
|
||||
aria-describedby={validationErrors.providerType ? 'provider-select-error' : undefined}
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
]}
|
||||
value={{
|
||||
value: engine,
|
||||
label:
|
||||
engine === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
: engine === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption) setEngine(selectedOption.value);
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
{validationErrors.providerType && (
|
||||
<p id="provider-select-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.providerType}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="display-name"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Display Name
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="display-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
aria-invalid={!!validationErrors.displayName}
|
||||
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.displayName && (
|
||||
<p id="display-name-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="api-url"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
API URL
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="api-url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1/messages"
|
||||
aria-invalid={!!validationErrors.apiUrl}
|
||||
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiUrl && (
|
||||
<p id="api-url-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiUrl}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
@@ -160,14 +179,14 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
API Key
|
||||
{!isLocalModel && <span className="text-red-500 ml-1">*</span>}
|
||||
{!isLocalModel && !initialData && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<Input
|
||||
id="api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Your API key"
|
||||
placeholder={initialData ? 'Leave blank to keep existing key' : 'Your API key'}
|
||||
aria-invalid={!!validationErrors.apiKey}
|
||||
aria-describedby={validationErrors.apiKey ? 'api-key-error' : undefined}
|
||||
className={validationErrors.apiKey ? 'border-red-500' : ''}
|
||||
@@ -179,62 +198,64 @@ export default function CustomProviderForm({ onSubmit, onCancel }: CustomProvide
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="local-model" checked={isLocalModel} onCheckedChange={handleLocalModels} />
|
||||
<label
|
||||
htmlFor="local-model"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
>
|
||||
This is a local model (no auth required)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="available-models"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Available Models (comma-separated)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="available-models"
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
aria-invalid={!!validationErrors.models}
|
||||
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.models && (
|
||||
<p id="available-models-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.models}
|
||||
</p>
|
||||
{!initialData && (
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="local-model" checked={isLocalModel} onCheckedChange={handleLocalModels} />
|
||||
<label
|
||||
htmlFor="local-model"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
>
|
||||
This is a local model (no auth required)
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mb-10">
|
||||
<Checkbox
|
||||
id="supports-streaming"
|
||||
checked={supportsStreaming}
|
||||
onCheckedChange={(checked) => setSupportsStreaming(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="supports-streaming"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
>
|
||||
Provider supports streaming responses
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isEditable && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="available-models"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
Available Models (comma-separated)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="available-models"
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
aria-invalid={!!validationErrors.models}
|
||||
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.models && (
|
||||
<p id="available-models-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.models}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-10">
|
||||
<Checkbox
|
||||
id="supports-streaming"
|
||||
checked={supportsStreaming}
|
||||
onCheckedChange={(checked) => setSupportsStreaming(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="supports-streaming"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
>
|
||||
Provider supports streaming responses
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<SecureStorageNotice />
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create Provider</Button>
|
||||
<Button type="submit">{initialData ? 'Update Provider' : 'Create Provider'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user