Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ SUPABASE_SERVICE_ROLE=your_supabase_service_role_key
# OpenAI
OPENAI_API_KEY=your_openai_api_key

# OpenRouter
OPENROUTER_API_KEY=your_openrouter_api_key

# Mistral
MISTRAL_API_KEY=your_mistral_api_key

Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ yarn-error.log*
.env.test.local
.env.production.local


# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# env
.env

# bun
bun.lockb
bun.lock

36 changes: 34 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ OPENAI_API_KEY=your_openai_api_key
# Mistral
MISTRAL_API_KEY=your_mistral_api_key

# OpenRouter
OPENROUTER_API_KEY=your_openrouter_api_key

# CSRF Protection
CSRF_SECRET=your_csrf_secret_key

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

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

#### Google OAuth Authentication

1. Go to your Supabase project dashboard
2. Navigate to Authentication > Providers
3. Find the "Google" provider
4. Enable it by toggling the switch
5. Configure the Google OAuth credentials:
- You'll need to set up OAuth 2.0 credentials in the Google Cloud Console
- Add your application's redirect URL: https://[YOUR_PROJECT_REF].supabase.co/auth/v1/callback
- Get the Client ID and Client Secret from Google Cloud Console
- Add these credentials to the Google provider settings in Supabase

Here are the detailed steps to set up Google OAuth:

1. Go to the Google Cloud Console
2. Create a new project or select an existing one
3. Enable the Google+ API
4. Go to Credentials > Create Credentials > OAuth Client ID
5. Configure the OAuth consent screen if you haven't already
6. Set the application type as "Web application"
7. Add these authorized redirect URIs:
- https://[YOUR_PROJECT_REF].supabase.co/auth/v1/callback
- http://localhost:3000/auth/callback (for local development)
8. Copy the Client ID and Client Secret
9. Go back to your Supabase dashboard
10. Paste the Client ID and Client Secret in the Google provider settings
11. Save the changes


## Local Installation

### macOS / Linux
Expand Down Expand Up @@ -118,8 +150,8 @@ CREATE TABLE users (
preferred_model TEXT,
premium BOOLEAN DEFAULT false,
profile_image TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
last_active_at: TIMESTAMPTZ DEFAULT NOW()
created_at TIMESTAMPTZ DEFAULT NOW(),
last_active_at TIMESTAMPTZ DEFAULT NOW()
);

-- Chats table
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ Apache License 2.0

## Notes

This is a beta release. The codebase is evolving and may change.
This is a beta release. The codebase is evolving and may change.
18 changes: 17 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { sanitizeUserInput } from "@/lib/sanitize"
import { validateUserIdentity } from "@/lib/server/api"
import { Attachment } from "@ai-sdk/ui-utils"
import { Message as MessageAISDK, streamText } from "ai"
import { createOpenRouter } from "@openrouter/ai-sdk-provider"


// Maximum allowed duration for streaming (in seconds)
export const maxDuration = 30
Expand Down Expand Up @@ -79,8 +81,22 @@ export async function POST(req: Request) {
}
}

const modelConfig = MODELS.find((m) => m.id === model)
if (!modelConfig){
throw new Error(`Model ${model} not found`)
}
let modelInstance
if (modelConfig.provider === 'openrouter') {
const openRouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
})
modelInstance = openRouter.chat(modelConfig.api_sdk) // this is a special case for openrouter. Normal openrouter models are not supported.
} else {
modelInstance = modelConfig.api_sdk
}

