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')
|
||||
await aclAdmin(c)
|
||||
|
||||
const deployment = await tryGetDeploymentByIdentifier(c, {
|
||||
const { project, ...deployment } = await tryGetDeploymentByIdentifier(c, {
|
||||
deploymentIdentifier,
|
||||
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(
|
||||
project,
|
||||
404,
|
||||
`Project not found for deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
const hasPopulateProject = populate.includes('project')
|
||||
|
||||
// TODO
|
||||
// TODO: switch from published to publishedAt?
|
||||
// if (deployment.published) {
|
||||
|
@ -69,7 +77,11 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
|
|||
// }
|
||||
|
||||
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'
|
||||
}),
|
||||
|
||||
// Stripe subscription status (synced via webhooks)
|
||||
stripeStatus: text(),
|
||||
// Stripe subscription status (synced via webhooks). Should move from
|
||||
// `incomplete` to `active` after the first successful payment.
|
||||
stripeStatus: text().default('incomplete').notNull(),
|
||||
|
||||
// Whether the consumer's subscription is currently active, depending on
|
||||
// `stripeStatus`.
|
||||
|
|
|
@ -193,7 +193,8 @@ Deployments are private to a developer or team until they are published, at whic
|
|||
|
||||
export const deploymentAdminSelectSchema = deploymentSelectSchema
|
||||
.extend({
|
||||
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl
|
||||
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl,
|
||||
_secret: z.string().nonempty()
|
||||
})
|
||||
.openapi('AdminDeployment')
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ export async function upsertStripeSubscription(
|
|||
)
|
||||
|
||||
const updateParams: Stripe.SubscriptionUpdateParams = {
|
||||
collection_method: 'charge_automatically',
|
||||
metadata: {
|
||||
userId: consumer.userId,
|
||||
consumerId: consumer.id,
|
||||
|
@ -259,10 +260,11 @@ export async function upsertStripeSubscription(
|
|||
|
||||
const createParams: Stripe.SubscriptionCreateParams = {
|
||||
customer: stripeCustomerId,
|
||||
description: `Agentic subscription to project "${project.id}"`,
|
||||
description: `Agentic subscription to project "${project.identifier}"`,
|
||||
// TODO: coupons
|
||||
// coupon: filterConsumerCoupon(ctx, consumer, deployment),
|
||||
items,
|
||||
collection_method: 'charge_automatically',
|
||||
metadata: {
|
||||
userId: consumer.userId,
|
||||
consumerId: consumer.id,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { RawConsumerUpdate } from '@/db'
|
||||
|
||||
// https://docs.stripe.com/api/subscriptions/object#subscription_object-status
|
||||
const stripeValidSubscriptionStatuses = new Set([
|
||||
'active',
|
||||
'trialing',
|
||||
|
|
|
@ -1,12 +1,28 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import type { Context } from './types'
|
||||
|
||||
export async function enforceRateLimit(
|
||||
ctx: Context,
|
||||
{}: {
|
||||
{
|
||||
id,
|
||||
interval,
|
||||
maxPerInterval,
|
||||
method,
|
||||
pathname
|
||||
}: {
|
||||
id: string
|
||||
interval: number
|
||||
maxPerInterval: number
|
||||
method: 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 type { Context } from './types'
|
||||
import type { AdminConsumer, Context } from './types'
|
||||
|
||||
export async function getConsumer(
|
||||
ctx: Context,
|
||||
token: string
|
||||
): Promise<Consumer> {
|
||||
): Promise<AdminConsumer> {
|
||||
const consumer = await ctx.client.adminGetConsumerByToken({
|
||||
token
|
||||
token,
|
||||
populate: ['user']
|
||||
})
|
||||
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 { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import type {
|
||||
AdminDeployment,
|
||||
Consumer,
|
||||
PricingPlan,
|
||||
RateLimit,
|
||||
Tool
|
||||
} from '@agentic/platform-types'
|
||||
import type { PricingPlan, RateLimit } from '@agentic/platform-types'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import type { Context } from './types'
|
||||
import type { AdminConsumer, Context, ResolvedOriginRequest } from './types'
|
||||
import { getConsumer } from './get-consumer'
|
||||
import { getDeployment } from './get-deployment'
|
||||
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-
|
||||
* specific headers to the origin request.
|
||||
*/
|
||||
export async function resolveOriginRequest(ctx: Context) {
|
||||
export async function resolveOriginRequest(
|
||||
ctx: Context
|
||||
): Promise<ResolvedOriginRequest> {
|
||||
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 date = Date.now()
|
||||
|
||||
const { search, pathname } = requestUrl
|
||||
const method = req.method.toLowerCase()
|
||||
|
@ -36,18 +31,19 @@ export async function resolveOriginRequest(ctx: Context) {
|
|||
deployment,
|
||||
toolPath
|
||||
})
|
||||
console.log('rqeuest', {
|
||||
|
||||
console.log('request', {
|
||||
method,
|
||||
pathname,
|
||||
search,
|
||||
deployment: deployment.identifier,
|
||||
deploymentIdentifier: deployment.identifier,
|
||||
toolPath,
|
||||
tool
|
||||
})
|
||||
|
||||
let reportUsage = true
|
||||
let pricingPlan: PricingPlan | undefined
|
||||
let consumer: Consumer | undefined
|
||||
let consumer: AdminConsumer | undefined
|
||||
let reportUsage = true
|
||||
|
||||
const token = (req.headers.get('authorization') || '')
|
||||
.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?
|
||||
// - support both MCP and OpenAPI / raw?
|
||||
|
||||
const originUrl = `${deployment.originUrl}${toolPath}${search}`
|
||||
console.log('originUrl', originUrl)
|
||||
let originRequest: Request | undefined
|
||||
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)
|
||||
updateOriginRequest(originReq, { consumer, deployment, ip })
|
||||
originRequest = new Request(originRequestUrl, req)
|
||||
updateOriginRequest(originRequest, { consumer, deployment, ip })
|
||||
}
|
||||
|
||||
return {
|
||||
originReq,
|
||||
deployment: deployment.id,
|
||||
project: deployment.project,
|
||||
tool,
|
||||
originRequest,
|
||||
deployment,
|
||||
consumer,
|
||||
date,
|
||||
tool,
|
||||
ip,
|
||||
method,
|
||||
plan: pricingPlan ? pricingPlan.slug : null,
|
||||
pricingPlanSlug: pricingPlan?.slug,
|
||||
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 {
|
||||
AdminDeployment,
|
||||
Consumer,
|
||||
Tool,
|
||||
User
|
||||
} from '@agentic/platform-types'
|
||||
|
||||
import type { AgenticEnv } from './env'
|
||||
|
||||
|
@ -7,3 +13,18 @@ export type Context = ExecutionContext & {
|
|||
env: AgenticEnv
|
||||
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;
|
||||
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
|
||||
deploymentId: string;
|
||||
stripeStatus?: string;
|
||||
stripeStatus: string;
|
||||
isStripeSubscriptionActive: boolean;
|
||||
};
|
||||
/** @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.
|
||||
*/
|
||||
originUrl: string;
|
||||
_secret: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
- same for pricing plan line-items
|
||||
- replace `ms` package
|
||||
- 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
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue