From 075a778dc6d9d27ca18f32cc45261c6cb063aaec Mon Sep 17 00:00:00 2001 From: Alison Sabuwala Date: Tue, 26 May 2026 10:23:46 -0400 Subject: [PATCH] feat: add options for adjusting LLM URL option --- .../smoke/preferences-interactions.test.ts | 72 +++++ .../main/__tests__/llm-config-service.test.ts | 31 +++ packages/insomnia/src/main/ipc/main.ts | 13 +- .../insomnia/src/main/llm-config-service.ts | 2 + .../ui/components/settings/ai-settings.tsx | 6 +- .../settings/llms/url-utils.test.ts | 177 ++++++++++++ .../ui/components/settings/llms/url-utils.ts | 86 ++++++ .../src/ui/components/settings/llms/url.tsx | 252 +++++++++++++++--- 8 files changed, 589 insertions(+), 50 deletions(-) create mode 100644 packages/insomnia/src/ui/components/settings/llms/url-utils.test.ts create mode 100644 packages/insomnia/src/ui/components/settings/llms/url-utils.ts diff --git a/packages/insomnia-smoke-test/tests/smoke/preferences-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/preferences-interactions.test.ts index aa210671b323..e785226ed75d 100644 --- a/packages/insomnia-smoke-test/tests/smoke/preferences-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/preferences-interactions.test.ts @@ -13,6 +13,78 @@ test('Preferences through keyboard shortcut', async ({ page }) => { await page.locator('text=Insomnia Preferences').first().click(); }); +test('AI URL settings persist advanced options', async ({ page }) => { + await page.evaluate(async () => { + await window.main.llm.updateBackendConfig('url', { + url: 'https://llm.local/v1', + model: 'gpt-4o-mini', + apiKey: 'persisted-token', + temperature: 0.7, + topP: 0.95, + maxTokens: 4096, + }); + await window.main.llm.setActiveBackend('url'); + }); + + await page.getByTestId('settings-button').click(); + await page.locator('text=Insomnia Preferences').first().click(); + await page.getByRole('tab', { name: 'AI Settings' }).click(); + await page.getByRole('button', { name: 'LLM URL Active' }).click(); + + await expect.soft(page.getByLabel('LLM URL')).toHaveValue('https://llm.local/v1'); + await expect.soft(page.getByLabel('API Token')).toHaveValue('persisted-token'); + + await page.getByRole('button', { name: 'Advanced Options' }).click(); + await expect.soft(page.getByLabel('Temperature (0-2):')).toHaveValue('0.7'); + await expect.soft(page.getByLabel('Top P (0-1):')).toHaveValue('0.95'); + await expect.soft(page.getByLabel('Max Tokens (1-128000):')).toHaveValue('4096'); +}); + +test('AI URL settings can deactivate active backend', async ({ page }) => { + await page.evaluate(async () => { + await window.main.llm.updateBackendConfig('url', { + url: 'https://llm-deactivate.local/v1', + model: 'gpt-4o-mini', + apiKey: 'activation-token', + temperature: 0.6, + topP: 0.9, + maxTokens: 8192, + }); + await window.main.llm.setActiveBackend('url'); + }); + + await page.getByTestId('settings-button').click(); + await page.locator('text=Insomnia Preferences').first().click(); + await page.getByRole('tab', { name: 'AI Settings' }).click(); + await page.getByRole('button', { name: 'LLM URL Active' }).click(); + + await expect.soft(page.getByText('Active model:')).toBeVisible(); + await expect.soft(page.getByText('gpt-4o-mini')).toBeVisible(); + await expect.soft(page.getByRole('button', { name: 'Deactivate' })).toBeVisible(); + + await page.getByRole('button', { name: 'Deactivate' }).click(); + + await expect.soft(page.getByRole('button', { name: 'LLM URL' })).toBeVisible(); + await expect.soft(page.getByRole('button', { name: 'LLM URL Active' })).toHaveCount(0); + + const [activeBackend, backendConfig] = await page.evaluate(async () => { + const active = await window.main.llm.getActiveBackend(); + const config = await window.main.llm.getBackendConfig('url'); + return [active, config] as const; + }); + + expect.soft(activeBackend).toBeNull(); + expect.soft(backendConfig).toMatchObject({ + backend: 'url', + url: 'https://llm-deactivate.local/v1', + model: 'gpt-4o-mini', + apiKey: 'activation-token', + temperature: 0.6, + topP: 0.9, + maxTokens: 8192, + }); +}); + // Quick reproduction for Kong/insomnia#5664 and INS-2267 test('Check filter responses by environment preference', async ({ app, page, insomnia }) => { const text = await loadFixture('simple.yaml'); diff --git a/packages/insomnia/src/main/__tests__/llm-config-service.test.ts b/packages/insomnia/src/main/__tests__/llm-config-service.test.ts index 5683cd0eb129..0289a8793a34 100644 --- a/packages/insomnia/src/main/__tests__/llm-config-service.test.ts +++ b/packages/insomnia/src/main/__tests__/llm-config-service.test.ts @@ -93,6 +93,25 @@ describe('llm-config-service', () => { expect(config.model).toBe('test-model'); }); + it('should parse numeric URL backend options from storage', async () => { + vi.mocked(services.pluginData.all).mockResolvedValue([ + mockPluginData('url.model', 'gpt-4.1-mini'), + mockPluginData('url.maxTokens', '4096'), + mockPluginData('url.temperature', '0.7'), + mockPluginData('url.topP', '0.95'), + ]); + + const config = await getBackendConfig('url'); + + expect(config).toEqual({ + backend: 'url', + model: 'gpt-4.1-mini', + maxTokens: 4096, + temperature: 0.7, + topP: 0.95, + }); + }); + it('should return empty config for unconfigured backend', async () => { vi.mocked(services.pluginData.all).mockResolvedValue([]); @@ -132,6 +151,18 @@ describe('llm-config-service', () => { ); }); + it('should save numeric URL backend options to storage', async () => { + await updateBackendConfig('url', { + maxTokens: 4096, + temperature: 0.7, + topP: 0.95, + }); + + expect(services.pluginData.upsertByKey).toHaveBeenCalledWith('insomnia-llm', 'url.maxTokens', '4096'); + expect(services.pluginData.upsertByKey).toHaveBeenCalledWith('insomnia-llm', 'url.temperature', '0.7'); + expect(services.pluginData.upsertByKey).toHaveBeenCalledWith('insomnia-llm', 'url.topP', '0.95'); + }); + it('should handle partial config updates', async () => { await updateBackendConfig('url', { url: 'https://new-url.com/v1', diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index e663724721f5..32383e678bcd 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -797,14 +797,19 @@ export function registerMainHandlers() { reject({ error: err.toString() }); }); const { systemPrompt, messages, modelConfig: modelConfigFromSamplingRequest } = input; + const mergedModelConfig = !modelConfig + ? modelConfigFromSamplingRequest + : modelConfig.backend === 'url' + ? modelConfig + : { + ...modelConfig, + ...modelConfigFromSamplingRequest, + }; process.postMessage({ messages, systemPrompt, - modelConfig: { - ...modelConfig, - ...modelConfigFromSamplingRequest, - }, + modelConfig: mergedModelConfig, aiPluginName: AI_PLUGIN_NAME, }); }); diff --git a/packages/insomnia/src/main/llm-config-service.ts b/packages/insomnia/src/main/llm-config-service.ts index 1e17a800a716..90606f7ab307 100644 --- a/packages/insomnia/src/main/llm-config-service.ts +++ b/packages/insomnia/src/main/llm-config-service.ts @@ -18,6 +18,7 @@ export interface LLMConfig { apiKey?: string; url?: string; baseURL?: string; + maxTokens?: number; temperature?: number; topP?: number; topK?: number; @@ -61,6 +62,7 @@ export const getBackendConfig = async (backend: LLMBackend): Promise { toggleAIFeature('aiMockServers', enabled)} + onChange={enabled => toggleAIFeature('aiMockServers', enabled)} isDisabled={isMockServerFeatureDisabled} className="group flex items-center gap-2" > @@ -144,7 +144,7 @@ export const AISettings = () => { toggleAIFeature('aiCommitMessages', enabled)} + onChange={enabled => toggleAIFeature('aiCommitMessages', enabled)} isDisabled={isCommitMessagesFeatureDisabled} className="group flex items-center gap-2" > @@ -170,7 +170,7 @@ export const AISettings = () => { toggleAIFeature('aiMcpClient', enabled)} + onChange={enabled => toggleAIFeature('aiMcpClient', enabled)} isDisabled={isMcpClientFeatureDisabled} className="group flex items-center gap-2" > diff --git a/packages/insomnia/src/ui/components/settings/llms/url-utils.test.ts b/packages/insomnia/src/ui/components/settings/llms/url-utils.test.ts new file mode 100644 index 000000000000..0e66866874f0 --- /dev/null +++ b/packages/insomnia/src/ui/components/settings/llms/url-utils.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, it } from 'vitest'; + +import { + DEFAULT_URL_MODEL_PARAMETERS, + getUrlActivateSettingsPayload, + getUrlAuthHeaders, + getUrlLoadModelsSettingsPayload, + getUrlModelParametersFromConfig, + hasUrlModelParameterChanges, + isUrlActivateDisabled, + urlModelParametersSchema, +} from './url-utils'; + +describe('url-utils', () => { + describe('getUrlModelParametersFromConfig()', () => { + it('returns defaults for empty config', () => { + expect(getUrlModelParametersFromConfig()).toEqual(DEFAULT_URL_MODEL_PARAMETERS); + }); + + it('returns defaults for null config', () => { + expect(getUrlModelParametersFromConfig(null)).toEqual(DEFAULT_URL_MODEL_PARAMETERS); + }); + + it('prefers configured values and falls back for missing fields', () => { + expect( + getUrlModelParametersFromConfig({ + temperature: 0.75, + topP: 0.8, + }), + ).toEqual({ + temperature: 0.75, + topP: 0.8, + maxTokens: DEFAULT_URL_MODEL_PARAMETERS.maxTokens, + }); + }); + }); + + describe('urlModelParametersSchema', () => { + it('accepts valid values', () => { + const result = urlModelParametersSchema.safeParse({ + temperature: 0.7, + topP: 0.9, + maxTokens: 4096, + }); + expect(result.success).toBe(true); + }); + + it('rejects out-of-range values', () => { + const result = urlModelParametersSchema.safeParse({ + temperature: 2.5, + topP: 1.2, + maxTokens: 0, + }); + expect(result.success).toBe(false); + }); + }); + + describe('hasUrlModelParameterChanges()', () => { + it('returns false when parameters match current config', () => { + const currentConfig = { + temperature: 0.7, + topP: 0.9, + maxTokens: 4096, + }; + expect(hasUrlModelParameterChanges(currentConfig, getUrlModelParametersFromConfig(currentConfig))).toBe(false); + }); + + it('returns true when parameters differ from current config', () => { + const currentConfig = { + temperature: 0.7, + topP: 0.9, + maxTokens: 4096, + }; + expect( + hasUrlModelParameterChanges(currentConfig, { + temperature: 0.8, + topP: 0.85, + maxTokens: 2048, + }), + ).toBe(true); + }); + }); + + describe('getUrlAuthHeaders()', () => { + it('returns undefined when API token is empty or whitespace', () => { + expect(getUrlAuthHeaders('')).toBeUndefined(); + expect(getUrlAuthHeaders(' ')).toBeUndefined(); + }); + + it('returns bearer Authorization header with trimmed token', () => { + expect(getUrlAuthHeaders(' sk-test ')).toEqual({ + Authorization: 'Bearer sk-test', + }); + }); + }); + + describe('settings payload helpers', () => { + it('builds load-models payload with all URL model properties', () => { + const payload = getUrlLoadModelsSettingsPayload('https://example.com/v1', ' token-1 ', { + temperature: 1.1, + topP: 0.8, + maxTokens: 1024, + }); + + expect(payload).toEqual({ + url: 'https://example.com/v1', + apiKey: 'token-1', + temperature: 1.1, + topP: 0.8, + maxTokens: 1024, + }); + }); + + it('builds activate payload with all URL model properties', () => { + const payload = getUrlActivateSettingsPayload('https://example.com/v1', 'gpt-test', 'token-2', { + temperature: 0.7, + topP: 0.95, + maxTokens: 2048, + }); + + expect(payload).toEqual({ + url: 'https://example.com/v1', + model: 'gpt-test', + apiKey: 'token-2', + temperature: 0.7, + topP: 0.95, + maxTokens: 2048, + }); + }); + }); + + describe('isUrlActivateDisabled()', () => { + it('enables activate for active URL backend with model selected and changes, even without reloaded models', () => { + expect( + isUrlActivateDisabled({ + hasLoadedModels: false, + isCurrentBackend: true, + selectedModel: 'gpt-test', + hasChanges: true, + }), + ).toBe(false); + }); + + it('disables activate for inactive backend when models have not been loaded', () => { + expect( + isUrlActivateDisabled({ + hasLoadedModels: false, + isCurrentBackend: false, + selectedModel: 'gpt-test', + hasChanges: true, + }), + ).toBe(true); + }); + + it('disables activate when no model is selected', () => { + expect( + isUrlActivateDisabled({ + hasLoadedModels: true, + isCurrentBackend: true, + selectedModel: '', + hasChanges: true, + }), + ).toBe(true); + }); + + it('disables activate when active backend has no changes', () => { + expect( + isUrlActivateDisabled({ + hasLoadedModels: true, + isCurrentBackend: true, + selectedModel: 'gpt-test', + hasChanges: false, + }), + ).toBe(true); + }); + }); +}); diff --git a/packages/insomnia/src/ui/components/settings/llms/url-utils.ts b/packages/insomnia/src/ui/components/settings/llms/url-utils.ts new file mode 100644 index 000000000000..41e7cbd9b0a7 --- /dev/null +++ b/packages/insomnia/src/ui/components/settings/llms/url-utils.ts @@ -0,0 +1,86 @@ +import z from 'zod/v4'; + +import type { LLMConfig } from '~/main/llm-config-service'; + +export const urlModelParametersSchema = z.object({ + temperature: z.number().min(0).max(2), + topP: z.number().min(0).max(1), + maxTokens: z.number().int().min(1).max(128_000), +}); + +export type UrlModelParameters = z.infer; + +export const DEFAULT_URL_MODEL_PARAMETERS: UrlModelParameters = { + temperature: 0.6, + topP: 0.9, + maxTokens: 8192, +}; + +export const getUrlModelParametersFromConfig = (config?: Partial | null): UrlModelParameters => ({ + temperature: config?.temperature ?? DEFAULT_URL_MODEL_PARAMETERS.temperature, + topP: config?.topP ?? DEFAULT_URL_MODEL_PARAMETERS.topP, + maxTokens: config?.maxTokens ?? DEFAULT_URL_MODEL_PARAMETERS.maxTokens, +}); + +export const hasUrlModelParameterChanges = ( + currentConfig: Partial | null, + modelParameters: UrlModelParameters, +): boolean => { + const current = getUrlModelParametersFromConfig(currentConfig); + return ( + modelParameters.temperature !== current.temperature || + modelParameters.topP !== current.topP || + modelParameters.maxTokens !== current.maxTokens + ); +}; + +export const getUrlAuthHeaders = (apiKey: string): Record | undefined => { + const trimmedApiKey = apiKey.trim(); + if (!trimmedApiKey) { + return undefined; + } + + return { + Authorization: `Bearer ${trimmedApiKey}`, + }; +}; + +export const getUrlLoadModelsSettingsPayload = ( + url: string, + apiKey: string, + modelParameters: UrlModelParameters, +): Partial => ({ + url, + apiKey: apiKey.trim(), + maxTokens: modelParameters.maxTokens, + temperature: modelParameters.temperature, + topP: modelParameters.topP, +}); + +export const getUrlActivateSettingsPayload = ( + url: string, + selectedModel: string, + apiKey: string, + modelParameters: UrlModelParameters, +): Partial => ({ + url, + model: selectedModel, + apiKey, + maxTokens: modelParameters.maxTokens, + temperature: modelParameters.temperature, + topP: modelParameters.topP, +}); + +export const isUrlActivateDisabled = ({ + hasLoadedModels, + isCurrentBackend, + selectedModel, + hasChanges, +}: { + hasLoadedModels: boolean; + isCurrentBackend: boolean; + selectedModel: string; + hasChanges: boolean; +}) => { + return (!hasLoadedModels && !isCurrentBackend) || !selectedModel || (isCurrentBackend && !hasChanges); +}; diff --git a/packages/insomnia/src/ui/components/settings/llms/url.tsx b/packages/insomnia/src/ui/components/settings/llms/url.tsx index 4e252e31f0d4..481d326d32af 100644 --- a/packages/insomnia/src/ui/components/settings/llms/url.tsx +++ b/packages/insomnia/src/ui/components/settings/llms/url.tsx @@ -3,6 +3,17 @@ import { Button, Input, Text } from 'react-aria-components'; import type { LLMBackend, LLMConfig } from '~/main/llm-config-service'; import { Icon } from '~/ui/components/icon'; +import { + DEFAULT_URL_MODEL_PARAMETERS, + getUrlActivateSettingsPayload, + getUrlAuthHeaders, + getUrlLoadModelsSettingsPayload, + getUrlModelParametersFromConfig, + hasUrlModelParameterChanges, + isUrlActivateDisabled, + type UrlModelParameters, + urlModelParametersSchema, +} from '~/ui/components/settings/llms/url-utils'; const URL_BACKEND: LLMBackend = 'url'; @@ -38,14 +49,37 @@ export const Url = ({ }) => { const urlId = useId(); const [url, setUrl] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [showApiKey, setShowApiKey] = useState(false); + const [modelParameters, setModelParameters] = useState({ ...DEFAULT_URL_MODEL_PARAMETERS }); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [isLoadingModels, setIsLoadingModels] = useState(false); const [availableModels, setAvailableModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [error, setError] = useState(null); + const [hasHydratedFromConfig, setHasHydratedFromConfig] = useState(false); + + const hasChanges = useMemo(() => { + const parametersChanged = hasUrlModelParameterChanges(currentLLM, modelParameters); + + return ( + url !== currentLLM?.url || + selectedModel !== currentLLM?.model || + apiKey !== (currentLLM?.apiKey || '') || + parametersChanged + ); + }, [url, selectedModel, currentLLM, apiKey, modelParameters]); const fetchAvailableModels = useCallback( async (urlOverride?: string) => { const realUrl = urlOverride || url; + const realApiKey = apiKey.trim(); + const previousSelectedModel = selectedModel; + const activeModel = currentLLM?.backend === URL_BACKEND ? currentLLM.model : ''; + + setAvailableModels([]); + setSelectedModel(''); + if (!validateUrl(realUrl)) { setError('Please enter a valid HTTP or HTTPS URL.'); return; @@ -56,38 +90,47 @@ export const Url = ({ setError(null); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10_000); - const modelsUrl = new URL('models', realUrl.endsWith('/') ? realUrl : `${realUrl}/`); - const response = await fetch(modelsUrl, { signal: controller.signal }); - clearTimeout(timeoutId); - if (!response.ok) { - if (response.status === 400 || response.status === 401 || response.status === 403) { - setError('Failed to authenticate with the LLM URL.'); - } else { - setError('Failed to load models. Please try again.'); - } - return; - } - let data: any; try { - data = await response.json(); - } catch { - setError('Invalid response from server. Expected JSON.'); - return; - } + const modelsUrl = new URL('models', realUrl.endsWith('/') ? realUrl : `${realUrl}/`); + const response = await fetch(modelsUrl, { + headers: getUrlAuthHeaders(realApiKey), + signal: controller.signal, + }); + if (!response.ok) { + if (response.status === 400 || response.status === 401 || response.status === 403) { + setError('Failed to authenticate with the LLM URL.'); + } else { + setError('Failed to load models. Please try again.'); + } + return; + } + let data: any; + try { + data = await response.json(); + } catch { + setError('Invalid response from server. Expected JSON.'); + return; + } - if (!data?.data?.length) { - setError('No models found at this URL.'); - return; - } + if (!data?.data?.length) { + setError('No models found at this URL.'); + return; + } - const models = (data.data as LLMModelData[]).filter(model => model.object === 'model'); - if (models.length === 0) { - console.error('No compatible models found in URL response:', data.data); - setError('No compatible models found at this URL.'); - return; + const models = (data.data as LLMModelData[]).filter(model => model.object === 'model'); + if (models.length === 0) { + setError('No compatible models found at this URL.'); + return; + } + const nextSelectedModel = [previousSelectedModel, activeModel].find( + modelId => !!modelId && modelId !== 'default' && models.some(model => model.id === modelId), + ); + setAvailableModels(models); + setSelectedModel(nextSelectedModel || ''); + saveLLMSettings(false, URL_BACKEND, getUrlLoadModelsSettingsPayload(realUrl, realApiKey, modelParameters)); + } finally { + clearTimeout(timeoutId); } - setAvailableModels(models); - saveLLMSettings(false, URL_BACKEND, { url: realUrl, model: 'default' }); } catch (error) { console.error('Error fetching models:', error); if (error instanceof DOMException && error.name === 'AbortError') { @@ -99,17 +142,23 @@ export const Url = ({ setIsLoadingModels(false); } }, - [saveLLMSettings, url], + [saveLLMSettings, url, apiKey, modelParameters, selectedModel, currentLLM], ); useEffect(() => { + if (hasHydratedFromConfig && currentLLM?.backend === URL_BACKEND && hasChanges) { + return; + } + if (configuredLLMs.length > 0) { if (configuredLLMs[0].url) { setUrl(configuredLLMs[0].url); } if (configuredLLMs[0].model) { - setSelectedModel(configuredLLMs[0].model); + setSelectedModel(configuredLLMs[0].model === 'default' ? '' : configuredLLMs[0].model); } + setApiKey(configuredLLMs[0].apiKey || ''); + setModelParameters(getUrlModelParametersFromConfig(configuredLLMs[0])); } // Also check currentLLM if (currentLLM?.backend === URL_BACKEND) { @@ -117,16 +166,21 @@ export const Url = ({ setUrl(currentLLM.url); } if (currentLLM.model) { - setSelectedModel(currentLLM.model); + setSelectedModel(currentLLM.model === 'default' ? '' : currentLLM.model); } + setApiKey(currentLLM.apiKey || ''); + setModelParameters(getUrlModelParametersFromConfig(currentLLM)); } - }, [configuredLLMs, currentLLM]); - - const hasChanges = useMemo(() => { - return url !== currentLLM?.url || selectedModel !== currentLLM?.model; - }, [url, selectedModel, currentLLM]); + setHasHydratedFromConfig(true); + }, [configuredLLMs, currentLLM, hasChanges, hasHydratedFromConfig]); const modelsId = useId(); + const apiKeyId = useId(); + const temperatureId = useId(); + const topPId = useId(); + const maxTokensId = useId(); + + const hasExplicitSelectedModel = selectedModel !== '' && selectedModel !== 'default'; const handleActivate = () => { setError(null); @@ -136,20 +190,35 @@ export const Url = ({ return; } - if (!selectedModel) { + if (!hasExplicitSelectedModel) { setError('Please select a model.'); return; } - saveLLMSettings(true, URL_BACKEND, { url, model: selectedModel } as Partial); + const validationResult = urlModelParametersSchema.safeParse(modelParameters); + if (!validationResult.success) { + setError('Please verify advanced options values.'); + return; + } + + saveLLMSettings( + true, + URL_BACKEND, + getUrlActivateSettingsPayload(url, selectedModel, apiKey, modelParameters) as Partial, + ); }; - // Extracted conditions for clearer rendering logic const isCurrentBackend = currentLLM?.backend === URL_BACKEND; const hasLoadedModels = availableModels.length > 0; const showActiveModel = isCurrentBackend && !hasLoadedModels; const showModelSelector = hasLoadedModels; const showActionButtons = hasLoadedModels || isCurrentBackend; + const activateDisabled = isUrlActivateDisabled({ + hasLoadedModels, + isCurrentBackend, + selectedModel: hasExplicitSelectedModel ? selectedModel : '', + hasChanges, + }); return (
@@ -162,7 +231,11 @@ export const Url = ({ type="text" placeholder="https://your-llm.example/v1" value={url} - onChange={e => setUrl(e.target.value)} + onChange={e => { + setUrl(e.target.value); + setAvailableModels([]); + setSelectedModel(''); + }} />
+
+ +
+ { + setApiKey(e.target.value); + setAvailableModels([]); + }} + /> + +
+
+ + {(selectedModel || isCurrentBackend) && ( +
+ + + {showAdvancedOptions && ( +
+
+
+ + { + const value = Number.parseFloat(e.target.value); + if (!Number.isNaN(value) && value >= 0 && value <= 2) { + setModelParameters(prev => ({ ...prev, temperature: value })); + } + }} + step="0.1" + min={urlModelParametersSchema.shape.temperature.min.toString()} + max={urlModelParametersSchema.shape.temperature.max.toString()} + /> +
+ +
+ + { + const value = Number.parseFloat(e.target.value); + if (!Number.isNaN(value) && value >= 0 && value <= 1) { + setModelParameters(prev => ({ ...prev, topP: value })); + } + }} + step="0.01" + min={urlModelParametersSchema.shape.topP.min.toString()} + max={urlModelParametersSchema.shape.topP.max.toString()} + /> +
+ +
+ + { + const value = Number.parseInt(e.target.value, 10); + if (!Number.isNaN(value) && value >= 1 && value <= 128_000) { + setModelParameters(prev => ({ ...prev, maxTokens: value })); + } + }} + step="1" + min="1" + max="128000" + /> +
+
+
+ )} +
+ )} {error && (

{error} @@ -193,7 +359,7 @@ export const Url = ({