diff --git a/src/modules/llms/vendors/openai/OpenAIHostAutocomplete.tsx b/src/modules/llms/vendors/openai/OpenAIHostAutocomplete.tsx
new file mode 100644
index 000000000..4135a3735
--- /dev/null
+++ b/src/modules/llms/vendors/openai/OpenAIHostAutocomplete.tsx
@@ -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
+ ? {matchedProvider.label} docs
+ : null;
+
+ return (
+
+
+
+ API Endpoint
+
+
+
+
+ {rightLabel && {rightLabel}}
+
+
+ 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) => (
+
+
+ {params.group}
+
+
+ {params.children}
+
+
+ )}
+ renderOption={(optionProps, option) => {
+ const { key, ...rest } = optionProps as any;
+ return (
+
+ {option.label}
+ {option.description}
+
+ );
+ }}
+ 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',
+ // },
+ },
+ },
+ }}
+ />
+
+ );
+}
diff --git a/src/modules/llms/vendors/openai/OpenAIServiceSetup.tsx b/src/modules/llms/vendors/openai/OpenAIServiceSetup.tsx
index bb68b6e62..1e27347d0 100644
--- a/src/modules/llms/vendors/openai/OpenAIServiceSetup.tsx
+++ b/src/modules/llms/vendors/openai/OpenAIServiceSetup.tsx
@@ -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 }) {
+
+ {(showAdvanced || !!oaiHost) && (
+ updateSettings({ oaiHost: host })}
+ />
+ )}
+
{needsUserKey
- ? !oaiKey && create key
- :
- } {oaiKey && keyValid && check usage}
+ ? (!oaiKey && !oaiHost && create key)
+ : (!oaiHost && /* only show "Already set" when using default OpenAI, not custom endpoints */)
+ } {oaiKey && !oaiHost && keyValid && check usage}
>}
value={oaiKey} onChange={value => updateSettings({ oaiKey: value })}
- required={needsUserKey} isError={keyError}
+ required={needsUserKey || !!oaiHost} isError={keyError}
placeholder='sk-...'
/>
+ {showAdvanced && }
+
{showAdvanced && Helicone, Cloudflare>}
- placeholder={`e.g., ${HELICONE_OPENAI_HOST}, https://gateway.ai.cloudflare.com/v1///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={
+ updateLabel('')}>
+
+
+ }
/>}
{showAdvanced &&
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.'}
}
- {showAdvanced &&
Overview,
{' '}policy
@@ -108,21 +124,7 @@ export function OpenAIServiceSetup(props: { serviceId: DModelsServiceId }) {
onChange={on => updateSettings({ moderationCheck: on })}
/>}
- {showAdvanced && updateLabel('')}>
-
-
- }
- />}
-
- {showAdvanced && updateSettings({ csf: on })}