kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: WIP kittens
rodzic
85646edfdc
commit
80bff87be6
|
@ -45,15 +45,23 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
|
||||||
const { deploymentIdentifier, populate = [] } = c.req.valid('query')
|
const { deploymentIdentifier, populate = [] } = c.req.valid('query')
|
||||||
await aclAdmin(c)
|
await aclAdmin(c)
|
||||||
|
|
||||||
const deployment = await tryGetDeploymentByIdentifier(c, {
|
const { project, ...deployment } = await tryGetDeploymentByIdentifier(c, {
|
||||||
deploymentIdentifier,
|
deploymentIdentifier,
|
||||||
with: {
|
with: {
|
||||||
...Object.fromEntries(populate.map((field) => [field, true]))
|
...Object.fromEntries(populate.map((field) => [field, true])),
|
||||||
|
project: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
|
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
|
||||||
|
assert(
|
||||||
|
project,
|
||||||
|
404,
|
||||||
|
`Project not found for deployment "${deploymentIdentifier}"`
|
||||||
|
)
|
||||||
await acl(c, deployment, { label: 'Deployment' })
|
await acl(c, deployment, { label: 'Deployment' })
|
||||||
|
|
||||||
|
const hasPopulateProject = populate.includes('project')
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// TODO: switch from published to publishedAt?
|
// TODO: switch from published to publishedAt?
|
||||||
// if (deployment.published) {
|
// if (deployment.published) {
|
||||||
|
@ -69,7 +77,11 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return c.json(
|
return c.json(
|
||||||
parseZodSchema(schema.deploymentAdminSelectSchema, deployment)
|
parseZodSchema(schema.deploymentAdminSelectSchema, {
|
||||||
|
...deployment,
|
||||||
|
...(hasPopulateProject ? { project } : {}),
|
||||||
|
_secret: project._secret
|
||||||
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,9 @@ export const consumers = pgTable(
|
||||||
onDelete: 'cascade'
|
onDelete: 'cascade'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Stripe subscription status (synced via webhooks)
|
// Stripe subscription status (synced via webhooks). Should move from
|
||||||
stripeStatus: text(),
|
// `incomplete` to `active` after the first successful payment.
|
||||||
|
stripeStatus: text().default('incomplete').notNull(),
|
||||||
|
|
||||||
// Whether the consumer's subscription is currently active, depending on
|
// Whether the consumer's subscription is currently active, depending on
|
||||||
// `stripeStatus`.
|
// `stripeStatus`.
|
||||||
|
|
|
@ -193,7 +193,8 @@ Deployments are private to a developer or team until they are published, at whic
|
||||||
|
|
||||||
export const deploymentAdminSelectSchema = deploymentSelectSchema
|
export const deploymentAdminSelectSchema = deploymentSelectSchema
|
||||||
.extend({
|
.extend({
|
||||||
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl
|
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl,
|
||||||
|
_secret: z.string().nonempty()
|
||||||
})
|
})
|
||||||
.openapi('AdminDeployment')
|
.openapi('AdminDeployment')
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ export async function upsertStripeSubscription(
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateParams: Stripe.SubscriptionUpdateParams = {
|
const updateParams: Stripe.SubscriptionUpdateParams = {
|
||||||
|
collection_method: 'charge_automatically',
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: consumer.userId,
|
userId: consumer.userId,
|
||||||
consumerId: consumer.id,
|
consumerId: consumer.id,
|
||||||
|
@ -259,10 +260,11 @@ export async function upsertStripeSubscription(
|
||||||
|
|
||||||
const createParams: Stripe.SubscriptionCreateParams = {
|
const createParams: Stripe.SubscriptionCreateParams = {
|
||||||
customer: stripeCustomerId,
|
customer: stripeCustomerId,
|
||||||
description: `Agentic subscription to project "${project.id}"`,
|
description: `Agentic subscription to project "${project.identifier}"`,
|
||||||
// TODO: coupons
|
// TODO: coupons
|
||||||
// coupon: filterConsumerCoupon(ctx, consumer, deployment),
|
// coupon: filterConsumerCoupon(ctx, consumer, deployment),
|
||||||
items,
|
items,
|
||||||
|
collection_method: 'charge_automatically',
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: consumer.userId,
|
userId: consumer.userId,
|
||||||
consumerId: consumer.id,
|
consumerId: consumer.id,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { RawConsumerUpdate } from '@/db'
|
import type { RawConsumerUpdate } from '@/db'
|
||||||
|
|
||||||
|
// https://docs.stripe.com/api/subscriptions/object#subscription_object-status
|
||||||
const stripeValidSubscriptionStatuses = new Set([
|
const stripeValidSubscriptionStatuses = new Set([
|
||||||
'active',
|
'active',
|
||||||
'trialing',
|
'trialing',
|
||||||
|
|
|
@ -1,12 +1,28 @@
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
import type { Context } from './types'
|
import type { Context } from './types'
|
||||||
|
|
||||||
export async function enforceRateLimit(
|
export async function enforceRateLimit(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
{}: {
|
{
|
||||||
|
id,
|
||||||
|
interval,
|
||||||
|
maxPerInterval,
|
||||||
|
method,
|
||||||
|
pathname
|
||||||
|
}: {
|
||||||
id: string
|
id: string
|
||||||
interval: number
|
interval: number
|
||||||
maxPerInterval: number
|
maxPerInterval: number
|
||||||
method: string
|
method: string
|
||||||
pathname: string
|
pathname: string
|
||||||
}
|
}
|
||||||
) {}
|
) {
|
||||||
|
// TODO
|
||||||
|
assert(ctx, 500, 'not implemented')
|
||||||
|
assert(id, 500, 'not implemented')
|
||||||
|
assert(interval > 0, 500, 'not implemented')
|
||||||
|
assert(maxPerInterval >= 0, 500, 'not implemented')
|
||||||
|
assert(method, 500, 'not implemented')
|
||||||
|
assert(pathname, 500, 'not implemented')
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import type { Consumer } from '@agentic/platform-api-client'
|
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
import type { Context } from './types'
|
import type { AdminConsumer, Context } from './types'
|
||||||
|
|
||||||
export async function getConsumer(
|
export async function getConsumer(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
token: string
|
token: string
|
||||||
): Promise<Consumer> {
|
): Promise<AdminConsumer> {
|
||||||
const consumer = await ctx.client.adminGetConsumerByToken({
|
const consumer = await ctx.client.adminGetConsumerByToken({
|
||||||
token
|
token,
|
||||||
|
populate: ['user']
|
||||||
})
|
})
|
||||||
assert(consumer, 404, `API token not found "${token}"`)
|
assert(consumer, 404, `API token not found "${token}"`)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AdminDeployment } from '@agentic/platform-api-client'
|
import type { AdminDeployment } from '@agentic/platform-types'
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import type {
|
import type { PricingPlan, RateLimit } from '@agentic/platform-types'
|
||||||
AdminDeployment,
|
|
||||||
Consumer,
|
|
||||||
PricingPlan,
|
|
||||||
RateLimit,
|
|
||||||
Tool
|
|
||||||
} from '@agentic/platform-types'
|
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
import type { Context } from './types'
|
import type { AdminConsumer, Context, ResolvedOriginRequest } from './types'
|
||||||
import { getConsumer } from './get-consumer'
|
import { getConsumer } from './get-consumer'
|
||||||
import { getDeployment } from './get-deployment'
|
import { getDeployment } from './get-deployment'
|
||||||
import { getTool } from './get-tool'
|
import { getTool } from './get-tool'
|
||||||
|
@ -20,11 +14,12 @@ import { updateOriginRequest } from './update-origin-request'
|
||||||
* Also ensures that the request is valid, enforces rate limits, and adds proxy-
|
* Also ensures that the request is valid, enforces rate limits, and adds proxy-
|
||||||
* specific headers to the origin request.
|
* specific headers to the origin request.
|
||||||
*/
|
*/
|
||||||
export async function resolveOriginRequest(ctx: Context) {
|
export async function resolveOriginRequest(
|
||||||
|
ctx: Context
|
||||||
|
): Promise<ResolvedOriginRequest> {
|
||||||
const { req } = ctx
|
const { req } = ctx
|
||||||
const ip = req.headers.get('cf-connecting-ip')
|
const ip = req.headers.get('cf-connecting-ip') || undefined
|
||||||
const requestUrl = new URL(req.url)
|
const requestUrl = new URL(req.url)
|
||||||
const date = Date.now()
|
|
||||||
|
|
||||||
const { search, pathname } = requestUrl
|
const { search, pathname } = requestUrl
|
||||||
const method = req.method.toLowerCase()
|
const method = req.method.toLowerCase()
|
||||||
|
@ -36,18 +31,19 @@ export async function resolveOriginRequest(ctx: Context) {
|
||||||
deployment,
|
deployment,
|
||||||
toolPath
|
toolPath
|
||||||
})
|
})
|
||||||
console.log('rqeuest', {
|
|
||||||
|
console.log('request', {
|
||||||
method,
|
method,
|
||||||
pathname,
|
pathname,
|
||||||
search,
|
search,
|
||||||
deployment: deployment.identifier,
|
deploymentIdentifier: deployment.identifier,
|
||||||
toolPath,
|
toolPath,
|
||||||
tool
|
tool
|
||||||
})
|
})
|
||||||
|
|
||||||
let reportUsage = true
|
|
||||||
let pricingPlan: PricingPlan | undefined
|
let pricingPlan: PricingPlan | undefined
|
||||||
let consumer: Consumer | undefined
|
let consumer: AdminConsumer | undefined
|
||||||
|
let reportUsage = true
|
||||||
|
|
||||||
const token = (req.headers.get('authorization') || '')
|
const token = (req.headers.get('authorization') || '')
|
||||||
.replace(/^Bearer /i, '')
|
.replace(/^Bearer /i, '')
|
||||||
|
@ -177,32 +173,26 @@ export async function resolveOriginRequest(ctx: Context) {
|
||||||
// TODO: what do we want the API gateway's interface to be?
|
// TODO: what do we want the API gateway's interface to be?
|
||||||
// - support both MCP and OpenAPI / raw?
|
// - support both MCP and OpenAPI / raw?
|
||||||
|
|
||||||
const originUrl = `${deployment.originUrl}${toolPath}${search}`
|
let originRequest: Request | undefined
|
||||||
console.log('originUrl', originUrl)
|
if (
|
||||||
|
deployment.originAdapter.type === 'openapi' ||
|
||||||
|
deployment.originAdapter.type === 'raw'
|
||||||
|
) {
|
||||||
|
const originRequestUrl = `${deployment.originUrl}${toolPath}${search}`
|
||||||
|
console.log('originRequestUrl', originRequestUrl)
|
||||||
|
|
||||||
const originReq = new Request(originUrl, req)
|
originRequest = new Request(originRequestUrl, req)
|
||||||
updateOriginRequest(originReq, { consumer, deployment, ip })
|
updateOriginRequest(originRequest, { consumer, deployment, ip })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
originReq,
|
originRequest,
|
||||||
deployment: deployment.id,
|
deployment,
|
||||||
project: deployment.project,
|
|
||||||
tool,
|
|
||||||
consumer,
|
consumer,
|
||||||
date,
|
tool,
|
||||||
ip,
|
ip,
|
||||||
method,
|
method,
|
||||||
plan: pricingPlan ? pricingPlan.slug : null,
|
pricingPlanSlug: pricingPlan?.slug,
|
||||||
reportUsage
|
reportUsage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedOriginRequest {
|
|
||||||
originRequest?: Request
|
|
||||||
deployment: AdminDeployment
|
|
||||||
tool: Tool
|
|
||||||
consumer: Consumer | undefined
|
|
||||||
date: number
|
|
||||||
ip: string
|
|
||||||
method: string
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import type { AgenticApiClient } from '@agentic/platform-api-client'
|
import type { AgenticApiClient } from '@agentic/platform-api-client'
|
||||||
|
import type {
|
||||||
|
AdminDeployment,
|
||||||
|
Consumer,
|
||||||
|
Tool,
|
||||||
|
User
|
||||||
|
} from '@agentic/platform-types'
|
||||||
|
|
||||||
import type { AgenticEnv } from './env'
|
import type { AgenticEnv } from './env'
|
||||||
|
|
||||||
|
@ -7,3 +13,18 @@ export type Context = ExecutionContext & {
|
||||||
env: AgenticEnv
|
env: AgenticEnv
|
||||||
client: AgenticApiClient
|
client: AgenticApiClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResolvedOriginRequest {
|
||||||
|
originRequest?: Request
|
||||||
|
deployment: AdminDeployment
|
||||||
|
consumer?: AdminConsumer
|
||||||
|
tool: Tool
|
||||||
|
method: string
|
||||||
|
reportUsage: boolean
|
||||||
|
ip?: string
|
||||||
|
pricingPlanSlug?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminConsumer = Consumer & {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import type { AdminDeployment } from '@agentic/platform-types'
|
||||||
|
|
||||||
|
import type { AdminConsumer } from './types'
|
||||||
|
|
||||||
|
// TODO: support custom auth providers
|
||||||
|
// const authProviders = ['github', 'google', 'spotify', 'linkedin', 'twitter']
|
||||||
|
|
||||||
|
export function updateOriginRequest(
|
||||||
|
originRequest: Request,
|
||||||
|
{
|
||||||
|
deployment,
|
||||||
|
consumer
|
||||||
|
}: {
|
||||||
|
deployment: AdminDeployment
|
||||||
|
consumer?: AdminConsumer
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// originRequest.headers.delete('authorization')
|
||||||
|
|
||||||
|
// for (const provider of authProviders) {
|
||||||
|
// const headerAccessToken = `x-${provider}-access-token`
|
||||||
|
// const headerRefreshToken = `x-${provider}-refresh-token`
|
||||||
|
// const headerAccessTokenSecret = `x-${provider}-access-token-secret`
|
||||||
|
// const headerId = `x-${provider}-id`
|
||||||
|
// const headerUsername = `x-${provider}-username`
|
||||||
|
|
||||||
|
// originRequest.headers.delete(headerAccessToken)
|
||||||
|
// originRequest.headers.delete(headerRefreshToken)
|
||||||
|
// originRequest.headers.delete(headerAccessTokenSecret)
|
||||||
|
// originRequest.headers.delete(headerId)
|
||||||
|
// originRequest.headers.delete(headerUsername)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Delete all Cloudflare headers since we want origin requests to be agnostic
|
||||||
|
// to Agentic's choice of API gateway hosting provider.
|
||||||
|
for (const headerKey of Object.keys(
|
||||||
|
Object.fromEntries(originRequest.headers.entries())
|
||||||
|
)) {
|
||||||
|
if (headerKey.startsWith('cf-')) {
|
||||||
|
originRequest.headers.delete(headerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
originRequest.headers.delete('x-agentic-consumer')
|
||||||
|
originRequest.headers.delete('x-agentic-user')
|
||||||
|
originRequest.headers.delete('x-agentic-plan')
|
||||||
|
originRequest.headers.delete('x-agentic-is-subscription-active')
|
||||||
|
originRequest.headers.delete('x-agentic-subscription-status')
|
||||||
|
originRequest.headers.delete('x-agentic-user-email')
|
||||||
|
originRequest.headers.delete('x-agentic-user-username')
|
||||||
|
originRequest.headers.delete('x-agentic-user-name')
|
||||||
|
originRequest.headers.delete('x-agentic-user-created-at')
|
||||||
|
originRequest.headers.delete('x-forwarded-for')
|
||||||
|
|
||||||
|
if (consumer) {
|
||||||
|
originRequest.headers.set('x-agentic-consumer', consumer.id)
|
||||||
|
originRequest.headers.set('x-agentic-user', consumer.user.id)
|
||||||
|
|
||||||
|
originRequest.headers.set(
|
||||||
|
'x-agentic-is-subscription-active',
|
||||||
|
consumer.isStripeSubscriptionActive.toString()
|
||||||
|
)
|
||||||
|
originRequest.headers.set(
|
||||||
|
'x-agentic-subscription-status',
|
||||||
|
consumer.stripeStatus
|
||||||
|
)
|
||||||
|
originRequest.headers.set('x-agentic-user-email', consumer.user.email)
|
||||||
|
originRequest.headers.set('x-agentic-user-username', consumer.user.username)
|
||||||
|
originRequest.headers.set(
|
||||||
|
'x-agentic-user-created-at',
|
||||||
|
consumer.user.createdAt
|
||||||
|
)
|
||||||
|
|
||||||
|
if (consumer.plan) {
|
||||||
|
originRequest.headers.set('x-agentic-plan', consumer.plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumer.user.name) {
|
||||||
|
originRequest.headers.set('x-agentic-user-name', consumer.user.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this header is causing some random upstream cloudflare errors
|
||||||
|
// https://support.cloudflare.com/hc/en-us/articles/360029779472-Troubleshooting-Cloudflare-1XXX-errors#error1000
|
||||||
|
// originRequest.headers.set('x-forwarded-for', ip)
|
||||||
|
|
||||||
|
originRequest.headers.set('x-agentic-proxy-secret', deployment._secret)
|
||||||
|
}
|
|
@ -451,7 +451,7 @@ export interface components {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
|
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
|
||||||
deploymentId: string;
|
deploymentId: string;
|
||||||
stripeStatus?: string;
|
stripeStatus: string;
|
||||||
isStripeSubscriptionActive: boolean;
|
isStripeSubscriptionActive: boolean;
|
||||||
};
|
};
|
||||||
/** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */
|
/** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */
|
||||||
|
@ -717,6 +717,7 @@ export interface components {
|
||||||
* 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;
|
||||||
|
_secret: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
- same for pricing plan line-items
|
- same for pricing plan line-items
|
||||||
- replace `ms` package
|
- replace `ms` package
|
||||||
- API gateway MCP server vs OpenAPI API gateway
|
- API gateway MCP server vs OpenAPI API gateway
|
||||||
|
- share hono middleware and utils across apps/api and apps/gateway
|
||||||
|
- or combine these together? ehhhh
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue