Skip to content

Commit a90777a

Browse files
author
Theodore Li
committed
Make billing actor id required for throttling
1 parent 2082bc4 commit a90777a

File tree

6 files changed

+90
-57
lines changed

6 files changed

+90
-57
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function useEditorSubblockLayout(
109109
// Check required feature if specified - declarative feature gating
110110
if (!isSubBlockFeatureEnabled(block)) return false
111111

112-
// Hide tool API key fields when hosted key is available
112+
// Hide tool API key fields when hosted
113113
if (isSubBlockHiddenByHostedKey(block)) return false
114114

115115
// Special handling for trigger-config type (legacy trigger configuration UI)

apps/sim/lib/core/rate-limiter/hosted-key/hosted-key-rate-limiter.test.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { loggerMock } from '@sim/testing'
22
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
3-
import { HostedKeyRateLimiter } from './hosted-key-rate-limiter'
4-
import type { CustomRateLimit, PerRequestRateLimit } from './types'
53
import type {
64
ConsumeResult,
75
RateLimitStorageAdapter,
86
TokenStatus,
97
} from '@/lib/core/rate-limiter/storage'
8+
import { HostedKeyRateLimiter } from './hosted-key-rate-limiter'
9+
import type { CustomRateLimit, PerRequestRateLimit } from './types'
1010

1111
vi.mock('@sim/logger', () => loggerMock)
1212

@@ -52,12 +52,24 @@ describe('HostedKeyRateLimiter', () => {
5252

5353
describe('acquireKey', () => {
5454
it('should return error when no keys are configured', async () => {
55-
delete process.env.EXA_API_KEY_COUNT
56-
delete process.env.EXA_API_KEY_1
57-
delete process.env.EXA_API_KEY_2
58-
delete process.env.EXA_API_KEY_3
55+
const allowedResult: ConsumeResult = {
56+
allowed: true,
57+
tokensRemaining: 9,
58+
resetAt: new Date(Date.now() + 60000),
59+
}
60+
mockAdapter.consumeTokens.mockResolvedValue(allowedResult)
61+
62+
process.env.EXA_API_KEY_COUNT = undefined
63+
process.env.EXA_API_KEY_1 = undefined
64+
process.env.EXA_API_KEY_2 = undefined
65+
process.env.EXA_API_KEY_3 = undefined
5966

60-
const result = await rateLimiter.acquireKey(testProvider, envKeyPrefix, perRequestRateLimit)
67+
const result = await rateLimiter.acquireKey(
68+
testProvider,
69+
envKeyPrefix,
70+
perRequestRateLimit,
71+
'workspace-1'
72+
)
6173

6274
expect(result.success).toBe(false)
6375
expect(result.error).toContain('No hosted keys configured')
@@ -143,24 +155,33 @@ describe('HostedKeyRateLimiter', () => {
143155
expect(r4.keyIndex).toBe(0) // Wraps back
144156
})
145157

146-
it('should work without billingActorId (no rate limiting)', async () => {
147-
const result = await rateLimiter.acquireKey(testProvider, envKeyPrefix, perRequestRateLimit)
148-
149-
expect(result.success).toBe(true)
150-
expect(result.key).toBe('test-key-1')
151-
expect(mockAdapter.consumeTokens).not.toHaveBeenCalled()
152-
})
153-
154158
it('should handle partial key availability', async () => {
155-
delete process.env.EXA_API_KEY_2
159+
const allowedResult: ConsumeResult = {
160+
allowed: true,
161+
tokensRemaining: 9,
162+
resetAt: new Date(Date.now() + 60000),
163+
}
164+
mockAdapter.consumeTokens.mockResolvedValue(allowedResult)
165+
166+
process.env.EXA_API_KEY_2 = undefined
156167

157-
const result = await rateLimiter.acquireKey(testProvider, envKeyPrefix, perRequestRateLimit)
168+
const result = await rateLimiter.acquireKey(
169+
testProvider,
170+
envKeyPrefix,
171+
perRequestRateLimit,
172+
'workspace-1'
173+
)
158174

159175
expect(result.success).toBe(true)
160176
expect(result.key).toBe('test-key-1')
161177
expect(result.envVarName).toBe('EXA_API_KEY_1')
162178

163-
const r2 = await rateLimiter.acquireKey(testProvider, envKeyPrefix, perRequestRateLimit)
179+
const r2 = await rateLimiter.acquireKey(
180+
testProvider,
181+
envKeyPrefix,
182+
perRequestRateLimit,
183+
'workspace-2'
184+
)
164185
expect(r2.keyIndex).toBe(2) // Skips missing key 1
165186
expect(r2.envVarName).toBe('EXA_API_KEY_3')
166187
})
@@ -256,14 +277,6 @@ describe('HostedKeyRateLimiter', () => {
256277
expect(result.error).toContain('tokens')
257278
})
258279

259-
it('should skip dimension pre-check without billingActorId', async () => {
260-
const result = await rateLimiter.acquireKey(testProvider, envKeyPrefix, customRateLimit)
261-
262-
expect(result.success).toBe(true)
263-
expect(mockAdapter.consumeTokens).not.toHaveBeenCalled()
264-
expect(mockAdapter.getTokenStatus).not.toHaveBeenCalled()
265-
})
266-
267280
it('should pre-check all dimensions and block on first depleted one', async () => {
268281
const multiDimensionConfig: CustomRateLimit = {
269282
mode: 'custom',

apps/sim/lib/core/rate-limiter/hosted-key/hosted-key-rate-limiter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import {
55
type TokenBucketConfig,
66
} from '@/lib/core/rate-limiter/storage'
77
import {
8-
DEFAULT_BURST_MULTIPLIER,
9-
DEFAULT_WINDOW_MS,
10-
toTokenBucketConfig,
118
type AcquireKeyResult,
129
type CustomRateLimit,
10+
DEFAULT_BURST_MULTIPLIER,
11+
DEFAULT_WINDOW_MS,
1312
type HostedKeyRateLimitConfig,
1413
type ReportUsageResult,
14+
toTokenBucketConfig,
1515
} from './types'
1616

1717
const logger = createLogger('HostedKeyRateLimiter')
@@ -21,7 +21,7 @@ const logger = createLogger('HostedKeyRateLimiter')
2121
* E.g. with `EXA_API_KEY_COUNT=5`, returns `['EXA_API_KEY_1', ..., 'EXA_API_KEY_5']`.
2222
*/
2323
function resolveEnvKeys(prefix: string): string[] {
24-
const count = parseInt(process.env[`${prefix}_COUNT`] || '0', 10)
24+
const count = Number.parseInt(process.env[`${prefix}_COUNT`] || '0', 10)
2525
const names: string[] = []
2626
for (let i = 1; i <= count; i++) {
2727
names.push(`${prefix}_${i}`)
@@ -192,9 +192,9 @@ export class HostedKeyRateLimiter {
192192
provider: string,
193193
envKeyPrefix: string,
194194
config: HostedKeyRateLimitConfig,
195-
billingActorId?: string
195+
billingActorId: string
196196
): Promise<AcquireKeyResult> {
197-
if (billingActorId && config.requestsPerMinute) {
197+
if (config.requestsPerMinute) {
198198
const rateLimitResult = await this.checkActorRateLimit(provider, billingActorId, config)
199199
if (rateLimitResult) {
200200
return {
@@ -206,7 +206,7 @@ export class HostedKeyRateLimiter {
206206
}
207207
}
208208

209-
if (billingActorId && config.mode === 'custom' && config.dimensions.length > 0) {
209+
if (config.mode === 'custom' && config.dimensions.length > 0) {
210210
const dimensionResult = await this.preCheckDimensions(provider, billingActorId, config)
211211
if (dimensionResult) {
212212
return {

apps/sim/lib/core/rate-limiter/hosted-key/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ export {
44
resetHostedKeyRateLimiter,
55
} from './hosted-key-rate-limiter'
66
export {
7-
DEFAULT_BURST_MULTIPLIER,
8-
DEFAULT_WINDOW_MS,
9-
toTokenBucketConfig,
107
type AcquireKeyResult,
118
type CustomRateLimit,
9+
DEFAULT_BURST_MULTIPLIER,
10+
DEFAULT_WINDOW_MS,
1211
type HostedKeyRateLimitConfig,
1312
type HostedKeyRateLimitMode,
1413
type PerRequestRateLimit,
1514
type RateLimitDimension,
1615
type ReportUsageResult,
16+
toTokenBucketConfig,
1717
} from './types'
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
1-
export type { RateLimitResult, RateLimitStatus } from './rate-limiter'
2-
export { RateLimiter } from './rate-limiter'
3-
export type { RateLimitStorageAdapter, TokenBucketConfig } from './storage'
4-
export type { RateLimitConfig, SubscriptionPlan, TriggerType } from './types'
5-
export { RATE_LIMITS, RateLimitError } from './types'
6-
export {
7-
getHostedKeyRateLimiter,
8-
HostedKeyRateLimiter,
9-
resetHostedKeyRateLimiter,
10-
} from './hosted-key'
111
export {
12-
DEFAULT_BURST_MULTIPLIER,
13-
DEFAULT_WINDOW_MS,
14-
toTokenBucketConfig,
152
type AcquireKeyResult,
163
type CustomRateLimit,
4+
DEFAULT_BURST_MULTIPLIER,
5+
DEFAULT_WINDOW_MS,
6+
getHostedKeyRateLimiter,
177
type HostedKeyRateLimitConfig,
8+
HostedKeyRateLimiter,
189
type HostedKeyRateLimitMode,
1910
type PerRequestRateLimit,
2011
type RateLimitDimension,
2112
type ReportUsageResult,
13+
resetHostedKeyRateLimiter,
14+
toTokenBucketConfig,
2215
} from './hosted-key'
16+
export type { RateLimitResult, RateLimitStatus } from './rate-limiter'
17+
export { RateLimiter } from './rate-limiter'
18+
export type { RateLimitStorageAdapter, TokenBucketConfig } from './storage'
19+
export type { RateLimitConfig, SubscriptionPlan, TriggerType } from './types'
20+
export { RATE_LIMITS, RateLimitError } from './types'

apps/sim/tools/index.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,17 @@ async function injectHostedKeyIfNeeded(
8080
const provider = byokProviderId || tool.id
8181
const billingActorId = executionContext?.workspaceId
8282

83-
const acquireResult = await rateLimiter.acquireKey(provider, envKeyPrefix, rateLimit, billingActorId)
83+
if (!billingActorId) {
84+
logger.error(`[${requestId}] No workspace ID available for hosted key rate limiting`)
85+
return { isUsingHostedKey: false }
86+
}
87+
88+
const acquireResult = await rateLimiter.acquireKey(
89+
provider,
90+
envKeyPrefix,
91+
rateLimit,
92+
billingActorId
93+
)
8494

8595
if (!acquireResult.success && acquireResult.billingActorRateLimited) {
8696
logger.warn(`[${requestId}] Billing actor ${billingActorId} rate limited for ${tool.id}`, {
@@ -304,10 +314,10 @@ async function reportCustomDimensionUsage(
304314

305315
for (const dim of result.dimensions) {
306316
if (!dim.allowed) {
307-
logger.warn(
308-
`[${requestId}] Dimension ${dim.name} overdrawn after ${tool.id} execution`,
309-
{ consumed: dim.consumed, tokensRemaining: dim.tokensRemaining }
310-
)
317+
logger.warn(`[${requestId}] Dimension ${dim.name} overdrawn after ${tool.id} execution`, {
318+
consumed: dim.consumed,
319+
tokensRemaining: dim.tokensRemaining,
320+
})
311321
}
312322
}
313323
} catch (error) {
@@ -736,7 +746,13 @@ export async function executeTool(
736746

737747
// Post-execution: report custom dimension usage and calculate cost
738748
if (hostedKeyInfo.isUsingHostedKey && finalResult.success) {
739-
await reportCustomDimensionUsage(tool, contextParams, finalResult.output, executionContext, requestId)
749+
await reportCustomDimensionUsage(
750+
tool,
751+
contextParams,
752+
finalResult.output,
753+
executionContext,
754+
requestId
755+
)
740756

741757
const { cost: hostedKeyCost, metadata } = await processHostedKeyCost(
742758
tool,
@@ -804,7 +820,13 @@ export async function executeTool(
804820

805821
// Post-execution: report custom dimension usage and calculate cost
806822
if (hostedKeyInfo.isUsingHostedKey && finalResult.success) {
807-
await reportCustomDimensionUsage(tool, contextParams, finalResult.output, executionContext, requestId)
823+
await reportCustomDimensionUsage(
824+
tool,
825+
contextParams,
826+
finalResult.output,
827+
executionContext,
828+
requestId
829+
)
808830

809831
const { cost: hostedKeyCost, metadata } = await processHostedKeyCost(
810832
tool,

0 commit comments

Comments
 (0)