Skip to content

Commit 60a061e

Browse files
authored
v0.3.47: race condition fixes, store rehydration consolidation, other bugs
2 parents fce1423 + ab71fcf commit 60a061e

File tree

48 files changed

+6885
-523
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+6885
-523
lines changed

apps/docs/content/docs/copilot/index.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,31 @@ Copilot is your in-editor assistant that helps you build, understand, and improv
9191
>
9292
<div className="m-0 text-sm">Maximum reasoning for deep planning, debugging, and complex architectural changes.</div>
9393
</Card>
94-
</Cards>
94+
</Cards>
95+
96+
## Billing and Cost Calculation
97+
98+
### How Costs Are Calculated
99+
100+
Copilot usage is billed per token from the underlying LLM:
101+
102+
- **Input tokens**: billed at the provider's base rate (**at-cost**)
103+
- **Output tokens**: billed at **1.5×** the provider's base output rate
104+
105+
```javascript
106+
copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5)) / 1,000,000
107+
```
108+
109+
| Component | Rate Applied |
110+
|----------|----------------------|
111+
| Input | inputPrice |
112+
| Output | outputPrice × 1.5 |
113+
114+
<Callout type="warning">
115+
Pricing shown reflects rates as of September 4, 2025. Check provider documentation for current pricing.
116+
</Callout>
117+
118+
<Callout type="info">
119+
Model prices are per million tokens. The calculation divides by 1,000,000 to get the actual cost. See <a href="/execution/advanced#cost-calculation">Logging and Cost Calculation</a> for background and examples.
120+
</Callout>
121+

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const UpdateCostSchema = z.object({
1616
input: z.number().min(0, 'Input tokens must be a non-negative number'),
1717
output: z.number().min(0, 'Output tokens must be a non-negative number'),
1818
model: z.string().min(1, 'Model is required'),
19-
multiplier: z.number().min(0),
19+
inputMultiplier: z.number().min(0),
20+
outputMultiplier: z.number().min(0),
2021
})
2122

