Skip to content

Commit 978ab95

Browse files
authored
Merge pull request #451 from miurla/feature/move-models-to-json
refactor: move model configurations to JSON and improve model management
2 parents 2e87f84 + 4445241 commit 978ab95

17 files changed

+317
-173
lines changed

.env.local.example

-5
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,10 @@ TAVILY_API_KEY=[YOUR_TAVILY_API_KEY] # Get your API key at: https://app.tavily.
4545

4646
# Ollama
4747
# OLLAMA_BASE_URL=http://localhost:11434
48-
# NEXT_PUBLIC_OLLAMA_MODEL=[YOUR_MODEL_NAME] (eg: deepseek-r1)
49-
# If you want to use a different model for tool call, set the model name here.
50-
# NEXT_PUBLIC_OLLAMA_TOOL_CALL_MODEL=[YOUR_MODEL_NAME] (eg: phi4) (optional)
5148

5249
# Azure OpenAI
5350
# AZURE_API_KEY=
5451
# AZURE_RESOURCE_NAME=
55-
# NEXT_PUBLIC_AZURE_DEPLOYMENT_NAME=
5652

5753
# DeepSeek
5854
# DEEPSEEK_API_KEY=[YOUR_DEEPSEEK_API_KEY]
@@ -64,7 +60,6 @@ TAVILY_API_KEY=[YOUR_TAVILY_API_KEY] # Get your API key at: https://app.tavily.
6460
# XAI_API_KEY=[YOUR_XAI_API_KEY]
6561

6662
# OpenAI Compatible Model
67-
# NEXT_PUBLIC_OPENAI_COMPATIBLE_MODEL=
6863
# OPENAI_COMPATIBLE_API_KEY=
6964
# OPENAI_COMPATIBLE_API_BASE_URL=
7065

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ An AI-powered search engine with a generative UI.
3434

3535
### AI Providers
3636

37+
The following AI providers are supported:
38+
3739
- OpenAI (Default)
3840
- Google Generative AI
3941
- Azure OpenAI
@@ -42,8 +44,11 @@ An AI-powered search engine with a generative UI.
4244
- Groq
4345
- DeepSeek
4446
- Fireworks
47+
- xAI (Grok)
4548
- OpenAI Compatible
4649

50+
Models are configured in `lib/config/models.json`. Each model requires its corresponding API key to be set in the environment variables. See [Configuration Guide](docs/CONFIGURATION.md) for details.
51+
4752
### Search Capabilities
4853

4954
- URL-specific search

app/api/chat/route.ts

+40-26
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { createManualToolStreamResponse } from '@/lib/streaming/create-manual-tool-stream'
22
import { createToolCallingStreamResponse } from '@/lib/streaming/create-tool-calling-stream'
3-
import { isProviderEnabled, isToolCallSupported } from '@/lib/utils/registry'
3+
import { Model } from '@/lib/types/models'
4+
import { isProviderEnabled } from '@/lib/utils/registry'
45
import { cookies } from 'next/headers'
56

67
export const maxDuration = 30
78

8-
const DEFAULT_MODEL = 'openai:gpt-4o-mini'
9+
const DEFAULT_MODEL: Model = {
10+
id: 'gpt-4o-mini',
11+
name: 'GPT-4o mini',
12+
provider: 'OpenAI',
13+
providerId: 'openai',
14+
enabled: true,
15+
toolCallType: 'native'
16+
}
917

