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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,11 +1,12 @@
import { z } from '@hono/zod-openapi' import { z } from '@hono/zod-openapi'
import { originAdapterSchema } from './origin-adapter'
import { import {
deploymentOriginAdapterSchema,
pricingIntervalListSchema, pricingIntervalListSchema,
type PricingPlan, type PricingPlan,
pricingPlanListSchema pricingPlanListSchema
} from './schemas' } from './pricing'
import { toolConfigSchema } from './tools'
// TODO: // TODO:
// - **service / tool definitions** // - **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.`), 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 * Optional origin API adapter used to configure the origin API server
* server downstream from Agentic's API gateway. It specifies whether the * downstream from Agentic's API gateway. It specifies whether the origin
* origin API server denoted by \`originUrl\` is hosted externally or deployed * API server denoted by \`originUrl\` is hosted externally or deployed
* internally to Agentic's infrastructure. It also specifies the format * internally to Agentic's infrastructure. It also specifies the format
* for how origin tools / services are defined: either as an OpenAPI spec, * for how origin tools / services are defined: either as an OpenAPI spec,
* an MCP server, or as a raw HTTP REST API. * an MCP server, or as a raw HTTP REST API.
*/ */
originAdapter: deploymentOriginAdapterSchema.optional().default({ originAdapter: originAdapterSchema.optional().default({
location: 'external', location: 'external',
type: 'raw' type: 'raw'
}), }),
/** Optional subscription pricing config */ /** Optional subscription pricing config for this project. */
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.'
@ -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']\`.` To add support for annual pricing plans, for example, you can use: \`['month', 'year']\`.`
) )
.optional() .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() .strip()

Wyświetl plik

