Skip to content
Merged
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 @@ -75,9 +75,13 @@
}

function handleSave() {
if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
if (
localConfig.customJson &&
typeof localConfig.customJson === 'string' &&
localConfig.customJson.trim()
) {
try {
JSON.parse(localConfig.custom);
JSON.parse(localConfig.customJson);
} catch (error) {
alert('Invalid JSON in custom parameters. Please check the format and try again.');
console.error(error);
Expand Down
3 changes: 2 additions & 1 deletion tools/ui/src/lib/constants/settings-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ export const SETTINGS_KEYS = {
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
CUSTOM: 'custom'
CUSTOM_JSON: 'customJson',
CUSTOM_CSS: 'customCss'
} as const;
14 changes: 13 additions & 1 deletion tools/ui/src/lib/constants/settings-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,12 +659,24 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
}
},
{
key: SETTINGS_KEYS.CUSTOM,
key: SETTINGS_KEYS.CUSTOM_JSON,
label: 'Custom JSON',
help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.CUSTOM_CSS,
label: 'Custom CSS',
help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.DEVELOPER,
sync: {
serverKey: SETTINGS_KEYS.CUSTOM_CSS,
paramType: SyncableParameterType.STRING
}
}
]
},
Expand Down
27 changes: 26 additions & 1 deletion tools/ui/src/lib/services/migration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,36 @@ const themeMigration: Migration = {

// Migration Registry & Runner

const CUSTOM_JSON_MIGRATION_ID = 'custom-json-key-v1';

const customJsonKeyMigration: Migration = {
id: CUSTOM_JSON_MIGRATION_ID,
description: 'Copy legacy custom config key to customJson (non-destructive)',

async run(): Promise<void> {
const configRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY);
if (configRaw === null) return;

const config = JSON.parse(configRaw);

if (!('custom' in config)) return;
if (SETTINGS_KEYS.CUSTOM_JSON in config) return;

config[SETTINGS_KEYS.CUSTOM_JSON] = config.custom;
localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config));

// Non-destructive: keep the legacy custom key for downgrade compatibility
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
console.log(`[Migration] Custom JSON: copied custom to customJson (preserved old key)`);
}
};

const migrations: Migration[] = [
localStorageMigration,
idxdbMigration,
legacyMessageMigration,
themeMigration
themeMigration,
customJsonKeyMigration
];

export const MigrationService = {
Expand Down
2 changes: 1 addition & 1 deletion tools/ui/src/lib/stores/chat.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,7 @@ class ChatStore {

apiOptions.backend_sampling = currentConfig.backend_sampling;

if (currentConfig.custom) apiOptions.custom = currentConfig.custom;
if (currentConfig.customJson) apiOptions.custom = currentConfig.customJson;

return apiOptions;
}
Expand Down
2 changes: 1 addition & 1 deletion tools/ui/src/lib/stores/tools.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ToolsStore {
}

get customTools(): OpenAIToolDefinition[] {
const raw = config().custom;
const raw = config().customJson;
if (!raw || typeof raw !== 'string') return [];

try {
Expand Down
4 changes: 2 additions & 2 deletions tools/ui/src/lib/types/settings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ export interface SettingsChatServiceOptions {
// Sampler configuration
samplers?: string | string[];
backend_sampling?: boolean;
// Custom parameters
custom?: string;
// Custom JSON parameters
customJson?: string;
timings_per_token?: boolean;
// Continuation control (vLLM compat), opt in to the explicit continue final message flag
continueFinalMessage?: boolean;
Expand Down
14 changes: 14 additions & 0 deletions tools/ui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@
}
});

// Inject custom CSS at runtime through an action on the head style node
// textContent keeps the value as text, never parsed as HTML
function customCss(node: HTMLStyleElement) {
$effect(() => {
node.textContent = (config().customCss as string | undefined) ?? '';
});
}

// Fetch router models when in router mode (for status and modalities)
// Wait for models to be loaded first, run only once
let routerModelsFetched = false;
Expand Down Expand Up @@ -227,6 +235,12 @@
});
</script>

<svelte:head>
{#if config().customCss}
<style use:customCss></style>
{/if}
</svelte:head>

<Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
<ModeWatcher />
<Toaster richColors />
Expand Down