kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
bf7f8f60f4
commit
23364946eb
|
@ -187,7 +187,7 @@ export async function upsertStripePricing({
|
||||||
)
|
)
|
||||||
|
|
||||||
priceParams.tiers_mode = pricingPlanLineItem.tiersMode
|
priceParams.tiers_mode = pricingPlanLineItem.tiersMode
|
||||||
priceParams.tiers = pricingPlanLineItem.tiers!.map((tierData) => {
|
priceParams.tiers = pricingPlanLineItem.tiers.map((tierData) => {
|
||||||
const tier: Stripe.PriceCreateParams.Tier = {
|
const tier: Stripe.PriceCreateParams.Tier = {
|
||||||
up_to: tierData.upTo
|
up_to: tierData.upTo
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
import { defineConfig, freePricingPlan } from '@agentic/platform-schemas'
|
import { defaultFreePricingPlan, defineConfig } from '@agentic/platform-schemas'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// TODO: resolve name / slug conflicts
|
// TODO: resolve name / slug conflicts
|
||||||
name: 'My Project',
|
name: 'My Project',
|
||||||
originUrl: 'https://httpbin.org',
|
originUrl: 'https://httpbin.org',
|
||||||
pricingPlans: [
|
pricingPlans: [
|
||||||
freePricingPlan,
|
defaultFreePricingPlan,
|
||||||
{
|
{
|
||||||
name: 'Basic',
|
name: 'Basic',
|
||||||
slug: 'basic',
|
slug: 'basic',
|
||||||
// interval: 'month',
|
|
||||||
trialPeriodDays: 7,
|
trialPeriodDays: 7,
|
||||||
lineItems: [
|
lineItems: [
|
||||||
{
|
{
|
||||||
slug: 'base',
|
slug: 'base',
|
||||||
usageType: 'licensed',
|
usageType: 'licensed',
|
||||||
amount: 490
|
amount: 499 // $4.99 USD
|
||||||
// interval: 'month'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,68 @@
|
||||||
exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
exports[`loadAgenticConfig > basic-raw-free-json 1`] = `
|
||||||
{
|
{
|
||||||
"name": "My Project",
|
"name": "My Project",
|
||||||
|
"originAdapter": {
|
||||||
|
"location": "external",
|
||||||
|
"type": "raw",
|
||||||
|
},
|
||||||
"originUrl": "https://jsonplaceholder.typicode.com",
|
"originUrl": "https://jsonplaceholder.typicode.com",
|
||||||
|
"pricingIntervals": [
|
||||||
|
"month",
|
||||||
|
],
|
||||||
|
"pricingPlans": [
|
||||||
|
{
|
||||||
|
"lineItems": [
|
||||||
|
{
|
||||||
|
"amount": 0,
|
||||||
|
"slug": "base",
|
||||||
|
"usageType": "licensed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "Free",
|
||||||
|
"slug": "free",
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
exports[`loadAgenticConfig > basic-raw-free-ts 1`] = `
|
||||||
{
|
{
|
||||||
"name": "My Project",
|
"name": "My Project",
|
||||||
|
"originAdapter": {
|
||||||
|
"location": "external",
|
||||||
|
"type": "raw",
|
||||||
|
},
|
||||||
"originUrl": "https://jsonplaceholder.typicode.com",
|
"originUrl": "https://jsonplaceholder.typicode.com",
|
||||||
|
"pricingIntervals": [
|
||||||
|
"month",
|
||||||
|
],
|
||||||
|
"pricingPlans": [
|
||||||
|
{
|
||||||
|
"lineItems": [
|
||||||
|
{
|
||||||
|
"amount": 0,
|
||||||
|
"slug": "base",
|
||||||
|
"usageType": "licensed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "Free",
|
||||||
|
"slug": "free",
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
||||||
{
|
{
|
||||||
"name": "My Project",
|
"name": "My Project",
|
||||||
|
"originAdapter": {
|
||||||
|
"location": "external",
|
||||||
|
"type": "raw",
|
||||||
|
},
|
||||||
"originUrl": "https://httpbin.org",
|
"originUrl": "https://httpbin.org",
|
||||||
|
"pricingIntervals": [
|
||||||
|
"month",
|
||||||
|
],
|
||||||
"pricingPlans": [
|
"pricingPlans": [
|
||||||
{
|
{
|
||||||
"lineItems": [
|
"lineItems": [
|
||||||
|
@ -34,7 +81,7 @@ exports[`loadAgenticConfig > pricing-freemium 1`] = `
|
||||||
"interval": "month",
|
"interval": "month",
|
||||||
"lineItems": [
|
"lineItems": [
|
||||||
{
|
{
|
||||||
"amount": 490,
|
"amount": 499,
|
||||||
"interval": "month",
|
"interval": "month",
|
||||||
"slug": "base",
|
"slug": "base",
|
||||||
"usageType": "licensed",
|
"usageType": "licensed",
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { parseZodSchema } from '@agentic/platform-core'
|
import type { AgenticProjectConfigOutput } from '@agentic/platform-schemas'
|
||||||
import {
|
|
||||||
type AgenticProjectConfigOutput,
|
|
||||||
agenticProjectConfigSchema
|
|
||||||
} from '@agentic/platform-schemas'
|
|
||||||
import { loadConfig } from 'unconfig'
|
import { loadConfig } from 'unconfig'
|
||||||
|
|
||||||
|
import { validateAgenticConfig } from './validate-agentic-config'
|
||||||
|
|
||||||
export async function loadAgenticConfig({
|
export async function loadAgenticConfig({
|
||||||
cwd
|
cwd
|
||||||
}: {
|
}: {
|
||||||
|
@ -20,5 +18,5 @@ export async function loadAgenticConfig({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
return parseZodSchema(agenticProjectConfigSchema, config)
|
return validateAgenticConfig(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
import type { ZodTypeDef } from 'zod'
|
||||||
|
import { assert, parseZodSchema } from '@agentic/platform-core'
|
||||||
|
import {
|
||||||
|
type AgenticProjectConfigInput,
|
||||||
|
type AgenticProjectConfigOutput,
|
||||||
|
agenticProjectConfigSchema,
|
||||||
|
type PricingPlanLineItem
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
|
||||||
|
export async function validateAgenticConfig(
|
||||||
|
inputConfig: unknown
|
||||||
|
): Promise<AgenticProjectConfigOutput> {
|
||||||
|
const config = parseZodSchema<
|
||||||
|
AgenticProjectConfigOutput,
|
||||||
|
ZodTypeDef,
|
||||||
|
AgenticProjectConfigInput
|
||||||
|
>(agenticProjectConfigSchema, inputConfig)
|
||||||
|
|
||||||
|
console.log('config', config)
|
||||||
|
|
||||||
|
const { pricingIntervals, pricingPlans } = config
|
||||||
|
assert(
|
||||||
|
pricingPlans?.length,
|
||||||
|
400,
|
||||||
|
'Invalid pricingPlans: must be a non-empty array'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
pricingIntervals?.length,
|
||||||
|
400,
|
||||||
|
'Invalid pricingIntervals: must be a non-empty array'
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
// Validate pricing interval
|
||||||
|
const pricingIntervalsSet = new Set(pricingIntervals)
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.size === pricingIntervals.length,
|
||||||
|
400,
|
||||||
|
'Invalid pricingIntervals: duplicate pricing intervals'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
pricingIntervals.length >= 1,
|
||||||
|
400,
|
||||||
|
'Invalid pricingIntervals: must contain at least one pricing interval'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pricingIntervals.length > 1) {
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
if (pricingPlan.interval) {
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(pricingPlan.interval),
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingPlan.slug === 'free') continue
|
||||||
|
|
||||||
|
assert(
|
||||||
|
pricingPlan.interval !== undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": non-free PricingPlan "${pricingPlan.slug}" must specify an "interval" because the project supports multiple pricing intervals.`
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
lineItem.interval ??= pricingPlan.interval
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.interval === pricingPlan.interval,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": non-free PricingPlan "${pricingPlan.slug}" LineItem "${lineItem.slug}" "interval" must match the PricingPlan interval "${pricingPlan.interval}" because the project supports multiple pricing intervals.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(lineItem.interval),
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" LineItem "${lineItem.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only a single pricing interval is supported, so default all pricing
|
||||||
|
// plans to use the default pricing interval.
|
||||||
|
const defaultPricingInterval = pricingIntervals[0]!
|
||||||
|
assert(
|
||||||
|
defaultPricingInterval,
|
||||||
|
400,
|
||||||
|
'Invalid pricingIntervals: must contain at least one valid pricing interval'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
if (pricingPlan.interval) {
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(pricingPlan.interval),
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingPlan.slug === 'free') continue
|
||||||
|
|
||||||
|
pricingPlan.interval ??= defaultPricingInterval
|
||||||
|
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
lineItem.interval ??= defaultPricingInterval
|
||||||
|
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(lineItem.interval),
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" LineItem "${lineItem.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Validate pricingPlans
|
||||||
|
const pricingPlanSlugsSet = new Set(pricingPlans.map((p) => p.slug))
|
||||||
|
assert(
|
||||||
|
pricingPlanSlugsSet.size === pricingPlans.length,
|
||||||
|
400,
|
||||||
|
'Invalid pricingPlans: duplicate PricingPlan slugs. All PricingPlan slugs must be unique (e.g. "free", "starter-monthly", "pro-annual", etc).'
|
||||||
|
)
|
||||||
|
|
||||||
|
const pricingPlanLineItemSlugMap: Record<string, PricingPlanLineItem[]> = {}
|
||||||
|
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
const lineItemSlugsSet = new Set(
|
||||||
|
pricingPlan.lineItems.map((lineItem) => lineItem.slug)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItemSlugsSet.size === pricingPlan.lineItems.length,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": duplicate line-item slugs`
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
if (!pricingPlanLineItemSlugMap[lineItem.slug]) {
|
||||||
|
pricingPlanLineItemSlugMap[lineItem.slug] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
pricingPlanLineItemSlugMap[lineItem.slug]!.push(lineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lineItems of Object.values(pricingPlanLineItemSlugMap)) {
|
||||||
|
if (lineItems.length <= 1) continue
|
||||||
|
|
||||||
|
const lineItem0 = lineItems[0]!
|
||||||
|
|
||||||
|
for (let i = 1; i < lineItems.length; ++i) {
|
||||||
|
const lineItem = lineItems[i]!
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.usageType === lineItem0.usageType,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlans: all PricingPlans which contain the same LineItems (by slug "${lineItem.slug}") must have the same usage type ("licensed" or "metered").`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate PricingPlanLineItems
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
if (lineItem.usageType === 'metered') {
|
||||||
|
switch (lineItem.billingScheme) {
|
||||||
|
case 'per_unit':
|
||||||
|
assert(
|
||||||
|
lineItem.unitAmount !== undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-negative "unitAmount" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiersMode === undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiersMode" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiers === undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiers" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tiered':
|
||||||
|
assert(
|
||||||
|
lineItem.unitAmount === undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "unitAmount" when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiers?.length,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-empty "tiers" array when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.transformQuantity === undefined,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "transformQuantity" when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
400,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a valid "billingScheme".`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import {
|
||||||
// - optional version
|
// - optional version
|
||||||
// - optional agentic version
|
// - optional agentic version
|
||||||
|
|
||||||
export const freePricingPlan = {
|
export const defaultFreePricingPlan = {
|
||||||
name: 'Free',
|
name: 'Free',
|
||||||
slug: 'free',
|
slug: 'free',
|
||||||
lineItems: [
|
lineItems: [
|
||||||
|
@ -70,30 +70,36 @@ export const agenticProjectConfigSchema = z.object({
|
||||||
NOTE: Agentic currently only supports \`external\` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so.`),
|
NOTE: Agentic currently only supports \`external\` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so.`),
|
||||||
|
|
||||||
/** Optional origin API config */
|
/** Optional origin API config */
|
||||||
originAdapter: deploymentOriginAdapterSchema
|
originAdapter: deploymentOriginAdapterSchema.optional().default({
|
||||||
.default({
|
location: 'external',
|
||||||
location: 'external',
|
type: 'raw'
|
||||||
type: 'raw'
|
}),
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
/** Optional subscription pricing config */
|
/** Optional subscription pricing config */
|
||||||
pricingPlans: pricingPlanListSchema
|
pricingPlans: pricingPlanListSchema
|
||||||
.describe(
|
.describe(
|
||||||
'List of PricingPlans configuring which Stripe subscriptions should be available for the project. Defaults to a single free plan which is useful for developing and testing.your project.'
|
'List of PricingPlans configuring which Stripe subscriptions should be available for the project. Defaults to a single free plan which is useful for developing and testing.your project.'
|
||||||
)
|
)
|
||||||
.default([freePricingPlan])
|
.optional()
|
||||||
.optional(),
|
.default([defaultFreePricingPlan]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional list of billing intervals to enable in the pricingPlans.
|
* Optional list of billing intervals to enable in the pricingPlans.
|
||||||
*
|
*
|
||||||
* Defaults to a single monthly interval `['month']`.
|
* Defaults to a single monthly interval `['month']`.
|
||||||
*
|
*
|
||||||
* To add an annual plan, you can use `['month', 'year']`.
|
* To add support for annual pricing plans, you can use `['month', 'year']`.
|
||||||
|
*
|
||||||
|
* Note that for every pricing interval, you must define a corresponding set
|
||||||
|
* of PricingPlans in the `pricingPlans` array. If you only have one pricing
|
||||||
|
* interval (like the default `month` interval), `pricingPlans` don't need to
|
||||||
|
* specify their `interval` property. Otherwise, all PricingPlans and
|
||||||
|
* LineItems must specify their `interval` property to differentiate between
|
||||||
|
* different pricing intervals.
|
||||||
*/
|
*/
|
||||||
pricingIntervals: z.array(pricingIntervalSchema).default(['month']).optional()
|
pricingIntervals: z.array(pricingIntervalSchema).optional().default(['month'])
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AgenticProjectConfigInput = z.input<
|
export type AgenticProjectConfigInput = z.input<
|
||||||
typeof agenticProjectConfigSchema
|
typeof agenticProjectConfigSchema
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { parseJson } from '@agentic/platform-core'
|
|
||||||
import { z } from '@hono/zod-openapi'
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
export const webhookSchema = z
|
export const webhookSchema = z
|
||||||
|
@ -10,7 +9,7 @@ export const webhookSchema = z
|
||||||
export type Webhook = z.infer<typeof webhookSchema>
|
export type Webhook = z.infer<typeof webhookSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate limit config for metered line-items.
|
* Rate limit config for metered LineItems.
|
||||||
*/
|
*/
|
||||||
export const rateLimitSchema = z
|
export const rateLimitSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -64,7 +63,9 @@ export const pricingPlanLineItemHashSchema = z
|
||||||
.describe('Internal PricingPlanLineItem hash')
|
.describe('Internal PricingPlanLineItem hash')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PricingPlanLineItem slug which acts as a unique lookup key for LineItems across deployments. They must be lower and kebab-cased ("base", "requests", "image-transformations").
|
* PricingPlanLineItem slug which acts as a unique lookup key for LineItems
|
||||||
|
* across deployments. They must be lower and kebab-cased ("base", "requests",
|
||||||
|
* "image-transformations", etc).
|
||||||
*/
|
*/
|
||||||
export const pricingPlanLineItemSlugSchema = z
|
export const pricingPlanLineItemSlugSchema = z
|
||||||
.string()
|
.string()
|
||||||
|
@ -111,7 +112,7 @@ const commonPricingPlanLineItemSchema = z.object({
|
||||||
* The `requests` slug is reserved for charging using `metered billing based
|
* The `requests` slug is reserved for charging using `metered billing based
|
||||||
* on the number of request made during a given billing interval.
|
* on the number of request made during a given billing interval.
|
||||||
*
|
*
|
||||||
* All other PricingPlanLineItem `slugs` are considered custom line-items.
|
* All other PricingPlanLineItem `slugs` are considered custom LineItems.
|
||||||
*/
|
*/
|
||||||
slug: z.union([z.string(), z.literal('base'), z.literal('requests')]),
|
slug: z.union([z.string(), z.literal('base'), z.literal('requests')]),
|
||||||
|
|
||||||
|
@ -123,6 +124,11 @@ const commonPricingPlanLineItemSchema = z.object({
|
||||||
*/
|
*/
|
||||||
interval: pricingIntervalSchema.optional(),
|
interval: pricingIntervalSchema.optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional label for the line-item which will be displayed on customer bills.
|
||||||
|
*
|
||||||
|
* If unset, the line-item's `slug` will be used as the label.
|
||||||
|
*/
|
||||||
label: z.string().optional().openapi('label', { example: 'API calls' })
|
label: z.string().optional().openapi('label', { example: 'API calls' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -136,6 +142,9 @@ export const pricingPlanLineItemSchema = z
|
||||||
.discriminatedUnion('usageType', [
|
.discriminatedUnion('usageType', [
|
||||||
commonPricingPlanLineItemSchema.merge(
|
commonPricingPlanLineItemSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
/**
|
||||||
|
* Licensed LineItems are used to charge for fixed-price services.
|
||||||
|
*/
|
||||||
usageType: z.literal('licensed'),
|
usageType: z.literal('licensed'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,11 +160,21 @@ export const pricingPlanLineItemSchema = z
|
||||||
|
|
||||||
commonPricingPlanLineItemSchema.merge(
|
commonPricingPlanLineItemSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
/**
|
||||||
|
* Metered LineItems are used to charge for usage-based services.
|
||||||
|
*/
|
||||||
usageType: z.literal('metered'),
|
usageType: z.literal('metered'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional label for the line-item which will be displayed on customer
|
||||||
|
* bills.
|
||||||
|
*
|
||||||
|
* If unset, the line-item's `slug` will be used as the unit label.
|
||||||
|
*/
|
||||||
unitLabel: z.string().optional(),
|
unitLabel: z.string().optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional rate limit to enforce for this metered LineItem.
|
* Optional rate limit to enforce for this metered line-item.
|
||||||
*
|
*
|
||||||
* You can use this, for example, to limit the number of API calls that
|
* You can use this, for example, to limit the number of API calls that
|
||||||
* can be made during a given interval.
|
* can be made during a given interval.
|
||||||
|
@ -175,7 +194,6 @@ export const pricingPlanLineItemSchema = z
|
||||||
*/
|
*/
|
||||||
billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]),
|
billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]),
|
||||||
|
|
||||||
//
|
|
||||||
/**
|
/**
|
||||||
* The fixed amount to charge per unit of usage.
|
* The fixed amount to charge per unit of usage.
|
||||||
*
|
*
|
||||||
|
@ -225,6 +243,8 @@ export const pricingPlanLineItemSchema = z
|
||||||
.object({
|
.object({
|
||||||
/**
|
/**
|
||||||
* Divide usage by this number.
|
* Divide usage by this number.
|
||||||
|
*
|
||||||
|
* Must be a positive number.
|
||||||
*/
|
*/
|
||||||
divideBy: z.number().positive(),
|
divideBy: z.number().positive(),
|
||||||
|
|
||||||
|
@ -246,7 +266,7 @@ export const pricingPlanLineItemSchema = z
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
(data) => ({
|
(data) => ({
|
||||||
message: `Invalid PricingPlanLineItem "${data.slug}": reserved "base" line-items must have "licensed" usage type.`
|
message: `Invalid PricingPlanLineItem "${data.slug}": reserved "base" LineItems must have "licensed" usage type.`
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -258,18 +278,18 @@ export const pricingPlanLineItemSchema = z
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
(data) => ({
|
(data) => ({
|
||||||
message: `Invalid PricingPlanLineItem "${data.slug}": reserved "requests" line-items must have "metered" usage type.`
|
message: `Invalid PricingPlanLineItem "${data.slug}": reserved "requests" LineItems must have "metered" usage type.`
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe(
|
.describe(
|
||||||
'PricingPlanLineItems represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Meter` for metered usage.'
|
'PricingPlanLineItems represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Meter` for usage-based line-items.'
|
||||||
)
|
)
|
||||||
.openapi('PricingPlanLineItem')
|
.openapi('PricingPlanLineItem')
|
||||||
export type PricingPlanLineItem = z.infer<typeof pricingPlanLineItemSchema>
|
export type PricingPlanLineItem = z.infer<typeof pricingPlanLineItemSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the config for a Stripe subscription with one or more
|
* Represents the config for a single Stripe subscription plan with one or more
|
||||||
* PricingPlanLineItems.
|
* LineItems.
|
||||||
*/
|
*/
|
||||||
export const pricingPlanSchema = z
|
export const pricingPlanSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -287,53 +307,32 @@ export const pricingPlanSchema = z
|
||||||
*/
|
*/
|
||||||
interval: pricingIntervalSchema.optional(),
|
interval: pricingIntervalSchema.optional(),
|
||||||
|
|
||||||
desc: z.string().optional(),
|
/**
|
||||||
|
* Optional description of the PricingPlan which is used for UI-only.
|
||||||
|
*/
|
||||||
|
description: z.string().optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional list of features of the PricingPlan which is used for UI-only.
|
||||||
|
*/
|
||||||
features: z.array(z.string()).optional(),
|
features: z.array(z.string()).optional(),
|
||||||
|
|
||||||
// TODO?
|
/**
|
||||||
|
* Optional number of days for a free trial period when a customer signs up
|
||||||
|
* for a new subscription.
|
||||||
|
*/
|
||||||
trialPeriodDays: z.number().nonnegative().optional(),
|
trialPeriodDays: z.number().nonnegative().optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of LineItems which are included in the PricingPlan.
|
||||||
|
*
|
||||||
|
* Note: we currently support a max of 20 LineItems per plan.
|
||||||
|
*/
|
||||||
lineItems: z.array(pricingPlanLineItemSchema).nonempty().max(20, {
|
lineItems: z.array(pricingPlanLineItemSchema).nonempty().max(20, {
|
||||||
message:
|
message:
|
||||||
'Stripe Checkout currently supports a max of 20 line-items per subscription.'
|
'Stripe Checkout currently supports a max of 20 LineItems per subscription.'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.interval === undefined) {
|
|
||||||
return data.slug === 'free'
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
(data) => ({
|
|
||||||
message: `Invalid PricingPlan "${data.slug}": non-free pricing plans must have a valid interval`
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.slug === 'free') {
|
|
||||||
return data.interval === undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
(data) => ({
|
|
||||||
message: `Invalid PricingPlan "${data.slug}": free pricing plans must not have an interval`
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const lineItemSlugs = new Set(
|
|
||||||
data.lineItems.map((lineItem) => lineItem.slug)
|
|
||||||
)
|
|
||||||
|
|
||||||
return lineItemSlugs.size === data.lineItems.length
|
|
||||||
},
|
|
||||||
(data) => ({
|
|
||||||
message: `Invalid PricingPlan "${data.slug}": duplicate line-item slugs`
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.describe(
|
.describe(
|
||||||
'Represents the config for a Stripe subscription with one or more PricingPlanLineItems.'
|
'Represents the config for a Stripe subscription with one or more PricingPlanLineItems.'
|
||||||
)
|
)
|
||||||
|
@ -360,49 +359,6 @@ export const pricingPlanListSchema = z
|
||||||
.nonempty({
|
.nonempty({
|
||||||
message: 'Must contain at least one PricingPlan'
|
message: 'Must contain at least one PricingPlan'
|
||||||
})
|
})
|
||||||
.refine(
|
|
||||||
(pricingPlans) => {
|
|
||||||
const slugs = new Set(pricingPlans.map((p) => p.slug))
|
|
||||||
return slugs.size === pricingPlans.length
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: `Invalid PricingPlanList: duplicate PricingPlan slugs`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(pricingPlans) => {
|
|
||||||
const pricingPlanLineItemSlugMap: Record<string, PricingPlanLineItem[]> =
|
|
||||||
{}
|
|
||||||
for (const pricingPlan of pricingPlans) {
|
|
||||||
for (const lineItem of pricingPlan.lineItems) {
|
|
||||||
if (!pricingPlanLineItemSlugMap[lineItem.slug]) {
|
|
||||||
pricingPlanLineItemSlugMap[lineItem.slug] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
pricingPlanLineItemSlugMap[lineItem.slug]!.push(lineItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const lineItems of Object.values(pricingPlanLineItemSlugMap)) {
|
|
||||||
if (lineItems.length <= 1) continue
|
|
||||||
|
|
||||||
const lineItem0 = lineItems[0]!
|
|
||||||
|
|
||||||
for (let i = 1; i < lineItems.length; ++i) {
|
|
||||||
const lineItem = lineItems[i]!
|
|
||||||
|
|
||||||
if (lineItem.usageType !== lineItem0.usageType) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: `Invalid PricingPlanList: all pricing plans which contain the same LineItems (by slug) must have the same usage type (licensed or metered).`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.describe('List of PricingPlans')
|
.describe('List of PricingPlans')
|
||||||
export type PricingPlanList = z.infer<typeof pricingPlanListSchema>
|
export type PricingPlanList = z.infer<typeof pricingPlanListSchema>
|
||||||
|
|
||||||
|
@ -487,34 +443,20 @@ export const deploymentOriginAdapterSchema = z
|
||||||
.discriminatedUnion('type', [
|
.discriminatedUnion('type', [
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
|
/**
|
||||||
|
* OpenAPI 3.x spec describing the origin API server.
|
||||||
|
*/
|
||||||
type: z.literal('openapi'),
|
type: z.literal('openapi'),
|
||||||
// NOTE: The origin API servers should be hidden in the embedded
|
|
||||||
// OpenAPI spec, because clients should only be aware of the upstream
|
/**
|
||||||
// Agentic API gateway.
|
* JSON stringified OpenAPI spec describing the origin API server.
|
||||||
|
*
|
||||||
|
* The origin API servers are be hidden in the embedded OpenAPI spec,
|
||||||
|
* because clients should only be aware of the upstream Agentic API
|
||||||
|
* gateway.
|
||||||
|
*/
|
||||||
spec: z
|
spec: z
|
||||||
.string()
|
.string()
|
||||||
.refine(
|
|
||||||
(spec) => {
|
|
||||||
try {
|
|
||||||
parseJson(spec)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(data) => {
|
|
||||||
try {
|
|
||||||
parseJson(data)
|
|
||||||
} catch (err: any) {
|
|
||||||
return {
|
|
||||||
message: `Invalid OpenAPI spec: ${err.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'Invalid OpenAPI spec'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.describe(
|
.describe(
|
||||||
'JSON stringified OpenAPI spec describing the origin API server.'
|
'JSON stringified OpenAPI spec describing the origin API server.'
|
||||||
)
|
)
|
||||||
|
@ -523,6 +465,13 @@ export const deploymentOriginAdapterSchema = z
|
||||||
|
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
|
/**
|
||||||
|
* Marks the origin server as a raw HTTP REST API without any additional
|
||||||
|
* tool or service definitions.
|
||||||
|
*
|
||||||
|
* In this mode, Agentic's API gateway acts as a simple reverse-proxy
|
||||||
|
* to the origin server, without validating tools or services.
|
||||||
|
*/
|
||||||
type: z.literal('raw')
|
type: z.literal('raw')
|
||||||
})
|
})
|
||||||
.merge(commonDeploymentOriginAdapterSchema)
|
.merge(commonDeploymentOriginAdapterSchema)
|
||||||
|
|
Ładowanie…
Reference in New Issue