Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9865592
Add exa to search online tool
Sg312 Oct 14, 2025
732b613
Larger font size
Sg312 Oct 15, 2025
ebbed53
Copilot UI improvements
Sg312 Oct 15, 2025
5efe404
Fix models options
Sg312 Oct 15, 2025
25b28dd
Add haiku 4.5 to copilot
Sg312 Oct 15, 2025
b90c239
Model ui for haiku
Sg312 Oct 15, 2025
d4fd007
Fix lint
Sg312 Oct 15, 2025
43d26f0
Revert
Sg312 Oct 16, 2025
8cd1949
Only allow one revert to message
Sg312 Oct 16, 2025
a7f3d87
Clear diff on revert
Sg312 Oct 16, 2025
c76e160
Fix welcome screen flash
Sg312 Oct 16, 2025
51ffcb3
Add focus onto the user input box when clicked
Sg312 Oct 16, 2025
e6dcb55
Fix grayout of new stream on old edit message
Sg312 Oct 16, 2025
52a1bc7
Lint
Sg312 Oct 16, 2025
a2173ca
Make edit message submit smoother
Sg312 Oct 16, 2025
5a551d0
Allow message sent while streaming
Sg312 Oct 16, 2025
ddf4cd4
Revert popup improvements: gray out stuff below, show cursor on revert
Sg312 Oct 16, 2025
81f5b2b
Fix lint
Sg312 Oct 16, 2025
e02db18
Improve chat history dropdown
Sg312 Oct 16, 2025
89c143b
Improve get block metadata tool
Sg312 Oct 16, 2025
f229cf9
Update update cost route
Sg312 Oct 17, 2025
c4a80ae
Fix env
Sg312 Oct 17, 2025
162a0b3
Context usage endpoint
Sg312 Oct 17, 2025
fdcbe2e
Make chat history scrollable
Sg312 Oct 17, 2025
960107b
Fix lint
Sg312 Oct 17, 2025
16efb15
Copilot revert popup updates
Sg312 Oct 17, 2025
7639865
Fix lint
Sg312 Oct 17, 2025
5fb6227
Merge branch 'staging' into improvement/copilot-2
Sg312 Oct 17, 2025
3f6ca39
Fix tests and lint
Sg312 Oct 18, 2025
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
78 changes: 11 additions & 67 deletions apps/sim/app/api/billing/update-cost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,17 @@ import { checkInternalApiKey } from '@/lib/copilot/utils'
import { isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { calculateCost } from '@/providers/utils'

const logger = createLogger('BillingUpdateCostAPI')

const UpdateCostSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
input: z.number().min(0, 'Input tokens must be a non-negative number'),
output: z.number().min(0, 'Output tokens must be a non-negative number'),
model: z.string().min(1, 'Model is required'),
inputMultiplier: z.number().min(0),
outputMultiplier: z.number().min(0),
cost: z.number().min(0, 'Cost must be a non-negative number'),
})

/**
* POST /api/billing/update-cost
* Update user cost based on token usage with internal API key auth
* Update user cost with a pre-calculated cost value (internal API key auth required)
*/
export async function POST(req: NextRequest) {
const requestId = generateRequestId()
Expand Down Expand Up @@ -77,45 +72,13 @@ export async function POST(req: NextRequest) {
)
}

const { userId, input, output, model, inputMultiplier, outputMultiplier } = validation.data
const { userId, cost } = validation.data

logger.info(`[${requestId}] Processing cost update`, {
userId,
input,
output,
model,
inputMultiplier,
outputMultiplier,
cost,
})

const finalPromptTokens = input
const finalCompletionTokens = output
const totalTokens = input + output

// Calculate cost using provided multiplier (required)
const costResult = calculateCost(
model,
finalPromptTokens,
finalCompletionTokens,
false,
inputMultiplier,
outputMultiplier
)

logger.info(`[${requestId}] Cost calculation result`, {
userId,
model,
promptTokens: finalPromptTokens,
completionTokens: finalCompletionTokens,
totalTokens: totalTokens,
inputMultiplier,
outputMultiplier,
costResult,
})

// Follow the exact same logic as ExecutionLogger.updateUserStats but with direct userId
const costToStore = costResult.total // No additional multiplier needed since calculateCost already applied it

// Check if user stats record exists (same as ExecutionLogger)
const userStatsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))