1018
export async function POST(req: Request) {
1119
try {
@@ -21,46 +29,52 @@ export async function POST(req: Request) {
2129
}
2230

2331
const cookieStore = await cookies()
24-
const modelFromCookie = cookieStore.get('selected-model')?.value
32+
const modelJson = cookieStore.get('selectedModel')?.value
2533
const searchMode = cookieStore.get('search-mode')?.value === 'true'
26-
const model = modelFromCookie || DEFAULT_MODEL
27-
const provider = model.split(':')[0]
28-
if (!isProviderEnabled(provider)) {
29-
return new Response(`Selected provider is not enabled ${provider}`, {
30-
status: 404,
31-
statusText: 'Not Found'
32-
})
34+
35+
let selectedModel = DEFAULT_MODEL
36+
37+
if (modelJson) {
38+
try {
39+
selectedModel = JSON.parse(modelJson) as Model
40+
} catch (e) {
41+
console.error('Failed to parse selected model:', e)
42+
}
43+
}
44+
45+
if (
46+
!isProviderEnabled(selectedModel.providerId) ||
47+
selectedModel.enabled === false
48+
) {
49+
return new Response(
50+
`Selected provider is not enabled ${selectedModel.providerId}`,
51+
{
52+
status: 404,
53+
statusText: 'Not Found'
54+
}
55+
)
3356
}
3457

35-
const supportsToolCalling = isToolCallSupported(model)
58+
const supportsToolCalling = selectedModel.toolCallType === 'native'
3659

3760
return supportsToolCalling
3861
? createToolCallingStreamResponse({
3962
messages,
40-
model,
63+
model: selectedModel,
4164
chatId,
4265
searchMode
4366
})
4467
: createManualToolStreamResponse({
4568
messages,
46-
model,
69+
model: selectedModel,
4770
chatId,
4871
searchMode
4972
})
5073
} catch (error) {
5174
console.error('API route error:', error)
52-
return new Response(
53-
JSON.stringify({
54-
error:
55-
error instanceof Error
56-
? error.message
57-
: 'An unexpected error occurred',
58-
status: 500
59-
}),
60-
{
61-
status: 500,
62-
headers: { 'Content-Type': 'application/json' }
63-
}
64-
)
75+
return new Response('Error processing your request', {
76+
status: 500,
77+
statusText: 'Internal Server Error'
78+
})
6579
}
6680
}

app/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Chat } from '@/components/chat'
2+
import modelsList from '@/lib/config/models.json'
3+
import { Model } from '@/lib/types/models'
24
import { generateId } from 'ai'
35

46
export default function Page() {
57
const id = generateId()
6-
return <Chat id={id} />
8+
return <Chat id={id} models={modelsList.models as Model[]} />
79
}

app/search/[id]/page.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { notFound, redirect } from 'next/navigation'
21
import { Chat } from '@/components/chat'
32
import { getChat } from '@/lib/actions/chat'
3+
import modelsList from '@/lib/config/models.json'
4+
import { Model } from '@/lib/types/models'
45
import { convertToUIMessages } from '@/lib/utils'
6+
import { notFound, redirect } from 'next/navigation'
57

68
export const maxDuration = 60
79

@@ -32,5 +34,11 @@ export default async function SearchPage(props: {
3234
notFound()
3335
}
3436

35-
return <Chat id={id} savedMessages={messages} />
37+
return (
38+
<Chat
39+
id={id}
40+
savedMessages={messages}
41+
models={modelsList.models as Model[]}
42+
/>
43+
)
3644
}

app/share/[id]/page.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { notFound } from 'next/navigation'
21
import { Chat } from '@/components/chat'
32
import { getSharedChat } from '@/lib/actions/chat'
3+
import modelsList from '@/lib/config/models.json'
4+
import { Model } from '@/lib/types/models'
45
import { convertToUIMessages } from '@/lib/utils'
6+
import { notFound } from 'next/navigation'
57

