Skip to content

Commit 147ee42

Browse files
authored
Add DeepSeek Model Support via OpenRouter Integration (#57)
* feat: add Deepseek model support via OpenRouter integration - Integrate OpenRouter as a third-party provider to enable Deepseek models - Scale up OpenRouter logo to match other provider icons - Maintain consistent provider interface for seamless model switching * fix: address review feedback from ibelick * fix: address the review feedback
1 parent 3d38cd2 commit 147ee42

File tree

10 files changed

+345
-51
lines changed

10 files changed

+345
-51
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ SUPABASE_SERVICE_ROLE=your_supabase_service_role_key
66
# OpenAI
77
OPENAI_API_KEY=your_openai_api_key
88

9+
# OpenRouter
10+
OPENROUTER_API_KEY=your_openrouter_api_key
11+
912
# Mistral
1013
MISTRAL_API_KEY=your_mistral_api_key
1114

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,18 @@ yarn-error.log*
3636
.env.test.local
3737
.env.production.local
3838

39+
3940
# vercel
4041
.vercel
4142

4243
# typescript
4344
*.tsbuildinfo
4445
next-env.d.ts
46+
47+
# env
48+
.env
49+
50+
# bun
51+
bun.lockb
52+
bun.lock
53+

INSTALL.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ OPENAI_API_KEY=your_openai_api_key
2828
# Mistral
2929
MISTRAL_API_KEY=your_mistral_api_key
3030

31+
# OpenRouter
32+
OPENROUTER_API_KEY=your_openrouter_api_key
33+
3134
# CSRF Protection
3235
CSRF_SECRET=your_csrf_secret_key
3336

@@ -59,6 +62,35 @@ python -c "import secrets; print(secrets.token_hex(32))"
5962

6063
Copy the generated value and add it to your `.env.local` file as the `CSRF_SECRET` value.
6164

65+
#### Google OAuth Authentication
66+
67+
1. Go to your Supabase project dashboard
68+
2. Navigate to Authentication > Providers
69+
3. Find the "Google" provider
70+
4. Enable it by toggling the switch
71+
5. Configure the Google OAuth credentials:
72+
- You'll need to set up OAuth 2.0 credentials in the Google Cloud Console
73+
- Add your application's redirect URL: https://[YOUR_PROJECT_REF].supabase.co/auth/v1/callback
74+
- Get the Client ID and Client Secret from Google Cloud Console
75+
- Add these credentials to the Google provider settings in Supabase
76+
77+
Here are the detailed steps to set up Google OAuth:
78+
79+
1. Go to the Google Cloud Console
80+
2. Create a new project or select an existing one
81+
3. Enable the Google+ API
82+
4. Go to Credentials > Create Credentials > OAuth Client ID
83+
5. Configure the OAuth consent screen if you haven't already
84+
6. Set the application type as "Web application"
85+
7. Add these authorized redirect URIs:
86+
- https://[YOUR_PROJECT_REF].supabase.co/auth/v1/callback
87+
- http://localhost:3000/auth/callback (for local development)
88+
8. Copy the Client ID and Client Secret
89+
9. Go back to your Supabase dashboard
90+
10. Paste the Client ID and Client Secret in the Google provider settings
91+
11. Save the changes
92+
93+
6294
## Local Installation
6395

6496
### macOS / Linux
@@ -118,8 +150,8 @@ CREATE TABLE users (
118150
preferred_model TEXT,
119151
premium BOOLEAN DEFAULT false,
120152
profile_image TEXT,
121-
created_at TIMESTAMPTZ DEFAULT NOW()
122-
last_active_at: TIMESTAMPTZ DEFAULT NOW()
153+
created_at TIMESTAMPTZ DEFAULT NOW(),
154+
last_active_at TIMESTAMPTZ DEFAULT NOW()
123155
);
124156

125157
-- Chats table

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ Apache License 2.0
3939

4040
## Notes
4141

42-
This is a beta release. The codebase is evolving and may change.
42+
This is a beta release. The codebase is evolving and may change.

app/api/chat/route.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { sanitizeUserInput } from "@/lib/sanitize"
55
import { validateUserIdentity } from "@/lib/server/api"
66
import { Attachment } from "@ai-sdk/ui-utils"
77
import { Message as MessageAISDK, streamText } from "ai"
8+
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
9+
810

911
// Maximum allowed duration for streaming (in seconds)
1012
export const maxDuration = 30
@@ -76,8 +78,22 @@ export async function POST(req: Request) {
7678
}
7779
}
7880

