Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public class Config {

private List<String> globalInterceptors = List.of();

// deployment-wide anchor for the DAY/WEEK/MONTH fixed calendar rate-limit windows
private RateLimitSchedule rateLimitSchedule = new RateLimitSchedule();

@JsonIgnore
public Deployment selectDeployment(String deploymentId) {
Application application = applications.get(deploymentId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.epam.aidial.core.config;

import com.epam.aidial.core.config.validation.ValidTimezone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.validation.constraints.Pattern;
import lombok.Data;

/**
* Deployment-wide anchor for the DAY/WEEK/MONTH fixed calendar rate-limit windows: what local time
* of day a period resets at, in which timezone, and - for WEEK - which day it starts on. Omitting
* this setting is equivalent to UTC/Mon/00:00, which reproduces the previous implicit UTC-midnight
* day/month behavior and only newly defines a week start day.
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class RateLimitSchedule {
@ValidTimezone
private String timezone = "UTC";
private WeekDay weekStartDay = WeekDay.Mon;
@Pattern(regexp = "^([01][0-9]|2[0-3]):[0-5][0-9]$", message = "resetTime must be in HH:mm 24h format")
private String resetTime = "00:00";
}
23 changes: 23 additions & 0 deletions config/src/main/java/com/epam/aidial/core/config/WeekDay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.epam.aidial.core.config;

import java.time.DayOfWeek;

public enum WeekDay {
Mon(DayOfWeek.MONDAY),
Tue(DayOfWeek.TUESDAY),
Wed(DayOfWeek.WEDNESDAY),
Thu(DayOfWeek.THURSDAY),
Fri(DayOfWeek.FRIDAY),
Sat(DayOfWeek.SATURDAY),
Sun(DayOfWeek.SUNDAY);

private final DayOfWeek dayOfWeek;

WeekDay(DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}

public DayOfWeek toDayOfWeek() {
return dayOfWeek;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.epam.aidial.core.config.validation;

import jakarta.validation.Constraint;
import jakarta.validation.ReportAsSingleViolation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;

@Documented
@Constraint(validatedBy = { ValidTimezoneValidator.class })
@Target({ FIELD })
@Retention(RetentionPolicy.RUNTIME)
@ReportAsSingleViolation
public @interface ValidTimezone {
String message() default "Timezone must be a valid IANA timezone id, e.g. \"Europe/Warsaw\" or \"UTC\"";
Class<?>[] groups() default {};
Class<? extends jakarta.validation.Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.epam.aidial.core.config.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.time.ZoneId;

public class ValidTimezoneValidator implements ConstraintValidator<ValidTimezone, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}

// restricted to real IANA region ids - a fixed offset like "+02:00" would also parse via
// ZoneId.of, but the schedule must survive DST transitions, which a fixed offset cannot
return ZoneId.getAvailableZoneIds().contains(value);
}
}
37 changes: 32 additions & 5 deletions docs/dynamic-settings/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,31 @@ An object containing parameters for each [role](#roles).
}
```

## rateLimitSchedule

A top-level (deployment-wide, not per-role) setting that anchors the fixed calendar windows used by the `day`/`week`/`month`/`requestDay` limits below - see [Fixed calendar windows](#rolesrole_namelimits). It is a single setting for the whole deployment: there is no per-role or per-user customization, and no self-service API for end users to change it.

Available values:

* `timezone`: An IANA timezone id (e.g. `"Europe/Warsaw"`, `"America/New_York"`, or `"UTC"`) the schedule is anchored to. Must be a real IANA region id, not a fixed UTC offset, since a fixed offset cannot express daylight-saving transitions. Default: `"UTC"`.
* `weekStartDay`: The day the `week` window starts on, using a 3-letter abbreviation (`Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun`). Default: `"Mon"`.
* `resetTime`: The 24h local time (`HH:mm`) at which `day`/`week`/`month`/`requestDay` periods reset. Default: `"00:00"`.

Omitting `rateLimitSchedule` entirely is equivalent to `{"timezone": "UTC", "weekStartDay": "Mon", "resetTime": "00:00"}`, which reproduces the previous implicit UTC-midnight day/month behavior.

**Example**:

```json
{
"rateLimitSchedule": {
"timezone": "Europe/Warsaw",
"weekStartDay": "Mon",
"resetTime": "09:00"
},
"roles": {}
}
```

#### roles.<role_name>.limits

Use to define token usage limits for resources.
Expand All @@ -68,12 +93,14 @@ Use to define token usage limits for resources.

Available values:

* `requestHour`: Total requests per hour that can be sent to a specific resource.
* `requestDay`: Total requests per day that can be sent to a specific resource.
* `requestHour`: Total requests per hour that can be sent to a specific resource, managed via floating window approach for well-distributed rate limiting.
* `requestDay`: Total requests per day that can be sent to a specific resource, managed via a fixed calendar window - see below.
* `minute`: Total tokens per minute that can be sent to a specific resource, managed via floating window approach for well-distributed rate limiting.
* `day`: Total tokens per day that can be sent to a specific resource, managed via floating window approach for balanced rate limiting.
* `week`: Total tokens per week that can be sent to a specific resource, managed via floating window approach for balanced rate limiting.
* `month`: Total tokens per month that can be sent to a specific resource, managed via floating window approach for balanced rate limiting.
* `day`: Total tokens per day that can be sent to a specific resource, managed via a fixed calendar window - see below.
* `week`: Total tokens per week that can be sent to a specific resource, managed via a fixed calendar window - see below.
* `month`: Total tokens per month that can be sent to a specific resource, managed via a fixed calendar window - see below.

**Fixed calendar windows:** unlike `minute`/`requestHour`, which age usage out gradually as time advances, `day`/`week`/`month`/`requestDay` reset all at once at a deterministic boundary aligned to the calendar (e.g. "this month" is the 1st through the last day of the calendar month), so usage cannot exceed the limit until the next boundary regardless of when within the period it accrued. The boundary is anchored to the deployment-wide `rateLimitSchedule` top-level config setting (timezone, week start day, and daily reset time); omitting it defaults to UTC, Monday, 00:00, which reproduces the previous implicit UTC-midnight day/month behavior. The bulk limits/usage endpoints (`GET /v1/deployments/{deployment_id}/limits`, `GET /v1/user/limits`, `GET /v1/user/usage`) report this boundary as `resetsAt` on each fixed-window entry; `minute`/`requestHour` entries omit it, since a floating window has no single reset instant.

**Requests served through a translator:** a model interface configured with `"mode": "translator"` does not add its tokens to these limits — the translator calls DIAL Core back to have the completion served, and that second call is what carries the usage, so it is counted once rather than twice. The limits are still checked before a translated request is forwarded, so an exhausted quota blocks it like any other request. `requestHour` and `requestDay` work the same way: the translated call is checked against them but spends no slot, so one client request never consumes two. Refer to [translators](translators.md#limits-and-a-translated-request).

Expand Down
60 changes: 54 additions & 6 deletions docs/open_api_core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12791,7 +12791,7 @@ paths:
summary: /v1/user/limits
operationId: getUserLimits
description: |
Returns limits and current rolling usage for every deployment available to the authenticated caller -
Returns limits and current usage for every deployment available to the authenticated caller -
a JWT user or an API-key project. It replaces calling `/v1/deployments/{deployment_name}/limits` once
per deployment, and one response labels an entire model picker, so switching model needs no refetch.

Expand All @@ -12814,11 +12814,20 @@ paths:
(`Long.MAX_VALUE`), meaning unlimited. That value exceeds JavaScript's `Number.MAX_SAFE_INTEGER`
(`9007199254740991`), so treat any `total` at or above 2^53 as unlimited rather than rendering it
as a `used / total` ratio.
5. **Every window is trailing, not calendar-aligned.** `day` is the last 24 hours, `week` the last 7
days, `month` the last 30 days - never "since midnight" or "since the 1st". As a result `used`
decreases on its own as older activity ages out; there is no refund and no periodic reset.
Eviction happens in steps on UTC boundaries at each window's granularity (1 hour for `day`, 1 day
for `week` and `month`).
5. **`minute` and `hour` windows are trailing; `day`, `week` and `month` are calendar-aligned.**
`minuteTokenStats`/`minuteCostStats` and `hourRequestStats` are a trailing window - the last 60
seconds or 60 minutes - so their `used` decreases on its own as older activity ages out, with no
periodic reset. `day`/`week`/`month` windows instead reset all at once at a deterministic
boundary (e.g. "since midnight" or "since the 1st") anchored to the deployment-wide
`rateLimitSchedule` config setting (default: UTC, Monday, 00:00) - `used` only ever grows within a
period, then drops to zero at the next boundary.
6. **`resetsAt` names that boundary, only for calendar-aligned windows.** `dayTokenStats`,
`weekTokenStats`, `monthTokenStats`, `dayRequestStats`, `dayCostStats`, `weekCostStats` and
`monthCostStats` carry a `resetsAt` field - an absolute ISO-8601 instant (e.g.
`"2026-10-01T00:00:00+02:00"`), not a countdown - for when that window's usage resets next.
`minuteTokenStats`, `minuteCostStats` and `hourRequestStats` omit the field entirely rather than
serializing it as `null`: a trailing window has no single reset instant to report, so clients
should treat a missing `resetsAt` as a permanent, expected omission rather than poll for it.

Per-deployment spend does not reconcile to the global figure. Attribution starts at rollout and does
not back-fill, and a model without `pricing` never contributes while still consuming tokens, so the
Expand Down Expand Up @@ -12846,30 +12855,37 @@ paths:
dayTokenStats:
total: 10000000
used: 42000
resetsAt: 2026-09-10T00:00:00Z
weekTokenStats:
total: 9223372036854775807
used: 180000
resetsAt: 2026-09-14T00:00:00Z
monthTokenStats:
total: 9223372036854775807
used: 640000
resetsAt: 2026-10-01T00:00:00Z
hourRequestStats:
total: 9223372036854775807
used: 3
dayRequestStats:
total: 9223372036854775807
used: 12
resetsAt: 2026-09-10T00:00:00Z
minuteCostStats:
total: 9223372036854775807
used: 0.02
dayCostStats:
total: 9223372036854775807
used: 1.85
resetsAt: 2026-09-10T00:00:00Z
weekCostStats:
total: 9223372036854775807
used: 6.2
resetsAt: 2026-09-14T00:00:00Z
monthCostStats:
total: 9223372036854775807
used: 21.4
resetsAt: 2026-10-01T00:00:00Z
gpt-4:
minuteTokenStats:
total: 100000
Expand Down Expand Up @@ -13062,30 +13078,37 @@ paths:
dayTokenStats:
total: 10000000
used: 42000
resetsAt: 2026-09-10T00:00:00Z
weekTokenStats:
total: 9223372036854775807
used: 180000
resetsAt: 2026-09-14T00:00:00Z
monthTokenStats:
total: 9223372036854775807
used: 640000
resetsAt: 2026-10-01T00:00:00Z
hourRequestStats:
total: 9223372036854775807
used: 3
dayRequestStats:
total: 9223372036854775807
used: 12
resetsAt: 2026-09-10T00:00:00Z
minuteCostStats:
total: 9223372036854775807
used: 0.02
dayCostStats:
total: 9223372036854775807
used: 1.85
resetsAt: 2026-09-10T00:00:00Z
weekCostStats:
total: 9223372036854775807
used: 6.2
resetsAt: 2026-09-14T00:00:00Z
monthCostStats:
total: 9223372036854775807
used: 21.4
resetsAt: 2026-10-01T00:00:00Z
minuteCostStats:
total: 10.0
used: 0.4
Expand Down Expand Up @@ -15398,6 +15421,8 @@ components:
type: string
translators:
$ref: "#/components/schemas/MapStringTranslator"
rateLimitSchedule:
$ref: "#/components/schemas/RateLimitSchedule"
ConfigFileMigrateRequest:
type: object
properties:
Expand Down Expand Up @@ -15523,6 +15548,8 @@ components:
type: number
used:
type: number
resetsAt:
type: string
CostLimit:
type: object
properties:
Expand Down Expand Up @@ -16288,6 +16315,8 @@ components:
type: integer
used:
type: integer
resetsAt:
type: string
JsonNode:
type: object
Key:
Expand Down Expand Up @@ -16942,6 +16971,15 @@ components:
- PENDING
- APPROVED
- REJECTED
RateLimitSchedule:
type: object
properties:
resetTime:
type: string
timezone:
type: string
weekStartDay:
$ref: "#/components/schemas/WeekDay"
RateRequest:
required:
- rate
Expand Down Expand Up @@ -17999,6 +18037,16 @@ components:
- SKIPPED
ValueNode:
type: object
WeekDay:
type: string
enum:
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
securitySchemes:
ApiKeyAuth:
type: apiKey
Expand Down
5 changes: 5 additions & 0 deletions sample/aidial.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@
"role": "default"
}
},
"rateLimitSchedule": {
"timezone": "UTC",
"weekStartDay": "Mon",
"resetTime": "00:00"
},
"roles": {
"default": {
"limits": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ vertx, settings("config"), null,
RuleService ruleService = new RuleService(resourceService);
AccessService accessService = new AccessService(encryptionService, shareService, ruleService, applicationSchemaService, settings("access"));
NotificationService notificationService = new NotificationService(resourceService, encryptionService);
RateLimiter rateLimiter = new RateLimiter(taskExecutor, resourceService);
RateLimiter rateLimiter = new RateLimiter(taskExecutor, resourceService, configStore);
CodeInterpreterService codeInterpreterService = new CodeInterpreterService(vertx, taskExecutor, redis, resourceService,
accessService, encryptionService, operatorService, generator, settings("codeInterpreter"));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.epam.aidial.core.server.data;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

import java.math.BigDecimal;
Expand All @@ -11,4 +12,11 @@
public class CostItemLimitStats {
private BigDecimal total = BigDecimal.ZERO;
private BigDecimal used = BigDecimal.ZERO;
}
/**
* Absolute instant this window's usage resets, as an ISO-8601 offset date-time - present only
* for the fixed calendar windows (DAY/WEEK/MONTH), omitted for the floating ones (MINUTE/HOUR),
* which have no single reset instant to report.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String resetsAt;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package com.epam.aidial.core.server.data;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

@Data
public class ItemLimitStats {
private long total;
private long used;
/**
* Absolute instant this window's usage resets, as an ISO-8601 offset date-time - present only
* for the fixed calendar windows (DAY/WEEK/MONTH), omitted for the floating ones (MINUTE/HOUR),
* which have no single reset instant to report.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String resetsAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.epam.aidial.core.server.limiter;

/**
* A fixed calendar rate-limit window: usage resets all at once at a deterministic boundary
* computed from the deployment-wide {@link com.epam.aidial.core.config.RateLimitSchedule}, unlike
* {@link RateWindow}'s MINUTE/HOUR, which keep aging usage out gradually.
*/
public enum CalendarPeriod {
DAY,
WEEK,
MONTH
}
Loading