Expand All @@ -128,25 +91,21 @@ export async function POST(req: NextRequest) {
)
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
}
// Update existing user stats record (same logic as ExecutionLogger)
// Update existing user stats record
const updateFields = {
totalTokensUsed: sql`total_tokens_used + ${totalTokens}`,
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
totalCost: sql`total_cost + ${cost}`,
currentPeriodCost: sql`current_period_cost + ${cost}`,
// Copilot usage tracking increments
totalCopilotCost: sql`total_copilot_cost + ${costToStore}`,
totalCopilotTokens: sql`total_copilot_tokens + ${totalTokens}`,
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
totalCopilotCalls: sql`total_copilot_calls + 1`,
totalApiCalls: sql`total_api_calls`,
lastActive: new Date(),
}

await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))

logger.info(`[${requestId}] Updated user stats record`, {
userId,
addedCost: costToStore,
addedTokens: totalTokens,
addedCost: cost,
})

// Check if user has hit overage threshold and bill incrementally
Expand All @@ -157,29 +116,14 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Cost update completed successfully`, {
userId,
duration,
cost: costResult.total,
totalTokens,
cost,
})

return NextResponse.json({
success: true,
data: {
userId,
input,
output,
totalTokens,
model,
cost: {
input: costResult.input,
output: costResult.output,
total: costResult.total,
},
tokenBreakdown: {
prompt: finalPromptTokens,
completion: finalCompletionTokens,
total: totalTokens,
},
pricing: costResult.pricing,
cost,
processedAt: new Date().toISOString(),
requestId,
},
Expand Down
35 changes: 35 additions & 0 deletions apps/sim/app/api/copilot/chat/delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('DeleteChatAPI')

const DeleteChatSchema = z.object({
chatId: z.string(),
})

export async function DELETE(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const parsed = DeleteChatSchema.parse(body)

// Delete the chat
await db.delete(copilotChats).where(eq(copilotChats.id, parsed.chatId))
Comment on lines +25 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

logic: Delete without verifying ownership - any authenticated user can delete any chat

Suggested change
// Delete the chat
await db.delete(copilotChats).where(eq(copilotChats.id, parsed.chatId))
// Verify chat ownership
const chat = await db.select().from(copilotChats).where(eq(copilotChats.id, parsed.chatId)).limit(1)
if (chat.length === 0 || chat[0].userId !== session.user.id) {
return NextResponse.json({ success: false, error: 'Chat not found or unauthorized' }, { status: 404 })
}
// Delete the chat
await db.delete(copilotChats).where(eq(copilotChats.id, parsed.chatId))
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/app/api/copilot/chat/delete/route.ts
Line: 25:26

Comment:
**logic:** Delete without verifying ownership - any authenticated user can delete any chat

```suggestion
    // Verify chat ownership
    const chat = await db.select().from(copilotChats).where(eq(copilotChats.id, parsed.chatId)).limit(1)
    if (chat.length === 0 || chat[0].userId !== session.user.id) {
      return NextResponse.json({ success: false, error: 'Chat not found or unauthorized' }, { status: 404 })
    }

    // Delete the chat
    await db.delete(copilotChats).where(eq(copilotChats.id, parsed.chatId))