2223
/**
@@ -75,14 +76,15 @@ export async function POST(req: NextRequest) {
7576
)
7677
}
7778

78-
const { userId, input, output, model, multiplier } = validation.data
79+
const { userId, input, output, model, inputMultiplier, outputMultiplier } = validation.data
7980

8081
logger.info(`[${requestId}] Processing cost update`, {
8182
userId,
8283
input,
8384
output,
8485
model,
85-
multiplier,
86+
inputMultiplier,
87+
outputMultiplier,
8688
})
8789

8890
const finalPromptTokens = input
@@ -95,7 +97,8 @@ export async function POST(req: NextRequest) {
9597
finalPromptTokens,
9698
finalCompletionTokens,
9799
false,
98-
multiplier
100+
inputMultiplier,
101+
outputMultiplier
99102
)
100103

101104
logger.info(`[${requestId}] Cost calculation result`, {
@@ -104,7 +107,8 @@ export async function POST(req: NextRequest) {
104107
promptTokens: finalPromptTokens,
105108
completionTokens: finalCompletionTokens,
106109
totalTokens: totalTokens,
107-
multiplier,
110+
inputMultiplier,
111+
outputMultiplier,
108112
costResult,
109113
})
110114

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ describe('Copilot Chat API Route', () => {
226226
mode: 'agent',
227227
messageId: 'mock-uuid-1234-5678',
228228
depth: 0,
229+
chatId: 'chat-123',
229230
}),
230231
})
231232
)
@@ -289,6 +290,7 @@ describe('Copilot Chat API Route', () => {
289290
mode: 'agent',
290291
messageId: 'mock-uuid-1234-5678',
291292
depth: 0,
293+
chatId: 'chat-123',
292294
}),
293295
})
294296
)
@@ -341,6 +343,7 @@ describe('Copilot Chat API Route', () => {
341343
mode: 'agent',
342344
messageId: 'mock-uuid-1234-5678',
343345
depth: 0,
346+
chatId: 'chat-123',
344347
}),
345348
})
346349
)
@@ -430,6 +433,7 @@ describe('Copilot Chat API Route', () => {
430433
mode: 'ask',
431434
messageId: 'mock-uuid-1234-5678',
432435
depth: 0,
436+
chatId: 'chat-123',
433437
}),
434438
})
435439
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export async function POST(req: NextRequest) {
378378
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
379379
...(session?.user?.name && { userName: session.user.name }),
380380
...(agentContexts.length > 0 && { context: agentContexts }),
381+
...(actualChatId ? { chatId: actualChatId } : {}),
381382
}
382383

383384
try {

apps/sim/app/api/schedules/execute/route.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NextResponse } from 'next/server'
44
import { v4 as uuidv4 } from 'uuid'
55
import { z } from 'zod'
66
import { checkServerSideUsageLimits } from '@/lib/billing'
7+
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
78
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
89
import { createLogger } from '@/lib/logs/console/logger'
910
import { LoggingSession } from '@/lib/logs/execution/logging-session'
@@ -18,7 +19,7 @@ import { decryptSecret } from '@/lib/utils'
1819
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
1920
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
2021
import { db } from '@/db'
21-
import { subscription, userStats, workflow, workflowSchedule } from '@/db/schema'
22+
import { userStats, workflow, workflowSchedule } from '@/db/schema'
2223
import { Executor } from '@/executor'
2324
import { Serializer } from '@/serializer'
2425
import { RateLimiter } from '@/services/queue'
@@ -108,19 +109,15 @@ export async function GET() {
108109
continue
109110
}
110111

111-
// Check rate limits for scheduled execution
112-
const [subscriptionRecord] = await db
113-
.select({ plan: subscription.plan })
114-
.from(subscription)
115-
.where(eq(subscription.referenceId, workflowRecord.userId))
116-
.limit(1)
112+
// Check rate limits for scheduled execution (checks both personal and org subscriptions)
113+
const userSubscription = await getHighestPrioritySubscription(workflowRecord.userId)
117114

118-
const subscriptionPlan = (subscriptionRecord?.plan || 'free') as SubscriptionPlan
115+
const subscriptionPlan = (userSubscription?.plan || 'free') as SubscriptionPlan
119116

120117
const rateLimiter = new RateLimiter()
121-
const rateLimitCheck = await rateLimiter.checkRateLimit(
118+
const rateLimitCheck = await rateLimiter.checkRateLimitWithSubscription(
122119
workflowRecord.userId,
123-
subscriptionPlan,
120+
userSubscription,
124121
'schedule',
125122
false // schedules are always sync
126123
)

apps/sim/app/api/users/me/rate-limit/route.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
4+
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { createErrorResponse } from '@/app/api/workflows/utils'
67
import { db } from '@/db'
7-
import { apiKey as apiKeyTable, subscription } from '@/db/schema'
8+
import { apiKey as apiKeyTable } from '@/db/schema'
89
import { RateLimiter } from '@/services/queue'
910

1011
const logger = createLogger('RateLimitAPI')
@@ -33,31 +34,22 @@ export async function GET(request: NextRequest) {
3334
return createErrorResponse('Authentication required', 401)
3435
}
3536

36-
const [subscriptionRecord] = await db
37-
.select({ plan: subscription.plan })
38-
.from(subscription)
39-
.where(eq(subscription.referenceId, authenticatedUserId))
40-
.limit(1)
41-
42-
const subscriptionPlan = (subscriptionRecord?.plan || 'free') as
43-
| 'free'
44-
| 'pro'
45-
| 'team'
46-
| 'enterprise'
37+
// Get user subscription (checks both personal and org subscriptions)
38+
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
4739

4840
const rateLimiter = new RateLimiter()
4941
const isApiAuth = !session?.user?.id
5042
const triggerType = isApiAuth ? 'api' : 'manual'
5143

52-
const syncStatus = await rateLimiter.getRateLimitStatus(
44+
const syncStatus = await rateLimiter.getRateLimitStatusWithSubscription(
5345
authenticatedUserId,
54-
subscriptionPlan,
46+
userSubscription,
5547
triggerType,
5648
false
5749
)
58-
const asyncStatus = await rateLimiter.getRateLimitStatus(
50+
const asyncStatus = await rateLimiter.getRateLimitStatusWithSubscription(
5951
authenticatedUserId,
60-
subscriptionPlan,
52+
userSubscription,
6153
triggerType,
6254
true
6355
)

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { tasks } from '@trigger.dev/sdk'
22
import { and, eq } from 'drizzle-orm'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { checkServerSideUsageLimits } from '@/lib/billing'
5+
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
56
import { env, isTruthy } from '@/lib/env'
67
import { createLogger } from '@/lib/logs/console/logger'
78
import {
@@ -11,7 +12,7 @@ import {
1112
} from '@/lib/webhooks/utils'
1213
import { executeWebhookJob } from '@/background/webhook-execution'
1314
import { db } from '@/db'
14-
import { subscription, webhook, workflow } from '@/db/schema'
15+
import { webhook, workflow } from '@/db/schema'
1516
import { RateLimiter } from '@/services/queue'
1617
import type { SubscriptionPlan } from '@/services/queue/types'
1718

@@ -247,21 +248,19 @@ export async function POST(
247248
}
248249

249250
// --- PHASE 3: Rate limiting for webhook execution ---
251+
let isEnterprise = false
250252
try {
251-
// Get user subscription for rate limiting
252-
const [subscriptionRecord] = await db
253-
.select({ plan: subscription.plan })
254-
.from(subscription)
255-
.where(eq(subscription.referenceId, foundWorkflow.userId))
256-
.limit(1)
253+
// Get user subscription for rate limiting (checks both personal and org subscriptions)
254+
const userSubscription = await getHighestPrioritySubscription(foundWorkflow.userId)
257255

258-
const subscriptionPlan = (subscriptionRecord?.plan || 'free') as SubscriptionPlan
256+
const subscriptionPlan = (userSubscription?.plan || 'free') as SubscriptionPlan
257+
isEnterprise = subscriptionPlan === 'enterprise'
259258

260259
// Check async rate limits (webhooks are processed asynchronously)
261260
const rateLimiter = new RateLimiter()
262-
const rateLimitCheck = await rateLimiter.checkRateLimit(
261+
const rateLimitCheck = await rateLimiter.checkRateLimitWithSubscription(
263262
foundWorkflow.userId,
264-
subscriptionPlan,
263+
userSubscription,
265264
'webhook',
266265
true // isAsync = true for webhook execution
267266
)
@@ -333,7 +332,7 @@ export async function POST(
333332
// Continue processing - better to risk usage limit bypass than fail webhook
334333
}
335334

336-
// --- PHASE 5: Queue webhook execution (trigger.dev or direct based on env) ---
335+
// --- PHASE 5: Queue webhook execution (trigger.dev or direct based on plan/env) ---
337336
try {
338337
const payload = {
339338
webhookId: foundWebhook.id,
@@ -346,7 +345,9 @@ export async function POST(
346345
blockId: foundWebhook.blockId,
347346
}
348347

349-
const useTrigger = isTruthy(env.TRIGGER_DEV_ENABLED)
348+
// Enterprise users always execute directly, others check TRIGGER_DEV_ENABLED env
349+
// Note: isEnterprise was already determined during rate limiting phase
350+
const useTrigger = !isEnterprise && isTruthy(env.TRIGGER_DEV_ENABLED)
350351

351352
if (useTrigger) {
352353
const handle = await tasks.trigger('webhook-execution', payload)
@@ -358,8 +359,9 @@ export async function POST(
358359
void executeWebhookJob(payload).catch((error) => {
359360
logger.error(`[${requestId}] Direct webhook execution failed`, error)
360361
})
362+
const reason = isEnterprise ? 'Enterprise plan' : 'Trigger.dev disabled'
361363
logger.info(
362-
`[${requestId}] Queued direct webhook execution for ${foundWebhook.provider} webhook (Trigger.dev disabled)`
364+
`[${requestId}] Queued direct webhook execution for ${foundWebhook.provider} webhook (${reason})`
363365
)
364366
}
365367

apps/sim/app/api/workflows/[id]/duplicate/route.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createLogger } from '@/lib/logs/console/logger'
77
import { getUserEntityPermissions } from '@/lib/permissions/utils'
88
import { db } from '@/db'
99
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
10+
import type { Variable } from '@/stores/panel/variables/types'
1011
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
1112

1213
const logger = createLogger('WorkflowDuplicateAPI')
@@ -97,7 +98,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
9798
isDeployed: false,
9899
collaborators: [],
99100
runCount: 0,
100-
variables: source.variables || {},
101+
// Duplicate variables with new IDs and new workflowId
102+
variables: (() => {
103+
const sourceVars = (source.variables as Record<string, Variable>) || {}
104+
const remapped: Record<string, Variable> = {}
105+
for (const [, variable] of Object.entries(sourceVars) as [string, Variable][]) {
106+
const newVarId = crypto.randomUUID()
107+
remapped[newVarId] = {
108+
...variable,
109+
id: newVarId,
110+
workflowId: newWorkflowId,
111+
}
112+
}
113+
return remapped
114+
})(),
101115
isPublished: false,
102116
marketplaceData: null,
103117
})

apps/sim/app/api/workflows/[id]/execute/route.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ describe('Workflow Execution API Route', () => {
4646
remaining: 10,
4747
resetAt: new Date(),
4848
}),
49+
checkRateLimitWithSubscription: vi.fn().mockResolvedValue({
50+
allowed: true,
51+
remaining: 10,
52+
resetAt: new Date(),
53+
}),
4954
})),
5055
RateLimitError: class RateLimitError extends Error {
5156
constructor(
@@ -66,6 +71,13 @@ describe('Workflow Execution API Route', () => {
6671
}),
6772
}))
6873

74+
vi.doMock('@/lib/billing/core/subscription', () => ({
75+
getHighestPrioritySubscription: vi.fn().mockResolvedValue({
76+
plan: 'free',
77+
referenceId: 'user-id',
78+
}),
79+
}))
80+
6981
vi.doMock('@/db/schema', () => ({
7082
subscription: {
7183
plan: 'plan',

0 commit comments

Comments
 (0)