kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: improve rate-limits support
rodzic
eda5915fe9
commit
0216b3612c
|
@ -1,7 +1,9 @@
|
||||||
import {
|
import {
|
||||||
agenticProjectConfigSchema,
|
agenticProjectConfigSchema,
|
||||||
|
defaultRequestsRateLimit,
|
||||||
type OriginAdapter,
|
type OriginAdapter,
|
||||||
type PricingPlanList,
|
type PricingPlanList,
|
||||||
|
type RateLimit,
|
||||||
resolvedAgenticProjectConfigSchema,
|
resolvedAgenticProjectConfigSchema,
|
||||||
type Tool,
|
type Tool,
|
||||||
type ToolConfig
|
type ToolConfig
|
||||||
|
@ -92,7 +94,16 @@ export const deployments = pgTable(
|
||||||
pricingPlans: jsonb().$type<PricingPlanList>().notNull(),
|
pricingPlans: jsonb().$type<PricingPlanList>().notNull(),
|
||||||
|
|
||||||
// Which pricing intervals are supported for subscriptions to this project
|
// Which pricing intervals are supported for subscriptions to this project
|
||||||
pricingIntervals: pricingIntervalEnum().array().default(['month']).notNull()
|
pricingIntervals: pricingIntervalEnum()
|
||||||
|
.array()
|
||||||
|
.default(['month'])
|
||||||
|
.notNull(),
|
||||||
|
|
||||||
|
// Default rate limit across all pricing plans
|
||||||
|
defaultRateLimit: jsonb()
|
||||||
|
.$type<RateLimit>()
|
||||||
|
.notNull()
|
||||||
|
.default(defaultRequestsRateLimit)
|
||||||
|
|
||||||
// TODO: metadata config (logo, keywords, examples, etc)
|
// TODO: metadata config (logo, keywords, examples, etc)
|
||||||
// TODO: webhooks
|
// TODO: webhooks
|
||||||
|
@ -159,7 +170,8 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||||
pricingPlans: resolvedAgenticProjectConfigSchema.shape.pricingPlans,
|
pricingPlans: resolvedAgenticProjectConfigSchema.shape.pricingPlans,
|
||||||
pricingIntervals: resolvedAgenticProjectConfigSchema.shape.pricingIntervals,
|
pricingIntervals: resolvedAgenticProjectConfigSchema.shape.pricingIntervals,
|
||||||
tools: resolvedAgenticProjectConfigSchema.shape.tools,
|
tools: resolvedAgenticProjectConfigSchema.shape.tools,
|
||||||
toolConfigs: resolvedAgenticProjectConfigSchema.shape.toolConfigs
|
toolConfigs: resolvedAgenticProjectConfigSchema.shape.toolConfigs,
|
||||||
|
defaultRateLimit: resolvedAgenticProjectConfigSchema.shape.defaultRateLimit
|
||||||
})
|
})
|
||||||
.omit({
|
.omit({
|
||||||
originUrl: true
|
originUrl: true
|
||||||
|
|
|
@ -16,7 +16,7 @@ const fixtures = [
|
||||||
// 'pricing-3-plans',
|
// 'pricing-3-plans',
|
||||||
// 'pricing-monthly-annual',
|
// 'pricing-monthly-annual',
|
||||||
// 'pricing-custom-0',
|
// 'pricing-custom-0',
|
||||||
// 'basic-openapi',
|
'basic-openapi',
|
||||||
'basic-mcp',
|
'basic-mcp',
|
||||||
'everything-openapi'
|
'everything-openapi'
|
||||||
]
|
]
|
||||||
|
|
|
@ -63,6 +63,7 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
|
||||||
fixtures: [
|
fixtures: [
|
||||||
{
|
{
|
||||||
path: '@dev/test-basic-openapi/getPost',
|
path: '@dev/test-basic-openapi/getPost',
|
||||||
|
only: true,
|
||||||
request: {
|
request: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
json: {
|
json: {
|
||||||
|
|
|
@ -19,7 +19,7 @@ const globalRateLimitCache: RateLimitCache = new Map()
|
||||||
export async function enforceRateLimit({
|
export async function enforceRateLimit({
|
||||||
id,
|
id,
|
||||||
interval,
|
interval,
|
||||||
maxPerInterval,
|
limit,
|
||||||
cost = 1,
|
cost = 1,
|
||||||
async = true,
|
async = true,
|
||||||
env,
|
env,
|
||||||
|
@ -39,7 +39,7 @@ export async function enforceRateLimit({
|
||||||
/**
|
/**
|
||||||
* Maximum number of requests that can be made per interval.
|
* Maximum number of requests that can be made per interval.
|
||||||
*/
|
*/
|
||||||
maxPerInterval: number
|
limit: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cost of the request.
|
* The cost of the request.
|
||||||
|
@ -84,18 +84,15 @@ export async function enforceRateLimit({
|
||||||
* and we can skip the request to the durable object entirely, which speeds
|
* and we can skip the request to the durable object entirely, which speeds
|
||||||
* everything up and is cheaper for us.
|
* everything up and is cheaper for us.
|
||||||
*/
|
*/
|
||||||
if (
|
if (rateLimitState.current > limit && now <= rateLimitState.resetTimeMs) {
|
||||||
rateLimitState.current > maxPerInterval &&
|
|
||||||
now <= rateLimitState.resetTimeMs
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
passed: false,
|
passed: false,
|
||||||
current: rateLimitState.current,
|
current: rateLimitState.current,
|
||||||
limit: maxPerInterval,
|
limit,
|
||||||
resetTimeMs: rateLimitState.resetTimeMs,
|
resetTimeMs: rateLimitState.resetTimeMs,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
remaining: Math.max(0, maxPerInterval - rateLimitState.current)
|
remaining: Math.max(0, limit - rateLimitState.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,11 +126,11 @@ export async function enforceRateLimit({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
passed: rateLimitState.current <= maxPerInterval,
|
passed: rateLimitState.current <= limit,
|
||||||
current: rateLimitState.current,
|
current: rateLimitState.current,
|
||||||
limit: maxPerInterval,
|
limit,
|
||||||
resetTimeMs: rateLimitState.resetTimeMs,
|
resetTimeMs: rateLimitState.resetTimeMs,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
remaining: Math.max(0, maxPerInterval - rateLimitState.current)
|
remaining: Math.max(0, limit - rateLimitState.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
AdminDeployment,
|
AdminDeployment,
|
||||||
PricingPlan,
|
PricingPlan,
|
||||||
RateLimit,
|
|
||||||
Tool
|
Tool
|
||||||
} from '@agentic/platform-types'
|
} from '@agentic/platform-types'
|
||||||
import type { DurableObjectStub } from '@cloudflare/workers-types'
|
import type { DurableObjectStub } from '@cloudflare/workers-types'
|
||||||
|
@ -61,27 +60,23 @@ export async function resolveOriginToolCall({
|
||||||
const { originAdapter } = deployment
|
const { originAdapter } = deployment
|
||||||
// TODO: make this configurable via `ToolConfig.cost`
|
// TODO: make this configurable via `ToolConfig.cost`
|
||||||
const numRequestsCost = 1
|
const numRequestsCost = 1
|
||||||
|
let rateLimit = deployment.defaultRateLimit
|
||||||
let rateLimitResult: RateLimitResult | undefined
|
let rateLimitResult: RateLimitResult | undefined
|
||||||
let rateLimit: RateLimit | undefined | null
|
|
||||||
let cacheStatus: CacheStatus | undefined
|
let cacheStatus: CacheStatus | undefined
|
||||||
let reportUsage = true
|
let reportUsage = true
|
||||||
|
|
||||||
// Resolve rate limit and whether to report `requests` usage based on the
|
// Resolve rate limit and whether to report `requests` usage based on the
|
||||||
// customer's pricing plan and deployment config.
|
// customer's pricing plan and deployment config.
|
||||||
if (pricingPlan) {
|
if (pricingPlan) {
|
||||||
|
if (pricingPlan.rateLimit) {
|
||||||
|
rateLimit = pricingPlan.rateLimit
|
||||||
|
}
|
||||||
|
|
||||||
const requestsLineItem = pricingPlan.lineItems.find(
|
const requestsLineItem = pricingPlan.lineItems.find(
|
||||||
(lineItem) => lineItem.slug === 'requests'
|
(lineItem) => lineItem.slug === 'requests'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (requestsLineItem) {
|
if (!requestsLineItem) {
|
||||||
assert(
|
|
||||||
requestsLineItem.slug === 'requests',
|
|
||||||
403,
|
|
||||||
`Invalid pricing plan "${pricingPlan.slug}" for project "${deployment.project}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
rateLimit = requestsLineItem.rateLimit
|
|
||||||
} else {
|
|
||||||
// No `requests` line-item, so we don't report usage for this tool.
|
// No `requests` line-item, so we don't report usage for this tool.
|
||||||
reportUsage = false
|
reportUsage = false
|
||||||
}
|
}
|
||||||
|
@ -97,8 +92,7 @@ export async function resolveOriginToolCall({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolConfig.rateLimit !== undefined) {
|
if (toolConfig.rateLimit !== undefined) {
|
||||||
// TODO: Improve RateLimitInput vs RateLimit types
|
rateLimit = toolConfig.rateLimit
|
||||||
rateLimit = toolConfig.rateLimit as RateLimit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheControl) {
|
if (cacheControl) {
|
||||||
|
@ -149,8 +143,7 @@ export async function resolveOriginToolCall({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pricingPlanToolOverride.rateLimit !== undefined) {
|
if (pricingPlanToolOverride.rateLimit !== undefined) {
|
||||||
// TODO: Improve RateLimitInput vs RateLimit types
|
rateLimit = pricingPlanToolOverride.rateLimit
|
||||||
rateLimit = pricingPlanToolOverride.rateLimit as RateLimit
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert(isToolConfigEnabled, 404, `Tool "${tool.name}" is disabled`)
|
assert(isToolConfigEnabled, 404, `Tool "${tool.name}" is disabled`)
|
||||||
|
@ -171,13 +164,13 @@ export async function resolveOriginToolCall({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rateLimit) {
|
if (rateLimit && rateLimit.enabled !== false) {
|
||||||
// TODO: Consider decrementing rate limit if the response is cached or
|
// TODO: Consider decrementing rate limit if the response is cached or
|
||||||
// errors? this doesn't seem too important, so will leave as-is for now.
|
// errors? this doesn't seem too important, so will leave as-is for now.
|
||||||
rateLimitResult = await enforceRateLimit({
|
rateLimitResult = await enforceRateLimit({
|
||||||
id: consumer?.id ?? ip ?? sessionId,
|
id: consumer?.id ?? ip ?? sessionId,
|
||||||
interval: rateLimit.interval,
|
interval: rateLimit.interval,
|
||||||
maxPerInterval: rateLimit.maxPerInterval,
|
limit: rateLimit.limit,
|
||||||
async: rateLimit.async,
|
async: rateLimit.async,
|
||||||
cost: numRequestsCost,
|
cost: numRequestsCost,
|
||||||
env,
|
env,
|
||||||
|
@ -224,10 +217,12 @@ export async function resolveOriginToolCall({
|
||||||
cacheKey,
|
cacheKey,
|
||||||
fetchResponse: async () => {
|
fetchResponse: async () => {
|
||||||
let response = await fetch(originRequest)
|
let response = await fetch(originRequest)
|
||||||
|
|
||||||
if (cacheControl && isResponsePubliclyCacheable(response)) {
|
if (cacheControl && isResponsePubliclyCacheable(response)) {
|
||||||
response = new Response(response.body, response)
|
response = new Response(response.body, response)
|
||||||
response.headers.set('cache-control', cacheControl)
|
response.headers.set('cache-control', cacheControl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
waitUntil
|
waitUntil
|
||||||
|
@ -244,6 +239,7 @@ export async function resolveOriginToolCall({
|
||||||
return {
|
return {
|
||||||
cacheStatus,
|
cacheStatus,
|
||||||
reportUsage,
|
reportUsage,
|
||||||
|
rateLimit,
|
||||||
rateLimitResult,
|
rateLimitResult,
|
||||||
toolCallArgs,
|
toolCallArgs,
|
||||||
originRequest,
|
originRequest,
|
||||||
|
@ -312,6 +308,7 @@ export async function resolveOriginToolCall({
|
||||||
return {
|
return {
|
||||||
cacheStatus: 'HIT',
|
cacheStatus: 'HIT',
|
||||||
reportUsage,
|
reportUsage,
|
||||||
|
rateLimit,
|
||||||
rateLimitResult,
|
rateLimitResult,
|
||||||
toolCallArgs,
|
toolCallArgs,
|
||||||
toolCallResponse: (await response.json()) as McpToolCallResponse,
|
toolCallResponse: (await response.json()) as McpToolCallResponse,
|
||||||
|
@ -346,6 +343,7 @@ export async function resolveOriginToolCall({
|
||||||
return {
|
return {
|
||||||
cacheStatus: cacheStatus ?? (cacheKey ? 'MISS' : 'BYPASS'),
|
cacheStatus: cacheStatus ?? (cacheKey ? 'MISS' : 'BYPASS'),
|
||||||
reportUsage,
|
reportUsage,
|
||||||
|
rateLimit,
|
||||||
rateLimitResult,
|
rateLimitResult,
|
||||||
toolCallArgs,
|
toolCallArgs,
|
||||||
toolCallResponse,
|
toolCallResponse,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
} from '@agentic/platform-hono'
|
} from '@agentic/platform-hono'
|
||||||
import type {
|
import type {
|
||||||
AdminConsumer as AdminConsumerImpl,
|
AdminConsumer as AdminConsumerImpl,
|
||||||
|
RateLimit,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
User
|
User
|
||||||
} from '@agentic/platform-types'
|
} from '@agentic/platform-types'
|
||||||
|
@ -65,6 +66,7 @@ export type ResolvedOriginToolCallResult = {
|
||||||
originRequest?: Request
|
originRequest?: Request
|
||||||
originResponse?: Response
|
originResponse?: Response
|
||||||
toolCallResponse?: McpToolCallResponse
|
toolCallResponse?: McpToolCallResponse
|
||||||
|
rateLimit?: RateLimit
|
||||||
rateLimitResult?: RateLimitResult
|
rateLimitResult?: RateLimitResult
|
||||||
cacheStatus: CacheStatus
|
cacheStatus: CacheStatus
|
||||||
reportUsage: boolean
|
reportUsage: boolean
|
||||||
|
|
|
@ -13,16 +13,35 @@ export function getRateLimitHeaders(
|
||||||
|
|
||||||
const { id, limit, remaining, resetTimeMs, intervalMs } = rateLimitResult
|
const { id, limit, remaining, resetTimeMs, intervalMs } = rateLimitResult
|
||||||
const intervalSeconds = Math.ceil(intervalMs / 1000)
|
const intervalSeconds = Math.ceil(intervalMs / 1000)
|
||||||
const retryAfterSeconds = Math.max(
|
const resetTimeSeconds = Math.ceil(resetTimeMs / 1000)
|
||||||
0,
|
|
||||||
Math.ceil((resetTimeMs - Date.now()) / 1000)
|
|
||||||
)
|
|
||||||
|
|
||||||
headers['RateLimit-Policy'] = `${limit};w=${intervalSeconds}`
|
const rateLimitPolicy = `${limit};w=${intervalSeconds}`
|
||||||
headers['RateLimit-Limit'] = limit.toString()
|
const limitString = limit.toString()
|
||||||
headers['RateLimit-Remaining'] = remaining.toString()
|
const remainingString = remaining.toString()
|
||||||
headers['Retry-After'] = retryAfterSeconds.toString()
|
const resetTimeString = resetTimeSeconds.toString()
|
||||||
headers['X-RateLimit-Id'] = id
|
|
||||||
|
// NOTE: Cloudflare and/or origin servers like to set the x- headers, which
|
||||||
|
// can be pretty confusing since the end user gets both ratelimit headers.
|
||||||
|
// I'm hesitant to remove any extra origin headers, since they're a nice
|
||||||
|
// escape hatch for sending extra metadata, and the origin may in fact have
|
||||||
|
// its own separate rate-limiting policy, which we don't necessarily want to
|
||||||
|
// hide. So for now, we'll just set the standard rate-limit headers and make
|
||||||
|
// sure this distinction is documented.
|
||||||
|
headers['ratelimit-policy'] = rateLimitPolicy
|
||||||
|
headers['ratelimit-limit'] = limitString
|
||||||
|
headers['ratelimit-remaining'] = remainingString
|
||||||
|
headers['ratelimit-reset'] = resetTimeString
|
||||||
|
headers['x-ratelimit-id'] = id
|
||||||
|
|
||||||
|
if (!rateLimitResult.passed) {
|
||||||
|
const retryAfterSeconds = Math.max(
|
||||||
|
0,
|
||||||
|
Math.ceil((resetTimeMs - Date.now()) / 1000)
|
||||||
|
)
|
||||||
|
const retryAfterString = retryAfterSeconds.toString()
|
||||||
|
|
||||||
|
headers['retry-after'] = retryAfterString
|
||||||
|
}
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
||||||
pure: true,
|
pure: true,
|
||||||
// cacheControl: 'no-cache',
|
// cacheControl: 'no-cache',
|
||||||
reportUsage: true,
|
reportUsage: true,
|
||||||
rateLimit: null,
|
rateLimit: { enabled: false },
|
||||||
pricingPlanOverridesMap: {
|
pricingPlanOverridesMap: {
|
||||||
free: {
|
free: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -59,12 +59,12 @@ export default defineConfig({
|
||||||
name: 'custom_rate_limit_tool',
|
name: 'custom_rate_limit_tool',
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
interval: '30s',
|
interval: '30s',
|
||||||
maxPerInterval: 10
|
limit: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'disabled_rate_limit_tool',
|
name: 'disabled_rate_limit_tool',
|
||||||
rateLimit: null
|
rateLimit: { enabled: false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'strict_additional_properties',
|
name: 'strict_additional_properties',
|
||||||
|
|
|
@ -30,15 +30,15 @@ export default defineConfig({
|
||||||
slug: 'custom-test',
|
slug: 'custom-test',
|
||||||
usageType: 'metered',
|
usageType: 'metered',
|
||||||
billingScheme: 'per_unit',
|
billingScheme: 'per_unit',
|
||||||
unitAmount: 100,
|
unitAmount: 100
|
||||||
rateLimit: {
|
|
||||||
maxPerInterval: 1000,
|
|
||||||
// TODO
|
|
||||||
// interval: '30d' // 60 * 60 * 24 * 30 // 30 days in seconds
|
|
||||||
interval: 60 * 60 * 24 * 30 // 30 days in seconds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
rateLimit: {
|
||||||
|
limit: 1000,
|
||||||
|
// TODO
|
||||||
|
// interval: '30d' // 60 * 60 * 24 * 30 // 30 days in seconds
|
||||||
|
interval: 60 * 60 * 24 * 30 // 30 days in seconds
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Basic Annual',
|
name: 'Basic Annual',
|
||||||
|
@ -54,13 +54,13 @@ export default defineConfig({
|
||||||
slug: 'custom-test',
|
slug: 'custom-test',
|
||||||
usageType: 'metered',
|
usageType: 'metered',
|
||||||
billingScheme: 'per_unit',
|
billingScheme: 'per_unit',
|
||||||
unitAmount: 80, // 20% discount
|
unitAmount: 80 // 20% discount
|
||||||
rateLimit: {
|
|
||||||
maxPerInterval: 1500,
|
|
||||||
interval: 60 * 60 * 24 * 30 // 30 days in seconds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
rateLimit: {
|
||||||
|
limit: 1500,
|
||||||
|
interval: 60 * 60 * 24 * 30 // 30 days in seconds
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,14 +17,14 @@ export default defineConfig({
|
||||||
slug: 'requests',
|
slug: 'requests',
|
||||||
usageType: 'metered',
|
usageType: 'metered',
|
||||||
billingScheme: 'per_unit',
|
billingScheme: 'per_unit',
|
||||||
unitAmount: 0,
|
unitAmount: 0
|
||||||
// Free but limited to 20 requests per day
|
// Free but limited to 20 requests per day
|
||||||
rateLimit: {
|
|
||||||
maxPerInterval: 20,
|
|
||||||
interval: 60 * 60 * 24 // 1 day in seconds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
rateLimit: {
|
||||||
|
limit: 20,
|
||||||
|
interval: 60 * 60 * 24 // 1 day in seconds
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pay-As-You-Go',
|
name: 'Pay-As-You-Go',
|
||||||
|
@ -48,7 +48,11 @@ export default defineConfig({
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
rateLimit: {
|
||||||
|
limit: 1000,
|
||||||
|
interval: '1d'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
exports[`loadAgenticConfig > basic-mcp 1`] = `
|
exports[`loadAgenticConfig > basic-mcp 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-basic-mcp",
|
"name": "test-basic-mcp",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -21,6 +27,12 @@ exports[`loadAgenticConfig > basic-mcp 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -31,6 +43,12 @@ exports[`loadAgenticConfig > basic-mcp 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > basic-openapi 1`] = `
|
exports[`loadAgenticConfig > basic-openapi 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-basic-openapi",
|
"name": "test-basic-openapi",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -51,6 +69,12 @@ exports[`loadAgenticConfig > basic-openapi 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -70,6 +94,12 @@ exports[`loadAgenticConfig > basic-openapi 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-basic-raw-free-json",
|
"name": "test-basic-raw-free-json",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -89,6 +119,12 @@ exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -99,6 +135,12 @@ exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-basic-raw-free-ts",
|
"name": "test-basic-raw-free-ts",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -118,6 +160,12 @@ exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -128,6 +176,12 @@ exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > everything-openapi 1`] = `
|
exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-everything-openapi",
|
"name": "test-everything-openapi",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -148,6 +202,12 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -162,7 +222,9 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"pure": true,
|
"pure": true,
|
||||||
"rateLimit": null,
|
"rateLimit": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -201,13 +263,16 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
"name": "custom_rate_limit_tool",
|
"name": "custom_rate_limit_tool",
|
||||||
"rateLimit": {
|
"rateLimit": {
|
||||||
"async": true,
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
"interval": 30,
|
"interval": 30,
|
||||||
"maxPerInterval": 10,
|
"limit": 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "disabled_rate_limit_tool",
|
"name": "disabled_rate_limit_tool",
|
||||||
"rateLimit": null,
|
"rateLimit": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputSchemaAdditionalProperties": false,
|
"inputSchemaAdditionalProperties": false,
|
||||||
|
@ -253,6 +318,12 @@ exports[`loadAgenticConfig > invalid: pricing-empty-2 1`] = `[ZodValidationError
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-3-plans 1`] = `
|
exports[`loadAgenticConfig > pricing-3-plans 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-pricing-3-plans",
|
"name": "test-pricing-3-plans",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -321,6 +392,12 @@ exports[`loadAgenticConfig > pricing-3-plans 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-custom-0 1`] = `
|
exports[`loadAgenticConfig > pricing-custom-0 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-pricing-custom-0",
|
"name": "test-pricing-custom-0",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -353,17 +430,18 @@ exports[`loadAgenticConfig > pricing-custom-0 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"billingScheme": "per_unit",
|
"billingScheme": "per_unit",
|
||||||
"rateLimit": {
|
|
||||||
"async": true,
|
|
||||||
"interval": 2592000,
|
|
||||||
"maxPerInterval": 1000,
|
|
||||||
},
|
|
||||||
"slug": "custom-test",
|
"slug": "custom-test",
|
||||||
"unitAmount": 100,
|
"unitAmount": 100,
|
||||||
"usageType": "metered",
|
"usageType": "metered",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Basic Monthly",
|
"name": "Basic Monthly",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 2592000,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "basic-monthly",
|
"slug": "basic-monthly",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -376,17 +454,18 @@ exports[`loadAgenticConfig > pricing-custom-0 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"billingScheme": "per_unit",
|
"billingScheme": "per_unit",
|
||||||
"rateLimit": {
|
|
||||||
"async": true,
|
|
||||||
"interval": 2592000,
|
|
||||||
"maxPerInterval": 1500,
|
|
||||||
},
|
|
||||||
"slug": "custom-test",
|
"slug": "custom-test",
|
||||||
"unitAmount": 80,
|
"unitAmount": 80,
|
||||||
"usageType": "metered",
|
"usageType": "metered",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Basic Annual",
|
"name": "Basic Annual",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 2592000,
|
||||||
|
"limit": 1500,
|
||||||
|
},
|
||||||
"slug": "basic-annual",
|
"slug": "basic-annual",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -397,6 +476,12 @@ exports[`loadAgenticConfig > pricing-custom-0 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-pricing-freemium",
|
"name": "test-pricing-freemium",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -416,6 +501,12 @@ exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -439,6 +530,12 @@ exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-monthly-annual 1`] = `
|
exports[`loadAgenticConfig > pricing-monthly-annual 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-pricing-monthly-annual",
|
"name": "test-pricing-monthly-annual",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -493,6 +590,12 @@ exports[`loadAgenticConfig > pricing-monthly-annual 1`] = `
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-pay-as-you-go 1`] = `
|
exports[`loadAgenticConfig > pricing-pay-as-you-go 1`] = `
|
||||||
{
|
{
|
||||||
|
"defaultRateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 60,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"name": "test-pricing-pay-as-you-go",
|
"name": "test-pricing-pay-as-you-go",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
|
@ -512,17 +615,18 @@ exports[`loadAgenticConfig > pricing-pay-as-you-go 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"billingScheme": "per_unit",
|
"billingScheme": "per_unit",
|
||||||
"rateLimit": {
|
|
||||||
"async": true,
|
|
||||||
"interval": 86400,
|
|
||||||
"maxPerInterval": 20,
|
|
||||||
},
|
|
||||||
"slug": "requests",
|
"slug": "requests",
|
||||||
"unitAmount": 0,
|
"unitAmount": 0,
|
||||||
"usageType": "metered",
|
"usageType": "metered",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 86400,
|
||||||
|
"limit": 20,
|
||||||
|
},
|
||||||
"slug": "free",
|
"slug": "free",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -546,6 +650,12 @@ exports[`loadAgenticConfig > pricing-pay-as-you-go 1`] = `
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Pay-As-You-Go",
|
"name": "Pay-As-You-Go",
|
||||||
|
"rateLimit": {
|
||||||
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 86400,
|
||||||
|
"limit": 1000,
|
||||||
|
},
|
||||||
"slug": "pay-as-you-go",
|
"slug": "pay-as-you-go",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,13 +8,12 @@ exports[`rateLimitSchema invalid 1`] = `
|
||||||
{
|
{
|
||||||
"issues": [
|
"issues": [
|
||||||
{
|
{
|
||||||
"code": "invalid_type",
|
"code": "invalid_literal",
|
||||||
"expected": "number",
|
"expected": false,
|
||||||
"received": "string",
|
|
||||||
"path": [
|
"path": [
|
||||||
"interval"
|
"enabled"
|
||||||
],
|
],
|
||||||
"message": "Expected number, received string"
|
"message": "Invalid literal value, expected false"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "ZodError"
|
"name": "ZodError"
|
||||||
|
@ -22,23 +21,49 @@ exports[`rateLimitSchema invalid 1`] = `
|
||||||
{
|
{
|
||||||
"issues": [
|
"issues": [
|
||||||
{
|
{
|
||||||
"code": "too_small",
|
"code": "invalid_union",
|
||||||
"minimum": 1,
|
"unionErrors": [
|
||||||
"type": "string",
|
{
|
||||||
"inclusive": true,
|
"issues": [
|
||||||
"exact": false,
|
{
|
||||||
"message": "String must contain at least 1 character(s)",
|
"code": "invalid_type",
|
||||||
|
"expected": "number",
|
||||||
|
"received": "string",
|
||||||
|
"path": [
|
||||||
|
"interval"
|
||||||
|
],
|
||||||
|
"message": "Expected number, received string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "ZodError"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"code": "too_small",
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "string",
|
||||||
|
"inclusive": true,
|
||||||
|
"exact": false,
|
||||||
|
"message": "String must contain at least 1 character(s)",
|
||||||
|
"path": [
|
||||||
|
"interval"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "ZodError"
|
||||||
|
}
|
||||||
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"interval"
|
"interval"
|
||||||
]
|
],
|
||||||
|
"message": "Invalid input"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "ZodError"
|
"name": "ZodError"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [],
|
||||||
"interval"
|
|
||||||
],
|
|
||||||
"message": "Invalid input"
|
"message": "Invalid input"
|
||||||
}
|
}
|
||||||
]]
|
]]
|
||||||
|
@ -83,7 +108,7 @@ exports[`rateLimitSchema invalid 4`] = `
|
||||||
"exact": false,
|
"exact": false,
|
||||||
"message": "Number must be greater than or equal to 0",
|
"message": "Number must be greater than or equal to 0",
|
||||||
"path": [
|
"path": [
|
||||||
"maxPerInterval"
|
"limit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]]
|
]]
|
||||||
|
@ -92,23 +117,38 @@ exports[`rateLimitSchema invalid 4`] = `
|
||||||
exports[`rateLimitSchema valid 1`] = `
|
exports[`rateLimitSchema valid 1`] = `
|
||||||
{
|
{
|
||||||
"async": true,
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
"maxPerInterval": 100,
|
"limit": 100,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`rateLimitSchema valid 2`] = `
|
exports[`rateLimitSchema valid 2`] = `
|
||||||
{
|
{
|
||||||
"async": true,
|
"async": true,
|
||||||
|
"enabled": true,
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
"maxPerInterval": 100,
|
"limit": 100,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`rateLimitSchema valid 3`] = `
|
exports[`rateLimitSchema valid 3`] = `
|
||||||
{
|
{
|
||||||
"async": false,
|
"async": false,
|
||||||
|
"enabled": true,
|
||||||
"interval": 86400,
|
"interval": 86400,
|
||||||
"maxPerInterval": 1000,
|
"limit": 1000,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`rateLimitSchema valid 4`] = `
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`rateLimitSchema valid 5`] = `
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -39,13 +39,13 @@ test('AgenticProjectConfig input types', () => {
|
||||||
usageType: 'metered'
|
usageType: 'metered'
|
||||||
billingScheme: 'per_unit'
|
billingScheme: 'per_unit'
|
||||||
unitAmount: 50
|
unitAmount: 50
|
||||||
rateLimit: {
|
|
||||||
// Make sure `interval` can use a string as input
|
|
||||||
interval: '30s'
|
|
||||||
maxPerInterval: 100
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
rateLimit: {
|
||||||
|
// Make sure `interval` can use a string as input
|
||||||
|
interval: '30s'
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}>().toExtend<AgenticProjectConfigInput>()
|
}>().toExtend<AgenticProjectConfigInput>()
|
||||||
|
@ -63,13 +63,13 @@ test('AgenticProjectConfig input types', () => {
|
||||||
usageType: 'metered'
|
usageType: 'metered'
|
||||||
billingScheme: 'per_unit'
|
billingScheme: 'per_unit'
|
||||||
unitAmount: 50
|
unitAmount: 50
|
||||||
rateLimit: {
|
|
||||||
// Make sure `interval` can use a number as input
|
|
||||||
interval: 300
|
|
||||||
maxPerInterval: 100
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
rateLimit: {
|
||||||
|
// Make sure `interval` can use a number as input
|
||||||
|
interval: 300
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}>().toExtend<AgenticProjectConfigInput>()
|
}>().toExtend<AgenticProjectConfigInput>()
|
||||||
|
|
|
@ -7,11 +7,17 @@ import {
|
||||||
} from './origin-adapter'
|
} from './origin-adapter'
|
||||||
import {
|
import {
|
||||||
defaultFreePricingPlan,
|
defaultFreePricingPlan,
|
||||||
|
defaultRequestsRateLimit,
|
||||||
pricingIntervalListSchema,
|
pricingIntervalListSchema,
|
||||||
type PricingPlanList,
|
type PricingPlanList,
|
||||||
type PricingPlanListInput,
|
type PricingPlanListInput,
|
||||||
pricingPlanListSchema
|
pricingPlanListSchema
|
||||||
} from './pricing'
|
} from './pricing'
|
||||||
|
import {
|
||||||
|
type RateLimit,
|
||||||
|
type RateLimitInput,
|
||||||
|
rateLimitSchema
|
||||||
|
} from './rate-limit'
|
||||||
import {
|
import {
|
||||||
type ToolConfig,
|
type ToolConfig,
|
||||||
type ToolConfigInput,
|
type ToolConfigInput,
|
||||||
|
@ -136,6 +142,20 @@ To add support for annual pricing plans, for example, you can use: \`['month', '
|
||||||
.optional()
|
.optional()
|
||||||
.default(['month']),
|
.default(['month']),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional default rate limits to enforce for all pricing plans.
|
||||||
|
*
|
||||||
|
* To disable the default rate-limit, set `defaultRateLimit.enabled` to
|
||||||
|
* `false`.
|
||||||
|
*
|
||||||
|
* Note that pricing-plan-specific rate-limits override this default (via
|
||||||
|
* `pricingPlans`), and tool-specific rate-limits may override both default
|
||||||
|
* and pricing-plan-specific rate-limits (via `toolConfigs`).
|
||||||
|
*/
|
||||||
|
defaultRateLimit: rateLimitSchema
|
||||||
|
.optional()
|
||||||
|
.default(defaultRequestsRateLimit),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional list of tool configs to customize the behavior of tools.
|
* Optional list of tool configs to customize the behavior of tools.
|
||||||
*
|
*
|
||||||
|
@ -170,6 +190,7 @@ export type AgenticProjectConfigInput = Simplify<
|
||||||
> & {
|
> & {
|
||||||
pricingPlans?: PricingPlanListInput
|
pricingPlans?: PricingPlanListInput
|
||||||
toolConfigs?: ToolConfigInput[]
|
toolConfigs?: ToolConfigInput[]
|
||||||
|
defaultRateLimit?: RateLimitInput
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
export type AgenticProjectConfigRaw = z.output<
|
export type AgenticProjectConfigRaw = z.output<
|
||||||
|
@ -179,6 +200,7 @@ export type AgenticProjectConfig = Simplify<
|
||||||
Omit<AgenticProjectConfigRaw, 'pricingPlans' | 'toolConfigs'> & {
|
Omit<AgenticProjectConfigRaw, 'pricingPlans' | 'toolConfigs'> & {
|
||||||
pricingPlans: PricingPlanList
|
pricingPlans: PricingPlanList
|
||||||
toolConfigs: ToolConfig[]
|
toolConfigs: ToolConfig[]
|
||||||
|
defaultRateLimit: RateLimit
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -194,5 +216,6 @@ export type ResolvedAgenticProjectConfig = Simplify<
|
||||||
> & {
|
> & {
|
||||||
pricingPlans: PricingPlanList
|
pricingPlans: PricingPlanList
|
||||||
toolConfigs: ToolConfig[]
|
toolConfigs: ToolConfig[]
|
||||||
|
defaultRateLimit: RateLimit
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -479,20 +479,25 @@ export interface components {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
RateLimit: {
|
RateLimit: {
|
||||||
|
/** @enum {boolean} */
|
||||||
|
enabled: false;
|
||||||
|
} | {
|
||||||
/** @description The interval at which the rate limit is applied. Either a positive integer expressed in seconds or a valid positive [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d", "1w", "1y", etc). */
|
/** @description The interval at which the rate limit is applied. Either a positive integer expressed in seconds or a valid positive [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d", "1w", "1y", etc). */
|
||||||
interval: number | string;
|
interval: number | string;
|
||||||
/** @description Maximum number of operations per interval (unitless). */
|
/** @description Maximum number of operations per interval (unitless). */
|
||||||
maxPerInterval: number;
|
limit: number;
|
||||||
/**
|
/**
|
||||||
* @description Whether to enforce the rate limit synchronously or asynchronously.
|
* @description Whether to enforce the rate limit synchronously (strict but slower) or asynchronously (approximate and faster, the default).
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
async: boolean;
|
async: boolean;
|
||||||
|
/** @default true */
|
||||||
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
PricingPlanToolOverride: {
|
PricingPlanToolOverride: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
reportUsage?: boolean;
|
reportUsage?: boolean;
|
||||||
rateLimit?: components["schemas"]["RateLimit"] | null;
|
rateLimit?: components["schemas"]["RateLimit"];
|
||||||
};
|
};
|
||||||
ToolConfig: {
|
ToolConfig: {
|
||||||
/** @description Agentic tool name */
|
/** @description Agentic tool name */
|
||||||
|
@ -501,7 +506,7 @@ export interface components {
|
||||||
pure?: boolean;
|
pure?: boolean;
|
||||||
cacheControl?: string;
|
cacheControl?: string;
|
||||||
reportUsage?: boolean;
|
reportUsage?: boolean;
|
||||||
rateLimit?: components["schemas"]["RateLimit"] | null;
|
rateLimit?: components["schemas"]["RateLimit"];
|
||||||
inputSchemaAdditionalProperties?: boolean;
|
inputSchemaAdditionalProperties?: boolean;
|
||||||
outputSchemaAdditionalProperties?: boolean;
|
outputSchemaAdditionalProperties?: boolean;
|
||||||
/** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */
|
/** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */
|
||||||
|
@ -603,7 +608,6 @@ export interface components {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
usageType: "metered";
|
usageType: "metered";
|
||||||
unitLabel?: string;
|
unitLabel?: string;
|
||||||
rateLimit?: components["schemas"]["RateLimit"];
|
|
||||||
billingScheme: "per_unit" | "tiered";
|
billingScheme: "per_unit" | "tiered";
|
||||||
unitAmount?: number;
|
unitAmount?: number;
|
||||||
tiersMode?: "graduated" | "volume";
|
tiersMode?: "graduated" | "volume";
|
||||||
|
@ -625,6 +629,7 @@ export interface components {
|
||||||
description?: string;
|
description?: string;
|
||||||
features?: string[];
|
features?: string[];
|
||||||
trialPeriodDays?: number;
|
trialPeriodDays?: number;
|
||||||
|
rateLimit?: components["schemas"]["RateLimit"];
|
||||||
lineItems: components["schemas"]["PricingPlanLineItem"][];
|
lineItems: components["schemas"]["PricingPlanLineItem"][];
|
||||||
};
|
};
|
||||||
/** @description A Deployment is a single, immutable instance of a Project. Each deployment contains pricing plans, origin server config (OpenAPI or MCP server), tool definitions, and metadata.
|
/** @description A Deployment is a single, immutable instance of a Project. Each deployment contains pricing plans, origin server config (OpenAPI or MCP server), tool definitions, and metadata.
|
||||||
|
@ -678,7 +683,13 @@ export interface components {
|
||||||
* "usageType": "licensed",
|
* "usageType": "licensed",
|
||||||
* "amount": 0
|
* "amount": 0
|
||||||
* }
|
* }
|
||||||
* ]
|
* ],
|
||||||
|
* "rateLimit": {
|
||||||
|
* "interval": 60,
|
||||||
|
* "limit": 1000,
|
||||||
|
* "async": true,
|
||||||
|
* "enabled": true
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
|
@ -694,6 +705,7 @@ export interface components {
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
pricingIntervals: components["schemas"]["PricingInterval"][];
|
pricingIntervals: components["schemas"]["PricingInterval"][];
|
||||||
|
defaultRateLimit?: components["schemas"]["RateLimit"] & unknown;
|
||||||
project?: components["schemas"]["Project"];
|
project?: components["schemas"]["Project"];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
|
@ -1654,7 +1666,13 @@ export interface operations {
|
||||||
* "usageType": "licensed",
|
* "usageType": "licensed",
|
||||||
* "amount": 0
|
* "amount": 0
|
||||||
* }
|
* }
|
||||||
* ]
|
* ],
|
||||||
|
* "rateLimit": {
|
||||||
|
* "interval": 60,
|
||||||
|
* "limit": 1000,
|
||||||
|
* "async": true,
|
||||||
|
* "enabled": true
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
|
@ -1670,6 +1688,7 @@ export interface operations {
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
pricingIntervals?: components["schemas"]["PricingInterval"][];
|
pricingIntervals?: components["schemas"]["PricingInterval"][];
|
||||||
|
defaultRateLimit?: components["schemas"]["RateLimit"] & unknown;
|
||||||
/** @default [] */
|
/** @default [] */
|
||||||
toolConfigs?: components["schemas"]["ToolConfig"][];
|
toolConfigs?: components["schemas"]["ToolConfig"][];
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Simplify } from 'type-fest'
|
import type { Simplify } from 'type-fest'
|
||||||
import { z } from '@hono/zod-openapi'
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import { rateLimitSchema } from './rate-limit'
|
import { type RateLimit, rateLimitSchema } from './rate-limit'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PricingPlanTier is a single tier in a tiered pricing plan.
|
* PricingPlanTier is a single tier in a tiered pricing plan.
|
||||||
|
@ -175,14 +175,6 @@ export const pricingPlanMeteredLineItemSchema =
|
||||||
*/
|
*/
|
||||||
unitLabel: z.string().optional(),
|
unitLabel: z.string().optional(),
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional rate limit to enforce for this metered line-item.
|
|
||||||
*
|
|
||||||
* You can use this, for example, to limit the number of API calls that
|
|
||||||
* can be made during a given interval.
|
|
||||||
*/
|
|
||||||
rateLimit: rateLimitSchema.optional(),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes how to compute the price per period. Either `per_unit` or
|
* Describes how to compute the price per period. Either `per_unit` or
|
||||||
* `tiered`.
|
* `tiered`.
|
||||||
|
@ -473,6 +465,15 @@ export type PricingPlanLineItem =
|
||||||
*/
|
*/
|
||||||
export const pricingPlanSchema = z
|
export const pricingPlanSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
/**
|
||||||
|
* Human-readable name for the pricing plan.
|
||||||
|
*
|
||||||
|
* Used in UI and billing invoices.
|
||||||
|
*
|
||||||
|
* @example "Free"
|
||||||
|
* @example "Starter Monthly"
|
||||||
|
* @example "Pro Annual"
|
||||||
|
*/
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.nonempty()
|
.nonempty()
|
||||||
|
@ -481,6 +482,21 @@ export const pricingPlanSchema = z
|
||||||
)
|
)
|
||||||
.openapi('name', { example: 'Starter Monthly' }),
|
.openapi('name', { example: 'Starter Monthly' }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique slug for the pricing plan which acts as a stable identifier
|
||||||
|
* across deployments.
|
||||||
|
*
|
||||||
|
* Should be lower-kebab-cased.
|
||||||
|
* Should be stable across deployments.
|
||||||
|
*
|
||||||
|
* For all plans aside from `free`, the `slug` should include the `interval`
|
||||||
|
* as a suffix so pricing plans can be uniquely differentiated from each
|
||||||
|
* other across billing intervals.
|
||||||
|
*
|
||||||
|
* @example "free"
|
||||||
|
* @example "starter-monthly"
|
||||||
|
* @example "pro-annual"
|
||||||
|
*/
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.nonempty()
|
.nonempty()
|
||||||
|
@ -495,12 +511,12 @@ export const pricingPlanSchema = z
|
||||||
interval: pricingIntervalSchema.optional(),
|
interval: pricingIntervalSchema.optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional description of the PricingPlan which is used for UI-only.
|
* Optional description of the pricing plan (UI-only).
|
||||||
*/
|
*/
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional list of features of the PricingPlan which is used for UI-only.
|
* Optional list of features of the pricing plan (UI-only).
|
||||||
*/
|
*/
|
||||||
features: z.array(z.string()).optional(),
|
features: z.array(z.string()).optional(),
|
||||||
|
|
||||||
|
@ -510,6 +526,23 @@ export const pricingPlanSchema = z
|
||||||
*/
|
*/
|
||||||
trialPeriodDays: z.number().nonnegative().optional(),
|
trialPeriodDays: z.number().nonnegative().optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional rate limit to enforce for this pricing plan.
|
||||||
|
*
|
||||||
|
* You can use this to limit the number of API requests that can be made by
|
||||||
|
* a customer during a given interval.
|
||||||
|
*
|
||||||
|
* If not set, the pricing plan will inherit the default platform rate-limit
|
||||||
|
* set by `defaultRateLimit` in the Agentic project config.
|
||||||
|
*
|
||||||
|
* You can disable rate-limiting for this pricing plan by setting
|
||||||
|
* `rateLimit.enabled` to `false`.
|
||||||
|
*
|
||||||
|
* Note that tool-specific rate limits may override pricing-plan-specific
|
||||||
|
* rate-limits via `toolConfigs` in the Agentic project config.
|
||||||
|
*/
|
||||||
|
rateLimit: rateLimitSchema.optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of custom LineItems which are included in the PricingPlan.
|
* List of custom LineItems which are included in the PricingPlan.
|
||||||
*
|
*
|
||||||
|
@ -601,6 +634,21 @@ export type StripeSubscriptionItemIdMap = z.infer<
|
||||||
// .openapi('Coupon')
|
// .openapi('Coupon')
|
||||||
// export type Coupon = z.infer<typeof couponSchema>
|
// export type Coupon = z.infer<typeof couponSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default platform rate limit for `requests` is a limit of 1000 requests
|
||||||
|
* per minute per customer.
|
||||||
|
*/
|
||||||
|
export const defaultRequestsRateLimit = {
|
||||||
|
interval: 60,
|
||||||
|
limit: 1000,
|
||||||
|
async: true,
|
||||||
|
enabled: true
|
||||||
|
} as const satisfies Readonly<RateLimit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default free pricing plan which is used for projects that don't specify
|
||||||
|
* custom pricing plans.
|
||||||
|
*/
|
||||||
export const defaultFreePricingPlan = {
|
export const defaultFreePricingPlan = {
|
||||||
name: 'Free',
|
name: 'Free',
|
||||||
slug: 'free',
|
slug: 'free',
|
||||||
|
@ -610,5 +658,6 @@ export const defaultFreePricingPlan = {
|
||||||
usageType: 'licensed',
|
usageType: 'licensed',
|
||||||
amount: 0
|
amount: 0
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
rateLimit: defaultRequestsRateLimit
|
||||||
} as const satisfies Readonly<PricingPlan>
|
} as const satisfies Readonly<PricingPlan>
|
||||||
|
|
|
@ -10,52 +10,67 @@ test('rateLimitSchema valid', () => {
|
||||||
expect(
|
expect(
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: 10,
|
interval: 10,
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
})
|
})
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: '10s',
|
interval: '10s',
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
})
|
})
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: '1 day',
|
interval: '1 day',
|
||||||
maxPerInterval: 1000,
|
limit: 1000,
|
||||||
async: false
|
async: false
|
||||||
})
|
})
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
rateLimitSchema.parse({
|
||||||
|
enabled: false
|
||||||
|
})
|
||||||
|
).toMatchSnapshot()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
rateLimitSchema.parse({
|
||||||
|
interval: '10m',
|
||||||
|
limit: 100,
|
||||||
|
async: false,
|
||||||
|
enabled: false
|
||||||
|
})
|
||||||
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('rateLimitSchema invalid', () => {
|
test('rateLimitSchema invalid', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: '',
|
interval: '',
|
||||||
maxPerInterval: 5
|
limit: 5
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingSnapshot()
|
).toThrowErrorMatchingSnapshot()
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: 0,
|
interval: 0,
|
||||||
maxPerInterval: 5
|
limit: 5
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingSnapshot()
|
).toThrowErrorMatchingSnapshot()
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: '--',
|
interval: '--',
|
||||||
maxPerInterval: 10
|
limit: 10
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingSnapshot()
|
).toThrowErrorMatchingSnapshot()
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
rateLimitSchema.parse({
|
rateLimitSchema.parse({
|
||||||
interval: '1 day',
|
interval: '1 day',
|
||||||
maxPerInterval: -1000
|
limit: -1000
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingSnapshot()
|
).toThrowErrorMatchingSnapshot()
|
||||||
})
|
})
|
||||||
|
@ -63,58 +78,78 @@ test('rateLimitSchema invalid', () => {
|
||||||
test('RateLimit types', () => {
|
test('RateLimit types', () => {
|
||||||
expectTypeOf({
|
expectTypeOf({
|
||||||
interval: 10,
|
interval: 10,
|
||||||
maxPerInterval: 100,
|
limit: 100,
|
||||||
async: false
|
async: false,
|
||||||
|
enabled: true
|
||||||
} as const).toExtend<RateLimit>()
|
} as const).toExtend<RateLimit>()
|
||||||
|
|
||||||
expectTypeOf<{
|
expectTypeOf<{
|
||||||
interval: 10
|
interval: 10
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
}>().toExtend<RateLimitInput>()
|
async: false
|
||||||
|
enabled: true
|
||||||
|
}>().toExtend<RateLimit>()
|
||||||
|
|
||||||
expectTypeOf({
|
expectTypeOf({
|
||||||
interval: '10s',
|
interval: '10s',
|
||||||
maxPerInterval: 100,
|
limit: 100,
|
||||||
async: true
|
async: true,
|
||||||
|
enabled: true
|
||||||
} as const).not.toExtend<RateLimit>()
|
} as const).not.toExtend<RateLimit>()
|
||||||
|
|
||||||
expectTypeOf<{
|
expectTypeOf<{
|
||||||
interval: '10s'
|
interval: '10s'
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
async: false
|
async: false
|
||||||
}>().not.toExtend<RateLimit>()
|
}>().not.toExtend<RateLimit>()
|
||||||
|
|
||||||
|
expectTypeOf({
|
||||||
|
enabled: false
|
||||||
|
} as const).toExtend<RateLimit>()
|
||||||
|
|
||||||
|
expectTypeOf<{
|
||||||
|
enabled: false
|
||||||
|
}>().toExtend<RateLimit>()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('RateLimitInput types', () => {
|
test('RateLimitInput types', () => {
|
||||||
expectTypeOf({
|
expectTypeOf({
|
||||||
interval: 10,
|
interval: 10,
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
} as const).toExtend<RateLimitInput>()
|
} as const).toExtend<RateLimitInput>()
|
||||||
|
|
||||||
expectTypeOf<{
|
expectTypeOf<{
|
||||||
interval: 10
|
interval: 10
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
}>().toExtend<RateLimitInput>()
|
}>().toExtend<RateLimitInput>()
|
||||||
|
|
||||||
expectTypeOf({
|
expectTypeOf({
|
||||||
interval: 10,
|
interval: 10,
|
||||||
maxPerInterval: 100,
|
limit: 100,
|
||||||
async: false
|
async: false
|
||||||
} as const).toExtend<RateLimitInput>()
|
} as const).toExtend<RateLimitInput>()
|
||||||
|
|
||||||
expectTypeOf<{
|
expectTypeOf<{
|
||||||
interval: 10
|
interval: 10
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
async: boolean
|
async: boolean
|
||||||
}>().toExtend<RateLimitInput>()
|
}>().toExtend<RateLimitInput>()
|
||||||
|
|
||||||
expectTypeOf({
|
expectTypeOf({
|
||||||
interval: '3h',
|
interval: '3h',
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
} as const).toExtend<RateLimitInput>()
|
} as const).toExtend<RateLimitInput>()
|
||||||
|
|
||||||
expectTypeOf<{
|
expectTypeOf<{
|
||||||
interval: '3h'
|
interval: '3h'
|
||||||
maxPerInterval: 100
|
limit: 100
|
||||||
|
}>().toExtend<RateLimitInput>()
|
||||||
|
|
||||||
|
expectTypeOf({
|
||||||
|
enabled: false
|
||||||
|
} as const).toExtend<RateLimitInput>()
|
||||||
|
|
||||||
|
expectTypeOf<{
|
||||||
|
enabled: false
|
||||||
}>().toExtend<RateLimitInput>()
|
}>().toExtend<RateLimitInput>()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,37 +1,59 @@
|
||||||
import { z } from '@hono/zod-openapi'
|
import { z } from '@hono/zod-openapi'
|
||||||
import parseIntervalAsMs from 'ms'
|
import parseIntervalAsMs from 'ms'
|
||||||
|
|
||||||
|
// TODO: Consider adding support for this in the future
|
||||||
|
// export const rateLimitBySchema = z.union([
|
||||||
|
// z.literal('ip'),
|
||||||
|
// z.literal('customer'),
|
||||||
|
// z.literal('all')
|
||||||
|
// ])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate limit config for metered LineItems.
|
* Rate limit config for metered LineItems.
|
||||||
*/
|
*/
|
||||||
export const rateLimitSchema = z
|
export const rateLimitSchema = z
|
||||||
.object({
|
.union([
|
||||||
/**
|
z.object({
|
||||||
* The interval at which the rate limit is applied.
|
enabled: z.literal(false)
|
||||||
*
|
}),
|
||||||
* Either a positive integer expressed in seconds or a valid positive
|
z.object({
|
||||||
* [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d",
|
/**
|
||||||
* "1w", "1y", etc).
|
* The interval at which the rate limit is applied.
|
||||||
*/
|
*
|
||||||
interval: z
|
* Either a positive integer expressed in seconds or a valid positive
|
||||||
.union([
|
* [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d",
|
||||||
z.number().positive(), // seconds
|
* "1w", "1y", etc).
|
||||||
|
*/
|
||||||
|
interval: z
|
||||||
|
.union([
|
||||||
|
z.number().positive(), // seconds
|
||||||
|
|
||||||
z
|
z
|
||||||
.string()
|
.string()
|
||||||
.nonempty()
|
.nonempty()
|
||||||
.transform((value, ctx) => {
|
.transform((value, ctx) => {
|
||||||
try {
|
try {
|
||||||
// TODO: `ms` module has broken types
|
// TODO: `ms` module has broken types
|
||||||
const ms = parseIntervalAsMs(value as any) as unknown as number
|
const ms = parseIntervalAsMs(value as any) as unknown as number
|
||||||
const seconds = Math.floor(ms / 1000)
|
const seconds = Math.floor(ms / 1000)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof ms !== 'number' ||
|
typeof ms !== 'number' ||
|
||||||
Number.isNaN(ms) ||
|
Number.isNaN(ms) ||
|
||||||
ms <= 0 ||
|
ms <= 0 ||
|
||||||
seconds <= 0
|
seconds <= 0
|
||||||
) {
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid interval "${value}"`,
|
||||||
|
path: ctx.path
|
||||||
|
})
|
||||||
|
|
||||||
|
return z.NEVER
|
||||||
|
}
|
||||||
|
|
||||||
|
return seconds
|
||||||
|
} catch {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: `Invalid interval "${value}"`,
|
message: `Invalid interval "${value}"`,
|
||||||
|
@ -40,52 +62,60 @@ export const rateLimitSchema = z
|
||||||
|
|
||||||
return z.NEVER
|
return z.NEVER
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.describe(
|
||||||
|
`The interval at which the rate limit is applied. Either a positive integer expressed in seconds or a valid positive [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d", "1w", "1y", etc).`
|
||||||
|
),
|
||||||
|
|
||||||
return seconds
|
/**
|
||||||
} catch {
|
* Maximum number of operations per interval (unitless).
|
||||||
ctx.addIssue({
|
*/
|
||||||
code: z.ZodIssueCode.custom,
|
limit: z
|
||||||
message: `Invalid interval "${value}"`,
|
.number()
|
||||||
path: ctx.path
|
.nonnegative()
|
||||||
})
|
.describe('Maximum number of operations per interval (unitless).'),
|
||||||
|
|
||||||
return z.NEVER
|
/**
|
||||||
}
|
* Whether to enforce the rate limit synchronously or asynchronously.
|
||||||
})
|
*
|
||||||
])
|
* The default rate-limiting mode is asynchronous, which means that requests
|
||||||
.describe(
|
* are allowed to proceed immediately, with the limit being enforced in the
|
||||||
`The interval at which the rate limit is applied. Either a positive integer expressed in seconds or a valid positive [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d", "1w", "1y", etc).`
|
* background. This is much faster than synchronous mode, but it is less
|
||||||
),
|
* consistent if precise adherence to rate-limits is required.
|
||||||
|
*
|
||||||
|
* With synchronous mode, requests are blocked until the current limit has
|
||||||
|
* been confirmed. The downside with this approach is that it introduces
|
||||||
|
* more latency to every request by default. The advantage is that it is
|
||||||
|
* more precise and consistent.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
async: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(
|
||||||
|
'Whether to enforce the rate limit synchronously (strict but slower) or asynchronously (approximate and faster, the default).'
|
||||||
|
),
|
||||||
|
|
||||||
/**
|
// TODO: Consider adding support for this in the future
|
||||||
* Maximum number of operations per interval (unitless).
|
// /**
|
||||||
*/
|
// * The key to rate-limit by.
|
||||||
maxPerInterval: z
|
// *
|
||||||
.number()
|
// * - `ip`: Rate-limit by incoming IP address.
|
||||||
.nonnegative()
|
// * - `customer`: Rate-limit by customer ID if available or IP address
|
||||||
.describe('Maximum number of operations per interval (unitless).'),
|
// * otherwise.
|
||||||
|
// * - `global`: Rate-limit all usage globally across customers.
|
||||||
|
// *
|
||||||
|
// * @default 'customer'
|
||||||
|
// */
|
||||||
|
// rateLimitBy: rateLimitBySchema.optional().default('customer'),
|
||||||
|
|
||||||
/**
|
enabled: z.boolean().optional().default(true)
|
||||||
* Whether to enforce the rate limit synchronously or asynchronously.
|
})
|
||||||
*
|
])
|
||||||
* The default rate-limiting mode is asynchronous, which means that requests
|
|
||||||
* are allowed to proceed immediately, with the limit being enforced in the
|
|
||||||
* background. This is much faster than synchronous mode, but it is less
|
|
||||||
* consistent if precise adherence to rate-limits is required.
|
|
||||||
*
|
|
||||||
* With synchronous mode, requests will be blocked until the current limit
|
|
||||||
* has been confirmed. The downside with this approach is that it can
|
|
||||||
* introduce more latency to every request by default. The advantage is that
|
|
||||||
* it is more accurate and consistent.
|
|
||||||
*/
|
|
||||||
async: z
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(
|
|
||||||
'Whether to enforce the rate limit synchronously or asynchronously.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.openapi('RateLimit')
|
.openapi('RateLimit')
|
||||||
|
|
||||||
export type RateLimitInput = z.input<typeof rateLimitSchema>
|
export type RateLimitInput = z.input<typeof rateLimitSchema>
|
||||||
export type RateLimit = z.infer<typeof rateLimitSchema>
|
export type RateLimit = z.infer<typeof rateLimitSchema>
|
||||||
|
|
|
@ -76,12 +76,12 @@ export const pricingPlanToolOverrideSchema = z
|
||||||
* Customize or disable rate limits for this tool for customers on a given
|
* Customize or disable rate limits for this tool for customers on a given
|
||||||
* pricing plan.
|
* pricing plan.
|
||||||
*
|
*
|
||||||
* Set to `null` to disable the default request-based rate-limiting for
|
* To disable rate-limiting for this tool on a given pricing plan, set
|
||||||
* this tool on a given pricing plan.
|
* `rateLimit.enabled` to `false`.
|
||||||
*
|
*
|
||||||
* @default undefined
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
rateLimit: z.union([rateLimitSchema, z.null()]).optional()
|
rateLimit: rateLimitSchema.optional()
|
||||||
})
|
})
|
||||||
.openapi('PricingPlanToolOverride')
|
.openapi('PricingPlanToolOverride')
|
||||||
export type PricingPlanToolOverride = z.infer<
|
export type PricingPlanToolOverride = z.infer<
|
||||||
|
@ -158,14 +158,15 @@ export const toolConfigSchema = z
|
||||||
/**
|
/**
|
||||||
* Customize the default `requests`-based rate-limiting for this tool.
|
* Customize the default `requests`-based rate-limiting for this tool.
|
||||||
*
|
*
|
||||||
* Set to `null` to disable the built-in rate-limiting.
|
* To disable rate-limiting for this tool, set `rateLimit.enabled` to
|
||||||
|
* `false`.
|
||||||
*
|
*
|
||||||
* If not set, the default rate-limiting for the active pricing plan will be
|
* If not set, the default rate-limiting for the active pricing plan will be
|
||||||
* used.
|
* used.
|
||||||
*
|
*
|
||||||
* @default undefined
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
rateLimit: z.union([rateLimitSchema, z.null()]).optional(),
|
rateLimit: rateLimitSchema.optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to allow additional properties in the tool's input schema.
|
* Whether to allow additional properties in the tool's input schema.
|
||||||
|
@ -175,7 +176,7 @@ export const toolConfigSchema = z
|
||||||
*
|
*
|
||||||
* @note This is only relevant if the tool has defined an `outputSchema`.
|
* @note This is only relevant if the tool has defined an `outputSchema`.
|
||||||
*
|
*
|
||||||
* @default true
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
inputSchemaAdditionalProperties: z.boolean().optional(),
|
inputSchemaAdditionalProperties: z.boolean().optional(),
|
||||||
|
|
||||||
|
@ -187,7 +188,7 @@ export const toolConfigSchema = z
|
||||||
*
|
*
|
||||||
* @note This is only relevant if the tool has defined an `outputSchema`.
|
* @note This is only relevant if the tool has defined an `outputSchema`.
|
||||||
*
|
*
|
||||||
* @default true
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
outputSchemaAdditionalProperties: z.boolean().optional(),
|
outputSchemaAdditionalProperties: z.boolean().optional(),
|
||||||
|
|
||||||
|
@ -216,6 +217,7 @@ export const toolConfigSchema = z
|
||||||
// headers
|
// headers
|
||||||
})
|
})
|
||||||
.openapi('ToolConfig')
|
.openapi('ToolConfig')
|
||||||
|
|
||||||
export type ToolConfigInput = z.input<typeof toolConfigSchema>
|
export type ToolConfigInput = z.input<typeof toolConfigSchema>
|
||||||
export type ToolConfig = z.infer<typeof toolConfigSchema>
|
export type ToolConfig = z.infer<typeof toolConfigSchema>
|
||||||
|
|
||||||
|
@ -321,6 +323,3 @@ export const toolSchema = z
|
||||||
.passthrough()
|
.passthrough()
|
||||||
.openapi('Tool')
|
.openapi('Tool')
|
||||||
export type Tool = z.infer<typeof toolSchema>
|
export type Tool = z.infer<typeof toolSchema>
|
||||||
|
|
||||||
// export const toolMapSchema = z.record(toolNameSchema, toolSchema)
|
|
||||||
// export type ToolMap = z.infer<typeof toolMapSchema>
|
|
||||||
|
|
|
@ -4,10 +4,12 @@
|
||||||
* types than what `@hono/zod-openapi` and `zod` v3 provide, but in general
|
* types than what `@hono/zod-openapi` and `zod` v3 provide, but in general
|
||||||
* these types are meant to use the backend API as a source of truth.
|
* these types are meant to use the backend API as a source of truth.
|
||||||
*/
|
*/
|
||||||
import type { PricingPlan, ToolConfig } from '@agentic/platform-types'
|
|
||||||
import type { Simplify } from 'type-fest'
|
import type { Simplify } from 'type-fest'
|
||||||
|
|
||||||
import type { components } from './openapi.d.ts'
|
import type { components } from './openapi.d.ts'
|
||||||
|
import type { PricingPlan } from './pricing'
|
||||||
|
import type { RateLimit } from './rate-limit.js'
|
||||||
|
import type { ToolConfig } from './tools'
|
||||||
|
|
||||||
export type Consumer = components['schemas']['Consumer']
|
export type Consumer = components['schemas']['Consumer']
|
||||||
export type Project = components['schemas']['Project']
|
export type Project = components['schemas']['Project']
|
||||||
|
@ -16,19 +18,24 @@ export type Team = components['schemas']['Team']
|
||||||
export type TeamMember = components['schemas']['TeamMember']
|
export type TeamMember = components['schemas']['TeamMember']
|
||||||
|
|
||||||
export type Deployment = Simplify<
|
export type Deployment = Simplify<
|
||||||
Omit<components['schemas']['Deployment'], 'pricingPlans' | 'toolConfigs'> & {
|
Omit<
|
||||||
|
components['schemas']['Deployment'],
|
||||||
|
'pricingPlans' | 'toolConfigs' | 'defaultRateLimit'
|
||||||
|
> & {
|
||||||
pricingPlans: PricingPlan[]
|
pricingPlans: PricingPlan[]
|
||||||
toolConfigs: ToolConfig[]
|
toolConfigs: ToolConfig[]
|
||||||
|
defaultRateLimit: RateLimit
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export type AdminDeployment = Simplify<
|
export type AdminDeployment = Simplify<
|
||||||
Omit<
|
Omit<
|
||||||
components['schemas']['AdminDeployment'],
|
components['schemas']['AdminDeployment'],
|
||||||
'pricingPlans' | 'toolConfigs'
|
'pricingPlans' | 'toolConfigs' | 'defaultRateLimit'
|
||||||
> & {
|
> & {
|
||||||
pricingPlans: PricingPlan[]
|
pricingPlans: PricingPlan[]
|
||||||
toolConfigs: ToolConfig[]
|
toolConfigs: ToolConfig[]
|
||||||
|
defaultRateLimit: RateLimit
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
- mcp-kitchen-sink
|
- mcp-kitchen-sink
|
||||||
- how to handle binary bodies and responses?
|
- how to handle binary bodies and responses?
|
||||||
- improve logger vs console for non-hono path and util methods
|
- improve logger vs console for non-hono path and util methods
|
||||||
|
- default rate limits?
|
||||||
- **test rate limiting**
|
- **test rate limiting**
|
||||||
- **test usage tracking and reporting**
|
- **test usage tracking and reporting**
|
||||||
- disallow `mcp` as a tool name or figure out another workaround
|
- disallow `mcp` as a tool name or figure out another workaround
|
||||||
|
@ -80,6 +81,11 @@
|
||||||
- handle or validate against dynamic MCP origin tools
|
- handle or validate against dynamic MCP origin tools
|
||||||
- allow config name to be `project-name` or `@namespace/project-name`?
|
- allow config name to be `project-name` or `@namespace/project-name`?
|
||||||
- upgrade to zod v4
|
- upgrade to zod v4
|
||||||
|
- add seed-db script which creates `dev` user (or team) and test fixtures
|
||||||
|
- decide whether deployment fields like `defaultRateLimit` and others should be generated and stored in the db, or should be inferred based on `undefined` values
|
||||||
|
- support multiple rate-limits by slug
|
||||||
|
- RateLimit-Policy: "burst";q=100;w=60,"daily";q=1000;w=86400
|
||||||
|
- https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue