feat: WIP initial work on API gateway

pull/715/head
Travis Fischer 2025-05-29 03:05:35 +07:00
rodzic fb5a9c0dd2
commit e997d9b8df
12 zmienionych plików z 337 dodań i 174 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
import {
agenticProjectConfigSchema,
type DeploymentOriginAdapter,
type OriginAdapter,
type PricingPlanList
} from '@agentic/platform-schemas'
import { validators } from '@agentic/platform-validators'
@ -88,7 +88,7 @@ export const deployments = pgTable(
originUrl: text().notNull(),
// Origin API adapter config (openapi, mcp, hosted externally or internally, etc)
originAdapter: jsonb().$type<DeploymentOriginAdapter>().notNull(),
originAdapter: jsonb().$type<OriginAdapter>().notNull(),
// Array<PricingPlan>
pricingPlans: jsonb().$type<PricingPlanList>().notNull(),

Wyświetl plik

@ -448,7 +448,7 @@ export interface components {
* "type": "raw"
* }
*/
DeploymentOriginAdapter: {
OriginAdapter: {
/** @enum {string} */
type: "openapi";
/** @description JSON stringified OpenAPI spec describing the origin API server. */
@ -552,7 +552,7 @@ export interface components {
teamId?: string;
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
projectId: string;
originAdapter?: components["schemas"]["DeploymentOriginAdapter"];
originAdapter?: components["schemas"]["OriginAdapter"];
/**
* @description 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 [
@ -1441,7 +1441,7 @@ export interface operations {
* 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.
*/
originUrl: string;
originAdapter?: components["schemas"]["DeploymentOriginAdapter"];
originAdapter?: components["schemas"]["OriginAdapter"];
/**
* @description 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 [

Wyświetl plik

@ -13,8 +13,7 @@ export type TeamMember = components['schemas']['TeamMember']
export type ProjectIdentifier = components['schemas']['ProjectIdentifier']
export type DeploymentIdentifier = components['schemas']['DeploymentIdentifier']
export type DeploymentOriginAdapter =
components['schemas']['DeploymentOriginAdapter']
export type OriginAdapter = components['schemas']['OriginAdapter']
export type RateLimit = components['schemas']['RateLimit']
export type PricingInterval = components['schemas']['PricingInterval']

Wyświetl plik

@ -1,11 +1,12 @@
import { z } from '@hono/zod-openapi'
import { originAdapterSchema } from './origin-adapter'
import {
deploymentOriginAdapterSchema,
pricingIntervalListSchema,
type PricingPlan,
pricingPlanListSchema
} from './schemas'
} from './pricing'
import { toolConfigSchema } from './tools'
// TODO:
// - **service / tool definitions**
@ -93,19 +94,19 @@ export const agenticProjectConfigSchema = z
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 deployment origin API adapter used to configure the origin API
* server downstream from Agentic's API gateway. It specifies whether the
* origin API server denoted by \`originUrl\` is hosted externally or deployed
* Optional origin API adapter used to configure the origin API server
* downstream from Agentic's API gateway. It specifies whether the origin
* API server denoted by \`originUrl\` is hosted externally or deployed
* internally to Agentic's infrastructure. It also specifies the format
* for how origin tools / services are defined: either as an OpenAPI spec,
* an MCP server, or as a raw HTTP REST API.
*/
originAdapter: deploymentOriginAdapterSchema.optional().default({
originAdapter: originAdapterSchema.optional().default({
location: 'external',
type: 'raw'
}),
/** Optional subscription pricing config */
/** Optional subscription pricing config for this project. */
pricingPlans: pricingPlanListSchema
.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.'
@ -137,7 +138,32 @@ Defaults to a single monthly interval \`['month']\`.
To add support for annual pricing plans, for example, you can use: \`['month', 'year']\`.`
)
.optional()
.default(['month'])
.default(['month']),
/**
* Optional list of tool configs to customize the behavior of tools.
*
* Make sure the tool `name` matches the origin server's tool names, either
* via its MCP server or OpenAPI operationIds.
*
* Tool names are expected to be unique and stable across deployments.
*
* With `toolConfigs`, tools can be disabled, set custom rate-limits,
* customize reporting usage for metered billing, and they can also
* override behavior for different pricing plans.
*
* For example, you may want to disable certain tools on a `free` pricing
* plan or remove the rate-limit for a specific tool on a `pro` pricing
* plan while keeping the defualt rate-limit in place for other tools.
*
* Note that tool-specific configs override the defaults defined in
* pricing plans.
*
* If a tool is defined on the origin server but not specified in
* `toolConfigs`, it will use the default behavior of the Agentic API
* gateway.
*/
toolConfigs: z.array(toolConfigSchema).optional()
})
.strip()

Wyświetl plik

@ -4,7 +4,7 @@ import {
type AgenticProjectConfig,
type AgenticProjectConfigInput,
agenticProjectConfigSchema
} from './agentic-project-config-schema'
} from './agentic-project-config'
/**
* This method allows Agentic projects to define their configs in a type-safe

Wyświetl plik

@ -1,29 +1,27 @@
import { z } from '@hono/zod-openapi'
export const deploymentOriginAdapterLocationSchema = z.literal('external')
export const originAdapterLocationSchema = z.literal('external')
// z.union([
// z.literal('external'),
// z.literal('internal')
// ])
export type DeploymentOriginAdapterLocation = z.infer<
typeof deploymentOriginAdapterLocationSchema
>
export type OriginAdapterLocation = z.infer<typeof originAdapterLocationSchema>
// export const deploymentOriginAdapterInternalTypeSchema = z.union([
// export const originAdapterInternalTypeSchema = z.union([
// z.literal('docker'),
// z.literal('mcp'),
// z.literal('python-fastapi'),
// // etc
// ])
// export type DeploymentOriginAdapterInternalType = z.infer<
// typeof deploymentOriginAdapterInternalTypeSchema
// export type OriginAdapterInternalType = z.infer<
// typeof originAdapterInternalTypeSchema
// >
export const commonDeploymentOriginAdapterSchema = z.object({
location: deploymentOriginAdapterLocationSchema
export const commonOriginAdapterSchema = z.object({
location: originAdapterLocationSchema
// TODO: Add support for `internal` hosted API servers
// internalType: deploymentOriginAdapterInternalTypeSchema.optional()
// internalType: originAdapterInternalTypeSchema.optional()
})
// TODO: add future support for:
@ -34,11 +32,18 @@ export const commonDeploymentOriginAdapterSchema = z.object({
// - etc
/**
* Deployment origin API adapter is used to configure the origin API server downstream from Agentic's API gateway. It specifies whether the origin API server denoted by `originUrl` is hosted externally or deployed internally to Agentic's infrastructure. It also specifies the format for how origin tools / services are defined: either as an OpenAPI spec, an MCP server, or as a raw HTTP REST API.
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.
* Origin API adapter is used to configure the origin API server downstream
* from Agentic's API gateway. It specifies whether the origin API server
* denoted by `originUrl` is hosted externally or deployed internally to
* Agentic's infrastructure. It also specifies the format for how origin tools
* are defined: either as an OpenAPI spec, an MCP server, or as a raw HTTP
* REST API.
*
* 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.
*/
export const deploymentOriginAdapterSchema = z
export const originAdapterSchema = z
.discriminatedUnion('type', [
z
.object({
@ -60,7 +65,7 @@ export const deploymentOriginAdapterSchema = z
'JSON stringified OpenAPI spec describing the origin API server.'
)
})
.merge(commonDeploymentOriginAdapterSchema),
.merge(commonOriginAdapterSchema),
z
.object({
@ -73,14 +78,12 @@ export const deploymentOriginAdapterSchema = z
*/
type: z.literal('raw')
})
.merge(commonDeploymentOriginAdapterSchema)
.merge(commonOriginAdapterSchema)
])
.describe(
`Deployment origin API adapter is used to configure the origin API server downstream from Agentic's API gateway. It specifies whether the origin API server denoted by \`originUrl\` is hosted externally or deployed internally to Agentic's infrastructure. It also specifies the format for how origin tools / services are defined: either as an OpenAPI spec, an MCP server, or as a raw HTTP REST API.
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.`
)
.openapi('DeploymentOriginAdapter')
export type DeploymentOriginAdapter = z.infer<
typeof deploymentOriginAdapterSchema
>
.openapi('OriginAdapter')
export type OriginAdapter = z.infer<typeof originAdapterSchema>

Wyświetl plik

@ -117,6 +117,139 @@ const commonPricingPlanLineItemSchema = z.object({
label: z.string().optional().openapi('label', { example: 'API calls' })
})
export const pricingPlanLicensedLineItemSchema =
commonPricingPlanLineItemSchema.merge(
z.object({
/**
* Licensed LineItems are used to charge for fixed-price services.
*/
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()
})
)
export const pricingPlanMeteredLineItemSchema =
commonPricingPlanLineItemSchema.merge(
z.object({
/**
* Metered LineItems are used to charge for usage-based services.
*/
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(),
/**
* 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
* `tiered`.
*
* `per_unit` indicates that the fixed amount (specified in
* `unitAmount`) will be charged per unit of total usage.
*
* `tiered` indicates that the unit pricing will be computed using a
* tiering strategy as defined using `tiers` and `tiersMode`.
*/
billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]),
/**
* 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
/**
* Defines if the tiering price should be `graduated` or `volume` based.
* In `volume`-based tiering, the maximum quantity within a period
* determines the per unit price, in `graduated` tiering pricing can
* successively change as the quantity grows.
*
* This field requires `billingScheme` to be set to `tiered`.
*/
tiersMode: z
.union([z.literal('graduated'), z.literal('volume')])
.optional(),
/**
* Pricing tiers for `tiered` billing schemes.
*
* This field requires `billingScheme` to be set to `tiered`.
*/
tiers: z.array(pricingPlanTierSchema).optional(),
// TODO: add support for tiered rate limits?
/**
* The default settings to aggregate the Stripe Meter's events with.
*
* Deafults to `{ formula: 'sum' }`.
*/
defaultAggregation: z
.object({
/**
* Specifies how events are aggregated for a Stripe Meter.
* Allowed values are `count` to count the number of events, `sum`
* to sum each event's value and `last` to take the last event's
* value in the window.
*
* Defaults to `sum`.
*/
formula: z
.union([z.literal('sum'), z.literal('count'), z.literal('last')])
.default('sum')
})
.optional(),
/**
* Optionally apply a transformation to the reported usage or set
* quantity before computing the amount billed. Cannot be combined
* with `tiers`.
*/
transformQuantity: z
.object({
/**
* Divide usage by this number.
*
* Must be a positive number.
*/
divideBy: z.number().positive(),
/**
* After division, either round the result `up` or `down`.
*/
round: z.union([z.literal('down'), z.literal('up')])
})
.optional()
})
)
/**
* PricingPlanLineItems represent a single line-item in a Stripe Subscription.
*
@ -125,136 +258,8 @@ const commonPricingPlanLineItemSchema = z.object({
*/
export const pricingPlanLineItemSchema = z
.discriminatedUnion('usageType', [
commonPricingPlanLineItemSchema.merge(
z.object({
/**
* Licensed LineItems are used to charge for fixed-price services.
*/
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()
})
),
commonPricingPlanLineItemSchema.merge(
z.object({
/**
* Metered LineItems are used to charge for usage-based services.
*/
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(),
/**
* 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
* `tiered`.
*
* `per_unit` indicates that the fixed amount (specified in
* `unitAmount`) will be charged per unit of total usage.
*
* `tiered` indicates that the unit pricing will be computed using a
* tiering strategy as defined using `tiers` and `tiersMode`.
*/
billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]),
/**
* 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
/**
* Defines if the tiering price should be `graduated` or `volume` based.
* In `volume`-based tiering, the maximum quantity within a period
* determines the per unit price, in `graduated` tiering pricing can
* successively change as the quantity grows.
*
* This field requires `billingScheme` to be set to `tiered`.
*/
tiersMode: z
.union([z.literal('graduated'), z.literal('volume')])
.optional(),
/**
* Pricing tiers for `tiered` billing schemes.
*
* This field requires `billingScheme` to be set to `tiered`.
*/
tiers: z.array(pricingPlanTierSchema).optional(),
// TODO: add support for tiered rate limits?
/**
* The default settings to aggregate the Stripe Meter's events with.
*
* Deafults to `{ formula: 'sum' }`.
*/
defaultAggregation: z
.object({
/**
* Specifies how events are aggregated for a Stripe Meter.
* Allowed values are `count` to count the number of events, `sum`
* to sum each event's value and `last` to take the last event's
* value in the window.
*
* Defaults to `sum`.
*/
formula: z
.union([z.literal('sum'), z.literal('count'), z.literal('last')])
.default('sum')
})
.optional(),
/**
* Optionally apply a transformation to the reported usage or set
* quantity before computing the amount billed. Cannot be combined
* with `tiers`.
*/
transformQuantity: z
.object({
/**
* Divide usage by this number.
*
* Must be a positive number.
*/
divideBy: z.number().positive(),
/**
* After division, either round the result `up` or `down`.
*/
round: z.union([z.literal('down'), z.literal('up')])
})
.optional()
})
)
pricingPlanLicensedLineItemSchema,
pricingPlanMeteredLineItemSchema
])
.refine(
(data) => {

Wyświetl plik

@ -9,7 +9,7 @@ export const rateLimitSchema = z
/**
* The interval at which the rate limit is applied.
*
* Either a positive number expressed in seconds or a valid positive
* 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).
*/
@ -54,7 +54,7 @@ export const rateLimitSchema = z
})
])
.describe(
`The interval at which the rate limit is applied. Either a positive number in seconds or a valid positive [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d", "1w", "1y", etc).`
`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).`
),
/**

Wyświetl plik

@ -1,5 +1,8 @@
import { z } from '@hono/zod-openapi'
import { pricingPlanSlugSchema } from './pricing'
import { rateLimitSchema } from './rate-limit'
export const toolNameSchema = z
.string()
// TODO: validate this regex constraint
@ -69,7 +72,8 @@ export const toolAnnotationsSchema = z
export const toolSchema = z
.object({
/**
* The name of the tool.
* The name of the tool, which acts as a unique, stable identifier for the
* tool across deployments.
*
* @example `"get_weather"`
* @example `"google_search"`
@ -115,5 +119,131 @@ export const toolSchema = z
.openapi('Tool')
export type Tool = z.infer<typeof toolSchema>
/**
* Customizes a tool's behavior for a given pricing plan.
*/
export const pricingPlanToolConfigSchema = z
.object({
/**
* Whether this tool should be enabled for customers on a given pricing plan.
*
* @default true
*/
enabled: z.boolean().default(true).optional(),
/**
* Overrides whether to report default `requests` usage for metered billing
* for customers a given pricing plan.
*
* Note: This is only relevant if the pricing plan includes a `requests`
* line-item.
*
* @default undefined
*/
reportUsage: z.boolean().optional(),
/**
* Customize or disable rate limits for this tool for customers on a given
* pricing plan.
*
* Set to `null` to disable the default request-based rate-limiting for
* this tool on a given pricing plan.
*
* @default undefined
*/
rateLimit: z.union([rateLimitSchema, z.null()]).optional()
})
.openapi('PricingPlanToolConfig')
export type PricingPlanToolConfig = z.infer<typeof pricingPlanToolConfigSchema>
/**
* Customizes a tool's default behavior across all pricing plans.
*/
export const toolConfigSchema = z
.object({
/**
* The name of the tool, which acts as a unique, stable identifier for the
* tool across deployments.
*/
name: toolNameSchema,
/**
* Whether this tool should be enabled for all customers (default).
*
* If you want to hide a tool from customers but still have it present on
* your origin server, set this to `false` for the given tool.
*
* @default true
*/
enabled: z.boolean().default(true).optional(),
/**
* Whether this tool's output is deterministic and idempotent given the
* same input.
*
* If `true`, tool outputs will be cached aggressively for identical
* requests, though origin server response headers can still override this
* behavior on a per-request basis.
*
* If `false`, tool outputs will be cached according to the origin server's
* response headers on a per-request basis.
*
* @default false
*/
immutable: z.boolean().default(false).optional(),
/**
* Whether calls to this tool should be reported as usage for the default
* `requests` line-item's metered billing.
*
* Note: This is only relevant if the customer's active pricing plan
* includes a `requests` line-item.
*
* @default true
*/
reportUsage: z.boolean().default(true).optional(),
/**
* Customize the default `requests`-based rate-limiting for this tool.
*
* Set to `null` to disable the built-in rate-limiting.
*
* If not set, the default rate-limiting for the active pricing plan will be
* used.
*
* @default undefined
*/
rateLimit: z.union([rateLimitSchema, z.null()]).optional(),
/**
* Allows you to customize this tool's behavior or disable it entirely for
* different pricing plans.
*
* This is a map from PricingPlan slug to PricingPlanToolConfig.
*
* @example
* {
* "free": {
* "disabled": true
* }
* }
*/
pricingPlanConfig: z
.record(pricingPlanSlugSchema, pricingPlanToolConfigSchema)
.optional()
.describe(
'Map of PricingPlan slug to tool config overrides for a given plan. This is useful to customize tool behavior or disable tools completely on different pricing plans.'
)
// TODO: mapping from OpenAPI operationId to tools
// TODO?
// name
// path, httpMethod
// examples
// headers
})
.openapi('ToolConfig')
export type ToolConfig = z.infer<typeof toolConfigSchema>
export const toolMapSchema = z.record(toolNameSchema, toolSchema)
export type ToolMap = z.infer<typeof toolMapSchema>

