MiningBudget.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.mining.core.budget;
/**
* Tracks request and commit budgets for LLM mining runs.
* <p>
* A value of {@code 0} for a maximum means unlimited. The class intentionally
* does not depend on a concrete LLM client so it can be tested and reused by
* the CLI, scheduled jobs, and dry-run tooling.
* </p>
*/
public final class MiningBudget {
private final int maxRequests;
private final int maxCommits;
private int requestsUsed;
private int commitsSent;
/**
* Create a new budget.
*
* @param maxRequests maximum number of LLM requests; {@code 0} means unlimited
* @param maxCommits maximum number of commits sent to the LLM; {@code 0} means unlimited
*/
public MiningBudget(int maxRequests, int maxCommits) {
if (maxRequests < 0) {
throw new IllegalArgumentException("maxRequests must be >= 0"); //$NON-NLS-1$
}
if (maxCommits < 0) {
throw new IllegalArgumentException("maxCommits must be >= 0"); //$NON-NLS-1$
}
this.maxRequests = maxRequests;
this.maxCommits = maxCommits;
}
/**
* Build a budget from a profile plus optional explicit overrides.
* <p>
* Pass {@code -1} for either override to use the profile default.
* Pass {@code 0} to explicitly request an unlimited budget for that dimension.
* Any value lower than {@code -1} is rejected as invalid.
* </p>
*
* @param profile the budget profile
* @param explicitMaxRequests explicit request limit, {@code 0} for unlimited, or {@code -1} for profile default
* @param explicitMaxCommits explicit commit limit, {@code 0} for unlimited, or {@code -1} for profile default
* @return budget instance
* @throws IllegalArgumentException if either explicit value is less than {@code -1}
*/
public static MiningBudget from(BudgetProfile profile, int explicitMaxRequests,
int explicitMaxCommits) {
if (explicitMaxRequests < -1) {
throw new IllegalArgumentException("explicitMaxRequests must be >= -1"); //$NON-NLS-1$
}
if (explicitMaxCommits < -1) {
throw new IllegalArgumentException("explicitMaxCommits must be >= -1"); //$NON-NLS-1$
}
BudgetProfile effectiveProfile = profile == null ? BudgetProfile.BALANCED : profile;
int requestLimit = explicitMaxRequests >= 0
? explicitMaxRequests
: effectiveProfile.defaultMaxRequests();
int commitLimit = explicitMaxCommits >= 0
? explicitMaxCommits
: effectiveProfile.defaultMaxCommits();
return new MiningBudget(requestLimit, commitLimit);
}
/**
* @return whether another LLM request may be started
*/
public boolean hasRequestCapacity() {
return maxRequests == 0 || requestsUsed < maxRequests;
}
/**
* Returns how many commits may be included in the next LLM request.
*
* @param proposedCommitCount requested number of commits
* @return allowed number of commits, possibly {@code 0}
*/
public int allowedCommitsForNextRequest(int proposedCommitCount) {
if (proposedCommitCount <= 0 || !hasRequestCapacity()) {
return 0;
}
if (maxCommits == 0) {
return proposedCommitCount;
}
int remaining = maxCommits - commitsSent;
return Math.max(0, Math.min(proposedCommitCount, remaining));
}
/**
* Records an LLM request after the caller has decided to send one.
*
* @param commitCount number of commits included in the request
*/
public void recordRequest(int commitCount) {
if (commitCount < 0) {
throw new IllegalArgumentException("commitCount must be >= 0"); //$NON-NLS-1$
}
requestsUsed++;
commitsSent += commitCount;
}
/**
* @return whether either configured limit has been exhausted
*/
public boolean isExhausted() {
boolean requestsExhausted = maxRequests > 0 && requestsUsed >= maxRequests;
boolean commitsExhausted = maxCommits > 0 && commitsSent >= maxCommits;
return requestsExhausted || commitsExhausted;
}
public int getMaxRequests() {
return maxRequests;
}
public int getMaxCommits() {
return maxCommits;
}
public int getRequestsUsed() {
return requestsUsed;
}
public int getCommitsSent() {
return commitsSent;
}
/**
* @return human-readable budget summary for logs and reports
*/
public String formatSummary() {
return "LLM budget: requests " + requestsUsed + '/' //$NON-NLS-1$
+ (maxRequests == 0 ? "unlimited" : Integer.toString(maxRequests)) //$NON-NLS-1$
+ ", commits " + commitsSent + '/' //$NON-NLS-1$
+ (maxCommits == 0 ? "unlimited" : Integer.toString(maxCommits)); //$NON-NLS-1$
}
}