Skip to content

Commit 455b1f8

Browse files
committed
refactor(billing): retire protocol rollout flags
1 parent af7a12c commit 455b1f8

13 files changed

Lines changed: 100 additions & 200 deletions

File tree

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

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ const ATTRIBUTION = {
105105
payerSubscription: null,
106106
}
107107

108-
const OLD_GO_HOSTED_UPDATE_COST_BODY = {
108+
const SELF_HOSTED_UPDATE_COST_BODY = {
109109
userId: 'user-1',
110110
cost: 0.4662453,
111111
model: 'claude-opus-4.8',
@@ -117,11 +117,11 @@ const OLD_GO_HOSTED_UPDATE_COST_BODY = {
117117
} as const
118118

119119
const EXPLICIT_LEGACY_HOSTED_UPDATE_COST_BODY = {
120-
...OLD_GO_HOSTED_UPDATE_COST_BODY,
120+
...SELF_HOSTED_UPDATE_COST_BODY,
121121
idempotencyKey: 'explicit-legacy-billing-id',
122122
} as const
123123

124-
const OLD_GO_WORKSPACELESS_UPDATE_COST_BODY = {
124+
const SELF_HOSTED_WORKSPACELESS_UPDATE_COST_BODY = {
125125
userId: 'user-1',
126126
cost: 0.5,
127127
model: 'gpt',
@@ -131,8 +131,8 @@ const OLD_GO_WORKSPACELESS_UPDATE_COST_BODY = {
131131
idempotencyKey: 'random-old-go-direct-billing-id',
132132
} as const
133133

134-
const OLD_GO_OPAQUE_WORKSPACE_UPDATE_COST_BODY = {
135-
...OLD_GO_WORKSPACELESS_UPDATE_COST_BODY,
134+
const SELF_HOSTED_OPAQUE_WORKSPACE_UPDATE_COST_BODY = {
135+
...SELF_HOSTED_WORKSPACELESS_UPDATE_COST_BODY,
136136
workspaceId: 'local-self-hosted-workspace',
137137
} as const
138138

@@ -148,7 +148,7 @@ const KEYLESS_UPDATE_COST_BODY = {
148148
describe('POST /api/billing/update-cost — workspaceId attribution', () => {
149149
beforeEach(() => {
150150
vi.clearAllMocks()
151-
setEnvFlags({ isBillingEnabled: true, isCopilotBillingProtocolRequired: false })
151+
setEnvFlags({ isBillingEnabled: true, isHosted: false })
152152
mockCheckInternalApiKey.mockReturnValue({ success: true })
153153
mockRecordCumulativeUsage.mockResolvedValue({ billed: true, delta: 0.5, total: 0.5 })
154154
mockCheckAndBillOverageThreshold.mockResolvedValue(undefined)
@@ -187,7 +187,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
187187
expect(mockRecordCumulativeUsage).not.toHaveBeenCalled()
188188
})
189189

190-
it('returns no-op success for old markerless Go when billing is disabled', async () => {
190+
it('returns no-op success for markerless local self-hosted Go when billing is disabled', async () => {
191191
setEnvFlags({ isBillingEnabled: false })
192192

193193
const res = await POST(
@@ -213,13 +213,13 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
213213
expect(mockRecordCumulativeUsage).not.toHaveBeenCalled()
214214
})
215215

216-
it('keeps the exact old-Go callback bodies contract-compatible', () => {
217-
expect(billingUpdateCostBodySchema.safeParse(OLD_GO_HOSTED_UPDATE_COST_BODY).success).toBe(true)
216+
it('keeps local self-hosted callback bodies contract-compatible', () => {
217+
expect(billingUpdateCostBodySchema.safeParse(SELF_HOSTED_UPDATE_COST_BODY).success).toBe(true)
218218
expect(
219-
billingUpdateCostBodySchema.safeParse(OLD_GO_WORKSPACELESS_UPDATE_COST_BODY).success
219+
billingUpdateCostBodySchema.safeParse(SELF_HOSTED_WORKSPACELESS_UPDATE_COST_BODY).success
220220
).toBe(true)
221221
expect(
222-
billingUpdateCostBodySchema.safeParse(OLD_GO_OPAQUE_WORKSPACE_UPDATE_COST_BODY).success
222+
billingUpdateCostBodySchema.safeParse(SELF_HOSTED_OPAQUE_WORKSPACE_UPDATE_COST_BODY).success
223223
).toBe(true)
224224
})
225225

@@ -234,9 +234,9 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
234234
expect(mockCheckAndBillPayerOverageThreshold).not.toHaveBeenCalled()
235235
})
236236

237-
it('bills the routed workspace payer for the exact markerless hosted callback', async () => {
237+
it('bills the routed workspace payer for a markerless self-hosted callback', async () => {
238238
const res = await POST(
239-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
239+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
240240
)
241241

242242
expect(res.status).toBe(200)
@@ -277,7 +277,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
277277
)
278278

279279
const res = await POST(
280-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
280+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
281281
)
282282

283283
expect(res.status).toBe(503)
@@ -290,10 +290,10 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
290290
})
291291
})
292292

293-
it('rejects markerless callbacks only when protocol-required is explicitly enabled', async () => {
294-
setEnvFlags({ isCopilotBillingProtocolRequired: true })
293+
it('rejects markerless callbacks on hosted Sim', async () => {
294+
setEnvFlags({ isHosted: true })
295295
const res = await POST(
296-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
296+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
297297
)
298298

299299
expect(res.status).toBe(400)
@@ -303,7 +303,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
303303

304304
it('does not let markerless legacy traffic fall through to a modern attribution envelope', async () => {
305305
const res = await POST(
306-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, {
306+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, {
307307
'x-api-key': 'internal',
308308
'x-sim-billing-attribution': 'serialized-attribution',
309309
})
@@ -316,7 +316,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
316316
})
317317

318318
it('rejects explicitly labeled legacy callbacks without admission attribution', async () => {
319-
setEnvFlags({ isCopilotBillingProtocolRequired: true })
319+
setEnvFlags({ isHosted: true })
320320
const res = await POST(
321321
createMockRequest('POST', EXPLICIT_LEGACY_HOSTED_UPDATE_COST_BODY, {
322322
'x-api-key': 'internal',
@@ -331,7 +331,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
331331
})
332332

333333
it('bills explicitly labeled legacy callbacks from their admission attribution', async () => {
334-
setEnvFlags({ isCopilotBillingProtocolRequired: true })
334+
setEnvFlags({ isHosted: true })
335335
const res = await POST(
336336
createMockRequest('POST', EXPLICIT_LEGACY_HOSTED_UPDATE_COST_BODY, {
337337
'x-api-key': 'internal',
@@ -507,13 +507,13 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
507507
expect(mockRecordCumulativeUsage).not.toHaveBeenCalled()
508508
})
509509

510-
it('does not expose context-mismatch 409 to markerless old Go', async () => {
510+
it('does not expose context-mismatch 409 to markerless self-hosted Go', async () => {
511511
mockRecordCumulativeUsage.mockRejectedValue(
512512
new MockCumulativeUsageContextMismatchError('different billing context')
513513
)
514514

515515
const res = await POST(
516-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
516+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
517517
)
518518

519519
expect(res.status).toBe(500)
@@ -525,10 +525,10 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
525525
expect(mockCheckAndBillPayerOverageThreshold).not.toHaveBeenCalled()
526526
})
527527

528-
it('preserves old Go duplicate-compatible 409 semantics for markerless callbacks', async () => {
528+
it('preserves duplicate-compatible 409 semantics for markerless self-hosted callbacks', async () => {
529529
mockRecordCumulativeUsage.mockResolvedValue({ billed: false, delta: 0, total: 0.4662453 })
530530
const res = await POST(
531-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
531+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
532532
)
533533
expect(res.status).toBe(409)
534534
await expect(res.json()).resolves.toMatchObject({
@@ -558,7 +558,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
558558
.mockRejectedValueOnce(new Error('Threshold settlement unavailable'))
559559
.mockResolvedValueOnce(undefined)
560560
const createRequest = () =>
561-
createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
561+
createMockRequest('POST', SELF_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
562562

563563
const firstResponse = await POST(createRequest())
564564
const retryResponse = await POST(createRequest())
@@ -669,9 +669,11 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
669669
)
670670
})
671671

672-
it('preserves account-ledger ownership for the exact workspace-less old-Go callback', async () => {
672+
it('preserves account-ledger ownership for a workspace-less self-hosted callback', async () => {
673673
const res = await POST(
674-
createMockRequest('POST', OLD_GO_WORKSPACELESS_UPDATE_COST_BODY, { 'x-api-key': 'internal' })
674+
createMockRequest('POST', SELF_HOSTED_WORKSPACELESS_UPDATE_COST_BODY, {
675+
'x-api-key': 'internal',
676+
})
675677
)
676678

677679
expect(res.status).toBe(200)
@@ -691,7 +693,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => {
691693
it('preserves account-ledger ownership for an opaque direct legacy workspace', async () => {
692694
mockResolveLegacyV0BillingAttribution.mockResolvedValueOnce(null)
693695
const res = await POST(
694-
createMockRequest('POST', OLD_GO_OPAQUE_WORKSPACE_UPDATE_COST_BODY, {
696+
createMockRequest('POST', SELF_HOSTED_OPAQUE_WORKSPACE_UPDATE_COST_BODY, {
695697
'x-api-key': 'internal',
696698
})
697699
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
3434
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
3535
import { checkInternalApiKey } from '@/lib/copilot/request/http'
3636
import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
37-
import { isBillingEnabled, isCopilotBillingProtocolRequired } from '@/lib/core/config/env-flags'
37+
import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags'
3838
import { generateRequestId } from '@/lib/core/utils/request'
3939
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
4040

@@ -163,7 +163,7 @@ async function updateCostInner(req: NextRequest, span: Span): Promise<NextRespon
163163
const suppliedAttributionHeader = parsed.data.headers?.[BILLING_ATTRIBUTION_HEADER]
164164
const suppliedAccountDecisionHeader = parsed.data.headers?.[BILLING_ACCOUNT_DECISION_HEADER]
165165
const isMarkerlessLegacy = requestedProtocol === undefined
166-
if (isMarkerlessLegacy && isCopilotBillingProtocolRequired) {
166+
if (isMarkerlessLegacy && isHosted) {
167167
return invalidBillingProtocolResponse(requestId, span)
168168
}
169169
const protocol: CopilotBillingProtocol = requestedProtocol ?? COPILOT_BILLING_PROTOCOL.legacy

apps/sim/app/api/copilot/api-keys/validate/route.test.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,16 @@ const ACCOUNT_BILLING_DECISION = {
6868
},
6969
}
7070

71-
const OLD_GO_HOSTED_VALIDATE_BODY = {
71+
const SELF_HOSTED_VALIDATE_BODY = {
7272
userId: 'user-1',
7373
workspaceId: 'ws-1',
7474
} as const
7575

76-
const OLD_GO_WORKSPACELESS_VALIDATE_BODY = {
76+
const SELF_HOSTED_WORKSPACELESS_VALIDATE_BODY = {
7777
userId: 'user-1',
7878
} as const
7979

80-
const OLD_GO_OPAQUE_WORKSPACE_VALIDATE_BODY = {
80+
const SELF_HOSTED_OPAQUE_WORKSPACE_VALIDATE_BODY = {
8181
userId: 'user-1',
8282
workspaceId: 'local-self-hosted-workspace',
8383
} as const
@@ -151,7 +151,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
151151
beforeEach(() => {
152152
vi.clearAllMocks()
153153
resetDbChainMock()
154-
setEnvFlags({ isCopilotBillingProtocolRequired: false })
154+
setEnvFlags({ isHosted: false })
155155
mockCheckInternalApiKey.mockReturnValue({ success: true })
156156
queueTableRows(schemaMock.user, [{ id: 'user-1' }])
157157
mockResolveBillingAttribution.mockResolvedValue(ATTRIBUTION)
@@ -199,25 +199,23 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
199199
resetDbChainMock()
200200
})
201201

202-
it('keeps the exact old-Go validate bodies contract-compatible', () => {
203-
expect(validateCopilotApiKeyBodySchema.safeParse(OLD_GO_HOSTED_VALIDATE_BODY).success).toBe(
204-
true
205-
)
202+
it('keeps local self-hosted validate bodies contract-compatible', () => {
203+
expect(validateCopilotApiKeyBodySchema.safeParse(SELF_HOSTED_VALIDATE_BODY).success).toBe(true)
206204
expect(
207-
validateCopilotApiKeyBodySchema.safeParse(OLD_GO_WORKSPACELESS_VALIDATE_BODY).success
205+
validateCopilotApiKeyBodySchema.safeParse(SELF_HOSTED_WORKSPACELESS_VALIDATE_BODY).success
208206
).toBe(true)
209207
expect(
210-
validateCopilotApiKeyBodySchema.safeParse(OLD_GO_OPAQUE_WORKSPACE_VALIDATE_BODY).success
208+
validateCopilotApiKeyBodySchema.safeParse(SELF_HOSTED_OPAQUE_WORKSPACE_VALIDATE_BODY).success
211209
).toBe(true)
212210
})
213211

214-
it('checks the routed workspace payer pool for exact markerless hosted admission', async () => {
212+
it('checks the routed workspace payer pool for markerless self-hosted admission', async () => {
215213
mockCheckAttributedUsageLimits.mockResolvedValue({
216214
isExceeded: true,
217215
payerUsage: { currentUsage: 200, limit: 100 },
218216
scope: 'payer',
219217
})
220-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
218+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
221219

222220
expect(res.status).toBe(402)
223221
expect(mockResolveLegacyV0BillingAttribution).toHaveBeenCalledWith({
@@ -228,20 +226,20 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
228226
expect(mockCheckServerSideUsageLimits).not.toHaveBeenCalled()
229227
})
230228

231-
it('preserves the exact actor member cap for markerless hosted admission', async () => {
229+
it('preserves the actor member cap for markerless self-hosted admission', async () => {
232230
mockCheckAttributedUsageLimits.mockResolvedValue({
233231
isExceeded: true,
234232
payerUsage: { currentUsage: 20, limit: 100 },
235233
memberUsage: { currentUsage: 5, limit: 4 },
236234
scope: 'member',
237235
})
238-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
236+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
239237

240238
expect(res.status).toBe(402)
241239
})
242240

243-
it('accepts the exact markerless hosted body under its routed workspace limits', async () => {
244-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
241+
it('accepts markerless self-hosted admission under its routed workspace limits', async () => {
242+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
245243

246244
expect(res.status).toBe(200)
247245
expect(res.headers.get('x-sim-billing-attribution')).toBeNull()
@@ -251,22 +249,22 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
251249
it('returns whether the validated key owner has an enterprise account', async () => {
252250
mockIsEnterprisePlan.mockResolvedValueOnce(true)
253251

254-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
252+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
255253

256254
expect(res.status).toBe(200)
257255
await expect(res.json()).resolves.toEqual({ isEnterprise: true })
258256
expect(mockIsEnterprisePlan).toHaveBeenCalledWith('user-1')
259257
})
260258

261259
it('returns false when the validated key owner is not enterprise', async () => {
262-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
260+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
263261

264262
expect(res.status).toBe(200)
265263
await expect(res.json()).resolves.toEqual({ isEnterprise: false })
266264
})
267265

268-
it('preserves account admission for the exact workspace-less old-Go body', async () => {
269-
const res = await POST(request(OLD_GO_WORKSPACELESS_VALIDATE_BODY))
266+
it('preserves account admission for a workspace-less self-hosted body', async () => {
267+
const res = await POST(request(SELF_HOSTED_WORKSPACELESS_VALIDATE_BODY))
270268

271269
expect(res.status).toBe(200)
272270
expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith('user-1')
@@ -276,26 +274,26 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
276274

277275
it('preserves account admission for an opaque direct legacy workspace', async () => {
278276
mockResolveLegacyV0BillingAttribution.mockResolvedValueOnce(null)
279-
const res = await POST(request(OLD_GO_OPAQUE_WORKSPACE_VALIDATE_BODY))
277+
const res = await POST(request(SELF_HOSTED_OPAQUE_WORKSPACE_VALIDATE_BODY))
280278

281279
expect(res.status).toBe(200)
282280
expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith('user-1')
283281
expect(mockCheckAttributedUsageLimits).not.toHaveBeenCalled()
284282
})
285283

286-
it('rejects markerless admission only when protocol-required is explicitly enabled', async () => {
287-
setEnvFlags({ isCopilotBillingProtocolRequired: true })
288-
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
284+
it('rejects markerless admission on hosted Sim', async () => {
285+
setEnvFlags({ isHosted: true })
286+
const res = await POST(request(SELF_HOSTED_VALIDATE_BODY))
289287

290288
expect(res.status).toBe(400)
291289
expect(mockCheckServerSideUsageLimits).not.toHaveBeenCalled()
292290
expect(mockCheckAttributedUsageLimits).not.toHaveBeenCalled()
293291
})
294292

295-
it('allows explicitly labeled legacy requests when markerless traffic is disabled', async () => {
296-
setEnvFlags({ isCopilotBillingProtocolRequired: true })
293+
it('allows explicitly labeled legacy requests on hosted Sim', async () => {
294+
setEnvFlags({ isHosted: true })
297295
const res = await POST(
298-
request(OLD_GO_HOSTED_VALIDATE_BODY, { 'x-sim-billing-protocol': 'legacy-v0' })
296+
request(SELF_HOSTED_VALIDATE_BODY, { 'x-sim-billing-protocol': 'legacy-v0' })
299297
)
300298

301299
expect(res.status).toBe(200)

apps/sim/app/api/copilot/api-keys/validate/route.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
3232
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
3333
import { checkInternalApiKey } from '@/lib/copilot/request/http'
3434
import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
35-
import { isCopilotBillingProtocolRequired } from '@/lib/core/config/env-flags'
35+
import { isHosted } from '@/lib/core/config/env-flags'
3636
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3737

3838
const logger = createLogger('CopilotApiKeysValidate')
@@ -63,12 +63,12 @@ type AdmissionBillingDecision =
6363
/**
6464
* Resolves admission against the versioned Go callback protocol.
6565
*
66-
* Markerless old-Go admission is explicitly legacy-v0. A locally resolvable
66+
* Markerless self-hosted admission is legacy-v0. A locally resolvable
6767
* workspace selects its current payer; an absent or opaque workspace preserves
68-
* account billing. Because old Go cannot return admission material, this
69-
* mutable resolution is repeated at callback time. Direct-v1 remains scoped
70-
* only to the authenticated Chat/Copilot key owner's hosted account, and
71-
* attributed-v1 never falls back from its immutable envelope.
68+
* account billing. This mutable resolution is repeated at callback time for
69+
* local self-hosted compatibility. Direct-v1 remains scoped only to the
70+
* authenticated Chat/Copilot key owner's hosted account, and attributed-v1
71+
* never falls back from its immutable envelope.
7272
*/
7373
async function resolveAdmissionBillingDecision(
7474
req: NextRequest,
@@ -117,7 +117,7 @@ async function resolveAdmissionBillingDecision(
117117
return invalidBillingProtocolResponse()
118118
}
119119

120-
if (protocol === undefined && isCopilotBillingProtocolRequired) {
120+
if (protocol === undefined && isHosted) {
121121
return invalidBillingProtocolResponse()
122122
}
123123

0 commit comments

Comments
 (0)