Wyświetl plik

@ -1,4 +1,4 @@
import type { PricingInterval, PricingPlan, PricingPlanList } from './schemas'
import type { PricingInterval, PricingPlan, PricingPlanList } from './pricing'
export function getPricingPlansByInterval({
pricingInterval,

Wyświetl plik

@ -3,12 +3,12 @@ import { assert, type Logger, parseZodSchema } from '@agentic/platform-core'
import { validators } from '@agentic/platform-validators'
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
import type { PricingPlanLineItem } from './schemas'
import type { PricingPlanLineItem } from './pricing'
import {
type AgenticProjectConfig,
type AgenticProjectConfigInput,
agenticProjectConfigSchema
} from './agentic-project-config-schema'
} from './agentic-project-config'
import { getPricingPlansByInterval } from './utils'
import { validateOriginAdapter } from './validate-origin-adapter'

Wyświetl plik

@ -1,4 +1,4 @@
import type { DeploymentOriginAdapter } from '@agentic/platform-schemas'
import type { OriginAdapter } from '@agentic/platform-schemas'
import { assert, type Logger } from '@agentic/platform-core'
import { validateOpenAPISpec } from '@agentic/platform-openapi'
@ -15,7 +15,7 @@ export async function validateOriginAdapter({
logger
}: {
originUrl: string
originAdapter: DeploymentOriginAdapter
originAdapter: OriginAdapter
label: string
cwd?: URL
logger?: Logger