Skip to content
Open
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
50 changes: 34 additions & 16 deletions app/api/completion/route.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { generateText } from 'ai';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
const headersList = await headers();
const googleApiKey = headersList.get('X-Google-API-Key');
const googleApiKey = headersList.get("X-Google-API-Key");
const openaiApiKey = headersList.get("X-OpenAI-API-Key");
const openrouterApiKey = headersList.get("X-OpenRouter-API-Key");

if (!googleApiKey) {
const { prompt, isTitle, messageId, threadId } = await req.json();

let model;

if (googleApiKey) {
const google = createGoogleGenerativeAI({
apiKey: googleApiKey,
});
model = google("gemini-2.5-flash-preview-04-17");
} else if (openaiApiKey) {
const openai = createOpenAI({
apiKey: openaiApiKey,
});
model = openai("gpt-4.1-mini");
} else if (openrouterApiKey) {
const openrouter = createOpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: openrouterApiKey,
});
model = openrouter("deepseek/deepseek-chat-v3-0324:free");
} else {
Comment on lines +17 to +33
Copy link

@coderabbitai coderabbitai bot Jun 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve consistency and model selection logic.

The provider selection logic works but has some inconsistencies:

  1. Hardcoded model IDs: Each provider uses a hardcoded model ID instead of leveraging the model configuration system used elsewhere in the codebase.

  2. OpenRouter setup: Uses createOpenAI with a custom base URL, which is correct but could be more explicit.

Consider using the existing model configuration system for consistency:

- let model;
+ let model;
+ let provider: 'google' | 'openai' | 'openrouter';

  if (googleApiKey) {
+   provider = 'google';
    const google = createGoogleGenerativeAI({
      apiKey: googleApiKey,
    });
-   model = google("gemini-2.5-flash-preview-04-17");
+   model = google("gemini-2.5-flash");
  } else if (openaiApiKey) {
+   provider = 'openai';
    const openai = createOpenAI({
      apiKey: openaiApiKey,
    });
-   model = openai("gpt-4.1-mini");
+   model = openai("gpt-4o-mini");
  } else if (openrouterApiKey) {
+   provider = 'openrouter';
    const openrouter = createOpenAI({
      baseURL: "https://openrouter.ai/api/v1",
      apiKey: openrouterApiKey,
    });
-   model = openrouter("deepseek/deepseek-chat-v3-0324:free");
+   model = openrouter("deepseek/deepseek-r1:free");

This approach would be more maintainable and consistent with the model configuration pattern used in app/api/chat/route.ts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/api/completion/route.ts between lines 17 and 33, the current provider
selection uses hardcoded model IDs and directly calls createOpenAI or
createGoogleGenerativeAI, which is inconsistent with the model configuration
system used elsewhere. Refactor this section to use the existing model
configuration system for selecting models, ensuring that model IDs come from the
configuration rather than being hardcoded. Also, make the OpenRouter setup more
explicit by clearly defining its client creation and model selection consistent
with other providers, following the pattern in app/api/chat/route.ts.

Copy link
Author

Choose a reason for hiding this comment

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

i think this cleanup should happen in a separate pr where we improve model registration. currently following whats already being done

Copy link

Choose a reason for hiding this comment

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

@mihirpenugonda That makes perfect sense! Keeping this PR focused on the optional key support and handling the model registration improvements separately is a good approach for maintaining clean, focused changes.

Would you like me to create a GitHub issue to track the model registration cleanup for the future PR? This could help ensure the refactoring doesn't get forgotten and provide a place to document the specific improvements needed.

🐰
┌─────────────────┐
│ ◕ ◕ │
│ ω │
│ Noted! 📝 │
└─────────────────┘

return NextResponse.json(
{
error: 'Google API key is required to enable chat title generation.',
error:
"At least one API key is required to enable chat title generation.",
},
{ status: 400 }
);
}

const google = createGoogleGenerativeAI({
apiKey: googleApiKey,
});

const { prompt, isTitle, messageId, threadId } = await req.json();

try {
const { text: title } = await generateText({
model: google('gemini-2.5-flash-preview-04-17'),
model,
system: `\n
- you will generate a short title based on the first message a user begins a conversation with
- ensure it is not more than 80 characters long
Expand All @@ -36,9 +54,9 @@ export async function POST(req: Request) {

return NextResponse.json({ title, isTitle, messageId, threadId });
} catch (error) {
console.error('Failed to generate title:', error);
console.error("Failed to generate title:", error);
return NextResponse.json(
{ error: 'Failed to generate title' },
{ error: "Failed to generate title" },
{ status: 500 }
);
}
Expand Down
13 changes: 8 additions & 5 deletions frontend/components/APIKeyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore';
import { Badge } from './ui/badge';

const formSchema = z.object({
google: z.string().trim().min(1, {
message: 'Google API key is required for Title Generation',
}),
google: z.string().trim().optional(),
openrouter: z.string().trim().optional(),
openai: z.string().trim().optional(),
});
}).refine(
(data) => data.google || data.openrouter || data.openai,
{
message: 'At least one API key is required',
path: ['google'],
}
);

type FormValues = z.infer<typeof formSchema>;

Expand Down Expand Up @@ -81,7 +85,6 @@ const Form = () => {
placeholder="AIza..."
register={register}
error={errors.google}
required
/>

<ApiKeyField
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { v4 as uuidv4 } from 'uuid';
import { StopIcon } from './ui/icons';
import { toast } from 'sonner';
import { useMessageSummary } from '../hooks/useMessageSummary';
import { useAutoSelectModel } from '../hooks/useAutoSelectModel';
import { useTitleLoadingStore } from '../stores/TitleLoadingStore';

interface ChatInputProps {
threadId: string;
Expand Down Expand Up @@ -59,6 +61,7 @@ function PureChatInput({
stop,
}: ChatInputProps) {
const canChat = useAPIKeyStore((state) => state.hasRequiredKeys());
useAutoSelectModel();

const { textareaRef, adjustHeight } = useAutoResizeTextarea({
minHeight: 72,
Expand Down
11 changes: 9 additions & 2 deletions frontend/components/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import { Button, buttonVariants } from './ui/button';
import { deleteThread, getThreads } from '@/frontend/dexie/queries';
import { useLiveQuery } from 'dexie-react-hooks';
import { Link, useNavigate, useParams } from 'react-router';
import { X } from 'lucide-react';
import { X, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { memo } from 'react';
import { useTitleLoadingStore } from '@/frontend/stores/TitleLoadingStore';

export default function ChatSidebar() {
const { id } = useParams();
const navigate = useNavigate();
const threads = useLiveQuery(() => getThreads(), []);
const isLoading = useTitleLoadingStore((state) => state.isLoading);

return (
<Sidebar>
Expand All @@ -45,7 +47,12 @@ export default function ChatSidebar() {
navigate(`/chat/${thread.id}`);
}}
>
<span className="truncate block">{thread.title}</span>
<span className="truncate block flex items-center gap-2">
{thread.title}
{isLoading(thread.id) && (
<Loader2 className="h-3 w-3 animate-spin shrink-0" />
)}
</span>
<Button
variant="ghost"
size="icon"
Expand Down
28 changes: 28 additions & 0 deletions frontend/hooks/useAutoSelectModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from 'react';
import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore';
import { useModelStore } from '@/frontend/stores/ModelStore';
import { AI_MODELS, getModelConfig } from '@/lib/models';

export const useAutoSelectModel = () => {
const getKey = useAPIKeyStore((state) => state.getKey);
const { selectedModel, setModel } = useModelStore();

useEffect(() => {
// Check if the currently selected model has a valid API key
const currentModelConfig = getModelConfig(selectedModel);
const currentApiKey = getKey(currentModelConfig.provider);

if (!currentApiKey) {
// Find the first model with an available API key
const firstAvailableModel = AI_MODELS.find((model) => {
const modelConfig = getModelConfig(model);
const apiKey = getKey(modelConfig.provider);
return !!apiKey;
});

if (firstAvailableModel && firstAvailableModel !== selectedModel) {
setModel(firstAvailableModel);
}
}
}, [getKey, selectedModel, setModel]);
};
44 changes: 33 additions & 11 deletions frontend/hooks/useMessageSummary.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useCompletion } from '@ai-sdk/react';
import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore';
import { toast } from 'sonner';
import { createMessageSummary, updateThread } from '@/frontend/dexie/queries';
import { useCompletion } from "@ai-sdk/react";
import { useAPIKeyStore } from "@/frontend/stores/APIKeyStore";
import { toast } from "sonner";
import { createMessageSummary, updateThread } from "@/frontend/dexie/queries";
import { useTitleLoadingStore } from "@/frontend/stores/TitleLoadingStore";
import { getProviderHeaderKey } from "@/lib/models";

interface MessageSummaryPayload {
title: string;
Expand All @@ -11,13 +13,20 @@ interface MessageSummaryPayload {
}

export const useMessageSummary = () => {
const getKey = useAPIKeyStore((state) => state.getKey);
const getFirstAvailableKey = useAPIKeyStore(
(state) => state.getFirstAvailableKey
);
const setLoading = useTitleLoadingStore((state) => state.setLoading);

const availableKey = getFirstAvailableKey();
Comment on lines +16 to +21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for available API keys before making requests.

The hook retrieves the first available key but doesn't validate that one exists before proceeding with API calls. This could lead to failed requests with empty headers.

  const availableKey = getFirstAvailableKey();
+ 
+ if (!availableKey) {
+   toast.error("No API key available. Please configure at least one API key.");
+   return {
+     complete: () => {},
+     isLoading: false,
+   };
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getFirstAvailableKey = useAPIKeyStore(
(state) => state.getFirstAvailableKey
);
const setLoading = useTitleLoadingStore((state) => state.setLoading);
const availableKey = getFirstAvailableKey();
const getFirstAvailableKey = useAPIKeyStore(
(state) => state.getFirstAvailableKey
);
const setLoading = useTitleLoadingStore((state) => state.setLoading);
const availableKey = getFirstAvailableKey();
if (!availableKey) {
toast.error("No API key available. Please configure at least one API key.");
return {
complete: () => {},
isLoading: false,
};
}
🤖 Prompt for AI Agents
In frontend/hooks/useMessageSummary.ts around lines 16 to 21, the code retrieves
the first available API key but does not check if the key exists before using it
in API requests. Add a validation step after getting the availableKey to confirm
it is not null or undefined. If no key is available, prevent the API call from
proceeding or handle the error gracefully to avoid making requests with empty
headers.


const { complete, isLoading } = useCompletion({
api: '/api/completion',
...(getKey('google') && {
headers: { 'X-Google-API-Key': getKey('google')! },
}),
api: "/api/completion",
headers: availableKey
? {
[getProviderHeaderKey(availableKey.provider)]: availableKey.key,
}
: {},
onResponse: async (response) => {
try {
const payload: MessageSummaryPayload = await response.json();
Expand All @@ -28,20 +37,33 @@ export const useMessageSummary = () => {
if (isTitle) {
await updateThread(threadId, title);
await createMessageSummary(threadId, messageId, title);
setLoading(threadId, false);
} else {
await createMessageSummary(threadId, messageId, title);
}
} else {
toast.error('Failed to generate a summary for the message');
toast.error("Failed to generate a summary for the message");
}
} catch (error) {
console.error(error);
}
},
});

const completeWithLoading = (
prompt: string,
options: {
body: { threadId: string; messageId: string; isTitle?: boolean };
}
) => {
if (options.body.isTitle) {
setLoading(options.body.threadId, true);
}
return complete(prompt, options);
};

return {
complete,
complete: completeWithLoading,
isLoading,
};
};
12 changes: 11 additions & 1 deletion frontend/stores/APIKeyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type APIKeyStore = {
setKeys: (newKeys: Partial<APIKeys>) => void;
hasRequiredKeys: () => boolean;
getKey: (provider: Provider) => string | null;
getFirstAvailableKey: () => { provider: Provider; key: string } | null;
};

type StoreWithPersist = Mutate<
Expand Down Expand Up @@ -48,13 +49,22 @@ export const useAPIKeyStore = create<APIKeyStore>()(
},

hasRequiredKeys: () => {
return !!get().keys.google;
const keys = get().keys;
return !!keys.google || !!keys.openrouter || !!keys.openai;
},

getKey: (provider) => {
const key = get().keys[provider];
return key ? key : null;
},

getFirstAvailableKey: () => {
const keys = get().keys;
if (keys.google) return { provider: 'google' as Provider, key: keys.google };
if (keys.openai) return { provider: 'openai' as Provider, key: keys.openai };
if (keys.openrouter) return { provider: 'openrouter' as Provider, key: keys.openrouter };
return null;
},
}),
{
name: 'api-keys',
Expand Down
27 changes: 27 additions & 0 deletions frontend/stores/TitleLoadingStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { create } from 'zustand';

type TitleLoadingStore = {
loadingThreads: Set<string>;
setLoading: (threadId: string, isLoading: boolean) => void;
isLoading: (threadId: string) => boolean;
};

export const useTitleLoadingStore = create<TitleLoadingStore>((set, get) => ({
loadingThreads: new Set(),

setLoading: (threadId, isLoading) => {
set((state) => {
const newSet = new Set(state.loadingThreads);
if (isLoading) {
newSet.add(threadId);
} else {
newSet.delete(threadId);
}
return { loadingThreads: newSet };
});
},

isLoading: (threadId) => {
return get().loadingThreads.has(threadId);
},
}));
Loading