Add model response timeout for local api server as configurable value via UI

This commit is contained in:
Maksym Krasovakyi 2025-09-07 12:02:12 +03:00
parent 59b2755810
commit 71e2e24112
16 changed files with 127 additions and 9 deletions

View File

@ -162,7 +162,7 @@ pub async fn load_llama_model<R: Runtime>(
}
// Wait for server to be ready or timeout
let timeout_duration = Duration::from_secs(180); // 3 minutes timeout
let timeout_duration = Duration::from_secs(300); // 5 minutes timeout
let start_time = Instant::now();
log::info!("Waiting for model session to be ready...");
loop {

View File

@ -13,6 +13,7 @@ pub async fn start_server<R: Runtime>(
prefix: String,
api_key: String,
trusted_hosts: Vec<String>,
proxy_timeout: u64,
) -> Result<bool, String> {
let server_handle = state.server_handle.clone();
let plugin_state: State<LlamacppState> = app_handle.state();
@ -26,6 +27,7 @@ pub async fn start_server<R: Runtime>(
prefix,
api_key,
vec![trusted_hosts],
proxy_timeout,
)
.await
.map_err(|e| e.to_string())?;

View File

@ -631,6 +631,7 @@ pub async fn start_server(
prefix: String,
proxy_api_key: String,
trusted_hosts: Vec<Vec<String>>,
proxy_timeout: u64,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let mut handle_guard = server_handle.lock().await;
if handle_guard.is_some() {
@ -648,7 +649,7 @@ pub async fn start_server(
};
let client = Client::builder()
.timeout(std::time::Duration::from_secs(300))
.timeout(std::time::Duration::from_secs(proxy_timeout))
.pool_max_idle_per_host(10)
.pool_idle_timeout(std::time::Duration::from_secs(30))
.build()?;

View File

@ -0,0 +1,39 @@
import { Input } from '@/components/ui/input'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { cn } from '@/lib/utils'
import { useState } from 'react'
export function ProxyTimeoutInput({ isServerRunning }: { isServerRunning?: boolean }) {
const { proxyTimeout, setProxyTimeout } = useLocalApiServer()
const [inputValue, setInputValue] = useState(proxyTimeout.toString())
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
}
const handleBlur = () => {
const timeout = parseInt(inputValue)
if (!isNaN(timeout) && timeout >= 0 && timeout <= 86400) {
setProxyTimeout(timeout)
} else {
// Reset to current value if invalid
setInputValue(proxyTimeout.toString())
}
}
return (
<Input
type="number"
min={0}
max={86400}
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
className={cn(
'w-24 h-8 text-sm',
isServerRunning && 'opacity-50 pointer-events-none'
)}
/>
)
}

View File

@ -32,6 +32,7 @@ describe('useLocalApiServer', () => {
store.setVerboseLogs(true)
store.setTrustedHosts([])
store.setApiKey('')
store.setProxyTimeout(600)
})
it('should initialize with default values', () => {
@ -45,6 +46,7 @@ describe('useLocalApiServer', () => {
expect(result.current.verboseLogs).toBe(true)
expect(result.current.trustedHosts).toEqual([])
expect(result.current.apiKey).toBe('')
expect(result.current.proxyTimeout).toBe(600)
})
describe('enableOnStartup', () => {
@ -317,6 +319,32 @@ describe('useLocalApiServer', () => {
})
})
describe('proxyTimeout', () => {
it('should set proxy timeout', () => {
const { result } = renderHook(() => useLocalApiServer())
act(() => {
result.current.setProxyTimeout(1800)
})
expect(result.current.proxyTimeout).toBe(1800)
})
it('should handle different proxy timeouts', () => {
const { result } = renderHook(() => useLocalApiServer())
const testTimeouts = [100, 300, 600, 3600]
testTimeouts.forEach((timeout) => {
act(() => {
result.current.setProxyTimeout(timeout)
})
expect(result.current.proxyTimeout).toBe(timeout)
})
})
})
describe('state persistence', () => {
it('should maintain state across multiple hook instances', () => {
const { result: result1 } = renderHook(() => useLocalApiServer())
@ -331,6 +359,7 @@ describe('useLocalApiServer', () => {
result1.current.setVerboseLogs(false)
result1.current.setApiKey('test-key')
result1.current.addTrustedHost('example.com')
result1.current.setProxyTimeout(1800)
})
expect(result2.current.enableOnStartup).toBe(false)
@ -341,6 +370,7 @@ describe('useLocalApiServer', () => {
expect(result2.current.verboseLogs).toBe(false)
expect(result2.current.apiKey).toBe('test-key')
expect(result2.current.trustedHosts).toEqual(['example.com'])
expect(result2.current.proxyTimeout).toBe(1800)
})
})
@ -356,6 +386,7 @@ describe('useLocalApiServer', () => {
result.current.addTrustedHost('localhost')
result.current.addTrustedHost('127.0.0.1')
result.current.setApiKey('sk-test-key')
result.current.setProxyTimeout(800)
})
expect(result.current.serverHost).toBe('0.0.0.0')
@ -364,6 +395,7 @@ describe('useLocalApiServer', () => {
expect(result.current.corsEnabled).toBe(false)
expect(result.current.trustedHosts).toEqual(['localhost', '127.0.0.1'])
expect(result.current.apiKey).toBe('sk-test-key')
expect(result.current.proxyTimeout).toBe(800)
})
it('should preserve independent state changes', () => {
@ -376,6 +408,17 @@ describe('useLocalApiServer', () => {
expect(result.current.serverPort).toBe(9000)
expect(result.current.serverHost).toBe('127.0.0.1') // Should remain default
expect(result.current.apiPrefix).toBe('/v1') // Should remain default
expect(result.current.proxyTimeout).toBe(600) // Should remain default
act(() => {
result.current.setProxyTimeout(400)
})
expect(result.current.proxyTimeout).toBe(400)
expect(result.current.serverPort).toBe(9000) // Should remain default
expect(result.current.serverHost).toBe('127.0.0.1') // Should remain default
expect(result.current.apiPrefix).toBe('/v1') // Should remain default
act(() => {
result.current.addTrustedHost('example.com')

View File

@ -28,6 +28,9 @@ type LocalApiServerState = {
addTrustedHost: (host: string) => void
removeTrustedHost: (host: string) => void
setTrustedHosts: (hosts: string[]) => void
// Server request timeout (default 600 sec)
proxyTimeout: number
setProxyTimeout: (value: number) => void
}
export const useLocalApiServer = create<LocalApiServerState>()(
@ -55,6 +58,8 @@ export const useLocalApiServer = create<LocalApiServerState>()(
trustedHosts: state.trustedHosts.filter((h) => h !== host),
})),
setTrustedHosts: (hosts) => set({ trustedHosts: hosts }),
proxyTimeout: 600,
setProxyTimeout: (value) => set({ proxyTimeout: value }),
apiKey: '',
setApiKey: (value) => set({ apiKey: value }),
}),

View File

@ -180,7 +180,9 @@
"cors": "Cross-Origin Resource Sharing (CORS)",
"corsDesc": "Erlaube Cross-Origin-Anfragen an den API-Server.",
"verboseLogs": "Ausführliche Server Logs",
"verboseLogsDesc": "Aktiviere detaillierte Server Logs zum Debuggen"
"verboseLogsDesc": "Aktiviere detaillierte Server Logs zum Debuggen",
"proxyTimeout": "Zeitüberschreitung bei der Anfrage",
"proxyTimeoutDesc": "Wartezeit auf eine Antwort vom lokalen Modell in Sekunden."
},
"privacy": {
"analytics": "Analytik",

View File

@ -183,7 +183,9 @@
"cors": "Cross-Origin Resource Sharing (CORS)",
"corsDesc": "Allow cross-origin requests to the API server.",
"verboseLogs": "Verbose Server Logs",
"verboseLogsDesc": "Enable detailed server logs for debugging."
"verboseLogsDesc": "Enable detailed server logs for debugging.",
"proxyTimeout": "Request timeout",
"proxyTimeoutDesc": "Time to wait for a response from the local model, seconds."
},
"privacy": {
"analytics": "Analytics",

View File

@ -180,7 +180,9 @@
"cors": "Berbagi Sumber Daya Lintas Asal (CORS)",
"corsDesc": "Izinkan permintaan lintas asal ke server API.",
"verboseLogs": "Log Server Verbose",
"verboseLogsDesc": "Aktifkan log server terperinci untuk debugging."
"verboseLogsDesc": "Aktifkan log server terperinci untuk debugging.",
"proxyTimeout": "Permintaan melebihi batas waktu",
"proxyTimeoutDesc": "Waktu tunggu untuk respons dari model lokal dalam detik."
},
"privacy": {
"analytics": "Analitik",

View File

@ -183,7 +183,9 @@
"cors": "Cross-Origin Resource Sharing (CORS)",
"corsDesc": "Pozwalaj na żądania cross-origin do serwera API.",
"verboseLogs": "Szczegółowe Wpisy Dzienników Serwera",
"verboseLogsDesc": "Włącz szczegółowe wpisy dzienników serwera na potrzeby rozwiązywania problemów."
"verboseLogsDesc": "Włącz szczegółowe wpisy dzienników serwera na potrzeby rozwiązywania problemów.",
"proxyTimeout": "Przekroczenie limitu czasu żądania",
"proxyTimeoutDesc": "Czas oczekiwania na odpowiedź od lokalnego modelu w sekundach."
},
"privacy": {
"analytics": "Dane Analityczne",

View File

@ -180,7 +180,9 @@
"cors": "Chia sẻ tài nguyên giữa các nguồn gốc (CORS)",
"corsDesc": "Cho phép các yêu cầu cross-origin đến máy chủ API.",
"verboseLogs": "Nhật ký máy chủ chi tiết",
"verboseLogsDesc": "Bật nhật ký máy chủ chi tiết để gỡ lỗi."
"verboseLogsDesc": "Bật nhật ký máy chủ chi tiết để gỡ lỗi.",
"proxyTimeout": "Hết thời gian chờ yêu cầu",
"proxyTimeoutDesc": "Thời gian chờ phản hồi từ mô hình cục bộ (tính bằng giây)."
},
"privacy": {
"analytics": "Phân tích",

View File

@ -180,7 +180,9 @@
"cors": "跨源资源共享 (CORS)",
"corsDesc": "允许跨源请求访问 API 服务器。",
"verboseLogs": "详细服务器日志",
"verboseLogsDesc": "启用详细服务器日志以进行调试。"
"verboseLogsDesc": "启用详细服务器日志以进行调试。",
"proxyTimeout": "请求超时",
"proxyTimeoutDesc": "等待本地模型响应的时间(单位:秒)。"
},
"privacy": {
"analytics": "分析",

View File

@ -180,7 +180,9 @@
"cors": "跨來源資源共用 (CORS)",
"corsDesc": "允許跨來源請求存取 API 伺服器。",
"verboseLogs": "詳細伺服器日誌",
"verboseLogsDesc": "啟用詳細伺服器日誌以進行偵錯。"
"verboseLogsDesc": "啟用詳細伺服器日誌以進行偵錯。",
"proxyTimeout": "請求逾時",
"proxyTimeoutDesc": "等待本地模型回應的時間(秒)。"
},
"privacy": {
"analytics": "分析",

View File

@ -36,6 +36,7 @@ export function DataProvider() {
trustedHosts,
corsEnabled,
verboseLogs,
proxyTimeout,
} = useLocalApiServer()
const { setServerStatus } = useAppState()
@ -169,6 +170,7 @@ export function DataProvider() {
trustedHosts,
isCorsEnabled: corsEnabled,
isVerboseEnabled: verboseLogs,
proxyTimeout: proxyTimeout,
})
})
.then(() => {

View File

@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { ServerHostSwitcher } from '@/containers/ServerHostSwitcher'
import { PortInput } from '@/containers/PortInput'
import { ProxyTimeoutInput } from '@/containers/ProxyTimeoutInput'
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
import { TrustedHostsInput } from '@/containers/TrustedHostsInput'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
@ -50,6 +51,7 @@ function LocalAPIServerContent() {
apiPrefix,
apiKey,
trustedHosts,
proxyTimeout,
} = useLocalApiServer()
const { serverStatus, setServerStatus } = useAppState()
@ -157,6 +159,7 @@ function LocalAPIServerContent() {
trustedHosts,
isCorsEnabled: corsEnabled,
isVerboseEnabled: verboseLogs,
proxyTimeout: proxyTimeout,
})
})
.then(() => {
@ -311,6 +314,11 @@ function LocalAPIServerContent() {
<TrustedHostsInput isServerRunning={isServerRunning} />
}
/>
<CardItem
title={t('settings:localApiServer.proxyTimeout')}
description={t('settings:localApiServer.proxyTimeoutDesc')}
actions={<ProxyTimeoutInput isServerRunning={isServerRunning} />}
/>
</Card>
{/* Advanced Settings */}

View File

@ -72,6 +72,10 @@ A mandatory secret key to authenticate requests.
### Trusted Hosts
A comma-separated list of hostnames allowed to access the server. This provides an additional layer of security when the server is exposed on your network.
### Request timeout
Request timeout for local model response in seconds.
- **`600`** (Default): You can change this to any suitable value.
## Advanced Settings
### Cross-Origin Resource Sharing (CORS)