Skip to content

feat(proxy): add project-level model_itpm_limit/model_otpm_limit - #35098

Closed
shivijain2323 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
shivijain2323:litellm_bedrock_mantle_quota_project
Closed

feat(proxy): add project-level model_itpm_limit/model_otpm_limit#35098
shivijain2323 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
shivijain2323:litellm_bedrock_mantle_quota_project

Conversation

@shivijain2323

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Bedrock Mantle bills input and output tokens separately, not one combined bucket
  • Project-level quotas only support combined TPM/RPM today, not split limits

How it solves it:

  • Adds model_itpm_limit/model_otpm_limit on project, same shape as model_tpm_limit
  • Enforces both via the project rate-limit hook's existing reservation/reconciliation path

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Type

🆕 New Feature

Changes

We already support model_tpm_limit/model_rpm_limit per project, but that treats input and output tokens as one combined bucket, which doesn't match how Bedrock Mantle actually meters usage. This adds model_itpm_limit/model_otpm_limit to LiteLLM_ProjectTable, NewProjectRequest, and UpdateProjectRequest, routed into project metadata the same way model_tpm_limit already is, with a matching Prisma migration.

There's already deployment-level itpm/otpm support in router_utils/pre_call_checks/io_token_rate_limit_check.py, but that code only ever deals with one deployment at a time, whereas the proxy hook (parallel_request_limiter_v3.py) has to juggle key/team/org/project limits together on the same request. Rather than bolt a second copy of the reservation logic on, this adds project-scoped ITPM/OTPM as two more descriptors into the same reservation path the hook already uses for TPM/RPM. If a project sets both the combined TPM limit and the new itpm/otpm limits on the same model, both get enforced, not one overriding the other, matching what the deployment-level check already does. Input and output are reserved separately up front, and if one side reserves fine but the other is over limit, the side that already went through gets rolled back so nothing is left over-counted. On success, cached prompt tokens get excluded from the ITPM count since that's how Bedrock bills it, but that's purely for the rate limit math; cost and usage logging still see the full token count.

Adds get_project_model_itpm_limit/get_project_model_otpm_limit to auth_utils.py for parity with the existing key/team/project rate-limit accessors.

Also fixes several correctness issues in the Responses API path of parallel_request_limiter_v3.py surfaced during review: the combined-TPM and OTPM output-cap checks classified any request with data["input"] set as an embedding, which also misclassifies the Responses API (it puts its prompt in "input" too, but does generate output) and skipped the output cap entirely for it; the implicit output cap was being written to data["max_tokens"], which the Responses-to-chat-completion transformation ignores (it only reads max_output_tokens), so the cap was silently dropped before provider dispatch; and an explicit max_output_tokens=0 was getting folded away by a truthy or chain and then re-floored to 1 by the OTPM reservation, which could false-reject a genuine zero-output request against an already-exhausted OTPM bucket. Also routes Responses API input through the standard transform_responses_api_input_to_messages helper before token counting, since token_counter's text argument only joins plain strings in a list and was silently dropping input_image content blocks from the ITPM estimate.

Out of scope for this PR: key/team/org-level model_itpm_limit/model_otpm_limit, and Admin UI support (only the auto-generated schema.d.ts types are updated; no form fields yet).

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Docs PR BerriAI/litellm-docs#638