```

How can I resolve this? If you propose a fix, please make it concise.


logger.info('Chat deleted', { chatId: parsed.chatId })

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error deleting chat:', error)
return NextResponse.json({ success: false, error: 'Failed to delete chat' }, { status: 500 })
}
}
46 changes: 9 additions & 37 deletions apps/sim/app/api/copilot/chat/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,26 +214,15 @@ describe('Copilot Chat API Route', () => {
'x-api-key': 'test-sim-agent-key',
},
body: JSON.stringify({
messages: [
{
role: 'user',
content: 'Hello',
},
],
chatMessages: [
{
role: 'user',
content: 'Hello',
},
],
message: 'Hello',
workflowId: 'workflow-123',
userId: 'user-123',
stream: true,
streamToolCalls: true,
model: 'claude-4.5-sonnet',
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
version: '1.0.1',
version: '1.0.2',
chatId: 'chat-123',
}),
})
Expand Down Expand Up @@ -286,24 +275,15 @@ describe('Copilot Chat API Route', () => {
'http://localhost:8000/api/chat-completion-streaming',
expect.objectContaining({
body: JSON.stringify({
messages: [
{ role: 'user', content: 'Previous message' },
{ role: 'assistant', content: 'Previous response' },
{ role: 'user', content: 'New message' },
],
chatMessages: [
{ role: 'user', content: 'Previous message' },
{ role: 'assistant', content: 'Previous response' },
{ role: 'user', content: 'New message' },
],
message: 'New message',
workflowId: 'workflow-123',
userId: 'user-123',
stream: true,
streamToolCalls: true,
model: 'claude-4.5-sonnet',
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
version: '1.0.1',
version: '1.0.2',
chatId: 'chat-123',
}),
})
Expand Down Expand Up @@ -341,27 +321,20 @@ describe('Copilot Chat API Route', () => {
const { POST } = await import('@/app/api/copilot/chat/route')
await POST(req)

// Verify implicit feedback was included as system message
// Verify implicit feedback was included
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:8000/api/chat-completion-streaming',
expect.objectContaining({
body: JSON.stringify({
messages: [
{ role: 'system', content: 'User seems confused about the workflow' },
{ role: 'user', content: 'Hello' },
],
chatMessages: [
{ role: 'system', content: 'User seems confused about the workflow' },
{ role: 'user', content: 'Hello' },
],
message: 'Hello',
workflowId: 'workflow-123',
userId: 'user-123',
stream: true,
streamToolCalls: true,
model: 'claude-4.5-sonnet',
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
version: '1.0.1',
version: '1.0.2',
chatId: 'chat-123',
}),
})
Expand Down Expand Up @@ -444,16 +417,15 @@ describe('Copilot Chat API Route', () => {
'http://localhost:8000/api/chat-completion-streaming',
expect.objectContaining({
body: JSON.stringify({
messages: [{ role: 'user', content: 'What is this workflow?' }],
chatMessages: [{ role: 'user', content: 'What is this workflow?' }],
message: 'What is this workflow?',
workflowId: 'workflow-123',
userId: 'user-123',
stream: true,
streamToolCalls: true,
model: 'claude-4.5-sonnet',
mode: 'ask',
messageId: 'mock-uuid-1234-5678',
version: '1.0.1',
version: '1.0.2',
chatId: 'chat-123',
}),
})
Expand Down
24 changes: 9 additions & 15 deletions apps/sim/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const ChatMessageSchema = z.object({
'gpt-4.1',
'o3',
'claude-4-sonnet',
'claude-4.5-haiku',
'claude-4.5-sonnet',
'claude-4.1-opus',
])
Expand Down Expand Up @@ -356,18 +357,12 @@ export async function POST(req: NextRequest) {
}
}

// Determine provider and conversationId to use for this request
// Determine conversationId to use for this request
const effectiveConversationId =
(currentChat?.conversationId as string | undefined) || conversationId

// If we have a conversationId, only send the most recent user message; else send full history
const latestUserMessage =
[...messages].reverse().find((m) => m?.role === 'user') || messages[messages.length - 1]
const messagesForAgent = effectiveConversationId ? [latestUserMessage] : messages

const requestPayload = {
messages: messagesForAgent,
chatMessages: messages, // Full unfiltered messages array
message: message, // Just send the current user message text
workflowId,
userId: authenticatedUserId,
stream: stream,
Expand All @@ -382,14 +377,16 @@ export async function POST(req: NextRequest) {
...(session?.user?.name && { userName: session.user.name }),
...(agentContexts.length > 0 && { context: agentContexts }),
...(actualChatId ? { chatId: actualChatId } : {}),
...(processedFileContents.length > 0 && { fileAttachments: processedFileContents }),
}

try {
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
context: (requestPayload as any).context,
messagesCount: messagesForAgent.length,
chatMessagesCount: messages.length,
logger.info(`[${tracker.requestId}] About to call Sim Agent`, {
hasContext: agentContexts.length > 0,
contextCount: agentContexts.length,
hasConversationId: !!effectiveConversationId,
hasFileAttachments: processedFileContents.length > 0,
messageLength: message.length,
})
} catch {}

Expand Down Expand Up @@ -463,8 +460,6 @@ export async function POST(req: NextRequest) {
logger.debug(`[${tracker.requestId}] Sent initial chatId event to client`)
}

// Note: context_usage events are forwarded from sim-agent (which has accurate token counts)

// Start title generation in parallel if needed
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
generateChatTitle(message)
Expand Down Expand Up @@ -596,7 +591,6 @@ export async function POST(req: NextRequest) {
lastSafeDoneResponseId = responseIdFromDone
}
}
// Note: context_usage events are forwarded from sim-agent
break

case 'error':
Expand Down
45 changes: 45 additions & 0 deletions apps/sim/app/api/copilot/chat/update-title/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('UpdateChatTitleAPI')

const UpdateTitleSchema = z.object({
chatId: z.string(),
title: z.string(),
})

export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const parsed = UpdateTitleSchema.parse(body)

// Update the chat title
await db
.update(copilotChats)
.set({
title: parsed.title,
updatedAt: new Date(),
})
.where(eq(copilotChats.id, parsed.chatId))

logger.info('Chat title updated', { chatId: parsed.chatId, title: parsed.title })

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error updating chat title:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update chat title' },
{ status: 500 }
)
}
}
Loading