Skip to content

Conversation

@rossmanko
Copy link
Contributor

@rossmanko rossmanko commented Nov 20, 2025

Summary by CodeRabbit

  • New Features

    • Dynamic step limits per subscription and interaction mode, giving tailored session capacity
    • New agent capabilities and updated model endpoints for improved reasoning and vision support
  • Improvements

    • Better vision model routing for PDFs and media
    • System prompt now always includes resumed context for more consistent responses
  • Bug Fixes

    • Safer regenerate/retry flows: avoid sending or deleting empty assistant messages to preserve conversation integrity

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Nov 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
hackerai Ready Ready Preview Comment Nov 20, 2025 9:43pm

@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

Replaces multiple AI provider mappings with new Gemini/Grok endpoints, adds a subscription-aware getMaxStepsForUser to control iteration limits, refines vision model selection between ask/agent modes, makes the resume section always appended at the end of system prompts, and guards regenerate/retry flows to avoid deleting or sending empty assistant messages.

Changes

Cohort / File(s) Summary
AI Provider Mappings
lib/ai/providers.ts
Replaced ask-model, ask-model-free, ask-vision-model, ask-vision-model-for-pdfs with Google Gemini endpoints; changed agent-model to XAI grok-code-fast-1; added agent-vision-model using grok-4-fast-reasoning; kept title-generator and summarization mappings.
Dynamic Step Calculation
lib/api/chat-handler.ts
Replaced hardcoded stopWhen values with dynamic call to getMaxStepsForUser(mode, subscription) to determine max iterations based on mode and subscription.
Chat Processing & Model Selection
lib/chat/chat-processor.ts
Added exported getMaxStepsForUser(mode, subscription) (Agent: 20; Ask: free 5 / pro/team 10 / ultra 15); updated vision model selection to use ask-vision-model, ask-vision-model-for-pdfs (ask mode) and agent-vision-model (agent mode).
System Prompt Structure
lib/system-prompt.ts
Resume section now unconditionally appended at the end of the system prompt build (moved to final position regardless of mode).
Regenerate / Retry Guards
app/hooks/useChatHandlers.ts
Added guards to skip deleting the last assistant message when it has no content; temporary chat flows now omit empty assistant messages when sending messages for regenerate/retry, preventing sending/deleting empty assistant messages.

Sequence Diagram

sequenceDiagram
    participant UI as Client
    participant Handler as chat-handler
    participant Processor as chat-processor
    participant Providers as ai/providers
    participant Prompt as system-prompt

    UI->>Handler: start/process chat (mode, subscription, messages...)
    Handler->>Processor: processChatMessages(mode, subscription, messages)
    Processor->>Processor: getMaxStepsForUser(mode, subscription)
    note right of Processor `#f9f0c1`: Agent → 20\nAsk Free → 5\nAsk Pro/Team → 10\nAsk Ultra → 15
    Processor->>Processor: selectModel(mode, messageType)
    alt Ask + PDF
        Processor->>Providers: use ask-vision-model-for-pdfs
    else Ask + Media
        Processor->>Providers: use ask-vision-model
    else Agent + Media
        Processor->>Providers: use agent-vision-model
    end
    Processor->>Prompt: buildSystemPrompt(finishReason)
    note right of Prompt `#e6f7ff`: Resume section now\nalways appended at end
    Prompt-->>Processor: system prompt
    Processor-->>Handler: respond (enforcing max steps)
    Handler->>UI: deliver response

    rect rgba(200,230,201,0.3)
      note over Handler,Processor: Regenerate/Retry paths check last assistant message content\nand avoid deleting/sending empty assistant messages
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Check provider endpoint strings and ensure no lingering old keys.
  • Verify getMaxStepsForUser aligns with product/subscription rules and is used in all relevant handlers.
  • Confirm vision-model selection is correct for PDF vs. media and for ask vs. agent modes.
  • Review regenerate/retry guards in useChatHandlers.ts to ensure no edge-case message loss.

Possibly related PRs

Poem

🐰 A little hop of code
New models bloom, the routes are changed,
Steps now dance by plan arranged.
Vision split where modes decide,
Resume tucked in at journey's side—
A rabbit cheers, code neatly ranged! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Daily branch 2025 11 20' is a generic timestamp reference that does not convey meaningful information about the actual changes made in the pull request. Replace with a descriptive title that summarizes the main changes, such as 'Switch to Google Gemini and XAI providers with dynamic step limits' or 'Update AI providers and implement subscription-based step controls'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch daily-branch-2025-11-20

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
lib/chat/chat-processor.ts (2)

6-31: Step-limit helper is consistent and centralizes the policy

getMaxStepsForUser cleanly captures the step policy:

  • Agent: always 20 steps.
  • Ask: 5 (free), 10 (pro/team), 15 (ultra).

This matches the usage in chat-handler.ts and makes future tuning easier.

One minor nit: the JSDoc line “Agent mode: Always 20 steps (for all paid users)” is slightly misleading since the helper itself doesn’t enforce “paid” (the free/agent block happens at a higher layer). You could drop the parenthetical to keep the comment strictly accurate.