Bedrock Mantle bills input and output tokens separately, but project-level
quotas only supported a single combined model_tpm_limit. Adds
model_itpm_limit/model_otpm_limit on the project, enforced via the
existing project rate-limit hook's reservation/reconciliation path
alongside (not instead of) the combined TPM limit, mirroring how the
deployment-level itpm/otpm check already handles that overlap.
@shivijain2323
shivijain2323 requested a review from a team July 29, 2026 15:24
Comment on lines +4243 to +4259
itpm_scopes = self._get_reserved_itpm_scopes_from_kwargs(kwargs=request_data)
pipeline_operations.extend(
self._build_reservation_aware_tpm_ops(
targets=list(itpm_scopes),
reserved_scopes=itpm_scopes,
actual_tokens=0,
reserved_tokens=itpm_reserved,
)
)
if otpm_reserved > 0:
otpm_scopes = self._get_reserved_otpm_scopes_from_kwargs(kwargs=request_data)
pipeline_operations.extend(
self._build_reservation_aware_tpm_ops(
targets=list(otpm_scopes),
reserved_scopes=otpm_scopes,
actual_tokens=0,
reserved_tokens=otpm_reserved,

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.

P1 security Disconnects refund consumed tokens

When a client disconnects after provider dispatch but before a partial-success event is available, this cleanup reconciles both reservations with actual_tokens=0, fully refunding provider-billable input and output usage. Repeated early stream cancellations can therefore evade the project's ITPM and OTPM quotas.

How this was verified: The streaming cleanup invokes this fallback only when no success event owns reconciliation, and both counter updates subtract the full reservation as zero actual usage.

Rule Used: What: Fail any PR which may contains a security in... (source)

Knowledge Base Used: Proxy Server Request Flow

Comment thread litellm/models/project.py
Comment on lines +59 to +67
dedicated = {
k: v
for k, v in {
"model_itpm_limit": self.model_itpm_limit,
"model_otpm_limit": self.model_otpm_limit,
"model_rpm_limit": self.model_rpm_limit,
"model_tpm_limit": self.model_tpm_limit,
}.items()
if v is not None

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.

P1 Empty columns erase legacy limits

When an existing project stores a model TPM or RPM limit in metadata while its dedicated JSON column has the database default {}, this merge treats the empty object as an explicit value and overwrites the configured limit. The limiter then finds no matching model descriptor, so the existing project quota is silently disabled.

Knowledge Base Used:

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds project-level input/output token quotas and integrates their persistence, authentication metadata, reservation, reconciliation, Responses API handling, and tests

  • Adds model_itpm_limit and model_otpm_limit to project schemas, migration, request models, and generated UI types
  • Extends the parallel request limiter with separate project ITPM/OTPM descriptors and accounting
  • Updates Responses API output-cap and multimodal input estimation behavior

Confidence Score: 2/5

This PR should not merge until disconnect accounting preserves billable usage and empty dedicated columns no longer disable existing project limits

The new limiter can remove reservations for dispatched requests on an early stream disconnect, and the metadata merge can replace configured legacy quotas with empty database defaults

Files Needing Attention: litellm/proxy/hooks/parallel_request_limiter_v3.py, litellm/models/project.py

Security Review

Client-disconnect cleanup can fully refund project token reservations after provider dispatch when no partial-success event is available, permitting repeated cancellations to evade the new quotas

Important Files Changed

Filename Overview
litellm/proxy/hooks/parallel_request_limiter_v3.py Adds separate ITPM/OTPM reservation and reconciliation, but the disconnect fallback can refund already-consumed usage
litellm/models/project.py Adds merged project rate-limit metadata, but empty database-default columns can erase legacy metadata limits
litellm/proxy/auth/user_api_key_auth.py Routes project metadata through the new merged view across authentication paths
litellm/proxy/_types.py Adds project ITPM/OTPM fields to management request contracts and metadata-field routing
litellm-proxy-extras/litellm_proxy_extras/migrations/20260722200000_add_project_itpm_otpm_limit/migration.sql Adds non-null JSONB columns for project input/output token limits
tests/test_litellm/proxy/hooks/test_tpm_concurrent.py Substantially expands reservation, reconciliation, concurrency, and Responses API coverage

Reviews (1): Last reviewed commit: "feat(proxy): add project-level model_itp..." | Re-trigger Greptile

Comment thread litellm/models/project.py
}.items()
if v is not None
}
return {**base, **dedicated}

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.

Medium: Empty columns disable configured project limits

NewProjectRequest and UpdateProjectRequest move these fields into metadata, but every dedicated database column has a non-null {} default. After loading the row, this merge therefore replaces configured metadata with {}, disabling the new ITPM/OTPM limits and existing per-model RPM/TPM limits; a project-key holder can then exceed those quotas. Persist management fields into the dedicated columns, or prevent empty defaults from shadowing legacy metadata and backfill existing rows.

explicit_max_tokens = next(
(
value
for value in (data.get("max_tokens"), data.get("max_completion_tokens"), data.get("max_output_tokens"))

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.

Medium: Conflicting Responses API caps bypass output reservation

A Responses caller can send max_tokens: 0 together with a large max_output_tokens. This selects zero for reservation and suppresses the implicit cap, while the Responses transformation only forwards max_output_tokens, allowing concurrent requests to generate output without reserving the corresponding TPM/OTPM budget. Select the cap canonical for the call type and reject conflicting output-cap fields.

self._build_reservation_aware_tpm_ops(
targets=list(itpm_scopes),
reserved_scopes=itpm_scopes,
actual_tokens=0,

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.

Low: Stream cancellation refunds processed tokens

This treats a disconnected request as consuming zero input and output tokens even after its prompt reached the provider or partial output was delivered. A caller can repeatedly submit large streaming requests and disconnect before the usage callback, recovering the full ITPM/OTPM reservation each time. Reconcile recovered partial usage when available and retain the reservation when usage is unknown rather than refunding it as zero.

@veria-ai

veria-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds project-level per-model input-token-per-minute (ITPM) and output-token-per-minute (OTPM) limits to the proxy. It updates project configuration storage and the parallel request limiter, including streaming and Responses API accounting.

Three issues remain open, and none have yet been addressed. Project limits can be shadowed by empty database values, conflicting Responses API token caps can avoid output-budget reservation, and stream cancellation can incorrectly refund consumed capacity. These behaviors allow project-key holders to exceed configured token quotas, though the impact is limited to quota and resource-accounting enforcement.

Open issues (3)

Fixed/addressed: 0 · PR risk: 6/10

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.92644% with 106 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/hooks/parallel_request_limiter_v3.py 79.66% 96 Missing ⚠️
litellm/proxy/auth/auth_utils.py 53.33% 7 Missing ⚠️
litellm/proxy/auth/user_api_key_auth.py 25.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing shivijain2323:litellm_bedrock_mantle_quota_project (1add408) with litellm_internal_staging (2f7574d)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (c274cf3) during the generation of this report, so 2f7574d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

1 participant