Skip to content

Commit 7b9e752

Browse files
authored
Merge pull request #6250 from menloresearch/feat/local-api-server
feat: run on startup setting for local api server
2 parents ec1a695 + 39df7b2 commit 7b9e752

File tree

5 files changed

+232
-26
lines changed

5 files changed

+232
-26
lines changed

web-app/src/hooks/__tests__/useLocalApiServer.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('useLocalApiServer', () => {
2424
vi.clearAllMocks()
2525
// Reset store state to defaults
2626
const store = useLocalApiServer.getState()
27-
store.setRunOnStartup(true)
27+
store.setEnableOnStartup(true)
2828
store.setServerHost('127.0.0.1')
2929
store.setServerPort(1337)
3030
store.setApiPrefix('/v1')
@@ -37,7 +37,7 @@ describe('useLocalApiServer', () => {
3737
it('should initialize with default values', () => {
3838
const { result } = renderHook(() => useLocalApiServer())
3939

40-
expect(result.current.runOnStartup).toBe(true)
40+
expect(result.current.enableOnStartup).toBe(true)
4141
expect(result.current.serverHost).toBe('127.0.0.1')
4242
expect(result.current.serverPort).toBe(1337)
4343
expect(result.current.apiPrefix).toBe('/v1')
@@ -47,21 +47,21 @@ describe('useLocalApiServer', () => {
4747
expect(result.current.apiKey).toBe('')
4848
})
4949

50-
describe('runOnStartup', () => {
50+
describe('enableOnStartup', () => {
5151
it('should set run on startup', () => {
5252
const { result } = renderHook(() => useLocalApiServer())
5353

5454
act(() => {
55-
result.current.setRunOnStartup(false)
55+
result.current.setEnableOnStartup(false)
5656
})
5757

58-
expect(result.current.runOnStartup).toBe(false)
58+
expect(result.current.enableOnStartup).toBe(false)
5959

6060
act(() => {
61-
result.current.setRunOnStartup(true)
61+
result.current.setEnableOnStartup(true)
6262
})
6363

64-
expect(result.current.runOnStartup).toBe(true)
64+
expect(result.current.enableOnStartup).toBe(true)
6565
})
6666
})
6767

@@ -323,7 +323,7 @@ describe('useLocalApiServer', () => {
323323
const { result: result2 } = renderHook(() => useLocalApiServer())
324324

325325
act(() => {
326-
result1.current.setRunOnStartup(false)
326+
result1.current.setEnableOnStartup(false)
327327
result1.current.setServerHost('0.0.0.0')
328328
result1.current.setServerPort(8080)
329329
result1.current.setApiPrefix('/api')
@@ -333,7 +333,7 @@ describe('useLocalApiServer', () => {
333333
result1.current.addTrustedHost('example.com')
334334
})
335335

336-
expect(result2.current.runOnStartup).toBe(false)
336+
expect(result2.current.enableOnStartup).toBe(false)
337337
expect(result2.current.serverHost).toBe('0.0.0.0')
338338
expect(result2.current.serverPort).toBe(8080)
339339
expect(result2.current.apiPrefix).toBe('/api')

web-app/src/hooks/useLocalApiServer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { localStorageKey } from '@/constants/localStorage'
44

55
type LocalApiServerState = {
66
// Run local API server once app opens
7-
runOnStartup: boolean
8-
setRunOnStartup: (value: boolean) => void
7+
enableOnStartup: boolean
8+
setEnableOnStartup: (value: boolean) => void
99
// Server host option (127.0.0.1 or 0.0.0.0)
1010
serverHost: '127.0.0.1' | '0.0.0.0'
1111
setServerHost: (value: '127.0.0.1' | '0.0.0.0') => void
@@ -33,8 +33,8 @@ type LocalApiServerState = {
3333
export const useLocalApiServer = create<LocalApiServerState>()(
3434
persist(
3535
(set) => ({
36-
runOnStartup: true,
37-
setRunOnStartup: (value) => set({ runOnStartup: value }),
36+
enableOnStartup: false,
37+
setEnableOnStartup: (value) => set({ enableOnStartup: value }),
3838
serverHost: '127.0.0.1',
3939
setServerHost: (value) => set({ serverHost: value }),
4040
serverPort: 1337,

web-app/src/locales/en/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@
160160
"serverLogs": "Server Logs",
161161
"serverLogsDesc": "View detailed logs of the local API server.",
162162
"openLogs": "Open Logs",
163+
"startupConfiguration": "Startup Configuration",
164+
"runOnStartup": "Enable by default on startup",
165+
"runOnStartupDesc": "Automatically start the Local API Server when the application launches.",
163166
"serverConfiguration": "Server Configuration",
164167
"serverHost": "Server Host",
165168
"serverHostDesc": "Network address for the server.",

web-app/src/providers/DataProvider.tsx

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ import {
1717
import { useNavigate } from '@tanstack/react-router'
1818
import { route } from '@/constants/routes'
1919
import { useThreads } from '@/hooks/useThreads'
20+
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
21+
import { useAppState } from '@/hooks/useAppState'
2022
import { AppEvent, events } from '@janhq/core'
23+
import { startModel } from '@/services/models'
24+
import { localStorageKey } from '@/constants/localStorage'
2125

2226
export function DataProvider() {
23-
const { setProviders } = useModelProvider()
27+
const { setProviders, selectedModel, selectedProvider, getProviderByName } =
28+
useModelProvider()
2429

2530
const { setMessages } = useMessages()
2631
const { checkForUpdate } = useAppUpdater()
@@ -29,6 +34,19 @@ export function DataProvider() {
2934
const { setThreads } = useThreads()
3035
const navigate = useNavigate()
3136

37+
// Local API Server hooks
38+
const {
39+
enableOnStartup,
40+
serverHost,
41+
serverPort,
42+
apiPrefix,
43+
apiKey,
44+
trustedHosts,
45+
corsEnabled,
46+
verboseLogs,
47+
} = useLocalApiServer()
48+
const { setServerStatus } = useAppState()
49+
3250
useEffect(() => {
3351
console.log('Initializing DataProvider...')
3452
getProviders().then(setProviders)
@@ -78,6 +96,102 @@ export function DataProvider() {
7896
// eslint-disable-next-line react-hooks/exhaustive-deps
7997
}, [])
8098

99+
const getLastUsedModel = (): { provider: string; model: string } | null => {
100+
try {
101+
const stored = localStorage.getItem(localStorageKey.lastUsedModel)
102+
return stored ? JSON.parse(stored) : null
103+
} catch (error) {
104+
console.debug('Failed to get last used model from localStorage:', error)
105+
return null
106+
}
107+
}
108+
109+
// Helper function to determine which model to start
110+
const getModelToStart = () => {
111+
// Use last used model if available
112+
const lastUsedModel = getLastUsedModel()
113+
if (lastUsedModel) {
114+
const provider = getProviderByName(lastUsedModel.provider)
115+
if (
116+
provider &&
117+
provider.models.some((m) => m.id === lastUsedModel.model)
118+
) {
119+
return { model: lastUsedModel.model, provider }
120+
}
121+
}
122+
123+
// Use selected model if available
124+
if (selectedModel && selectedProvider) {
125+
const provider = getProviderByName(selectedProvider)
126+
if (provider) {
127+
return { model: selectedModel.id, provider }
128+
}
129+
}
130+
131+
// Use first model from llamacpp provider
132+
const llamacppProvider = getProviderByName('llamacpp')
133+
if (
134+
llamacppProvider &&
135+
llamacppProvider.models &&
136+
llamacppProvider.models.length > 0
137+
) {
138+
return {
139+
model: llamacppProvider.models[0].id,
140+
provider: llamacppProvider,
141+
}
142+
}
143+
144+
return null
145+
}
146+
147+
// Auto-start Local API Server on app startup if enabled
148+
useEffect(() => {
149+
if (enableOnStartup) {
150+
// Validate API key before starting
151+
if (!apiKey || apiKey.toString().trim().length === 0) {
152+
console.warn('Cannot start Local API Server: API key is required')
153+
return
154+
}
155+
156+
const modelToStart = getModelToStart()
157+
158+
// Only start server if we have a model to load
159+
if (!modelToStart) {
160+
console.warn(
161+
'Cannot start Local API Server: No model available to load'
162+
)
163+
return
164+
}
165+
166+
setServerStatus('pending')
167+
168+
// Start the model first
169+
startModel(modelToStart.provider, modelToStart.model)
170+
.then(() => {
171+
console.log(`Model ${modelToStart.model} started successfully`)
172+
173+
// Then start the server
174+
return window.core?.api?.startServer({
175+
host: serverHost,
176+
port: serverPort,
177+
prefix: apiPrefix,
178+
apiKey,
179+
trustedHosts,
180+
isCorsEnabled: corsEnabled,
181+
isVerboseEnabled: verboseLogs,
182+
})
183+
})
184+
.then(() => {
185+
setServerStatus('running')
186+
})
187+
.catch((error: unknown) => {
188+
console.error('Failed to start Local API Server on startup:', error)
189+
setServerStatus('stopped')
190+
})
191+
}
192+
// eslint-disable-next-line react-hooks/exhaustive-deps
193+
}, [])
194+
81195
const handleDeepLink = (urls: string[] | null) => {
82196
if (!urls) return
83197
console.log('Received deeplink:', urls)

web-app/src/routes/settings/local-api-server.tsx

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { TrustedHostsInput } from '@/containers/TrustedHostsInput'
1313
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
1414
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
1515
import { useAppState } from '@/hooks/useAppState'
16+
import { useModelProvider } from '@/hooks/useModelProvider'
17+
import { startModel } from '@/services/models'
18+
import { localStorageKey } from '@/constants/localStorage'
1619
import { windowKey } from '@/constants/windows'
1720
import { IconLogs } from '@tabler/icons-react'
1821
import { cn } from '@/lib/utils'
@@ -32,6 +35,8 @@ function LocalAPIServer() {
3235
setCorsEnabled,
3336
verboseLogs,
3437
setVerboseLogs,
38+
enableOnStartup,
39+
setEnableOnStartup,
3540
serverHost,
3641
serverPort,
3742
apiPrefix,
@@ -40,6 +45,7 @@ function LocalAPIServer() {
4045
} = useLocalApiServer()
4146

4247
const { serverStatus, setServerStatus } = useAppState()
48+
const { selectedModel, selectedProvider, getProviderByName } = useModelProvider()
4349
const [showApiKeyError, setShowApiKeyError] = useState(false)
4450
const [isApiKeyEmpty, setIsApiKeyEmpty] = useState(
4551
!apiKey || apiKey.toString().trim().length === 0
@@ -60,6 +66,54 @@ function LocalAPIServer() {
6066
setIsApiKeyEmpty(!isValid)
6167
}
6268

69+
const getLastUsedModel = (): { provider: string; model: string } | null => {
70+
try {
71+
const stored = localStorage.getItem(localStorageKey.lastUsedModel)
72+
return stored ? JSON.parse(stored) : null
73+
} catch (error) {
74+
console.debug('Failed to get last used model from localStorage:', error)
75+
return null
76+
}
77+
}
78+
79+
// Helper function to determine which model to start
80+
const getModelToStart = () => {
81+
// Use last used model if available
82+
const lastUsedModel = getLastUsedModel()
83+
if (lastUsedModel) {
84+
const provider = getProviderByName(lastUsedModel.provider)
85+
if (
86+
provider &&
87+
provider.models.some((m) => m.id === lastUsedModel.model)
88+
) {
89+
return { model: lastUsedModel.model, provider }
90+
}
91+
}
92+
93+
// Use selected model if available
94+
if (selectedModel && selectedProvider) {
95+
const provider = getProviderByName(selectedProvider)
96+
if (provider) {
97+
return { model: selectedModel.id, provider }
98+
}
99+
}
100+
101+
// Use first model from llamacpp provider
102+
const llamacppProvider = getProviderByName('llamacpp')
103+
if (
104+
llamacppProvider &&
105+
llamacppProvider.models &&
106+
llamacppProvider.models.length > 0
107+
) {
108+
return {
109+
model: llamacppProvider.models[0].id,
110+
provider: llamacppProvider,
111+
}
112+
}
113+
114+
return null
115+
}
116+
63117
const toggleAPIServer = async () => {
64118
// Validate API key before starting server
65119
if (serverStatus === 'stopped') {
@@ -68,19 +122,33 @@ function LocalAPIServer() {
68122
return
69123
}
70124
setShowApiKeyError(false)
71-
}
72125

73-
setServerStatus('pending')
74-
if (serverStatus === 'stopped') {
75-
window.core?.api
76-
?.startServer({
77-
host: serverHost,
78-
port: serverPort,
79-
prefix: apiPrefix,
80-
apiKey,
81-
trustedHosts,
82-
isCorsEnabled: corsEnabled,
83-
isVerboseEnabled: verboseLogs,
126+
const modelToStart = getModelToStart()
127+
// Only start server if we have a model to load
128+
if (!modelToStart) {
129+
console.warn(
130+
'Cannot start Local API Server: No model available to load'
131+
)
132+
return
133+
}
134+
135+
setServerStatus('pending')
136+
137+
// Start the model first
138+
startModel(modelToStart.provider, modelToStart.model)
139+
.then(() => {
140+
console.log(`Model ${modelToStart.model} started successfully`)
141+
142+
// Then start the server
143+
return window.core?.api?.startServer({
144+
host: serverHost,
145+
port: serverPort,
146+
prefix: apiPrefix,
147+
apiKey,
148+
trustedHosts,
149+
isCorsEnabled: corsEnabled,
150+
isVerboseEnabled: verboseLogs,
151+
})
84152
})
85153
.then(() => {
86154
setServerStatus('running')
@@ -90,6 +158,7 @@ function LocalAPIServer() {
90158
setServerStatus('stopped')
91159
})
92160
} else {
161+
setServerStatus('pending')
93162
window.core?.api
94163
?.stopServer()
95164
.then(() => {
@@ -199,6 +268,26 @@ function LocalAPIServer() {
199268
/>
200269
</Card>
201270

271+
{/* Startup Configuration */}
272+
<Card title={t('settings:localApiServer.startupConfiguration')}>
273+
<CardItem
274+
title={t('settings:localApiServer.runOnStartup')}
275+
description={t('settings:localApiServer.runOnStartupDesc')}
276+
actions={
277+
<Switch
278+
checked={enableOnStartup}
279+
onCheckedChange={(checked) => {
280+
if (!apiKey || apiKey.toString().trim().length === 0) {
281+
setShowApiKeyError(true)
282+
return
283+
}
284+
setEnableOnStartup(checked)
285+
}}
286+
/>
287+
}
288+
/>
289+
</Card>
290+
202291
{/* Server Configuration */}
203292
<Card title={t('settings:localApiServer.serverConfiguration')}>
204293
<CardItem

0 commit comments

Comments
 (0)