refactor(cloud-agent-next): gate billing by model classification#4465
refactor(cloud-agent-next): gate billing by model classification#4465eshurakov wants to merge 3 commits into
Conversation
Replace the blanket $1 minimum + x-skip-balance-check escape hatch with per-model billing classification. The worker balance middleware now asks a new POST /api/profile/cloud-agent-admission endpoint, which classifies the model as free/byok/balance-required and returns balance only when needed. free/byok sessions are admitted regardless of balance; balance-required sessions require a positive balance (previously a $1 floor). Drops the skip-balance-check header across the client, App Builder, Discord/Slack bot, code-review orchestrator, and webhook-agent-ingest. Refines frontend eligibility to distinguish zero-credit from low-funds states.
|
|
||
| const client = createAppBuilderCloudAgentNextClient(authToken); | ||
| const client = createCloudAgentNextClient(authToken); | ||
| const { cloudAgentSessionId } = await client.prepareSession({ |
There was a problem hiding this comment.
WARNING: InsufficientCreditsError from prepareSession is downgraded to a generic 500
App Builder no longer bypasses the balance gate (the x-skip-balance-check escape hatch and createAppBuilderCloudAgentNextClient were removed in this PR), so client.prepareSession(...) can now throw InsufficientCreditsError for balance-required models. The enclosing catch block (a few lines below) does code: error instanceof TRPCError ? error.code : 'INTERNAL_SERVER_ERROR'. InsufficientCreditsError is a plain Error, not a TRPCError, so this always downgrades it to INTERNAL_SERVER_ERROR instead of the intended PAYMENT_REQUIRED/402. Compare with cloud-agent-next-router.ts and security-agent/services/analysis-service.ts, which both call rethrowAsPaymentRequired/explicitly check InsufficientCreditsError to preserve 402 semantics. App Builder's own frontend (components/app-builder/project-manager/logging.ts) checks for code === 'PAYMENT_REQUIRED' || httpStatus === 402 to show a friendly "add credits" message, so zero-balance users hitting a platform-billed model here will instead see a generic internal-error UI.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
|
||
| const client = createAppBuilderCloudAgentNextClient(authToken); | ||
| const client = createCloudAgentNextClient(authToken); | ||
| const result = await client.sendMessage({ |
There was a problem hiding this comment.
WARNING: Same InsufficientCreditsError handling gap for sendMessage
client.sendMessage(...) can now also throw InsufficientCreditsError since App Builder no longer bypasses the balance gate for balance-required models. sendToExistingCloudAgentNextSession has no try/catch at all, so the error propagates raw up through the caller (e.g. sendMessage tRPC procedure) as an unstructured error instead of the intended PAYMENT_REQUIRED/402 that other Cloud Agent Next call sites (cloud-agent-next-router.ts, security-agent/services/analysis-service.ts) produce via rethrowAsPaymentRequired.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| 'Balance bypass is not supported for public Kilo prompt mutations' | ||
| ); | ||
| } | ||
| const balance = await (params.deps?.validatePromptBalance ?? defaultValidatePromptBalance)({ |
There was a problem hiding this comment.
WARNING: validatePromptBalance call is unguarded against the new throwing admission preflight
defaultValidatePromptBalance was rewritten to call preflightCloudAgentModelBilling, which — unlike the previous fetchOrgIdForSession/validateBalanceOnly pair — can throw a TRPCError (e.g. SERVICE_UNAVAILABLE on admission/DB failure, NOT_FOUND/FORBIDDEN on session-access issues, BAD_REQUEST on a missing model). This call site is not wrapped in try/catch, and nothing further up the chain (admitBasicPrompt → handlePromptAsyncMutation → handleKiloFacadeRequest → UserKiloFacade.fetch()) catches it either — only the later preflightAndAdmitPromptMessage call a few lines below is wrapped in try/catch { return promptPreflightError(error); }. Any admission failure for kilo.prompt_async (including an ordinary session-not-found or a transient admission-service outage) becomes an unhandled exception instead of the intended structured facadeError(...) response, breaking the documented "fail closed (503, retryable)" contract for this path.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Executive SummaryThe previously flagged Overview
Issue Details (click to expand)WARNING
Files Reviewed (1 file)
Fix these issues in Kilo Cloud Previous Review Summaries (2 snapshots, latest commit 42870ba)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 42870ba)Status: 4 Issues Found | Recommendation: Address before merge Executive SummaryA new commit's cleanup of a Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (41 files)
Fix these issues in Kilo Cloud Previous review (commit 8e277aa)Status: 3 Issues Found | Recommendation: Address before merge Executive SummaryRemoving the Overview
Issue Details (click to expand)WARNING
Files Reviewed (41 files)
Reviewed by claude-sonnet-5-20260630 · Input: 28 · Output: 7.2K · Cached: 637.2K Review guidance: REVIEW.md from base branch |
oxlint forbids `import()` type annotations. Replace the
`typeof import('./cloud-agent-client')` annotation in the client test
with a type-only namespace import so the CI lint check passes.
| createAppBuilderCloudAgentNextClient: mockCreateAppBuilderCloudAgentNextClient, | ||
| createCloudAgentNextClientForModel, | ||
| } = clientModule; | ||
| let clientModule: ClientModule; |
There was a problem hiding this comment.
CRITICAL: Namespace used bare as a type — will not compile
import type * as ClientModule from './cloud-agent-client' binds ClientModule as a namespace of the module's exports; a namespace import cannot be used directly as a type annotation (TS2709 "Cannot use namespace 'ClientModule' as a type"). This replaces the previously-working typeof import('./cloud-agent-client'), which correctly referred to the module's type. Every other import type * as X usage in this codebase (e.g. apps/web/src/lib/contributor-champions/service.test.ts, apps/web/src/lib/kilo-pass/apple-store-sdk.test.ts) always wraps the namespace in typeof X when using it as a type; this file omits it, so let clientModule: ClientModule; should be let clientModule: typeof ClientModule;.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Using the type-only namespace directly as a type annotation (`let clientModule: ClientModule`) passes tsc but fails tsgo with TS2709 "Cannot use namespace 'ClientModule' as a type". Mirror the repo's existing pattern (auth.test.ts) and use `typeof ClientModule`.
Summary
Re-architects how cloud-agent-next gates sessions on balance, replacing the
"blanket $1 minimum for all mutations + an opt-out
x-skip-balance-checkheader" model with per-model billing classification.
POST /api/profile/cloud-agent-admissionendpoint, which classifies therequested model as
free,byok, orbalance-required(source of truth:classifyCloudAgentModelBillingin apps/web) and — only forbalance-required— returns balance + depletion in the same call.free/byoksessions are admitted regardless of balance;balance-requiredsessions require a positive balance (previously a $1minimum).
model-billing-preflight.tsextracts the model +owner from each procedure body (
prepareSession,start,initiateFromKilocodeSessionV2,sendMessageV2,send,kilo.prompt_async),validates org membership / session access, and calls admission. The middleware
delegates to it and maps tRPC error codes to HTTP statuses.
balance-validation.tsshrinks to procedure-name extraction + the mutationallowlist.
x-skip-balance-checkescape hatch. Dropped from the client(
createAppBuilderCloudAgentNextClient/createCloudAgentNextClientForModelremoved;
createCloudAgentNextClienttakes no options), App Builder, theDiscord/Slack bot session spawn, the code-review orchestrator
(
skipBalanceCheckremoved fromCodeReview/CodeReviewRequest/payload), andwebhook-agent-ingest. These flows are now admitted normally: free/BYOK models
pass; platform-billed models still require balance.
buildCloudAgentNextEligibilitynow exposesaccessLevel(full/limited),isLowBalance, andisEligible.NewSessionPaneldistinguishes zero-credit (free/BYOK models only, "AddCredits" banner) from low-funds (all models available, "Credits are below $1"
notice); filtering + notice logic extracted to
new-session-balance.ts.assertOrganizationMembershipmoved fromsession-start.tstosession-access.tsfor reuse by the preflight. Error copy changes from "$1minimum required" to "a positive credit balance is required".
Net effect: users with a $0 balance can now start free or BYOK-billed Cloud
Agent sessions; platform-billed models require a positive balance instead of a
$1 floor.
Verification
stack was not exercised end-to-end.
Visual Changes
Reviewer Notes
required" for platform-billed models, broadening who can run paid models to
anyone with > $0 — confirm this is the intended product policy.
mutation (
/api/profile/cloud-agent-admission); on failure it fails closed(503, retryable) so platform-billed models are never served for free during an
outage — verify that retry behavior is acceptable for callers.
prepareSessionis newly added toBALANCE_REQUIRED_MUTATIONS; it previouslycould be bypassed via the skip header for App Builder/code reviews and now runs
the admission preflight like everything else.
cloud-agent-admission,model-billing-preflight,classify-model-billing,new-session-balance, and the admission route.