feat: implement usage quota for AI provider calls with configuration#136
feat: implement usage quota for AI provider calls with configuration#136shenxianpeng wants to merge 7 commits intomainfrom
Conversation
src/main/java/io/jenkins/plugins/explain_error/GlobalConfigurationImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/io/jenkins/plugins/explain_error/GlobalConfigurationImpl.java
Fixed
Show fixed
Hide fixed
There was a problem hiding this comment.
Pull request overview
Implements an optional, controller-scoped usage quota to limit real AI provider calls (while keeping cache hits free), helping administrators control cost/usage across the Jenkins controller.
Changes:
- Adds global quota configuration (enable flag, window size, max calls) and UI fields in global config.
- Introduces
QuotaEnforcer+QuotaWindowand enforces quota checks before provider calls inErrorExplainer. - Extends test coverage to validate quota-rejected behavior, quota-disabled behavior, and “cache hit does not consume quota”.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/io/jenkins/plugins/explain_error/GlobalConfigurationImpl.java | Stores quota settings and exposes UI binding/validation endpoints + tryAcquireQuota() |
| src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java | Applies quota enforcement for pipeline-step and console-action provider calls |
| src/main/java/io/jenkins/plugins/explain_error/QuotaEnforcer.java | Thread-safe rolling-window counter for quota enforcement |
| src/main/java/io/jenkins/plugins/explain_error/QuotaWindow.java | Defines HOURLY/DAILY windows and their durations |
| src/main/resources/io/jenkins/plugins/explain_error/GlobalConfigurationImpl/config.jelly | Adds quota configuration controls to Jenkins global config UI |
| src/test/java/io/jenkins/plugins/explain_error/UsageTrackingTest.java | Adds quota-related usage-event assertions and cache-hit behavior validation |
| src/test/java/io/jenkins/plugins/explain_error/QuotaEnforcerTest.java | Adds unit tests for quota enforcement and reset behavior |
| src/test/java/io/jenkins/plugins/explain_error/GlobalConfigurationImplTest.java | Verifies default quota values and persistence of quota settings |
| public ListBoxModel doFillQuotaWindowItems() { | ||
| ListBoxModel items = new ListBoxModel(); | ||
| for (QuotaWindow window : QuotaWindow.values()) { | ||
| items.add(window.getDisplayName(), window.name()); | ||
| } | ||
| return items; | ||
| } | ||
|
|
||
| public FormValidation doCheckMaxProviderCallsPerWindow(@QueryParameter int value) { | ||
| if (value < 0) { | ||
| return FormValidation.error("Max provider calls per window must be 0 or greater."); | ||
| } | ||
| return FormValidation.ok(); | ||
| } |
There was a problem hiding this comment.
doFillQuotaWindowItems and doCheckMaxProviderCallsPerWindow are exposed Stapler endpoints but currently lack the @POST annotation and any permission check/suppression. In this codebase, comparable descriptor validation endpoints use @POST (and either check Jenkins.ADMINISTER or explicitly suppress the permission-check warning). Please align these methods with that pattern to avoid CSRF / permission-audit findings.
| @@ -118,6 +118,17 @@ String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int | |||
| String effectiveCustomContext = StringUtils.isNotBlank(customContext) ? customContext : GlobalConfigurationImpl.get().getCustomContext(); | |||
| logToConsole(listener, "Custom context source: " + resolveCustomContextSource(customContext) + "."); | |||
|
|
|||
| // Check quota before making a real provider call | |||
| if (!GlobalConfigurationImpl.get().tryAcquireQuota()) { | |||
| GlobalConfigurationImpl config = GlobalConfigurationImpl.get(); | |||
| String msg = "Provider call quota exceeded. Limit: " + config.getMaxProviderCallsPerWindow() | |||
| + " calls per " + config.getQuotaWindow().getDisplayName().toLowerCase() + " window."; | |||
| logToConsole(listener, msg); | |||
| recordUsage(entryPoint, UsageEvent.Result.QUOTA_REJECTED, provider, startTimeNanos, | |||
| inputLogLineCount, collectDownstreamLogs); | |||
| return null; | |||
| } | |||
There was a problem hiding this comment.
Quota is checked only after running log extraction/filtering. When quota is exhausted, this still performs potentially expensive PipelineLogExtractor work even though the provider call will be rejected. Consider moving the quota check earlier (before extracting logs) so quota-rejected requests are cheap and reduce controller load under sustained quota exhaustion.
| void windowRolloverResetsCount() throws InterruptedException { | ||
| // Use a very short custom duration to simulate window rollover without waiting an hour. | ||
| // We do this by resetting the enforcer, which simulates a fresh window. | ||
| QuotaEnforcer enforcer = new QuotaEnforcer(); | ||
|
|
||
| // Fill up the window | ||
| assertTrue(enforcer.tryAcquire(QuotaWindow.HOURLY, 1)); | ||
| assertFalse(enforcer.tryAcquire(QuotaWindow.HOURLY, 1)); | ||
|
|
||
| // Simulate rollover by resetting | ||
| enforcer.reset(); | ||
|
|
There was a problem hiding this comment.
This test’s comment says it uses a “very short custom duration” to simulate rollover, but the implementation actually just calls reset(). Also, the test declares throws InterruptedException without sleeping/waiting. Please update the comment/signature so the test accurately reflects the behavior being verified.
src/main/java/io/jenkins/plugins/explain_error/BillingReconciliationManagementLink.java
Fixed
Show fixed
Hide fixed
src/main/java/io/jenkins/plugins/explain_error/ExplainErrorFolderProperty.java
Fixed
Show fixed
Hide fixed
…provider usage statistics
…perty in ExplainErrorFolderProperty
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ction" This reverts commit 0a43b02.
closes #132
Testing done
Submitter checklist