Skip to content

Commit cc0ace7

Browse files
authored
improvement(copilot): version update (#1689)
* Add exa to search online tool * Larger font size * Copilot UI improvements * Fix models options * Add haiku 4.5 to copilot * Model ui for haiku * Fix lint * Revert * Only allow one revert to message * Clear diff on revert * Fix welcome screen flash * Add focus onto the user input box when clicked * Fix grayout of new stream on old edit message * Lint * Make edit message submit smoother * Allow message sent while streaming * Revert popup improvements: gray out stuff below, show cursor on revert * Fix lint * Improve chat history dropdown * Improve get block metadata tool * Update update cost route * Fix env * Context usage endpoint * Make chat history scrollable * Fix lint * Copilot revert popup updates * Fix lint * Fix tests and lint * Add summary tool * Fix env.ts
1 parent de1ac9a commit cc0ace7

File tree

26 files changed

+3043
-1208
lines changed

26 files changed

+3043
-1208
lines changed

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 11 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,17 @@ import { checkInternalApiKey } from '@/lib/copilot/utils'
88
import { isBillingEnabled } from '@/lib/environment'
99
import { createLogger } from '@/lib/logs/console/logger'
1010
import { generateRequestId } from '@/lib/utils'
11-
import { calculateCost } from '@/providers/utils'
1211

1312
const logger = createLogger('BillingUpdateCostAPI')
1413

1514
const UpdateCostSchema = z.object({
1615
userId: z.string().min(1, 'User ID is required'),
17-
input: z.number().min(0, 'Input tokens must be a non-negative number'),
18-
output: z.number().min(0, 'Output tokens must be a non-negative number'),
19-
model: z.string().min(1, 'Model is required'),
20-
inputMultiplier: z.number().min(0),
21-
outputMultiplier: z.number().min(0),
16+
cost: z.number().min(0, 'Cost must be a non-negative number'),
2217
})
2318

2419
/**
2520
* POST /api/billing/update-cost
26-
* Update user cost based on token usage with internal API key auth
21+
* Update user cost with a pre-calculated cost value (internal API key auth required)
2722
*/
2823
export async function POST(req: NextRequest) {
2924
const requestId = generateRequestId()
@@ -77,45 +72,13 @@ export async function POST(req: NextRequest) {
7772
)
7873
}
7974

80-
const { userId, input, output, model, inputMultiplier, outputMultiplier } = validation.data
75+
const { userId, cost } = validation.data
8176

8277
logger.info(`[${requestId}] Processing cost update`, {
8378
userId,
84-
input,
85-
output,
86-
model,
87-
inputMultiplier,
88-
outputMultiplier,
79+
cost,
8980
})
9081

91-
const finalPromptTokens = input
92-
const finalCompletionTokens = output
93-
const totalTokens = input + output
94-
95-
// Calculate cost using provided multiplier (required)
96-
const costResult = calculateCost(
97-
model,
98-
finalPromptTokens,
99-
finalCompletionTokens,
100-
false,
101-
inputMultiplier,
102-
outputMultiplier
103-
)
104-
105-
logger.info(`[${requestId}] Cost calculation result`, {
106-
userId,
107-
model,
108-
promptTokens: finalPromptTokens,
109-
completionTokens: finalCompletionTokens,
110-
totalTokens: totalTokens,
111-
inputMultiplier,
112-
outputMultiplier,
113-
costResult,
114-
})
115-
116-
// Follow the exact same logic as ExecutionLogger.updateUserStats but with direct userId
117-
const costToStore = costResult.total // No additional multiplier needed since calculateCost already applied it
118-
11982
// Check if user stats record exists (same as ExecutionLogger)
12083
const userStatsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))
12184

@@ -128,25 +91,21 @@ export async function POST(req: NextRequest) {
12891
)
12992
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
13093
}
131-
// Update existing user stats record (same logic as ExecutionLogger)
94+
// Update existing user stats record
13295
const updateFields = {
133-
totalTokensUsed: sql`total_tokens_used + ${totalTokens}`,
134-
totalCost: sql`total_cost + ${costToStore}`,
135-
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
96+
totalCost: sql`total_cost + ${cost}`,
97+
currentPeriodCost: sql`current_period_cost + ${cost}`,
13698
// Copilot usage tracking increments
137-
totalCopilotCost: sql`total_copilot_cost + ${costToStore}`,
138-
totalCopilotTokens: sql`total_copilot_tokens + ${totalTokens}`,
99+
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
139100
totalCopilotCalls: sql`total_copilot_calls + 1`,
140-
totalApiCalls: sql`total_api_calls`,
141101
lastActive: new Date(),
142102
}
143103

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

146106
logger.info(`[${requestId}] Updated user stats record`, {
147107
userId,
148-
addedCost: costToStore,
149-
addedTokens: totalTokens,
108+
addedCost: cost,
150109
})
151110

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

