LLMs: OpenAI: Autocomplete + suggest hosts for Chutes, Fireworks, Novita. #921

This commit is contained in:
Enrico Ros
2026-01-19 22:37:17 -08:00
parent 971f737846
commit b73df7b2ce
2 changed files with 187 additions and 35 deletions
@@ -0,0 +1,150 @@
import * as React from 'react';
import { Autocomplete, AutocompleteOption, Box, FormControl, FormHelperText, FormLabel, Typography } from '@mui/joy';
import InfoIcon from '@mui/icons-material/Info';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { Link } from '~/common/components/Link';
// Verified OpenAI-compatible providers that work with the 'openai' dialect
interface VerifiedProvider {
id: string;
label: string;
host: string;
description: string;
category: 'Example Proxies' | 'Example Providers';
docsUrl?: string; // optional link to provider docs
hostMatch?: string; // substring to match against current host (defaults to host)
}
const OPENAI_COMPATIBLE_PROVIDERS: VerifiedProvider[] = [
// Example Providers
{ id: 'chutes', label: 'Chutes AI', host: 'https://llm.chutes.ai', hostMatch: '.chutes.ai', category: 'Example Providers', description: 'GPU marketplace for AI inference', docsUrl: 'https://chutes.ai/docs' },
{ id: 'fireworks', label: 'Fireworks AI', host: 'https://api.fireworks.ai/inference', hostMatch: 'fireworks.ai', category: 'Example Providers', description: 'Fast inference for open models', docsUrl: 'https://docs.fireworks.ai/getting-started/quickstart' },
{ id: 'novita', label: 'Novita AI', host: 'https://api.novita.ai/openai', hostMatch: 'novita.ai', category: 'Example Providers', description: 'OpenAI-compatible inference', docsUrl: 'https://novita.ai/docs' },
// Example Proxies
{ id: 'helicone', label: 'Helicone', host: 'https://oai.hconeai.com', hostMatch: 'hconeai.com', category: 'Example Proxies', description: 'OpenAI observability and caching proxy', docsUrl: 'https://docs.helicone.ai/getting-started/quick-start' },
{ id: 'cloudflare', label: 'Cloudflare AI Gateway', host: 'https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai', hostMatch: 'gateway.ai.cloudflare.com', category: 'Example Proxies', description: 'AI Gateway with caching and analytics', docsUrl: 'https://developers.cloudflare.com/ai-gateway/' },
];
// Find matching provider based on current host value
export function findMatchingOpenAIAutoProvider(host: string): VerifiedProvider | undefined {
if (!host) return undefined;
return OPENAI_COMPATIBLE_PROVIDERS.find(p =>
host.includes(p.hostMatch ?? p.host),
);
}
// Autocomplete component for selecting verified OpenAI-compatible providers or typing custom URLs
export function OpenAIHostAutocomplete(props: {
value: string;
onChange: (value: string) => void;
}) {
// local input state for freeSolo
const [inputValue, setInputValue] = React.useState(props.value ?? '');
// derived state
const matchedProvider = findMatchingOpenAIAutoProvider(props.value);
// sync input when value prop changes externally
React.useEffect(() => {
setInputValue(props.value ?? '');
}, [props.value]);
// handlers
const handleChange = React.useCallback((_event: unknown, newValue: string | VerifiedProvider | null) => {
// newValue can be: string (typed), VerifiedProvider (selected), or null (cleared)
if (newValue === null)
props.onChange('');
else if (typeof newValue === 'string')
props.onChange(newValue);
else
props.onChange(newValue.host);
}, [props]);
const handleInputChange = React.useCallback((_event: unknown, newInputValue: string, reason: string) => {
// Only update on user input, not on programmatic changes
if (reason !== 'input')
return;
setInputValue(newInputValue);
props.onChange(newInputValue);
}, [props]);
// dynamic right label: show docs link when a provider is matched
const rightLabel = matchedProvider?.docsUrl
? <Link level='body-sm' href={matchedProvider.docsUrl} target='_blank'>{matchedProvider.label} docs</Link>
: null;
return (
<FormControl>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', flexWrap: 'wrap', justifyContent: 'space-between' }}>
<FormLabel sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
API Endpoint
<GoodTooltip title={`An OpenAI compatible endpoint to be used in place of 'api.openai.com'.\n\nSelect a verified provider from the list, or type any custom URL.`} arrow placement='top'>
<InfoIcon sx={{ ml: 0.5, cursor: 'pointer', fontSize: 'md', color: 'primary.solidBg' }} />
</GoodTooltip>
</FormLabel>
{rightLabel && <FormHelperText sx={{ display: 'block' }}>{rightLabel}</FormHelperText>}
</Box>
<Autocomplete<VerifiedProvider, false, false, true>
freeSolo
openOnFocus
clearOnEscape
placeholder='Select or type endpoint...'
options={OPENAI_COMPATIBLE_PROVIDERS}
groupBy={(option) => option.category}
getOptionKey={(option) => typeof option === 'string' ? option : option.id}
getOptionLabel={(option) => typeof option === 'string' ? option : option.host}
isOptionEqualToValue={(option, val) => option.host === (typeof val === 'string' ? val : val.host)}
value={OPENAI_COMPATIBLE_PROVIDERS.find(p => p.host === props.value) ?? (props.value || null)}
onChange={handleChange}
inputValue={inputValue}
onInputChange={handleInputChange}
renderGroup={(params) => (
<Box component='li' key={params.key}>
<Typography level='body-sm' sx={{ textAlign: 'center', my: 1 }}>
{params.group}
</Typography>
<Box component='ul' sx={{ p: 0 }}>
{params.children}
</Box>
</Box>
)}
renderOption={(optionProps, option) => {
const { key, ...rest } = optionProps as any;
return (
<AutocompleteOption key={key} {...rest} sx={{ display: 'block', py: 1 }}>
<Typography level='title-sm'>{option.label}</Typography>
<Typography level='body-xs' textColor='text.tertiary' className='agi-ellipsize' mt={0.25}>{option.description}</Typography>
</AutocompleteOption>
);
}}
slotProps={{
root: {
sx: { boxShadow: 'none' },
},
listbox: {
sx: {
maxWidth: 'min(450px, calc(100dvw - 1rem))',
// // Add footer hint
// '&::after': {
// content: '"Or type any OpenAI-compatible base URL"',
// display: 'block',
// p: 1.5,
// color: 'text.tertiary',
// fontSize: 'xs',
// fontStyle: 'italic',
// borderTop: '1px solid',
// borderColor: 'divider',
// },
},
},
}}
/>
</FormControl>
);
}
+37 -35
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Alert, IconButton } from '@mui/joy';
import { Alert, Divider, IconButton } from '@mui/joy';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import type { DModelsServiceId } from '~/common/stores/llms/llms.service.types';
@@ -11,6 +11,7 @@ import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
import { FormTextField } from '~/common/components/forms/FormTextField';
import { InlineError } from '~/common/components/InlineError';
import { Link } from '~/common/components/Link';
import { SetupFormClientSideToggle } from '~/common/components/forms/SetupFormClientSideToggle';
import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
import { useToggleableBoolean } from '~/common/util/hooks/useToggleableBoolean';
@@ -19,7 +20,7 @@ import { useLlmUpdateModels } from '../../llm.client.hooks';
import { useServiceSetup } from '../useServiceSetup';
import { ModelVendorOpenAI } from './openai.vendor';
import { SetupFormClientSideToggle } from '~/common/components/forms/SetupFormClientSideToggle';
import { OpenAIHostAutocomplete } from './OpenAIHostAutocomplete';
// avoid repeating it all over
@@ -28,9 +29,6 @@ const HELICONE_OPENAI_HOST = 'oai.hconeai.com';
export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
// state
const advanced = useToggleableBoolean(!!props.serviceId?.includes('-'));
// external state
const { service, serviceAccess, serviceHasCloudTenantConfig, serviceHasLLMs, updateSettings, updateLabel } =
useServiceSetup(props.serviceId, ModelVendorOpenAI);
@@ -38,7 +36,11 @@ export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
// derived state
const { clientSideFetch, oaiKey, oaiOrg, oaiHost, heliKey, moderationCheck } = serviceAccess;
const needsUserKey = !serviceHasCloudTenantConfig;
const showAdvanced = advanced.on || !!clientSideFetch;
// state
const initialShowOAIAdvanced = !!props.serviceId?.includes('-') /* likely a custom service */ && needsUserKey && !oaiKey && !oaiHost /* missing both */;
const advanced = useToggleableBoolean(initialShowOAIAdvanced);
const showAdvanced = advanced.on;
const keyValid = true; //isValidOpenAIApiKey(oaiKey);
const keyError = (/*needsUserKey ||*/ !!oaiKey) && !keyValid;
@@ -52,26 +54,40 @@ export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
<ApproximateCosts serviceId={service?.id} />
{(showAdvanced || !!oaiHost) && (
<OpenAIHostAutocomplete
value={oaiHost}
onChange={host => updateSettings({ oaiHost: host })}
/>
)}
<FormInputKey
autoCompleteId='openai-key' label='API Key'
rightLabel={<>{needsUserKey
? !oaiKey && <Link level='body-sm' href='https://platform.openai.com/account/api-keys' target='_blank'>create key</Link>
: <AlreadySet />
} {oaiKey && keyValid && <Link level='body-sm' href='https://platform.openai.com/account/usage' target='_blank'>check usage</Link>}
? (!oaiKey && !oaiHost && <Link level='body-sm' href='https://platform.openai.com/account/api-keys' target='_blank'>create key</Link>)
: (!oaiHost && <AlreadySet /> /* only show "Already set" when using default OpenAI, not custom endpoints */)
} {oaiKey && !oaiHost && keyValid && <Link level='body-sm' href='https://platform.openai.com/account/usage' target='_blank'>check usage</Link>}
</>}
value={oaiKey} onChange={value => updateSettings({ oaiKey: value })}
required={needsUserKey} isError={keyError}
required={needsUserKey || !!oaiHost} isError={keyError}
placeholder='sk-...'
/>
{showAdvanced && <Divider sx={{ mx: 4, my: 2 }} />}
{showAdvanced && <FormTextField
autoCompleteId='openai-host'
title='API Endpoint'
tooltip={`An OpenAI compatible endpoint to be used in place of 'api.openai.com'.\n\nCould be used for Helicone, Cloudflare, or other OpenAI compatible cloud or local services.\n\nExamples:\n - ${HELICONE_OPENAI_HOST}\n - localhost:1234`}
description={<><Link level='body-sm' href='https://www.helicone.ai' target='_blank'>Helicone</Link>, <Link level='body-sm' href='https://developers.cloudflare.com/ai-gateway/' target='_blank'>Cloudflare</Link></>}
placeholder={`e.g., ${HELICONE_OPENAI_HOST}, https://gateway.ai.cloudflare.com/v1/<ACCOUNT_TAG>/<GATEWAY_URL_SLUG>/openai, etc..`}
value={oaiHost}
onChange={text => updateSettings({ oaiHost: text })}
autoCompleteId='openai-service-name'
title='Custom Name'
// tooltip='Custom name for this service. Useful when you have multiple OpenAI-compatible services configured.'
placeholder='e.g., Fireworks, etc.'
value={service?.label || ''}
onChange={updateLabel}
endDecorator={
<IconButton size='sm' variant='plain' color='neutral' onClick={() => updateLabel('')}>
<RestartAltIcon />
</IconButton>
}
/>}
{showAdvanced && <FormTextField
@@ -94,12 +110,12 @@ export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
{!!heliKey && <Alert variant='soft' color={oaiHost?.includes(HELICONE_OPENAI_HOST) ? 'success' : 'warning'}>
Advanced: You set the Helicone key. {!oaiHost?.includes(HELICONE_OPENAI_HOST)
? `But you also need to set the OpenAI Host to ${HELICONE_OPENAI_HOST} to use Helicone.`
? `But you also need to set the OpenAI Host to https://${HELICONE_OPENAI_HOST} to use Helicone.`
: 'OpenAI traffic will now be routed through Helicone.'}
</Alert>}
{showAdvanced && <FormSwitchControl
title='Moderation' on='Enabled' fullWidth
{showAdvanced && (!oaiHost || moderationCheck) && <FormSwitchControl
title='OpenAI Moderation' on='Enabled'
description={<>
<Link level='body-sm' href='https://platform.openai.com/docs/guides/moderation/moderation' target='_blank'>Overview</Link>,
{' '}<Link level='body-sm' href='https://openai.com/policies/usage-policies' target='_blank'>policy</Link>
@@ -108,21 +124,7 @@ export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
onChange={on => updateSettings({ moderationCheck: on })}
/>}
{showAdvanced && <FormTextField
autoCompleteId='openai-service-name'
title='Custom Name'
// tooltip='Custom name for this service. Useful when you have multiple OpenAI-compatible services configured.'
placeholder='e.g., Fireworks, Together AI, etc.'
value={service?.label || ''}
onChange={updateLabel}
endDecorator={
<IconButton size='sm' variant='plain' color='neutral' onClick={() => updateLabel('')}>
<RestartAltIcon />
</IconButton>
}
/>}
{showAdvanced && <SetupFormClientSideToggle
{(showAdvanced || clientSideFetch) && <SetupFormClientSideToggle
visible={!!oaiHost || !!oaiKey}
checked={!!clientSideFetch}
onChange={on => updateSettings({ csf: on })}