fix: per-user and per-tenant rate limiting on AI endpoints#2904
fix: per-user and per-tenant rate limiting on AI endpoints#2904Nancy-3012 wants to merge 1 commit into
Conversation
- Replace IP-based limiter key with user:<uuid> for authenticated requests - Fallback to IP for unauthenticated requests - Add get_rate_limit_key() using extract_token() + supabase.auth.get_user() - Add log_ai_usage() to track calls in ai_usage_log table - Add check_tenant_quota() reading max_ai_requests_per_hour from system_settings - Apply quota check + usage logging to /ai/analyze and /ai/analyze_stream Fixes ritesh-1918#2903
|
@Nancy-3012 is attempting to deploy a commit to the ritesh Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthrough
ChangesAI Rate Limiting and Quota Enforcement
Sequence Diagram(s)sequenceDiagram
participant Client
participant get_rate_limit_key
participant check_tenant_quota
participant log_ai_usage
participant Supabase
Client->>get_rate_limit_key: HTTP request (bearer/cookie token)
get_rate_limit_key->>Supabase: auth.get_user(token)
Supabase-->>get_rate_limit_key: user or error
get_rate_limit_key-->>Client: "user:<uuid>" or remote IP
Client->>check_tenant_quota: company_id, max_ai_requests_per_hour
check_tenant_quota->>Supabase: count ai_usage_log (last hour)
Supabase-->>check_tenant_quota: count
check_tenant_quota-->>Client: HTTP 429 if over limit
Client->>log_ai_usage: user_id, company_id, endpoint
log_ai_usage->>Supabase: insert into ai_usage_log
Supabase-->>log_ai_usage: ack
Client->>Client: proceed with AI analysis
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/main.py`:
- Around line 793-800: The line containing the
get_system_settings(request_body.company) assignment has inconsistent
indentation with only 3 spaces, while all subsequent lines (check_tenant_quota,
log_ai_usage, confidence_threshold, duplicate_sensitivity, enable_auto_resolve)
use 4 spaces. Fix this by aligning all lines to use consistent 4-space
indentation throughout the entire block to resolve the Python syntax error that
prevents module loading.
- Around line 965-971: Fix the indentation inconsistency on lines 965-966 by
ensuring all lines use the same indentation level (8 spaces). Additionally, move
the check_tenant_quota() call and log_ai_usage() call from inside the
event_generator() function to the outer analyze_stream() function, before the
StreamingResponse object is created. This ensures quota validation happens
before the response headers are sent and allows proper HTTPException handling to
return a 429 status code instead of breaking the stream.
- Around line 77-79: The SELECT query on the system_settings table is attempting
to retrieve the max_ai_requests_per_hour column, but this column is not defined
in the migration file supabase/migrations/20260531_add_company_settings.sql. To
fix this schema mismatch, either add the max_ai_requests_per_hour column
definition to the migration file with an appropriate data type and default
value, or remove max_ai_requests_per_hour from the SELECT query in the
supabase.table("system_settings").select() call if this field is intentionally
managed only through application defaults rather than stored in the database.
- Around line 87-99: The log_ai_usage function attempts to insert records into
the ai_usage_log table, but this table has not been created in any migration.
Create a new database migration file that defines the ai_usage_log table with
the following columns: id as a primary key (UUID or auto-increment integer),
user_id (nullable string or UUID), company_id (nullable string or UUID),
endpoint (string), and timestamp (timestamp with timezone). Ensure the schema
matches the fields being inserted by the log_ai_usage function to prevent
runtime errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| res = supabase.table("system_settings").select( | ||
| "ai_confidence_threshold, duplicate_sensitivity, enable_auto_resolve" | ||
| "ai_confidence_threshold, duplicate_sensitivity, enable_auto_resolve, max_ai_requests_per_hour" | ||
| ).eq("company_id", company_id).single().execute() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if max_ai_requests_per_hour column is defined in any migration
rg -n "max_ai_requests_per_hour" --type sqlRepository: ritesh-1918/HELPDESK.AI
Length of output: 49
🏁 Script executed:
# Find and examine the migration file mentioned in the review
fd -t f "20260531_add_company_settings.sql" .Repository: ritesh-1918/HELPDESK.AI
Length of output: 123
🏁 Script executed:
# Also check what migration files exist in the supabase directory
fd -t f "\.sql$" supabase/migrations/ 2>/dev/null | head -20Repository: ritesh-1918/HELPDESK.AI
Length of output: 498
🏁 Script executed:
# Examine the get_system_settings function in backend/main.py (lines 67-84)
sed -n '67,84p' backend/main.pyRepository: ritesh-1918/HELPDESK.AI
Length of output: 797
🏁 Script executed:
# Check if max_ai_requests_per_hour is referenced anywhere in Python files
rg -n "max_ai_requests_per_hour" --type pyRepository: ritesh-1918/HELPDESK.AI
Length of output: 338
🏁 Script executed:
# Examine the migration file content that was found
cat supabase/migrations/20260531_add_company_settings.sqlRepository: ritesh-1918/HELPDESK.AI
Length of output: 1808
🏁 Script executed:
# Also check if there are any other references to system_settings table definition
rg -n "CREATE TABLE.*system_settings|ALTER TABLE.*system_settings" --type sqlRepository: ritesh-1918/HELPDESK.AI
Length of output: 280
Schema mismatch: max_ai_requests_per_hour column missing from system_settings table.
The migration supabase/migrations/20260531_add_company_settings.sql does not define the max_ai_requests_per_hour column, yet the code at lines 77-79 attempts to select it. While the code mitigates this with a merge strategy ({**defaults, **res.data}) that falls back to the default value of 100, this still represents a schema integrity issue where code expectations diverge from the actual table structure.
Add max_ai_requests_per_hour to the migration, or remove it from the SELECT query if intentionally managed only via defaults.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 77 - 79, The SELECT query on the
system_settings table is attempting to retrieve the max_ai_requests_per_hour
column, but this column is not defined in the migration file
supabase/migrations/20260531_add_company_settings.sql. To fix this schema
mismatch, either add the max_ai_requests_per_hour column definition to the
migration file with an appropriate data type and default value, or remove
max_ai_requests_per_hour from the SELECT query in the
supabase.table("system_settings").select() call if this field is intentionally
managed only through application defaults rather than stored in the database.
| def log_ai_usage(user_id: str | None, company_id: str | None, endpoint: str) -> None: | ||
| """Log each AI call to ai_usage_log for audit and billing.""" | ||
| if not supabase: | ||
| return | ||
| try: | ||
| supabase.table("ai_usage_log").insert({ | ||
| "user_id": user_id, | ||
| "company_id": company_id, | ||
| "endpoint": endpoint, | ||
| "timestamp": datetime.datetime.utcnow().isoformat() + "Z" | ||
| }).execute() | ||
| except Exception as e: | ||
| print(f"[WARNING] ai_usage_log insert failed: {e}") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for ai_usage_log table migration
rg -n "ai_usage_log" --type sqlRepository: ritesh-1918/HELPDESK.AI
Length of output: 49
🏁 Script executed:
# Find migration files in any format
find . -type f \( -name "*migration*" -o -name "*schema*" \) | head -20Repository: ritesh-1918/HELPDESK.AI
Length of output: 157
🏁 Script executed:
# Check for Supabase-specific files
find . -type f -name "supabase*" -o -name "*supabase*" | head -20Repository: ritesh-1918/HELPDESK.AI
Length of output: 193
🏁 Script executed:
# Search more broadly for ai_usage_log references
rg -i "ai_usage_log" --type-list | head -10
rg -i "ai_usage_log" -lRepository: ritesh-1918/HELPDESK.AI
Length of output: 491
🏁 Script executed:
# Look for database initialization or setup documentation
find . -type f \( -name "*.md" -o -name "README*" -o -name "SETUP*" \) -exec grep -l "database\|table\|migration\|ai_usage_log" {} \;Repository: ritesh-1918/HELPDESK.AI
Length of output: 652
🏁 Script executed:
# Check the migration file
cat -n ./supabase/migrations/20260330131213_remote_schema.sql | head -100Repository: ritesh-1918/HELPDESK.AI
Length of output: 49
🏁 Script executed:
# Check if ai_usage_log is in the migration file
rg "ai_usage_log" ./supabase/migrations/Repository: ritesh-1918/HELPDESK.AI
Length of output: 49
🏁 Script executed:
# Check Supabase directory structure
ls -la ./supabase/Repository: ritesh-1918/HELPDESK.AI
Length of output: 352
🏁 Script executed:
# Get file size and content
wc -l ./supabase/migrations/20260330131213_remote_schema.sqlRepository: ritesh-1918/HELPDESK.AI
Length of output: 124
🏁 Script executed:
# Read the file
cat ./supabase/migrations/20260330131213_remote_schema.sql | grep -i "table\|create" | head -30Repository: ritesh-1918/HELPDESK.AI
Length of output: 49
🏁 Script executed:
# Search for any table creation in all migration files
rg "CREATE TABLE" ./supabase/migrations/Repository: ritesh-1918/HELPDESK.AI
Length of output: 168
🏁 Script executed:
# List all migration files
ls -1 ./supabase/migrations/Repository: ritesh-1918/HELPDESK.AI
Length of output: 338
🏁 Script executed:
# Check all migration files for ai_usage_log
for file in ./supabase/migrations/*.sql; do echo "=== $file ==="; cat "$file" | grep -i "ai_usage_log" || echo "NOT FOUND"; doneRepository: ritesh-1918/HELPDESK.AI
Length of output: 658
🏁 Script executed:
# Check if table creation happens in Python code or elsewhere
rg -i "create.*table\|alter.*table" --type py backend/Repository: ritesh-1918/HELPDESK.AI
Length of output: 49
Create migration for ai_usage_log table.
The log_ai_usage() function inserts into the ai_usage_log table, but this table is not defined in any migration. Add a migration to create the table with columns: id (primary key), user_id, company_id, endpoint, and timestamp.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 87 - 99, The log_ai_usage function attempts to
insert records into the ai_usage_log table, but this table has not been created
in any migration. Create a new database migration file that defines the
ai_usage_log table with the following columns: id as a primary key (UUID or
auto-increment integer), user_id (nullable string or UUID), company_id (nullable
string or UUID), endpoint (string), and timestamp (timestamp with timezone).
Ensure the schema matches the fields being inserted by the log_ai_usage function
to prevent runtime errors.
|
|
||
| settings = get_system_settings(request_body.company) | ||
| check_tenant_quota(request_body.company, settings) | ||
| log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze") | ||
| confidence_threshold = settings["ai_confidence_threshold"] | ||
| duplicate_sensitivity = settings["duplicate_sensitivity"] | ||
| enable_auto_resolve = settings["enable_auto_resolve"] | ||
|
|
There was a problem hiding this comment.
Critical: Indentation syntax error breaks module loading.
Lines 794-795 have inconsistent indentation (3 spaces vs 4 spaces), causing Python syntax errors as flagged by static analysis. This will prevent the entire module from loading.
Proposed fix
-
- settings = get_system_settings(request_body.company)
- check_tenant_quota(request_body.company, settings)
- log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
+ settings = get_system_settings(request_body.company)
+ check_tenant_quota(request_body.company, settings)
+ log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
confidence_threshold = settings["ai_confidence_threshold"]📝 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.
| settings = get_system_settings(request_body.company) | |
| check_tenant_quota(request_body.company, settings) | |
| log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze") | |
| confidence_threshold = settings["ai_confidence_threshold"] | |
| duplicate_sensitivity = settings["duplicate_sensitivity"] | |
| enable_auto_resolve = settings["enable_auto_resolve"] | |
| settings = get_system_settings(request_body.company) | |
| check_tenant_quota(request_body.company, settings) | |
| log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze") | |
| confidence_threshold = settings["ai_confidence_threshold"] | |
| duplicate_sensitivity = settings["duplicate_sensitivity"] | |
| enable_auto_resolve = settings["enable_auto_resolve"] |
🧰 Tools
🪛 Ruff (0.15.17)
[warning] 794-794: unindent does not match any outer indentation level
(invalid-syntax)
[warning] 795-795: Unexpected indentation
(invalid-syntax)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 793 - 800, The line containing the
get_system_settings(request_body.company) assignment has inconsistent
indentation with only 3 spaces, while all subsequent lines (check_tenant_quota,
log_ai_usage, confidence_threshold, duplicate_sensitivity, enable_auto_resolve)
use 4 spaces. Fix this by aligning all lines to use consistent 4-space
indentation throughout the entire block to resolve the Python syntax error that
prevents module loading.
Source: Linters/SAST tools
| settings = get_system_settings(request_body.company) | ||
| check_tenant_quota(request_body.company, settings) | ||
| log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream") | ||
| confidence_threshold = settings["ai_confidence_threshold"] | ||
| duplicate_sensitivity = settings["duplicate_sensitivity"] | ||
| enable_auto_resolve = settings["enable_auto_resolve"] | ||
|
|
||
There was a problem hiding this comment.
Critical: Indentation syntax error and HTTPException inside generator won't return 429.
Two issues here:
-
Syntax error: Lines 965-966 have inconsistent indentation (7 vs 8 spaces), same issue as in
analyze_only(). -
HTTPException in streaming context:
check_tenant_quota()is called insideevent_generator()afterStreamingResponsehas started. By that point, response headers (200 OK) are already sent. RaisingHTTPExceptionwill break the stream rather than returning a proper 429 response to the client.
Move quota checks and usage logging to the outer analyze_stream() function, before creating the StreamingResponse.
Proposed fix
`@app.post`("/ai/analyze_stream")
async def analyze_stream(request_body: TicketRequest):
"""
REAL-TIME SSE ENDPOINT: Streams the AI progress to the frontend dynamically.
"""
import datetime
def get_now_ist():
return datetime.datetime.utcnow().isoformat() + "Z"
+
+ # Quota enforcement BEFORE starting the stream
+ settings = get_system_settings(request_body.company)
+ check_tenant_quota(request_body.company, settings)
+ log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream")
async def event_generator():
text = request_body.text
env_metadata = {
"timestamp": get_now_ist(),
"model_version": "3.0.0-PRO",
"api_endpoint": "/ai/analyze_stream"
}
timeline = {"received": get_now_ist()}
- settings = get_system_settings(request_body.company)
- check_tenant_quota(request_body.company, settings)
- log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream")
confidence_threshold = settings["ai_confidence_threshold"]
duplicate_sensitivity = settings["duplicate_sensitivity"]
enable_auto_resolve = settings["enable_auto_resolve"]🧰 Tools
🪛 Ruff (0.15.17)
[warning] 965-965: unindent does not match any outer indentation level
(invalid-syntax)
[warning] 966-966: Unexpected indentation
(invalid-syntax)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 965 - 971, Fix the indentation inconsistency on
lines 965-966 by ensuring all lines use the same indentation level (8 spaces).
Additionally, move the check_tenant_quota() call and log_ai_usage() call from
inside the event_generator() function to the outer analyze_stream() function,
before the StreamingResponse object is created. This ensures quota validation
happens before the response headers are sent and allows proper HTTPException
handling to return a 429 status code instead of breaking the stream.
Source: Linters/SAST tools
|
Good work buddy! |
Fixes #2903
Summary by CodeRabbit