68
export async function generateMetadata(props: {
79
params: Promise<{ id: string }>
@@ -30,5 +32,11 @@ export default async function SharePage(props: {
3032
notFound()
3133
}
3234

33-
return <Chat id={id} savedMessages={messages} />
35+
return (
36+
<Chat
37+
id={id}
38+
savedMessages={messages}
39+
models={modelsList.models as Model[]}
40+
/>
41+
)
3442
}

components/chat-panel.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { Model } from '@/lib/types/models'
34
import { cn } from '@/lib/utils'
45
import { Message } from 'ai'
56
import { ArrowUp, MessageCirclePlus, Square } from 'lucide-react'
@@ -22,6 +23,7 @@ interface ChatPanelProps {
2223
query?: string
2324
stop: () => void
2425
append: (message: any) => void
26+
models?: Model[]
2527
}
2628

2729
export function ChatPanel({
@@ -33,7 +35,8 @@ export function ChatPanel({
3335
setMessages,
3436
query,
3537
stop,
36-
append
38+
append,
39+
models
3740
}: ChatPanelProps) {
3841
const [showEmptyScreen, setShowEmptyScreen] = useState(false)
3942
const router = useRouter()
@@ -130,7 +133,7 @@ export function ChatPanel({
130133
{/* Bottom menu area */}
131134
<div className="flex items-center justify-between p-3">
132135
<div className="flex items-center gap-2">
133-
<ModelSelector />
136+
<ModelSelector models={models || []} />
134137
<SearchModeToggle />
135138
</div>
136139
<div className="flex items-center gap-2">

components/chat.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { CHAT_ID } from '@/lib/constants'
4+
import { Model } from '@/lib/types/models'
45
import { Message, useChat } from 'ai/react'
56
import { useEffect } from 'react'
67
import { toast } from 'sonner'
@@ -10,11 +11,13 @@ import { ChatPanel } from './chat-panel'
1011
export function Chat({
1112
id,
1213
savedMessages = [],
13-
query
14+
query,
15+
models
1416
}: {
1517
id: string
1618
savedMessages?: Message[]
1719
query?: string
20+
models?: Model[]
1821
}) {
1922
const {
2023
messages,
@@ -78,6 +81,7 @@ export function Chat({
7881
stop={stop}
7982
query={query}
8083
append={append}
84+
models={models}
8185
/>
8286
</div>
8387
)

components/model-selector.tsx

+26-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { Model, models } from '@/lib/types/models'
3+
import { Model } from '@/lib/types/models'
44
import { getCookie, setCookie } from '@/lib/utils/cookies'
55
import { isReasoningModel } from '@/lib/utils/registry'
66
import { Check, ChevronsUpDown, Lightbulb } from 'lucide-react'
@@ -29,25 +29,42 @@ function groupModelsByProvider(models: Model[]) {
2929
}, {} as Record<string, Model[]>)
3030
}
3131

32-
export function ModelSelector() {
32+
interface ModelSelectorProps {
33+
models: Model[]
34+
}
35+
36+
export function ModelSelector({ models }: ModelSelectorProps) {
3337
const [open, setOpen] = useState(false)
34-
const [selectedModelId, setSelectedModelId] = useState<string>('')
38+
const [value, setValue] = useState('')
3539

3640
useEffect(() => {
37-
const savedModel = getCookie('selected-model')
41+
const savedModel = getCookie('selectedModel')
3842
if (savedModel) {
39-
setSelectedModelId(savedModel)
43+
try {
44+
const model = JSON.parse(savedModel) as Model
45+
setValue(createModelId(model))
46+
} catch (e) {
47+
console.error('Failed to parse saved model:', e)
48+
}
4049
}
4150
}, [])
4251

4352
const handleModelSelect = (id: string) => {
44-
setSelectedModelId(id === selectedModelId ? '' : id)
45-
setCookie('selected-model', id)
53+
const newValue = id === value ? '' : id
54+
setValue(newValue)
55+
56+
const selectedModel = models.find(model => createModelId(model) === newValue)
57+
if (selectedModel) {
58+
setCookie('selectedModel', JSON.stringify(selectedModel))
59+
} else {
60+
setCookie('selectedModel', '')
61+
}
62+
4663
setOpen(false)
4764
}
4865

66+
const selectedModel = models.find(model => createModelId(model) === value)
4967
const groupedModels = groupModelsByProvider(models)
50-
const selectedModel = models.find(m => createModelId(m) === selectedModelId)
5168

5269
return (
5370
<Popover open={open} onOpenChange={setOpen}>
@@ -108,9 +125,7 @@ export function ModelSelector() {
108125
</div>
109126
<Check
110127
className={`h-4 w-4 ${
111-
selectedModelId === modelId
112-
? 'opacity-100'
113-
: 'opacity-0'
128+
value === modelId ? 'opacity-100' : 'opacity-0'
114129
}`}
115130
/>
116131
</CommandItem>

0 commit comments

Comments
 (0)