|
|
@@ -39,6 +39,13 @@ interface FormValues {
|
|
|
session_ttl_minutes: string // string for controlled input
|
|
|
webui_enabled: boolean
|
|
|
default_project: string
|
|
|
+ embedding_connect_timeout_sec: string
|
|
|
+ embedding_read_timeout_sec: string
|
|
|
+ embedding_cache_size: string
|
|
|
+ embedding_cache_ttl_sec: string
|
|
|
+ embedding_cache_max_bytes: string
|
|
|
+ embedding_cache_normalize: boolean
|
|
|
+ embedding_client_pool_size: string
|
|
|
}
|
|
|
|
|
|
function settingsToForm(s: SettingsView): FormValues {
|
|
|
@@ -50,6 +57,13 @@ function settingsToForm(s: SettingsView): FormValues {
|
|
|
session_ttl_minutes: String(s.session_ttl_minutes),
|
|
|
webui_enabled: s.webui_enabled,
|
|
|
default_project: s.default_project,
|
|
|
+ embedding_connect_timeout_sec: String(s.embedding_connect_timeout_sec),
|
|
|
+ embedding_read_timeout_sec: String(s.embedding_read_timeout_sec),
|
|
|
+ embedding_cache_size: String(s.embedding_cache_size),
|
|
|
+ embedding_cache_ttl_sec: String(s.embedding_cache_ttl_sec),
|
|
|
+ embedding_cache_max_bytes: String(s.embedding_cache_max_bytes),
|
|
|
+ embedding_cache_normalize: s.embedding_cache_normalize,
|
|
|
+ embedding_client_pool_size: String(s.embedding_client_pool_size),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -72,6 +86,13 @@ export default function Settings() {
|
|
|
session_ttl_minutes: '60',
|
|
|
webui_enabled: true,
|
|
|
default_project: 'default',
|
|
|
+ embedding_connect_timeout_sec: '10',
|
|
|
+ embedding_read_timeout_sec: '60',
|
|
|
+ embedding_cache_size: '4096',
|
|
|
+ embedding_cache_ttl_sec: '86400',
|
|
|
+ embedding_cache_max_bytes: '268435456',
|
|
|
+ embedding_cache_normalize: false,
|
|
|
+ embedding_client_pool_size: '4',
|
|
|
})
|
|
|
|
|
|
// Populate the form once settings load, but don't overwrite user's edits on re-render.
|
|
|
@@ -98,6 +119,42 @@ export default function Settings() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ const connectTimeout = parseInt(form.embedding_connect_timeout_sec, 10)
|
|
|
+ if (isNaN(connectTimeout) || connectTimeout < 1) {
|
|
|
+ push('Embedding connect timeout must be a positive integer.', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const readTimeout = parseInt(form.embedding_read_timeout_sec, 10)
|
|
|
+ if (isNaN(readTimeout) || readTimeout < 1) {
|
|
|
+ push('Embedding read timeout must be a positive integer.', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const cacheSize = parseInt(form.embedding_cache_size, 10)
|
|
|
+ if (isNaN(cacheSize) || cacheSize < 1) {
|
|
|
+ push('Embedding cache size must be a positive integer.', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const cacheTtl = parseInt(form.embedding_cache_ttl_sec, 10)
|
|
|
+ if (isNaN(cacheTtl) || cacheTtl < 0) {
|
|
|
+ push('Embedding cache TTL must be a non-negative integer (0 = never expire).', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const cacheMaxBytes = parseInt(form.embedding_cache_max_bytes, 10)
|
|
|
+ if (isNaN(cacheMaxBytes) || cacheMaxBytes < 1) {
|
|
|
+ push('Embedding cache max bytes must be a positive integer.', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const poolSize = parseInt(form.embedding_client_pool_size, 10)
|
|
|
+ if (isNaN(poolSize) || poolSize < 1) {
|
|
|
+ push('Embedding client pool size must be a positive integer.', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
const patch: Partial<Omit<SettingsView, 'openai_api_key_set'> & { openai_api_key?: string }> = {
|
|
|
openai_api_base: form.openai_api_base.trim(),
|
|
|
default_embedding_model: form.default_embedding_model.trim(),
|
|
|
@@ -108,6 +165,13 @@ export default function Settings() {
|
|
|
session_ttl_minutes: ttl,
|
|
|
webui_enabled: form.webui_enabled,
|
|
|
default_project: form.default_project.trim(),
|
|
|
+ embedding_connect_timeout_sec: connectTimeout,
|
|
|
+ embedding_read_timeout_sec: readTimeout,
|
|
|
+ embedding_cache_size: cacheSize,
|
|
|
+ embedding_cache_ttl_sec: cacheTtl,
|
|
|
+ embedding_cache_max_bytes: cacheMaxBytes,
|
|
|
+ embedding_cache_normalize: form.embedding_cache_normalize,
|
|
|
+ embedding_client_pool_size: poolSize,
|
|
|
}
|
|
|
|
|
|
// Only include the key if the user typed something
|
|
|
@@ -281,6 +345,129 @@ export default function Settings() {
|
|
|
</label>
|
|
|
</FormRow>
|
|
|
|
|
|
+ {/* ── Embedding performance ─────────────────────────────────── */}
|
|
|
+ <h2 className="text-sm font-semibold text-slate-300 border-t border-slate-700 pt-4 mt-1">
|
|
|
+ Embedding performance
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ {/* Embedding connect timeout */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding connect timeout (s)"
|
|
|
+ hint="TCP connection timeout when reaching the embeddings endpoint."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_connect_timeout_sec}
|
|
|
+ onChange={(e) => setField('embedding_connect_timeout_sec', e.target.value)}
|
|
|
+ min={1}
|
|
|
+ placeholder="10"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Embedding read timeout */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding read timeout (s)"
|
|
|
+ hint="Read timeout for each embeddings API response."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_read_timeout_sec}
|
|
|
+ onChange={(e) => setField('embedding_read_timeout_sec', e.target.value)}
|
|
|
+ min={1}
|
|
|
+ placeholder="60"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Embedding cache size */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding cache size (entries)"
|
|
|
+ hint="Max distinct (model, text) embeddings cached. 0 disables via size, but use the toggle/TTL instead."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_cache_size}
|
|
|
+ onChange={(e) => setField('embedding_cache_size', e.target.value)}
|
|
|
+ min={1}
|
|
|
+ placeholder="4096"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Embedding cache TTL */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding cache TTL (s)"
|
|
|
+ hint="0 = never expire."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_cache_ttl_sec}
|
|
|
+ onChange={(e) => setField('embedding_cache_ttl_sec', e.target.value)}
|
|
|
+ min={0}
|
|
|
+ placeholder="86400"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Embedding cache max bytes */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding cache max bytes"
|
|
|
+ hint="Byte budget for cached vectors (e.g. 268435456 = 256 MB)."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_cache_max_bytes}
|
|
|
+ onChange={(e) => setField('embedding_cache_max_bytes', e.target.value)}
|
|
|
+ min={1}
|
|
|
+ placeholder="268435456"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Embedding client pool size */}
|
|
|
+ <FormRow
|
|
|
+ label="Embedding client pool size"
|
|
|
+ hint="Keep-alive HTTP clients per upstream origin (concurrency)."
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={form.embedding_client_pool_size}
|
|
|
+ onChange={(e) => setField('embedding_client_pool_size', e.target.value)}
|
|
|
+ min={1}
|
|
|
+ placeholder="4"
|
|
|
+ className={inputCls}
|
|
|
+ />
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
+ {/* Normalize cache keys */}
|
|
|
+ <FormRow
|
|
|
+ label="Normalize cache keys"
|
|
|
+ hint="Trim + lowercase query text before caching (collapses near-duplicate queries)."
|
|
|
+ >
|
|
|
+ <label className="flex items-center gap-3 cursor-pointer select-none w-fit">
|
|
|
+ <div
|
|
|
+ role="switch"
|
|
|
+ aria-checked={form.embedding_cache_normalize}
|
|
|
+ onClick={() => setField('embedding_cache_normalize', !form.embedding_cache_normalize)}
|
|
|
+ className={[
|
|
|
+ 'relative inline-flex h-6 w-11 shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none',
|
|
|
+ form.embedding_cache_normalize ? 'bg-indigo-600' : 'bg-slate-600',
|
|
|
+ ].join(' ')}
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ className={[
|
|
|
+ 'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform',
|
|
|
+ form.embedding_cache_normalize ? 'translate-x-5' : 'translate-x-0',
|
|
|
+ ].join(' ')}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <span className="text-sm text-slate-300">
|
|
|
+ {form.embedding_cache_normalize ? 'Enabled' : 'Disabled'}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ </FormRow>
|
|
|
+
|
|
|
{/* Save */}
|
|
|
<div className="pt-2">
|
|
|
<button
|