128-131: Team subscriptions and “paid” moderation behavior may be inconsistent

Here getModerationResult is called with subscription === "pro" || subscription === "ultra", so “team” subscribers are treated like “free” for the paid/uncensored flag, while elsewhere (e.g., rate limiting in chat-handler.ts) any non‑free tier is treated as paid.

If “team” is meant to be a paid tier with the same moderation behavior as pro/ultra, consider updating this condition to include it; if not, a short comment explaining why “team” is excluded would avoid confusion.

lib/ai/providers.ts (1)

9-20: Provider mappings are consistent with model selection; consider typing the keys

The updated baseProviders entries cover all model names returned by selectModel ("ask-model", "ask-model-free", "ask-vision-model", "ask-vision-model-for-pdfs", "agent-model", "agent-vision-model") plus the existing title/summarization models, so there’s no obvious risk of missing provider mappings.

To guard against future string mismatches between selectModel and baseProviders, you might eventually introduce a shared string union/type for model ids (or derive the return type of selectModel from keyof typeof baseProviders) so TypeScript enforces that they stay in sync.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4c0592 and 80aba28.

📒 Files selected for processing (4)
  • lib/ai/providers.ts (1 hunks)
  • lib/api/chat-handler.ts (2 hunks)
  • lib/chat/chat-processor.ts (2 hunks)
  • lib/system-prompt.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-06T13:31:08.519Z
Learnt from: RostyslavManko
Repo: hackerai-tech/hackerai PR: 21
File: app/api/chat/route.ts:121-128
Timestamp: 2025-09-06T13:31:08.519Z
Learning: The vision model (NEXT_PUBLIC_VISION_MODEL) used for processing media files also supports tool calling, so overriding to vision-model in Agent mode with media files does not break tool functionality.

Applied to files:

  • lib/chat/chat-processor.ts
🧬 Code graph analysis (2)
lib/chat/chat-processor.ts (1)
types/chat.ts (2)
  • ChatMode (6-6)
  • SubscriptionTier (8-8)
lib/api/chat-handler.ts (1)
lib/chat/chat-processor.ts (1)
  • getMaxStepsForUser (11-31)
🔇 Additional comments (3)
lib/api/chat-handler.ts (1)

55-55: Dynamic step limit wiring is correct; double-check intended tier thresholds

The new getMaxStepsForUser import and its use in stopWhen: stepCountIs(getMaxStepsForUser(mode, subscription)) correctly centralize step limits and keep agent mode gated for non‑free users via the earlier guard.

Note that compared to the previous mode === "ask" ? 5 : 20 behavior, Ask mode now gives:

  • free: 5 steps
  • pro/team: 10 steps
  • ultra: 15 steps

If that increase for paid tiers is intentional, this looks good and keeps the logic in one place.

Also applies to: 351-351

lib/system-prompt.ts (1)

294-295: Resume context placement at end of prompt looks correct

Always appending getResumeSection(finishReason) and then filtering falsy entries ensures:

  • Resume instructions are present when finishReason is "tool-calls" or "length".
  • No extra noise when there is no resume context.
  • The resume section consistently comes last, after personality and memory, which matches the described intent.
lib/chat/chat-processor.ts (1)

45-63: Model selection for media/PDFs aligns with new model keys

The updated selectModel logic:

  • prefers "ask-vision-model-for-pdfs" for PDFs in ask mode,
  • uses "ask-vision-model" for other media in ask mode,
  • uses "agent-vision-model" for media in agent mode,
  • otherwise falls back to "ask-model-free", "ask-model", or "agent-model" based on mode + subscription,

is coherent and matches the new model keys defined in lib/ai/providers.ts. The containsPdfFiles && mode === "ask" check being first ensures PDF‑specific handling wins when both PDFs and other media are present.

Just ensure processMessageFiles sets containsMediaFiles / containsPdfFiles in a way that matches this intent (e.g., PDF‑only cases in agent mode still route to the vision model if that’s desired).

- Check if last assistant message has content before deleting from DB
- Only delete messages that were actually saved (have parts)
- Filter out empty assistant messages in temporary chat retry
- Fixes issue where clicking regenerate after an error would delete the previous valid message
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80aba28 and 03c96f6.

📒 Files selected for processing (1)
  • app/hooks/useChatHandlers.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/hooks/useChatHandlers.ts (1)
convex/messages.ts (1)
  • deleteLastAssistantMessage (443-551)

Comment on lines +301 to +311
// Check if the last assistant message has actual content
// If it's empty or has no parts, it was never saved (error occurred)
// In this case, we shouldn't delete anything from the database
const hasContent = lastAssistant?.parts && lastAssistant.parts.length > 0;

