Skip to content

fix(gateway): re-check credits on env-var retry - #2149

Merged
steebchen merged 1 commit into
mainfrom
fix-zero-credits-bypass
May 4, 2026
Merged

steebchen merged 1 commit into
mainfrom
fix-zero-credits-bypass

Conversation

@steebchen

@steebchen steebchen commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

  • The initial credit gate in chat.ts only fires when the first attempt is about to use LLMGateway env-var tokens; BYOK paths intentionally skip it so a $0-credits org can still use its own provider key.
  • Retry/fallback went through resolveProviderContext, which switched to env-var tokens without re-validating credits, allowing a primary BYOK attempt to fail over and bill the org via used_mode="credits" with $0 balance.
  • Mirror the same gate inside resolveProviderContext so the env-var fallback paths (credits and hybrid-no-key branches) refuse fallback when total available credits are non-positive, preserving the BYOK-with-$0 use case.
  • Added a clarifying comment in chat.ts explaining that the bare modelInfo.free flag is intentional and isModelTrulyFree should not be substituted here.

Test plan

  • pnpm test:unit passes
  • Repro a hybrid-mode org with $0 credits + a deliberately broken BYOK key on a paid model — request should now 402 instead of falling back and billing credits

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved credit validation to accurately determine which models are available when organization credits are insufficient
    • Added mandatory credit verification before using backup authentication methods
    • Enhanced error response messages for low-credit scenarios with plan type and renewal date information

The initial credit gate in chat.ts only fires when the first attempt is
about to use LLMGateway env-var tokens. Retry/fallback paths went through
resolveProviderContext, which switched to env-var tokens without
re-validating credits — letting a $0-credits org be billed via
used_mode="credits" once a primary attempt failed over.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 07:10
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The PR modifies credit validation logic in the gateway's chat module. It redefines which models are considered "free" for credit-gate purposes to use only the catalog's free flag, and introduces a new helper function to enforce credit checks before using environment-variable fallback provider tokens in both credits and hybrid modes.

Changes

Credit Validation and Provider Fallback Gating

Layer / File(s) Summary
Credit Eligibility Definition
apps/gateway/src/chat/chat.ts
Credits-mode insufficient-credits check now uses ModelDefinition.free directly instead of isModelTrulyFree() helper; includes clarifying comments that models with free: true bypass credits validation.
Credit Gate Helper
apps/gateway/src/chat/tools/resolve-provider-context.ts (lines 114–146)
New assertOrganizationHasCreditsForEnvFallback() function computes total available credits (regular + remaining dev-plan credits), bypasses validation for free models, and throws HTTPException(402) with dev-plan or generic insufficient-credits messages.
Integration in Credits & Hybrid Modes
apps/gateway/src/chat/tools/resolve-provider-context.ts (lines 221–223, 249–251)
Helper is called immediately before getProviderEnv in credits mode and in hybrid mode when no provider key exists, enforcing the credit gate before fallback token retrieval.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(gateway): re-check credits on env-var retry' accurately summarizes the main change: adding a credit check before falling back to environment-variable tokens during retry logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-zero-credits-bypass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 13 minutes and 53 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/chat/tools/resolve-provider-context.ts`:
- Around line 143-145: Replace the user-facing error that exposes
organization.id in the HTTPException thrown in resolve-provider-context.ts:
remove the interpolated organization.id and use a generic, non-identifying
message (e.g. "Not enough credits. Please add more credits or contact support.")
when constructing the HTTPException instance so the thrown HTTPException no
longer leaks internal organization identifiers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d35b77d2-c910-41fe-8c48-cf762be36092

📥 Commits

Reviewing files that changed from the base of the PR and between 03c8a87 and 1d0a377.

📒 Files selected for processing (2)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/resolve-provider-context.ts