@ -4,7 +4,7 @@ import {
type AgenticProjectConfig, type AgenticProjectConfig,
type AgenticProjectConfigInput, type AgenticProjectConfigInput,
agenticProjectConfigSchema agenticProjectConfigSchema
} from './agentic-project-config-schema' } from './agentic-project-config'
/** /**
* This method allows Agentic projects to define their configs in a type-safe * 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' import { z } from '@hono/zod-openapi'
export const deploymentOriginAdapterLocationSchema = z.literal('external') export const originAdapterLocationSchema = z.literal('external')
// z.union([ // z.union([
// z.literal('external'), // z.literal('external'),
// z.literal('internal') // z.literal('internal')
// ]) // ])
export type DeploymentOriginAdapterLocation = z.infer< export type OriginAdapterLocation = z.infer<typeof originAdapterLocationSchema>
typeof deploymentOriginAdapterLocationSchema
>
// export const deploymentOriginAdapterInternalTypeSchema = z.union([ // export const originAdapterInternalTypeSchema = z.union([
// z.literal('docker'), // z.literal('docker'),
// z.literal('mcp'), // z.literal('mcp'),
// z.literal('python-fastapi'), // z.literal('python-fastapi'),
// // etc // // etc
// ]) // ])
// export type DeploymentOriginAdapterInternalType = z.infer< // export type OriginAdapterInternalType = z.infer<
// typeof deploymentOriginAdapterInternalTypeSchema // typeof originAdapterInternalTypeSchema
// > // >
export const commonDeploymentOriginAdapterSchema = z.object({ export const commonOriginAdapterSchema = z.object({
location: deploymentOriginAdapterLocationSchema location: originAdapterLocationSchema
// TODO: Add support for `internal` hosted API servers // TODO: Add support for `internal` hosted API servers
// internalType: deploymentOriginAdapterInternalTypeSchema.optional() // internalType: originAdapterInternalTypeSchema.optional()
}) })
// TODO: add future support for: // TODO: add future support for:
@ -34,11 +32,18 @@ export const commonDeploymentOriginAdapterSchema = z.object({
// - etc // - 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. * Origin API adapter is used to configure the origin API server downstream
* from Agentic's API gateway. It specifies whether the origin API server
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. * 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', [ .discriminatedUnion('type', [
z z
.object({ .object({
@ -60,7 +65,7 @@ export const deploymentOriginAdapterSchema = z
'JSON stringified OpenAPI spec describing the origin API server.' 'JSON stringified OpenAPI spec describing the origin API server.'
) )
}) })
.merge(commonDeploymentOriginAdapterSchema), .merge(commonOriginAdapterSchema),
z z
.object({ .object({
@ -73,14 +78,12 @@ export const deploymentOriginAdapterSchema = z
*/ */
type: z.literal('raw') type: z.literal('raw')
}) })
.merge(commonDeploymentOriginAdapterSchema) .merge(commonOriginAdapterSchema)
]) ])
.describe( .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. `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.` 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') .openapi('OriginAdapter')
export type DeploymentOriginAdapter = z.infer< export type OriginAdapter = z.infer<typeof originAdapterSchema>
typeof deploymentOriginAdapterSchema
>

Wyświetl plik

@ -117,14 +117,7 @@ const commonPricingPlanLineItemSchema = z.object({
label: z.string().optional().openapi('label', { example: 'API calls' }) label: z.string().optional().openapi('label', { example: 'API calls' })
}) })
/** export const pricingPlanLicensedLineItemSchema =
* 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.
*/
export const pricingPlanLineItemSchema = z
.discriminatedUnion('usageType', [
commonPricingPlanLineItemSchema.merge( commonPricingPlanLineItemSchema.merge(
z.object({ z.object({
/** /**
@ -141,8 +134,9 @@ export const pricingPlanLineItemSchema = z
*/ */
amount: z.number().nonnegative() amount: z.number().nonnegative()
}) })
), )
export const pricingPlanMeteredLineItemSchema =
commonPricingPlanLineItemSchema.merge( commonPricingPlanLineItemSchema.merge(
z.object({ z.object({
/** /**
@ -255,6 +249,17 @@ export const pricingPlanLineItemSchema = z
.optional() .optional()
}) })
) )
/**
* 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.
*/
export const pricingPlanLineItemSchema = z
.discriminatedUnion('usageType', [
pricingPlanLicensedLineItemSchema,
pricingPlanMeteredLineItemSchema
]) ])
.refine( .refine(
(data) => { (data) => {

Wyświetl plik

@ -9,7 +9,7 @@ export const rateLimitSchema = z
/** /**
* The interval at which the rate limit is applied. * 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", * [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "8h", "2d",
* "1w", "1y", etc). * "1w", "1y", etc).
*/ */
@ -54,7 +54,7 @@ export const rateLimitSchema = z
}) })
]) ])
.describe( .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 { z } from '@hono/zod-openapi'
import { pricingPlanSlugSchema } from './pricing'
import { rateLimitSchema } from './rate-limit'
export const toolNameSchema = z export const toolNameSchema = z
.string() .string()
// TODO: validate this regex constraint // TODO: validate this regex constraint
@ -69,7 +72,8 @@ export const toolAnnotationsSchema = z
export const toolSchema = z export const toolSchema = z
.object({ .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 `"get_weather"`
* @example `"google_search"` * @example `"google_search"`
@ -115,5 +119,131 @@ export const toolSchema = z
.openapi('Tool') .openapi('Tool')
export type Tool = z.infer<typeof toolSchema> 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 const toolMapSchema = z.record(toolNameSchema, toolSchema)
export type ToolMap = z.infer<typeof toolMapSchema> 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({ export function getPricingPlansByInterval({
pricingInterval, pricingInterval,

Wyświetl plik

@ -3,12 +3,12 @@ import { assert, type Logger, parseZodSchema } from '@agentic/platform-core'
import { validators } from '@agentic/platform-validators' import { validators } from '@agentic/platform-validators'
import { clean as cleanSemver, valid as isValidSemver } from 'semver' import { clean as cleanSemver, valid as isValidSemver } from 'semver'
import type { PricingPlanLineItem } from './schemas' import type { PricingPlanLineItem } from './pricing'
import { import {
type AgenticProjectConfig, type AgenticProjectConfig,
type AgenticProjectConfigInput, type AgenticProjectConfigInput,
agenticProjectConfigSchema agenticProjectConfigSchema
} from './agentic-project-config-schema' } from './agentic-project-config'
import { getPricingPlansByInterval } from './utils' import { getPricingPlansByInterval } from './utils'
import { validateOriginAdapter } from './validate-origin-adapter' 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 { assert, type Logger } from '@agentic/platform-core'
import { validateOpenAPISpec } from '@agentic/platform-openapi' import { validateOpenAPISpec } from '@agentic/platform-openapi'
@ -15,7 +15,7 @@ export async function validateOriginAdapter({
logger logger
}: { }: {
originUrl: string originUrl: string
originAdapter: DeploymentOriginAdapter originAdapter: OriginAdapter
label: string label: string
cwd?: URL cwd?: URL
logger?: Logger logger?: Logger