164122
return NextResponse.json({
165123
success: true,
166124
data: {
167125
userId,
168-
input,
169-
output,
170-
totalTokens,
171-
model,
172-
cost: {
173-
input: costResult.input,
174-
output: costResult.output,
175-
total: costResult.total,
176-
},
177-
tokenBreakdown: {
178-
prompt: finalPromptTokens,
179-
completion: finalCompletionTokens,
180-
total: totalTokens,
181-
},
182-
pricing: costResult.pricing,
126+
cost,
183127
processedAt: new Date().toISOString(),
184128
requestId,
185129
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { db } from '@sim/db'
2+
import { copilotChats } from '@sim/db/schema'
3+
import { eq } from 'drizzle-orm'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { z } from 'zod'
6+
import { getSession } from '@/lib/auth'
7+
import { createLogger } from '@/lib/logs/console/logger'
8+
9+
const logger = createLogger('DeleteChatAPI')
10+
11+
const DeleteChatSchema = z.object({
12+
chatId: z.string(),
13+
})
14+
15+
export async function DELETE(request: NextRequest) {
16+
try {
17+
const session = await getSession()
18+
if (!session?.user?.id) {
19+
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
20+
}
21+
22+
const body = await request.json()
23+
const parsed = DeleteChatSchema.parse(body)
24+
25+
// Delete the chat
26+
await db.delete(copilotChats).where(eq(copilotChats.id, parsed.chatId))
27+
28+
logger.info('Chat deleted', { chatId: parsed.chatId })
29+
30+
return NextResponse.json({ success: true })
31+
} catch (error) {
32+
logger.error('Error deleting chat:', error)
33+
return NextResponse.json({ success: false, error: 'Failed to delete chat' }, { status: 500 })
34+
}
35+
}

apps/sim/app/api/copilot/chat/route.test.ts

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -214,26 +214,15 @@ describe('Copilot Chat API Route', () => {
214214
'x-api-key': 'test-sim-agent-key',
215215
},
216216
body: JSON.stringify({
217-
messages: [
218-
{
219-
role: 'user',
220-
content: 'Hello',
221-
},
222-
],
223-
chatMessages: [
224-
{
225-
role: 'user',
226-
content: 'Hello',
227-
},
228-
],
217+
message: 'Hello',
229218
workflowId: 'workflow-123',
230219
userId: 'user-123',
231220
stream: true,
232221
streamToolCalls: true,
233222
model: 'claude-4.5-sonnet',
234223
mode: 'agent',
235224
messageId: 'mock-uuid-1234-5678',
236-
version: '1.0.1',
225+
version: '1.0.2',
237226
chatId: 'chat-123',
238227
}),
239228
})
@@ -286,24 +275,15 @@ describe('Copilot Chat API Route', () => {
286275
'http://localhost:8000/api/chat-completion-streaming',
287276
expect.objectContaining({
288277
body: JSON.stringify({
289-
messages: [
290-
{ role: 'user', content: 'Previous message' },
291-
{ role: 'assistant', content: 'Previous response' },
292-
{ role: 'user', content: 'New message' },
293-
],
294-
chatMessages: [
295-
{ role: 'user', content: 'Previous message' },
296-
{ role: 'assistant', content: 'Previous response' },
297-
{ role: 'user', content: 'New message' },
298-
],
278+
message: 'New message',
299279
workflowId: 'workflow-123',
300280
userId: 'user-123',
301281
stream: true,
302282
streamToolCalls: true,
303283
model: 'claude-4.5-sonnet',
304284
mode: 'agent',
305285
messageId: 'mock-uuid-1234-5678',
306-
version: '1.0.1',
286+
version: '1.0.2',
307287
chatId: 'chat-123',
308288
}),
309289
})
@@ -341,27 +321,20 @@ describe('Copilot Chat API Route', () => {
341321
const { POST } = await import('@/app/api/copilot/chat/route')
342322
await POST(req)
343323

344-
// Verify implicit feedback was included as system message
324+
// Verify implicit feedback was included
345325
expect(global.fetch).toHaveBeenCalledWith(
346326
'http://localhost:8000/api/chat-completion-streaming',
347327
expect.objectContaining({
348328
body: JSON.stringify({
349-
messages: [
350-
{ role: 'system', content: 'User seems confused about the workflow' },
351-
{ role: 'user', content: 'Hello' },
352-
],
353-
chatMessages: [
354-
{ role: 'system', content: 'User seems confused about the workflow' },
355-
{ role: 'user', content: 'Hello' },
356-
],
329+
message: 'Hello',
357330
workflowId: 'workflow-123',
358331
userId: 'user-123',
359332
stream: true,
360333
streamToolCalls: true,
361334
model: 'claude-4.5-sonnet',
362335
mode: 'agent',
363336
messageId: 'mock-uuid-1234-5678',
364-
version: '1.0.1',
337+
version: '1.0.2',
365338
chatId: 'chat-123',
366339
}),
367340
})
@@ -444,16 +417,15 @@ describe('Copilot Chat API Route', () => {
444417
'http://localhost:8000/api/chat-completion-streaming',
445418
expect.objectContaining({
446419
body: JSON.stringify({
447-
messages: [{ role: 'user', content: 'What is this workflow?' }],
448-
chatMessages: [{ role: 'user', content: 'What is this workflow?' }],
420+
message: 'What is this workflow?',
449421
workflowId: 'workflow-123',
450422
userId: 'user-123',
451423
stream: true,
452424
streamToolCalls: true,
453425
model: 'claude-4.5-sonnet',
454426
mode: 'ask',
455427
messageId: 'mock-uuid-1234-5678',
456-
version: '1.0.1',
428+
version: '1.0.2',
457429
chatId: 'chat-123',
458430
}),
459431
})

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const ChatMessageSchema = z.object({
4848
'gpt-4.1',
4949
'o3',
5050
'claude-4-sonnet',
51+
'claude-4.5-haiku',
5152
'claude-4.5-sonnet',
5253
'claude-4.1-opus',
5354
])
@@ -356,18 +357,12 @@ export async function POST(req: NextRequest) {
356357
}
357358
}
358359

359-
// Determine provider and conversationId to use for this request
360+
// Determine conversationId to use for this request
360361
const effectiveConversationId =
361362
(currentChat?.conversationId as string | undefined) || conversationId
362363

363-
// If we have a conversationId, only send the most recent user message; else send full history
364-
const latestUserMessage =
365-
[...messages].reverse().find((m) => m?.role === 'user') || messages[messages.length - 1]
366-
const messagesForAgent = effectiveConversationId ? [latestUserMessage] : messages
367-
368364
const requestPayload = {
369-
messages: messagesForAgent,
370-
chatMessages: messages, // Full unfiltered messages array
365+
message: message, // Just send the current user message text
371366
workflowId,
372367
userId: authenticatedUserId,
373368
stream: stream,
@@ -382,14 +377,16 @@ export async function POST(req: NextRequest) {
382377
...(session?.user?.name && { userName: session.user.name }),
383378
...(agentContexts.length > 0 && { context: agentContexts }),
384379
...(actualChatId ? { chatId: actualChatId } : {}),
380+
...(processedFileContents.length > 0 && { fileAttachments: processedFileContents }),
385381
}
386382

387383
try {
388-
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
389-
context: (requestPayload as any).context,
390-
messagesCount: messagesForAgent.length,
391-
chatMessagesCount: messages.length,
384+
logger.info(`[${tracker.requestId}] About to call Sim Agent`, {
385+
hasContext: agentContexts.length > 0,
386+
contextCount: agentContexts.length,
392387
hasConversationId: !!effectiveConversationId,
388+
hasFileAttachments: processedFileContents.length > 0,
389+
messageLength: message.length,
393390
})
394391
} catch {}
395392

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

466-
// Note: context_usage events are forwarded from sim-agent (which has accurate token counts)
467-
468463
// Start title generation in parallel if needed
469464
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
470465
generateChatTitle(message)
@@ -596,7 +591,6 @@ export async function POST(req: NextRequest) {
596591
lastSafeDoneResponseId = responseIdFromDone
597592
}
598593
}
599-
// Note: context_usage events are forwarded from sim-agent
600594
break
601595

602596
case 'error':
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { db } from '@sim/db'
2+
import { copilotChats } from '@sim/db/schema'
3+
import { eq } from 'drizzle-orm'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { z } from 'zod'
6+
import { getSession } from '@/lib/auth'
7+
import { createLogger } from '@/lib/logs/console/logger'
8+
9+
const logger = createLogger('UpdateChatTitleAPI')
10+
11+
const UpdateTitleSchema = z.object({
12+
chatId: z.string(),
13+
title: z.string(),
14+
})
15+
16+
export async function POST(request: NextRequest) {
17+
try {
18+
const session = await getSession()
19+
if (!session?.user?.id) {
20+
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
21+
}
22+
23+
const body = await request.json()
24+
const parsed = UpdateTitleSchema.parse(body)
25+
26+
// Update the chat title
27+
await db
28+
.update(copilotChats)
29+
.set({
30+
title: parsed.title,
31+
updatedAt: new Date(),
32+
})
33+
.where(eq(copilotChats.id, parsed.chatId))
34+
35+
logger.info('Chat title updated', { chatId: parsed.chatId, title: parsed.title })
36+
37+
return NextResponse.json({ success: true })
38+
} catch (error) {
39+
logger.error('Error updating chat title:', error)
40+
return NextResponse.json(
41+
{ success: false, error: 'Failed to update chat title' },
42+
{ status: 500 }
43+
)
44+
}
45+
}

0 commit comments

Comments
 (0)