81+
const modelConfig = MODELS.find((m) => m.id === model)
82+
if (!modelConfig){
83+
throw new Error(`Model ${model} not found`)
84+
}
85+
let modelInstance
86+
if (modelConfig.provider === 'openrouter') {
87+
const openRouter = createOpenRouter({
88+
apiKey: process.env.OPENROUTER_API_KEY,
89+
})
90+
modelInstance = openRouter.chat(modelConfig.api_sdk) // this is a special case for openrouter. Normal openrouter models are not supported.
91+
} else {
92+
modelInstance = modelConfig.api_sdk
93+
}
94+
7995
const result = streamText({
80-
model: MODELS.find((m) => m.id === model)?.api_sdk!,
96+
model: modelInstance,
8197
system: effectiveSystemPrompt,
8298
messages,
8399
onError: (err) => {

app/api/rate-limits/route.ts

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,42 @@ import {
44
} from "@/lib/config"
55
import { validateUserIdentity } from "../../../lib/server/api"
66

7+
78
export async function GET(req: Request) {
8-
const { searchParams } = new URL(req.url)
9-
const userId = searchParams.get("userId")
10-
const isAuthenticated = searchParams.get("isAuthenticated") === "true"
11-
if (!userId) {
12-
return new Response(JSON.stringify({ error: "Missing userId" }), {
13-
status: 400,
14-
})
15-
}
16-
const supabase = await validateUserIdentity(userId, isAuthenticated)
17-
18-
const { data, error } = await supabase
19-
.from("users")
20-
.select("daily_message_count")
21-
.eq("id", userId)
22-
.maybeSingle()
23-
24-
if (error || !data) {
25-
return new Response(JSON.stringify({ error: error?.message }), {
26-
status: 500,
9+
10+
const { searchParams } = new URL(req.url)
11+
const userId = searchParams.get("userId")
12+
const isAuthenticated = searchParams.get("isAuthenticated") === "true"
13+
14+
if (!userId) {
15+
return new Response(JSON.stringify({error: "Missing userId"}), {
16+
status: 400,
17+
})
18+
}
19+
20+
const supabase = await validateUserIdentity(userId, isAuthenticated)
21+
22+
const { data, error } = await supabase
23+
.from("users")
24+
.select("daily_message_count")
25+
.eq("id", userId)
26+
.maybeSingle()
27+
28+
if (error || !data){
29+
return new Response(JSON.stringify({ error: error?.message }), {
30+
status: 500,
31+
})
32+
}
33+
34+
const dailyLimit = isAuthenticated
35+
? AUTH_DAILY_MESSAGE_LIMIT
36+
: NON_AUTH_DAILY_MESSAGE_LIMIT
37+
const dailyCount = data.daily_message_count || 0
38+
const remaining = dailyLimit - dailyCount
39+
40+
return new Response(JSON.stringify({ dailyCount, dailyLimit, remaining }), {
41+
status: 200,
2742
})
28-
}
29-
30-
const dailyLimit = isAuthenticated
31-
? AUTH_DAILY_MESSAGE_LIMIT
32-
: NON_AUTH_DAILY_MESSAGE_LIMIT
33-
const dailyCount = data.daily_message_count || 0
34-
const remaining = dailyLimit - dailyCount
35-
36-
return new Response(JSON.stringify({ dailyCount, dailyLimit, remaining }), {
37-
status: 200,
38-
})
39-
}
43+
}
44+
45+

components/icons/openrouter.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from "react"
2+
import type { SVGProps } from "react"
3+
4+
const Icon = (props: SVGProps<SVGSVGElement>) => (
5+
<svg fill="none"
6+
fillRule="evenodd"
7+
style={{ flex: "none", lineHeight: "1" }}
8+
height={64}
9+
width={64}
10+
viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" {...props}>
11+
12+
<path fill="#000"
13+
fillRule="evenodd"
14+
d="M42.01 4.893l18.05 10.263v0.218l-18.235 10.153 0.043-5.293-2.053-0.075c-2.648-0.07-4.028 0.005-5.67 0.275-2.66 0.438-5.095 1.443-7.868 3.38l-5.415 3.743c-0.71 0.488-1.238 0.84-1.7 1.138l-1.288 0.805-0.993 0.585 0.963 0.575 1.325 0.845c1.19 0.785 2.925 1.99 6.753 4.665 2.775 1.938 5.208 2.943 7.868 3.38l0.75 0.113c1.735 0.228 3.438 0.235 7.063 0.083l0.055-5.398 18.05 10.263v0.218L41.473 55l0.035-4.655-1.588 0.055c-3.465 0.105-5.343 0.005-7.845-0.405-4.235-0.7-8.15-2.315-12.203-5.148l-5.395-3.75a54.993 54.993 0 01-1.888-1.245l-1.168-0.7a139.818 139.818 0 01-1.9-1.075C7.27 36.825 1.408 35.29 0 35.29V24.72l0.35 0.01c1.41-0.018 7.275-1.555 9.523-2.81l2.54-1.45 1.095-0.685c1.07-0.7 2.68-1.815 6.715-4.633 4.053-2.833 7.965-4.45 12.203-5.148 2.88-0.475 4.935-0.533 9.535-0.345l0.05-4.768z"
15+
clipRule="evenodd">
16+
</path>
17+
</svg>
18+
)
19+
20+
export default Icon

lib/config.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Gemini from "@/components/icons/gemini"
44
import Grok from "@/components/icons/grok"
55
import Mistral from "@/components/icons/mistral"
66
import OpenAI from "@/components/icons/openai"
7+
import OpenRouter from "@/components/icons/openrouter"
78
import { mistral } from "@ai-sdk/mistral"
89
import { openai } from "@ai-sdk/openai"
910
import {
@@ -42,19 +43,7 @@ export type Model = {
4243
}
4344

4445
export const MODELS_NOT_AVAILABLE = [
45-
{
46-
id: "deepseek-r1",
47-
name: "DeepSeek R1",
48-
provider: "deepseek",
49-
available: false,
50-
api_sdk: false,
51-
features: [
52-
{
53-
id: "file-upload",
54-
enabled: false,
55-
},
56-
],
57-
},
46+
5847
{
5948
id: "gemini-1.5-pro",
6049
name: "Gemini 1.5 Pro",
@@ -206,8 +195,22 @@ export const MODELS = [
206195
},
207196
],
208197
api_sdk: mistral("mistral-large-latest"),
198+
icon: Mistral,
199+
description: "Fine-tuned for chat. A lighter, faster option for everyday use.",
200+
},
201+
{
202+
id: "deepseek-r1",
203+
name: "DeepSeek R1",
204+
provider: "openrouter",
205+
features: [
206+
{
207+
id: "file-upload",
208+
enabled: true,
209+
},
210+
],
211+
api_sdk: "deepseek/deepseek-r1:free", // this is a special case for openrouter
209212
description:
210-
"Fine-tuned for chat. A lighter, faster option for everyday use.",
213+
"A reasoning-first model trained with reinforcement learning, built for math, code, and complex problem solving",
211214
},
212215
] as Model[]
213216

@@ -254,6 +257,11 @@ const PROVIDERS_NOT_AVAILABLE = [
254257
] as Provider[]
255258

256259
export const PROVIDERS = [
260+
{
261+
id: "openrouter",
262+
name: "OpenRouter",
263+
icon: OpenRouter,
264+
},
257265
{
258266
id: "openai",
259267
name: "OpenAI",
@@ -411,7 +419,7 @@ export const SUGGESTIONS = [
411419
highlight: "Explain",
412420
prompt: `Explain`,
413421
items: [
414-
"Explain quantum physics like Im 10",
422+
"Explain quantum physics like I'm 10",
415423
"Explain stoicism in simple terms",
416424
"Explain how a neural network works",
417425
"Explain the difference between AI and AGI",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "chat-ai",
2+
"name": "zola",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@ai-sdk/mistral": "^1.2.0",
1313
"@ai-sdk/openai": "^1.3.12",
14+
"@openrouter/ai-sdk-provider": "^0.4.5",
1415
"@phosphor-icons/react": "^2.1.7",
1516
"@radix-ui/react-alert-dialog": "^1.1.6",
1617
"@radix-ui/react-avatar": "^1.1.3",

0 commit comments

Comments
 (0)