diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/src/commands.rs b/src-tauri/plugins/tauri-plugin-llamacpp/src/commands.rs index a25d7825f..79ec81f5a 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/src/commands.rs +++ b/src-tauri/plugins/tauri-plugin-llamacpp/src/commands.rs @@ -162,7 +162,7 @@ pub async fn load_llama_model( } // 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 { diff --git a/src-tauri/src/core/server/commands.rs b/src-tauri/src/core/server/commands.rs index c1c6a9294..85450bee5 100644 --- a/src-tauri/src/core/server/commands.rs +++ b/src-tauri/src/core/server/commands.rs @@ -13,6 +13,7 @@ pub async fn start_server( prefix: String, api_key: String, trusted_hosts: Vec, + proxy_timeout: u64, ) -> Result { let server_handle = state.server_handle.clone(); let plugin_state: State = app_handle.state(); @@ -26,6 +27,7 @@ pub async fn start_server( prefix, api_key, vec![trusted_hosts], + proxy_timeout, ) .await .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/core/server/proxy.rs b/src-tauri/src/core/server/proxy.rs index 47f642716..9b33d4ba5 100644 --- a/src-tauri/src/core/server/proxy.rs +++ b/src-tauri/src/core/server/proxy.rs @@ -631,6 +631,7 @@ pub async fn start_server( prefix: String, proxy_api_key: String, trusted_hosts: Vec>, + proxy_timeout: u64, ) -> Result> { 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()?; diff --git a/web-app/src/containers/ProxyTimeoutInput.tsx b/web-app/src/containers/ProxyTimeoutInput.tsx new file mode 100644 index 000000000..daa1e61bf --- /dev/null +++ b/web-app/src/containers/ProxyTimeoutInput.tsx @@ -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) => { + 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 ( + + ) +} diff --git a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts index e347d2307..27c0c7ea0 100644 --- a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts +++ b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts @@ -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') diff --git a/web-app/src/hooks/useLocalApiServer.ts b/web-app/src/hooks/useLocalApiServer.ts index a353df75c..712277425 100644 --- a/web-app/src/hooks/useLocalApiServer.ts +++ b/web-app/src/hooks/useLocalApiServer.ts @@ -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()( @@ -55,6 +58,8 @@ export const useLocalApiServer = create()( 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 }), }), diff --git a/web-app/src/locales/de-DE/settings.json b/web-app/src/locales/de-DE/settings.json index 5e16f2679..9e199eef0 100644 --- a/web-app/src/locales/de-DE/settings.json +++ b/web-app/src/locales/de-DE/settings.json @@ -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", diff --git a/web-app/src/locales/en/settings.json b/web-app/src/locales/en/settings.json index be6b15b98..734736f01 100644 --- a/web-app/src/locales/en/settings.json +++ b/web-app/src/locales/en/settings.json @@ -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", diff --git a/web-app/src/locales/id/settings.json b/web-app/src/locales/id/settings.json index 8747c96d1..394acb298 100644 --- a/web-app/src/locales/id/settings.json +++ b/web-app/src/locales/id/settings.json @@ -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", diff --git a/web-app/src/locales/pl/settings.json b/web-app/src/locales/pl/settings.json index d5f1e30a5..1695f1ccd 100644 --- a/web-app/src/locales/pl/settings.json +++ b/web-app/src/locales/pl/settings.json @@ -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", diff --git a/web-app/src/locales/vn/settings.json b/web-app/src/locales/vn/settings.json index 789fc3344..45b6f9f22 100644 --- a/web-app/src/locales/vn/settings.json +++ b/web-app/src/locales/vn/settings.json @@ -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", diff --git a/web-app/src/locales/zh-CN/settings.json b/web-app/src/locales/zh-CN/settings.json index e1897c0bd..33f752f9d 100644 --- a/web-app/src/locales/zh-CN/settings.json +++ b/web-app/src/locales/zh-CN/settings.json @@ -180,7 +180,9 @@ "cors": "跨源资源共享 (CORS)", "corsDesc": "允许跨源请求访问 API 服务器。", "verboseLogs": "详细服务器日志", - "verboseLogsDesc": "启用详细服务器日志以进行调试。" + "verboseLogsDesc": "启用详细服务器日志以进行调试。", + "proxyTimeout": "请求超时", + "proxyTimeoutDesc": "等待本地模型响应的时间(单位:秒)。" }, "privacy": { "analytics": "分析", diff --git a/web-app/src/locales/zh-TW/settings.json b/web-app/src/locales/zh-TW/settings.json index 714ca7e19..9af67dcbb 100644 --- a/web-app/src/locales/zh-TW/settings.json +++ b/web-app/src/locales/zh-TW/settings.json @@ -180,7 +180,9 @@ "cors": "跨來源資源共用 (CORS)", "corsDesc": "允許跨來源請求存取 API 伺服器。", "verboseLogs": "詳細伺服器日誌", - "verboseLogsDesc": "啟用詳細伺服器日誌以進行偵錯。" + "verboseLogsDesc": "啟用詳細伺服器日誌以進行偵錯。", + "proxyTimeout": "請求逾時", + "proxyTimeoutDesc": "等待本地模型回應的時間(秒)。" }, "privacy": { "analytics": "分析", diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index 352026175..a734cd39f 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -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(() => { diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx index 840e1fdb7..9f2b2315b 100644 --- a/web-app/src/routes/settings/local-api-server.tsx +++ b/web-app/src/routes/settings/local-api-server.tsx @@ -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() { } /> + } + /> {/* Advanced Settings */} diff --git a/website/src/content/docs/local-server/api-server.mdx b/website/src/content/docs/local-server/api-server.mdx index 9ab97865e..491c31db9 100644 --- a/website/src/content/docs/local-server/api-server.mdx +++ b/website/src/content/docs/local-server/api-server.mdx @@ -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)