Comment on lines +143 to +145
throw new HTTPException(402, {
message: `Organization ${organization.id} has insufficient credits`,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid exposing internal organization ID in user-facing error message.

The error message includes organization.id, which leaks an internal identifier to the end user. The equivalent check in chat.ts uses a more generic message without exposing IDs:

"Not enough credits. Please add more credits or contact support."

Consider aligning with the existing pattern for consistency and to avoid exposing internal data.

Suggested fix
 	throw new HTTPException(402, {
-		message: `Organization ${organization.id} has insufficient credits`,
+		message: `Not enough credits. Please add more credits or contact support.`,
 	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new HTTPException(402, {
message: `Organization ${organization.id} has insufficient credits`,
});
throw new HTTPException(402, {
message: `Not enough credits. Please add more credits or contact support.`,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/tools/resolve-provider-context.ts` around lines 143 -
145, Replace the user-facing error that exposes organization.id in the
HTTPException thrown in resolve-provider-context.ts: remove the interpolated
organization.id and use a generic, non-identifying message (e.g. "Not enough
credits. Please add more credits or contact support.") when constructing the
HTTPException instance so the thrown HTTPException no longer leaks internal
organization identifiers.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes a billing loophole in the gateway retry/fallback flow where a request that initially used a BYOK provider key could later retry using LLMGateway environment tokens without re-checking that the organization has positive credits.

Changes:

  • Added a credit gate inside resolveProviderContext to prevent env-var token fallback when org credits are non-positive (except for catalog-flagged free models).
  • Wired the new gate into both credits mode and hybrid mode (no-provider-key) branches in resolveProviderContext.
  • Added an explanatory comment in chat.ts about intentionally using the bare modelInfo.free flag in the initial credit gate.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
apps/gateway/src/chat/tools/resolve-provider-context.ts Adds a mirrored credit check to protect env-var fallback paths during retries/fallback.
apps/gateway/src/chat/chat.ts Clarifies why the initial credit gate uses modelInfo.free rather than isModelTrulyFree.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +116 to +122
// with non-positive credits. Free models (explicitly flagged in the catalog)
// are exempt.
function assertOrganizationHasCreditsForEnvFallback(
organization: OrgInfo,
modelInfo: ModelDefinition,
): void {
if (modelInfo.free) {
Comment on lines +125 to +145
const regularCredits = parseFloat(organization.credits ?? "0");
const devPlanCreditsRemaining =
organization.devPlan !== "none"
? parseFloat(organization.devPlanCreditsLimit ?? "0") -
parseFloat(organization.devPlanCreditsUsed ?? "0")
: 0;
const totalAvailableCredits = regularCredits + devPlanCreditsRemaining;
if (totalAvailableCredits > 0) {
return;
}
if (organization.devPlan !== "none" && devPlanCreditsRemaining <= 0) {
const renewalDate = organization.devPlanExpiresAt
? new Date(organization.devPlanExpiresAt).toLocaleDateString()
: "your next billing date";
throw new HTTPException(402, {
message: `Dev Plan credit limit reached. Upgrade your plan or wait for renewal on ${renewalDate}.`,
});
}
throw new HTTPException(402, {
message: `Organization ${organization.id} has insufficient credits`,
});
Comment on lines 219 to 252
@@ -211,6 +246,7 @@ export async function resolveProviderContext(
if (providerKey) {
usedToken = providerKey.token;
} else {
assertOrganizationHasCreditsForEnvFallback(organization, modelInfo);
const envResult = getProviderEnv(usedProvider as Provider, {
excludedIndices: options.excludedEnvKeyIndices,
});
Comment on lines +2855 to +2857
// We trust the bare `modelInfo.free` flag here: free models are always
// marked explicitly in the catalog, so a `free: true` model is intended
// to be usable without credits. Do not switch this to isModelTrulyFree.
@steebchen
steebchen merged commit e5e63d3 into main May 4, 2026
21 checks passed
@steebchen
steebchen deleted the fix-zero-credits-bypass branch May 4, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants