Skip to content

Commit 2370b87

Browse files
waleedlatif1claude
andcommitted
fix(search): keep a bulk lane bucket large enough for one valid reservation
At the smallest supported budgets the 90% share floored below a single request or token cost and locked the bulk lane. The bucket now holds at least the reservation it is asked for, while the refill rate keeps the share. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JBacX6HGVhPMUySuMfANwn
1 parent fe50edb commit 2370b87

2 files changed

Lines changed: 34 additions & 4 deletions

File tree

‎apps/sim/lib/core/rate-limiter/provider-admission.test.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
*/
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55

6-
const { consumeTokens, getCooldownUntil, setCooldownUntil } = vi.hoisted(() => ({
6+
const { consumeTokens, getCooldownUntil, setCooldownUntil, mockEnv } = vi.hoisted(() => ({
77
consumeTokens: vi.fn(),
88
getCooldownUntil: vi.fn(),
99
setCooldownUntil: vi.fn(),
10+
mockEnv: {} as Record<string, string | undefined>,
11+
}))
12+
vi.mock('@/lib/core/config/env', () => ({
13+
env: mockEnv,
14+
envNumber: (value: string | undefined, fallback: number) =>
15+
value === undefined ? fallback : Number(value),
1016
}))
1117
vi.mock('@/lib/core/rate-limiter/storage/factory', () => ({
1218
createStorageAdapter: () => ({
@@ -118,6 +124,26 @@ describe('provider admission', () => {
118124
expect(interactiveOptions.cooldownKeys).toEqual(bulkOptions.cooldownKeys)
119125
})
120126

127+
it('never shrinks a bulk bucket below one valid reservation', async () => {
128+
mockEnv.KB_CONFIG_EMBEDDING_REQUESTS_PER_MINUTE = '1'
129+
mockEnv.KB_CONFIG_EMBEDDING_TOKENS_PER_MINUTE = '100'
130+
try {
131+
await waitForProviderAdmission({ ...INPUT, inputTokens: 95, lane: 'bulk' })
132+
} finally {
133+
mockEnv.KB_CONFIG_EMBEDDING_REQUESTS_PER_MINUTE = undefined
134+
mockEnv.KB_CONFIG_EMBEDDING_TOKENS_PER_MINUTE = undefined
135+
}
136+
expect(consumeTokens.mock.calls[0][0]).toMatchObject([
137+
{ key: 'provider:embedding:openai:hashed-credential:tokens', config: { maxTokens: 100 } },
138+
{ key: 'provider:embedding:openai:hashed-credential:requests', config: { maxTokens: 1 } },
139+
{ key: 'provider:embedding:openai:hashed-credential:bulk:tokens', config: { maxTokens: 95 } },
140+
{
141+
key: 'provider:embedding:openai:hashed-credential:bulk:requests',
142+
config: { maxTokens: 1 },
143+
},
144+
])
145+
})
146+
121147
it('isolates another credential and does not impose token costs on OCR', async () => {
122148
await waitForProviderAdmission({
123149
...INPUT,

‎apps/sim/lib/core/rate-limiter/provider-admission.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,24 @@ export async function waitForProviderAdmission(input: ProviderAdmissionInput): P
8383
requestsPerMinute
8484
)
8585
const reservations: TokenBucketReservation[] = []
86+
/** A lane bucket always holds at least one valid reservation, so a tiny budget cannot lock the lane. */
8687
const reserveBuckets = (bucketKey: string, share: number) => {
8788
if (tokenBudget) {
88-
const maxTokens = Math.floor(tokenBudget.perMinute * share)
8989
reservations.push({
9090
key: `${bucketKey}:tokens`,
9191
cost: tokenBudget.cost,
92-
config: { maxTokens, refillRate: maxTokens / 60, refillIntervalMs: 1000 },
92+
config: {
93+
maxTokens: Math.max(tokenBudget.cost, Math.floor(tokenBudget.perMinute * share)),
94+
refillRate: (tokenBudget.perMinute * share) / 60,
95+
refillIntervalMs: 1000,
96+
},
9397
})
9498
}
9599
reservations.push({
96100
key: `${bucketKey}:requests`,
97101
cost: 1,
98102
config: {
99-
maxTokens: Math.floor(requestBurst * share),
103+
maxTokens: Math.max(1, Math.floor(requestBurst * share)),
100104
refillRate: (requestsPerMinute * share) / 60,
101105
refillIntervalMs: 1000,
102106
},

0 commit comments

Comments
 (0)