Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
31 changes: 31 additions & 0 deletions packages/insomnia/src/main/__tests__/llm-config-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);

Expand Down Expand Up @@ -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',
Expand Down
13 changes: 9 additions & 4 deletions packages/insomnia/src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,14 +664,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,
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/main/llm-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface LLMConfig {
apiKey?: string;
url?: string;
baseURL?: string;
maxTokens?: number;
temperature?: number;
topP?: number;
topK?: number;
Expand Down Expand Up @@ -61,6 +62,7 @@ export const getBackendConfig = async (backend: LLMBackend): Promise<Partial<LLM
case 'temperature':
case 'topP':
case 'topK':
case 'maxTokens':
case 'repeatPenalty': {
config[field] = Number.parseFloat(value);
break;
Expand Down
6 changes: 3 additions & 3 deletions packages/insomnia/src/ui/components/settings/ai-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const AISettings = () => {
<span className="group relative inline-flex h-6 w-11">
<Switch
isSelected={aiFeatures.aiMockServers && isMockServerEnabledByOrg}
onChange={(enabled) => toggleAIFeature('aiMockServers', enabled)}
onChange={enabled => toggleAIFeature('aiMockServers', enabled)}
isDisabled={isMockServerFeatureDisabled}
className="group flex items-center gap-2"
>
Expand All @@ -144,7 +144,7 @@ export const AISettings = () => {
<span className="group relative inline-flex h-6 w-11">
<Switch
isSelected={aiFeatures.aiCommitMessages && isCommitMessagesEnabledByOrg}
onChange={(enabled) => toggleAIFeature('aiCommitMessages', enabled)}
onChange={enabled => toggleAIFeature('aiCommitMessages', enabled)}
isDisabled={isCommitMessagesFeatureDisabled}
className="group flex items-center gap-2"
>
Expand All @@ -170,7 +170,7 @@ export const AISettings = () => {
<span className="group relative inline-flex h-6 w-11">
<Switch
isSelected={aiFeatures.aiMcpClient && isMcpClientEnabledByOrg}
onChange={(enabled) => toggleAIFeature('aiMcpClient', enabled)}
onChange={enabled => toggleAIFeature('aiMcpClient', enabled)}
isDisabled={isMcpClientFeatureDisabled}
className="group flex items-center gap-2"
>
Expand Down
178 changes: 178 additions & 0 deletions packages/insomnia/src/ui/components/settings/llms/url-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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',
model: 'default',
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);
});
});
});
Loading
Loading