diff --git a/packages/cli/fixtures/pricing-freemium/agentic.config.ts b/packages/cli/fixtures/pricing-freemium/agentic.config.ts new file mode 100644 index 00000000..34bd6947 --- /dev/null +++ b/packages/cli/fixtures/pricing-freemium/agentic.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, freePricingPlan } from '@agentic/platform-schemas' + +export default defineConfig({ + // TODO: resolve name / slug conflicts + name: 'My Project', + originUrl: 'https://httpbin.org', + pricingPlans: [ + freePricingPlan, + { + name: 'Basic', + slug: 'basic', + interval: 'month', + trialPeriodDays: 7, + lineItems: [ + { + slug: 'base', + usageType: 'licensed', + amount: 490, + interval: 'month' + } + ] + } + ] +}) diff --git a/packages/cli/src/lib/__snapshots__/load-agentic-config.test.ts.snap b/packages/cli/src/lib/__snapshots__/load-agentic-config.test.ts.snap index e2ea1c31..9a21e2b0 100644 --- a/packages/cli/src/lib/__snapshots__/load-agentic-config.test.ts.snap +++ b/packages/cli/src/lib/__snapshots__/load-agentic-config.test.ts.snap @@ -3,35 +3,21 @@ exports[`loadAgenticConfig > basic-raw-free-json 1`] = ` { "name": "My Project", - "originAdapter": { - "location": "external", - "type": "raw", - }, "originUrl": "https://jsonplaceholder.typicode.com", - "pricingPlans": [ - { - "lineItems": [ - { - "amount": 0, - "slug": "base", - "usageType": "licensed", - }, - ], - "name": "Free", - "slug": "free", - }, - ], } `; exports[`loadAgenticConfig > basic-raw-free-ts 1`] = ` { "name": "My Project", - "originAdapter": { - "location": "external", - "type": "raw", - }, "originUrl": "https://jsonplaceholder.typicode.com", +} +`; + +exports[`loadAgenticConfig > pricing-freemium 1`] = ` +{ + "name": "My Project", + "originUrl": "https://httpbin.org", "pricingPlans": [ { "lineItems": [ @@ -44,6 +30,20 @@ exports[`loadAgenticConfig > basic-raw-free-ts 1`] = ` "name": "Free", "slug": "free", }, + { + "interval": "month", + "lineItems": [ + { + "amount": 490, + "interval": "month", + "slug": "base", + "usageType": "licensed", + }, + ], + "name": "Basic", + "slug": "basic", + "trialPeriodDays": 7, + }, ], } `; diff --git a/packages/cli/src/lib/load-agentic-config.test.ts b/packages/cli/src/lib/load-agentic-config.test.ts index 2e105391..075874c4 100644 --- a/packages/cli/src/lib/load-agentic-config.test.ts +++ b/packages/cli/src/lib/load-agentic-config.test.ts @@ -5,7 +5,11 @@ import { describe, expect, test } from 'vitest' import { loadAgenticConfig } from './load-agentic-config' -const fixtures = ['basic-raw-free-ts', 'basic-raw-free-json'] +const fixtures = [ + 'basic-raw-free-ts', + 'basic-raw-free-json', + 'pricing-freemium' +] const fixturesDir = path.join( fileURLToPath(import.meta.url), diff --git a/packages/schemas/bin/generate-project-config-json-schema.ts b/packages/schemas/bin/generate-project-config-json-schema.ts index b54029ef..6f753dc6 100644 --- a/packages/schemas/bin/generate-project-config-json-schema.ts +++ b/packages/schemas/bin/generate-project-config-json-schema.ts @@ -13,9 +13,9 @@ async function main() { $schema: 'https://json-schema.org/draft-07/schema', // TODO // $id: 'https://agentic.so/docs/schema.json', - title: 'Agentic project schema', + title: 'Agentic Project Config Schema', description: - "Schema used by 'agentic.json' files to configure Agentic projects." + 'JSON Schema used by `agentic.config.{ts,js,json}` files to configure Agentic projects.' } // eslint-disable-next-line no-console diff --git a/packages/schemas/src/agentic-project-config-schema.json b/packages/schemas/src/agentic-project-config-schema.json index 004fe23e..3fd4eb7f 100644 --- a/packages/schemas/src/agentic-project-config-schema.json +++ b/packages/schemas/src/agentic-project-config-schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft-07/schema", - "title": "Agentic project schema", - "description": "Schema used by 'agentic.json' files to configure Agentic projects.", + "title": "Agentic Project Config Schema", + "description": "JSON Schema used by `agentic.config.{ts,js,json}` files to configure Agentic projects.", "additionalProperties": false, "type": "object", "properties": { diff --git a/packages/schemas/src/agentic-project-config-schema.ts b/packages/schemas/src/agentic-project-config-schema.ts index ea1c8cb1..bde884b9 100644 --- a/packages/schemas/src/agentic-project-config-schema.ts +++ b/packages/schemas/src/agentic-project-config-schema.ts @@ -1,6 +1,10 @@ import { z } from '@hono/zod-openapi' -import { deploymentOriginAdapterSchema, pricingPlanListSchema } from './schemas' +import { + deploymentOriginAdapterSchema, + type PricingPlan, + pricingPlanListSchema +} from './schemas' // TODO: // - **service / tool definitions** @@ -12,6 +16,18 @@ import { deploymentOriginAdapterSchema, pricingPlanListSchema } from './schemas' // - optional version // - optional agentic version +export const freePricingPlan = { + name: 'Free', + slug: 'free', + lineItems: [ + { + slug: 'base', + usageType: 'licensed', + amount: 0 + } + ] +} as const satisfies PricingPlan + export const agenticProjectConfigSchema = z.object({ name: z.string().describe('Name of the project.'), @@ -46,29 +62,20 @@ 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.`), // Optional origin API config - originAdapter: deploymentOriginAdapterSchema.default({ - location: 'external', - type: 'raw' - }), + originAdapter: deploymentOriginAdapterSchema + .default({ + location: 'external', + type: 'raw' + }) + .optional(), // Optional subscription pricing config pricingPlans: pricingPlanListSchema .describe( - 'List of PricingPlans to enable subscriptions for the project. Defaults to a single free tier.' + '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([ - { - name: 'Free', - slug: 'free', - lineItems: [ - { - slug: 'base', - usageType: 'licensed', - amount: 0 - } - ] - } - ]) + .default([freePricingPlan]) + .optional() }) export type AgenticProjectConfigInput = z.input< typeof agenticProjectConfigSchema diff --git a/packages/schemas/src/schemas.ts b/packages/schemas/src/schemas.ts index ec1c365c..07cf6506 100644 --- a/packages/schemas/src/schemas.ts +++ b/packages/schemas/src/schemas.ts @@ -80,15 +80,20 @@ const commonPricingPlanLineItemSchema = z.object({ * Slugs act as the primary key for LineItems. They should be lower and * kebab-cased ("base", "requests", "image-transformations"). * - * TODO: ensure user-provided custom LineItems don't use reserved 'base' - * and 'requests' slugs. + * The `base` slug is reserved for a plan's default `licensed` line-item. + * + * The `requests` slug is reserved for charging using `metered billing based + * on the number of request made during a given billing interval. + * + * All other PricingPlanLineItem `slugs` are considered custom line-items. */ slug: z.union([z.string(), z.literal('base'), z.literal('requests')]), /** * The frequency at which a subscription is billed. * - * Only optional when `PricingPlan.slug` is `free`. + * Only optional on free plans (when `PricingPlan.slug` is `free`), since + * free plans don't depend on a billing interval. */ interval: pricingIntervalSchema.optional(), @@ -106,6 +111,14 @@ export const pricingPlanLineItemSchema = z commonPricingPlanLineItemSchema.merge( z.object({ usageType: z.literal('licensed'), + + /** + * The fixed amount to charge per billing interval. + * + * Specified in the smallest currency unit (e.g. cents for USD). + * + * So 100 = $1.00 USD, 1000 = $10.00 USD, etc. + */ amount: z.number().nonnegative() }) ), @@ -136,7 +149,16 @@ export const pricingPlanLineItemSchema = z */ billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]), - // Only applicable for `per_unit` billing schemes + // + /** + * The fixed amount to charge per unit of usage. + * + * Only applicable for `per_unit` billing schemes. + * + * Specified in the smallest currency unit (e.g. cents for USD). + * + * So 100 = $1.00 USD, 1000 = $10.00 USD, etc. + */ unitAmount: z.number().nonnegative().optional(), // Only applicable for `tiered` billing schemes