const result = streamText({
model: MODELS.find((m) => m.id === model)?.api_sdk!,
model: modelInstance,
system: effectiveSystemPrompt,
messages,
// When the response finishes, insert the assistant messages to supabase
Expand Down
68 changes: 37 additions & 31 deletions app/api/rate-limits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,42 @@ import {
} from "@/lib/config"
import { validateUserIdentity } from "../../../lib/server/api"


export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const userId = searchParams.get("userId")
const isAuthenticated = searchParams.get("isAuthenticated") === "true"
if (!userId) {
return new Response(JSON.stringify({ error: "Missing userId" }), {
status: 400,
})
}
const supabase = await validateUserIdentity(userId, isAuthenticated)

const { data, error } = await supabase
.from("users")
.select("daily_message_count")
.eq("id", userId)
.maybeSingle()

if (error || !data) {
return new Response(JSON.stringify({ error: error?.message }), {
status: 500,

const { searchParams } = new URL(req.url)
const userId = searchParams.get("userId")
const isAuthenticated = searchParams.get("isAuthenticated") === "true"

if (!userId) {
return new Response(JSON.stringify({error: "Missing userId"}), {
status: 400,
})
}

const supabase = await validateUserIdentity(userId, isAuthenticated)

const { data, error } = await supabase
.from("users")
.select("daily_message_count")
.eq("id", userId)
.maybeSingle()

if (error || !data){
return new Response(JSON.stringify({ error: error?.message }), {
status: 500,
})
}

const dailyLimit = isAuthenticated
? AUTH_DAILY_MESSAGE_LIMIT
: NON_AUTH_DAILY_MESSAGE_LIMIT
const dailyCount = data.daily_message_count || 0
const remaining = dailyLimit - dailyCount

return new Response(JSON.stringify({ dailyCount, dailyLimit, remaining }), {
status: 200,
})
}

const dailyLimit = isAuthenticated
? AUTH_DAILY_MESSAGE_LIMIT
: NON_AUTH_DAILY_MESSAGE_LIMIT
const dailyCount = data.daily_message_count || 0
const remaining = dailyLimit - dailyCount

return new Response(JSON.stringify({ dailyCount, dailyLimit, remaining }), {
status: 200,
})
}
}


20 changes: 20 additions & 0 deletions components/icons/openrouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from "react"
import type { SVGProps } from "react"

const Icon = (props: SVGProps<SVGSVGElement>) => (
<svg fill="none"
fillRule="evenodd"
style={{ flex: "none", lineHeight: "1" }}
height={64}
width={64}
viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" {...props}>

<path fill="#000"
fillRule="evenodd"
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"
clipRule="evenodd">
</path>
</svg>
)

export default Icon
38 changes: 23 additions & 15 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Gemini from "@/components/icons/gemini"
import Grok from "@/components/icons/grok"
import Mistral from "@/components/icons/mistral"
import OpenAI from "@/components/icons/openai"
import OpenRouter from "@/components/icons/openrouter"
import { mistral } from "@ai-sdk/mistral"
import { openai } from "@ai-sdk/openai"
import {
Expand Down Expand Up @@ -41,19 +42,7 @@ export type Model = {
}

export const MODELS_NOT_AVAILABLE = [
{
id: "deepseek-r1",
name: "DeepSeek R1",
provider: "deepseek",
available: false,
api_sdk: false,
features: [
{
id: "file-upload",
enabled: false,
},
],
},

{
id: "gemini-1.5-pro",
name: "Gemini 1.5 Pro",
Expand Down Expand Up @@ -205,8 +194,22 @@ export const MODELS = [
},
],
api_sdk: mistral("mistral-large-latest"),
icon: Mistral,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep mistral-large-latest description

    description:
      "Fine-tuned for chat. A lighter, faster option for everyday use.",

description: "Fine-tuned for chat. A lighter, faster option for everyday use.",
},
{
id: "deepseek-r1",
name: "DeepSeek R1",
provider: "openrouter",
features: [
{
id: "file-upload",
enabled: true,
},
],
api_sdk: "deepseek/deepseek-r1:free", // this is a special case for openrouter
description:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add description for deepseek-r1, like: A reasoning-first model trained with reinforcement learning, built for math, code, and complex problem solving.

"Fine-tuned for chat. A lighter, faster option for everyday use.",
"A reasoning-first model trained with reinforcement learning, built for math, code, and complex problem solving",
},
] as Model[]

Expand Down Expand Up @@ -253,6 +256,11 @@ const PROVIDERS_NOT_AVAILABLE = [
] as Provider[]

export const PROVIDERS = [
{
id: "openrouter",
name: "OpenRouter",
icon: OpenRouter,
},
{
id: "openai",
name: "OpenAI",
Expand Down Expand Up @@ -410,7 +418,7 @@ export const SUGGESTIONS = [
highlight: "Explain",
prompt: `Explain`,
items: [
"Explain quantum physics like Im 10",
"Explain quantum physics like I'm 10",
"Explain stoicism in simple terms",
"Explain how a neural network works",
"Explain the difference between AI and AGI",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "chat-ai",
"name": "zola",
"version": "0.1.0",
"private": true,
"scripts": {
Expand All @@ -11,6 +11,7 @@
"dependencies": {
"@ai-sdk/mistral": "^1.2.0",
"@ai-sdk/openai": "^1.3.12",
"@openrouter/ai-sdk-provider": "^0.4.5",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
Expand Down
Loading