if (!temporaryChatsEnabled) {
// Delete last assistant message and update todos in a single transaction
await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
// Only delete if the last assistant message has content
// This prevents deleting previous valid messages when an error occurred
if (hasContent) {
await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential bug: Empty assistant messages may still be saved to the database.

The guard assumes that if hasContent is false, the message was never saved to the database. However, handleStop (lines 266-276) calls saveAssistantMessage without checking whether parts is empty. This means an empty assistant message can be saved if an error occurs immediately after the stream starts.

When regenerating, if the empty message exists in the database but hasContent is false in the UI state, the deletion will be skipped. The backend will then fetch the empty assistant message from the database, which could cause incorrect regeneration context.

Consider one of these approaches:

Option 1: Always attempt deletion (simplest)

-    // Check if the last assistant message has actual content
-    // If it's empty or has no parts, it was never saved (error occurred)
-    // In this case, we shouldn't delete anything from the database
-    const hasContent = lastAssistant?.parts && lastAssistant.parts.length > 0;
-
     if (!temporaryChatsEnabled) {
-      // Only delete if the last assistant message has content
-      // This prevents deleting previous valid messages when an error occurred
-      if (hasContent) {
-        await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
-      }
+      // Always delete the last assistant message if it exists
+      // The mutation safely handles the case where it doesn't exist in DB
+      if (lastAssistant) {
+        await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
+      }

Option 2: Prevent saving empty messages in handleStop
Modify handleStop to check lastMessage.parts?.length > 0 before calling saveAssistantMessage.

Option 3: Delete when lastAssistant exists (regardless of content)

     if (!temporaryChatsEnabled) {
-      // Only delete if the last assistant message has content
-      // This prevents deleting previous valid messages when an error occurred
-      if (hasContent) {
+      // Delete the last assistant message if one exists in the UI state
+      // This includes empty messages that may have been saved during errors
+      if (lastAssistant) {
         await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
       }
📝 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
// Check if the last assistant message has actual content
// If it's empty or has no parts, it was never saved (error occurred)
// In this case, we shouldn't delete anything from the database
const hasContent = lastAssistant?.parts && lastAssistant.parts.length > 0;
if (!temporaryChatsEnabled) {
// Delete last assistant message and update todos in a single transaction
await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
// Only delete if the last assistant message has content
// This prevents deleting previous valid messages when an error occurred
if (hasContent) {
await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
}
if (!temporaryChatsEnabled) {
// Always delete the last assistant message if it exists
// The mutation safely handles the case where it doesn't exist in DB
if (lastAssistant) {
await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
}
🤖 Prompt for AI Agents
In app/hooks/useChatHandlers.ts around lines 301-311 (and related handleStop at
~266-276), the UI assumes empty assistant parts mean the message was never
saved, but handleStop may save an empty assistant message; modify handleStop to
avoid persisting empty messages by checking lastMessage.parts?.length > 0 before
calling saveAssistantMessage, and only call saveAssistantMessage when there is
at least one non-empty part; this prevents orphan empty records and keeps the
existing deletion guard intact.

Comment on lines +359 to +373
// For temporary chats, filter out empty assistant message if present (from error)
// Check if last message is an empty assistant message
const lastMessage = messages[messages.length - 1];
const isLastMessageEmptyAssistant =
lastMessage?.role === "assistant" &&
(!lastMessage.parts || lastMessage.parts.length === 0);

const messagesToSend = isLastMessageEmptyAssistant
? messages.slice(0, -1)
: messages;

regenerate({
body: {
mode: chatMode,
messages,
messages: messagesToSend,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Good implementation for temporary chats, but consider persisted chats consistency.

The filtering logic for empty assistant messages in temporary chats is well-implemented with clear variable naming and proper conditional handling.

However, there's an inconsistency: persisted chats (lines 347-357) don't handle empty assistant messages at all. If an empty assistant message exists in the database (due to the issue flagged above), the backend will fetch it during retry, while temporary chats would filter it out. This creates divergent behavior between the two modes.

For consistency, consider deleting empty assistant messages in persisted chats as well:

   if (!temporaryChatsEnabled) {
+    // Check if last message is an empty assistant message from an error
+    const lastMessage = messages[messages.length - 1];
+    const isLastMessageEmptyAssistant =
+      lastMessage?.role === "assistant" &&
+      (!lastMessage.parts || lastMessage.parts.length === 0);
+    
+    // Delete empty assistant message if present
+    if (isLastMessageEmptyAssistant) {
+      await deleteLastAssistantMessage({ chatId, todos: cleanedTodos });
+    }
+    
     // For persisted chats, backend fetches from database - explicitly send no messages
     regenerate({

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

🤖 Prompt for AI Agents
In app/hooks/useChatHandlers.ts around lines 347-373, persisted chats (lines
~347-357) don't handle empty assistant messages while temporary chats (lines
359-373) filter them out; make persisted behavior consistent by detecting if the
last message is an assistant with no parts (lastMessage?.role === "assistant" &&
(!lastMessage.parts || lastMessage.parts.length === 0)), then remove it from the
messages array before calling regenerate and also delete that empty message from
the database (or mark it removed) within the persisted-chat code path so retries
won't re-fetch the empty assistant message.

@rossmanko rossmanko merged commit 0652f6b into main Nov 21, 2025
4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants