kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: clean up gateway resolving edge request logic
rodzic
b066de9c5f
commit
b61641240e
|
@ -37,6 +37,7 @@
|
||||||
"@agentic/platform-hono": "workspace:*",
|
"@agentic/platform-hono": "workspace:*",
|
||||||
"@agentic/platform-types": "workspace:*",
|
"@agentic/platform-types": "workspace:*",
|
||||||
"@agentic/platform-validators": "workspace:*",
|
"@agentic/platform-validators": "workspace:*",
|
||||||
|
"@cloudflare/workers-oauth-provider": "^0.0.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "catalog:",
|
"@modelcontextprotocol/sdk": "catalog:",
|
||||||
"@sentry/cloudflare": "catalog:",
|
"@sentry/cloudflare": "catalog:",
|
||||||
|
|
|
@ -7,17 +7,18 @@ import {
|
||||||
responseTime,
|
responseTime,
|
||||||
sentry
|
sentry
|
||||||
} from '@agentic/platform-hono'
|
} from '@agentic/platform-hono'
|
||||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
import type { GatewayHonoEnv, ResolvedOriginToolCallResult } from './lib/types'
|
import type {
|
||||||
|
GatewayHonoEnv,
|
||||||
|
ResolvedHttpEdgeRequest,
|
||||||
|
ResolvedOriginToolCallResult
|
||||||
|
} from './lib/types'
|
||||||
import { createAgenticClient } from './lib/agentic-client'
|
import { createAgenticClient } from './lib/agentic-client'
|
||||||
import { createHttpResponseFromMcpToolCallResponse } from './lib/create-http-response-from-mcp-tool-call-response'
|
import { createHttpResponseFromMcpToolCallResponse } from './lib/create-http-response-from-mcp-tool-call-response'
|
||||||
import { recordToolCallUsage } from './lib/record-tool-call-usage'
|
import { recordToolCallUsage } from './lib/record-tool-call-usage'
|
||||||
import {
|
import { resolveEdgeRequest } from './lib/resolve-edge-request'
|
||||||
type ResolvedHttpEdgeRequest,
|
import { resolveHttpEdgeRequest } from './lib/resolve-http-edge-request'
|
||||||
resolveHttpEdgeRequest
|
|
||||||
} from './lib/resolve-http-edge-request'
|
|
||||||
import { resolveMcpEdgeRequest } from './lib/resolve-mcp-edge-request'
|
import { resolveMcpEdgeRequest } from './lib/resolve-mcp-edge-request'
|
||||||
import { resolveOriginToolCall } from './lib/resolve-origin-tool-call'
|
import { resolveOriginToolCall } from './lib/resolve-origin-tool-call'
|
||||||
import { isRequestPubliclyCacheable } from './lib/utils'
|
import { isRequestPubliclyCacheable } from './lib/utils'
|
||||||
|
@ -65,21 +66,17 @@ app.all(async (ctx) => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Clean up the duplication between this block,
|
const resolvedEdgeRequest = await resolveEdgeRequest(ctx)
|
||||||
// `resolveMcpEdgeRequest`, and `resolveHttpEdgeRequest`.
|
const { toolName } = resolvedEdgeRequest.parsedToolIdentifier
|
||||||
const requestUrl = new URL(ctx.req.url)
|
|
||||||
const { pathname } = requestUrl
|
|
||||||
const requestedToolIdentifier = pathname.replace(/^\//, '').replace(/\/$/, '')
|
|
||||||
const { toolName } = parseToolIdentifier(requestedToolIdentifier)
|
|
||||||
|
|
||||||
// Handle MCP requests
|
// Handle MCP requests
|
||||||
if (toolName === 'mcp') {
|
if (toolName === 'mcp') {
|
||||||
ctx.set('isJsonRpcRequest', true)
|
ctx.set('isJsonRpcRequest', true)
|
||||||
const executionCtx = ctx.executionCtx as any
|
const executionCtx = ctx.executionCtx as any
|
||||||
const mcpInfo = await resolveMcpEdgeRequest(ctx)
|
const mcpInfo = await resolveMcpEdgeRequest(ctx, resolvedEdgeRequest)
|
||||||
executionCtx.props = mcpInfo
|
executionCtx.props = mcpInfo
|
||||||
|
|
||||||
return DurableMcpServer.serve(pathname, {
|
return DurableMcpServer.serve('/*', {
|
||||||
binding: 'DO_MCP_SERVER'
|
binding: 'DO_MCP_SERVER'
|
||||||
}).fetch(ctx.req.raw, ctx.env, executionCtx)
|
}).fetch(ctx.req.raw, ctx.env, executionCtx)
|
||||||
}
|
}
|
||||||
|
@ -92,14 +89,16 @@ app.all(async (ctx) => {
|
||||||
try {
|
try {
|
||||||
// Resolve the http edge request to a specific deployment, consumer, and
|
// Resolve the http edge request to a specific deployment, consumer, and
|
||||||
// tool call.
|
// tool call.
|
||||||
resolvedHttpEdgeRequest = await resolveHttpEdgeRequest(ctx)
|
resolvedHttpEdgeRequest = await resolveHttpEdgeRequest(
|
||||||
|
ctx,
|
||||||
|
resolvedEdgeRequest
|
||||||
|
)
|
||||||
|
|
||||||
// Invoke the origin tool call.
|
// Invoke the origin tool call.
|
||||||
resolvedOriginToolCallResult = await resolveOriginToolCall({
|
resolvedOriginToolCallResult = await resolveOriginToolCall({
|
||||||
...resolvedHttpEdgeRequest,
|
...resolvedHttpEdgeRequest,
|
||||||
args: resolvedHttpEdgeRequest.toolCallArgs,
|
args: resolvedHttpEdgeRequest.toolCallArgs,
|
||||||
sessionId: ctx.get('sessionId')!,
|
sessionId: ctx.get('sessionId')!,
|
||||||
ip: ctx.get('ip'),
|
|
||||||
env: ctx.env,
|
env: ctx.env,
|
||||||
waitUntil
|
waitUntil
|
||||||
})
|
})
|
||||||
|
@ -129,12 +128,10 @@ app.all(async (ctx) => {
|
||||||
if (resolvedHttpEdgeRequest && res) {
|
if (resolvedHttpEdgeRequest && res) {
|
||||||
recordToolCallUsage({
|
recordToolCallUsage({
|
||||||
...resolvedHttpEdgeRequest,
|
...resolvedHttpEdgeRequest,
|
||||||
requestMode: 'http',
|
edgeRequestMode: 'http',
|
||||||
httpResponse: res,
|
httpResponse: res,
|
||||||
resolvedOriginToolCallResult,
|
resolvedOriginToolCallResult,
|
||||||
sessionId: ctx.get('sessionId')!,
|
sessionId: ctx.get('sessionId')!,
|
||||||
requestId: ctx.get('requestId')!,
|
|
||||||
ip: ctx.get('ip'),
|
|
||||||
env: ctx.env,
|
env: ctx.env,
|
||||||
waitUntil
|
waitUntil
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
|
||||||
import { assert, getRateLimitHeaders } from '@agentic/platform-core'
|
import { assert, getRateLimitHeaders } from '@agentic/platform-core'
|
||||||
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
@ -11,8 +10,8 @@ import { McpAgent } from 'agents/mcp'
|
||||||
|
|
||||||
import type { RawEnv } from './env'
|
import type { RawEnv } from './env'
|
||||||
import type {
|
import type {
|
||||||
AdminConsumer,
|
|
||||||
McpToolCallResponse,
|
McpToolCallResponse,
|
||||||
|
ResolvedMcpEdgeRequest,
|
||||||
ResolvedOriginToolCallResult
|
ResolvedOriginToolCallResult
|
||||||
} from './types'
|
} from './types'
|
||||||
import { handleMcpToolCallError } from './handle-mcp-tool-call-error'
|
import { handleMcpToolCallError } from './handle-mcp-tool-call-error'
|
||||||
|
@ -23,13 +22,8 @@ import { createAgenticMcpMetadata } from './utils'
|
||||||
|
|
||||||
export class DurableMcpServerBase extends McpAgent<
|
export class DurableMcpServerBase extends McpAgent<
|
||||||
RawEnv,
|
RawEnv,
|
||||||
never, // TODO: do we need local state?
|
never, // We aren't currently using local state, so set it to `never`.
|
||||||
{
|
ResolvedMcpEdgeRequest
|
||||||
deployment: AdminDeployment
|
|
||||||
consumer?: AdminConsumer
|
|
||||||
pricingPlan?: PricingPlan
|
|
||||||
ip?: string
|
|
||||||
}
|
|
||||||
> {
|
> {
|
||||||
protected _serverP = Promise.withResolvers<Server>()
|
protected _serverP = Promise.withResolvers<Server>()
|
||||||
override server = this._serverP.promise
|
override server = this._serverP.promise
|
||||||
|
@ -40,7 +34,7 @@ export class DurableMcpServerBase extends McpAgent<
|
||||||
}
|
}
|
||||||
|
|
||||||
override async init() {
|
override async init() {
|
||||||
const { consumer, deployment, pricingPlan, ip } = this.props
|
const { consumer, deployment, pricingPlan } = this.props
|
||||||
const { projectIdentifier } = parseDeploymentIdentifier(
|
const { projectIdentifier } = parseDeploymentIdentifier(
|
||||||
deployment.identifier
|
deployment.identifier
|
||||||
)
|
)
|
||||||
|
@ -62,20 +56,17 @@ export class DurableMcpServerBase extends McpAgent<
|
||||||
)
|
)
|
||||||
|
|
||||||
if (toolConfig) {
|
if (toolConfig) {
|
||||||
const pricingPlanToolConfig = pricingPlan
|
const pricingPlanToolOverride = pricingPlan
|
||||||
? toolConfig.pricingPlanOverridesMap?.[pricingPlan.slug]
|
? toolConfig.pricingPlanOverridesMap?.[pricingPlan.slug]
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (pricingPlanToolConfig?.enabled === false) {
|
if (pricingPlanToolOverride?.enabled === true) {
|
||||||
// Tool is disabled / hidden for the customer's current pricing plan
|
// Tool is explicitly enabled for the customer's pricing plan
|
||||||
|
} else if (pricingPlanToolOverride?.enabled === false) {
|
||||||
|
// Tool is disabled for the customer's pricing plan
|
||||||
return undefined
|
return undefined
|
||||||
}
|
} else if (toolConfig.enabled === false) {
|
||||||
|
// Tool is disabled for all pricing plans
|
||||||
if (
|
|
||||||
pricingPlanToolConfig?.enabled !== true &&
|
|
||||||
toolConfig.enabled === false
|
|
||||||
) {
|
|
||||||
// Tool is disabled / hidden for all pricing plans
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,15 +92,12 @@ export class DurableMcpServerBase extends McpAgent<
|
||||||
assert(tool, 404, `Unknown tool "${toolName}"`)
|
assert(tool, 404, `Unknown tool "${toolName}"`)
|
||||||
|
|
||||||
resolvedOriginToolCallResult = await resolveOriginToolCall({
|
resolvedOriginToolCallResult = await resolveOriginToolCall({
|
||||||
|
...this.props,
|
||||||
tool,
|
tool,
|
||||||
args,
|
args,
|
||||||
deployment,
|
|
||||||
consumer,
|
|
||||||
pricingPlan,
|
|
||||||
cacheControl,
|
cacheControl,
|
||||||
sessionId,
|
sessionId,
|
||||||
env: this.env,
|
env: this.env,
|
||||||
ip,
|
|
||||||
waitUntil: this.ctx.waitUntil.bind(this.ctx)
|
waitUntil: this.ctx.waitUntil.bind(this.ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -155,13 +143,11 @@ export class DurableMcpServerBase extends McpAgent<
|
||||||
// Record tool call usage, whether the call was successful or not.
|
// Record tool call usage, whether the call was successful or not.
|
||||||
recordToolCallUsage({
|
recordToolCallUsage({
|
||||||
...this.props,
|
...this.props,
|
||||||
requestMode: 'mcp',
|
edgeRequestMode: 'mcp',
|
||||||
tool,
|
tool,
|
||||||
mcpToolCallResponse: toolCallResponse!,
|
mcpToolCallResponse: toolCallResponse!,
|
||||||
resolvedOriginToolCallResult,
|
resolvedOriginToolCallResult,
|
||||||
sessionId,
|
sessionId,
|
||||||
// TODO: requestId
|
|
||||||
ip,
|
|
||||||
env: this.env,
|
env: this.env,
|
||||||
waitUntil: this.ctx.waitUntil.bind(this.ctx)
|
waitUntil: this.ctx.waitUntil.bind(this.ctx)
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,8 +7,8 @@ import type {
|
||||||
import type { RawEnv } from './env'
|
import type { RawEnv } from './env'
|
||||||
import type {
|
import type {
|
||||||
AdminConsumer,
|
AdminConsumer,
|
||||||
|
EdgeRequestMode,
|
||||||
McpToolCallResponse,
|
McpToolCallResponse,
|
||||||
RequestMode,
|
|
||||||
ResolvedOriginToolCallResult,
|
ResolvedOriginToolCallResult,
|
||||||
WaitUntil
|
WaitUntil
|
||||||
} from './types'
|
} from './types'
|
||||||
|
@ -31,7 +31,7 @@ import { createStripe } from './external/stripe'
|
||||||
* @see https://developers.cloudflare.com/analytics/analytics-engine/limits/
|
* @see https://developers.cloudflare.com/analytics/analytics-engine/limits/
|
||||||
*/
|
*/
|
||||||
export function recordToolCallUsage({
|
export function recordToolCallUsage({
|
||||||
requestMode,
|
edgeRequestMode,
|
||||||
deployment,
|
deployment,
|
||||||
consumer,
|
consumer,
|
||||||
tool,
|
tool,
|
||||||
|
@ -44,7 +44,7 @@ export function recordToolCallUsage({
|
||||||
env,
|
env,
|
||||||
waitUntil
|
waitUntil
|
||||||
}: {
|
}: {
|
||||||
requestMode: RequestMode
|
edgeRequestMode: EdgeRequestMode
|
||||||
deployment: AdminDeployment
|
deployment: AdminDeployment
|
||||||
consumer?: AdminConsumer
|
consumer?: AdminConsumer
|
||||||
pricingPlan?: PricingPlan
|
pricingPlan?: PricingPlan
|
||||||
|
@ -60,13 +60,13 @@ export function recordToolCallUsage({
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
// For http requests, an http response is required.
|
// For http requests, an http response is required.
|
||||||
requestMode: 'http'
|
edgeRequestMode: 'http'
|
||||||
httpResponse: Response
|
httpResponse: Response
|
||||||
mcpToolCallResponse?: never
|
mcpToolCallResponse?: never
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// For mcp cool call requests, an mcp tool call response is required.
|
// For mcp cool call requests, an mcp tool call response is required.
|
||||||
requestMode: 'mcp'
|
edgeRequestMode: 'mcp'
|
||||||
httpResponse?: never
|
httpResponse?: never
|
||||||
mcpToolCallResponse: McpToolCallResponse
|
mcpToolCallResponse: McpToolCallResponse
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ export function recordToolCallUsage({
|
||||||
tool?.name ?? null,
|
tool?.name ?? null,
|
||||||
|
|
||||||
// Whether this request was made via MCP or HTTP
|
// Whether this request was made via MCP or HTTP
|
||||||
requestMode,
|
edgeRequestMode,
|
||||||
|
|
||||||
// IP address or session ID
|
// IP address or session ID
|
||||||
ip ?? sessionId,
|
ip ?? sessionId,
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AdminConsumer,
|
||||||
|
GatewayHonoContext,
|
||||||
|
ResolvedEdgeRequest
|
||||||
|
} from './types'
|
||||||
|
import { getAdminConsumer } from './get-admin-consumer'
|
||||||
|
import { getAdminDeployment } from './get-admin-deployment'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an input HTTP request to a specific deployment.
|
||||||
|
*/
|
||||||
|
export async function resolveEdgeRequest(
|
||||||
|
ctx: GatewayHonoContext
|
||||||
|
): Promise<ResolvedEdgeRequest> {
|
||||||
|
const requestUrl = new URL(ctx.req.url)
|
||||||
|
const { pathname } = requestUrl
|
||||||
|
const requestedToolIdentifier = pathname.replace(/^\//, '').replace(/\/$/, '')
|
||||||
|
const parsedToolIdentifier = parseToolIdentifier(requestedToolIdentifier)
|
||||||
|
|
||||||
|
const deployment = await getAdminDeployment(
|
||||||
|
ctx,
|
||||||
|
parsedToolIdentifier.deploymentIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsedToolIdentifier,
|
||||||
|
deployment,
|
||||||
|
requestId: ctx.get('requestId'),
|
||||||
|
ip: ctx.get('ip')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a consumer and pricing plan for an edge request.
|
||||||
|
*/
|
||||||
|
export async function resolveConsumerForEdgeRequest(
|
||||||
|
ctx: GatewayHonoContext,
|
||||||
|
{
|
||||||
|
deployment,
|
||||||
|
apiKey
|
||||||
|
}: {
|
||||||
|
deployment: AdminDeployment
|
||||||
|
apiKey?: string
|
||||||
|
}
|
||||||
|
): Promise<{
|
||||||
|
consumer?: AdminConsumer
|
||||||
|
pricingPlan?: PricingPlan
|
||||||
|
}> {
|
||||||
|
let pricingPlan: PricingPlan | undefined
|
||||||
|
let consumer: AdminConsumer | undefined
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
consumer = await getAdminConsumer(ctx, apiKey)
|
||||||
|
assert(consumer, 401, `Invalid API key "${apiKey}"`)
|
||||||
|
assert(
|
||||||
|
consumer.isStripeSubscriptionActive,
|
||||||
|
402,
|
||||||
|
`API key "${apiKey}" does not have an active subscription`
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
consumer.projectId === deployment.projectId,
|
||||||
|
403,
|
||||||
|
`API key "${apiKey}" is not authorized for project "${deployment.projectId}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Ensure that consumer.plan is compatible with the target deployment?
|
||||||
|
// TODO: This could definitely cause issues when changing pricing plans.
|
||||||
|
|
||||||
|
pricingPlan = deployment.pricingPlans.find(
|
||||||
|
(pricingPlan) => consumer!.plan === pricingPlan.slug
|
||||||
|
)
|
||||||
|
|
||||||
|
// assert(
|
||||||
|
// pricingPlan,
|
||||||
|
// 403,
|
||||||
|
// `Auth token "${token}" unable to find matching pricing plan for project "${deployment.project}"`
|
||||||
|
// )
|
||||||
|
} else {
|
||||||
|
// For unauthenticated requests, default to a free pricing plan if available.
|
||||||
|
pricingPlan = deployment.pricingPlans.find((plan) => plan.slug === 'free')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
consumer,
|
||||||
|
pricingPlan
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +1,22 @@
|
||||||
import type {
|
|
||||||
AdminDeployment,
|
|
||||||
PricingPlan,
|
|
||||||
Tool
|
|
||||||
} from '@agentic/platform-types'
|
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
|
||||||
|
|
||||||
import type { AdminConsumer, GatewayHonoContext, ToolCallArgs } from './types'
|
import type {
|
||||||
import { getAdminConsumer } from './get-admin-consumer'
|
GatewayHonoContext,
|
||||||
import { getAdminDeployment } from './get-admin-deployment'
|
ResolvedEdgeRequest,
|
||||||
|
ResolvedHttpEdgeRequest
|
||||||
|
} from './types'
|
||||||
import { getTool } from './get-tool'
|
import { getTool } from './get-tool'
|
||||||
import { getToolArgsFromRequest } from './get-tool-args-from-request'
|
import { getToolArgsFromRequest } from './get-tool-args-from-request'
|
||||||
|
import { resolveConsumerForEdgeRequest } from './resolve-edge-request'
|
||||||
import { isRequestPubliclyCacheable } from './utils'
|
import { isRequestPubliclyCacheable } from './utils'
|
||||||
|
|
||||||
export type ResolvedHttpEdgeRequest = {
|
|
||||||
deployment: AdminDeployment
|
|
||||||
consumer?: AdminConsumer
|
|
||||||
pricingPlan?: PricingPlan
|
|
||||||
|
|
||||||
tool: Tool
|
|
||||||
toolCallArgs: ToolCallArgs
|
|
||||||
cacheControl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves an input HTTP request to a specific deployment, tool call, and
|
* Resolves an input HTTP request to a specific deployment, tool call, consumer,
|
||||||
* billing subscription.
|
* and pricing plan.
|
||||||
*
|
|
||||||
* Also ensures that the request is valid, enforces rate limits, and adds proxy-
|
|
||||||
* specific headers to the origin request.
|
|
||||||
*/
|
*/
|
||||||
export async function resolveHttpEdgeRequest(
|
export async function resolveHttpEdgeRequest(
|
||||||
ctx: GatewayHonoContext
|
ctx: GatewayHonoContext,
|
||||||
|
resolvedEdgeRequest: ResolvedEdgeRequest
|
||||||
): Promise<ResolvedHttpEdgeRequest> {
|
): Promise<ResolvedHttpEdgeRequest> {
|
||||||
const logger = ctx.get('logger')
|
const logger = ctx.get('logger')
|
||||||
const ip = ctx.get('ip')
|
const ip = ctx.get('ip')
|
||||||
|
@ -40,15 +25,9 @@ export async function resolveHttpEdgeRequest(
|
||||||
? ctx.req.header('cache-control')
|
? ctx.req.header('cache-control')
|
||||||
: 'no-store'
|
: 'no-store'
|
||||||
|
|
||||||
|
const { deployment, parsedToolIdentifier } = resolvedEdgeRequest
|
||||||
|
const { toolName } = parsedToolIdentifier
|
||||||
const { method } = ctx.req
|
const { method } = ctx.req
|
||||||
const requestUrl = new URL(ctx.req.url)
|
|
||||||
const { pathname } = requestUrl
|
|
||||||
const requestedToolIdentifier = pathname.replace(/^\//, '').replace(/\/$/, '')
|
|
||||||
const { toolName, deploymentIdentifier } = parseToolIdentifier(
|
|
||||||
requestedToolIdentifier
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployment = await getAdminDeployment(ctx, deploymentIdentifier)
|
|
||||||
|
|
||||||
const tool = getTool({
|
const tool = getTool({
|
||||||
method,
|
method,
|
||||||
|
@ -58,66 +37,36 @@ export async function resolveHttpEdgeRequest(
|
||||||
|
|
||||||
logger.debug('request', {
|
logger.debug('request', {
|
||||||
method,
|
method,
|
||||||
pathname,
|
|
||||||
deploymentIdentifier: deployment.identifier,
|
deploymentIdentifier: deployment.identifier,
|
||||||
toolName,
|
toolName,
|
||||||
tool
|
tool
|
||||||
})
|
})
|
||||||
|
|
||||||
let pricingPlan: PricingPlan | undefined
|
const apiKey = (ctx.req.header('authorization') || '')
|
||||||
let consumer: AdminConsumer | undefined
|
|
||||||
|
|
||||||
const token = (ctx.req.header('authorization') || '')
|
|
||||||
.replace(/^Bearer /i, '')
|
.replace(/^Bearer /i, '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
if (token) {
|
const { consumer, pricingPlan } = await resolveConsumerForEdgeRequest(ctx, {
|
||||||
consumer = await getAdminConsumer(ctx, token)
|
deployment,
|
||||||
assert(consumer, 401, `Invalid auth token "${token}"`)
|
apiKey
|
||||||
assert(
|
})
|
||||||
consumer.isStripeSubscriptionActive,
|
|
||||||
402,
|
|
||||||
`Auth token "${token}" does not have an active subscription`
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
consumer.projectId === deployment.projectId,
|
|
||||||
403,
|
|
||||||
`Auth token "${token}" is not authorized for project "${deployment.projectId}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Ensure that consumer.plan is compatible with the target deployment?
|
|
||||||
// TODO: This could definitely cause issues when changing pricing plans.
|
|
||||||
|
|
||||||
pricingPlan = deployment.pricingPlans.find(
|
|
||||||
(pricingPlan) => consumer!.plan === pricingPlan.slug
|
|
||||||
)
|
|
||||||
|
|
||||||
// assert(
|
|
||||||
// pricingPlan,
|
|
||||||
// 403,
|
|
||||||
// `Auth token "${token}" unable to find matching pricing plan for project "${deployment.project}"`
|
|
||||||
// )
|
|
||||||
|
|
||||||
|
if (consumer) {
|
||||||
if (!ctx.get('sessionId')) {
|
if (!ctx.get('sessionId')) {
|
||||||
ctx.set('sessionId', `${consumer.id}:${deployment.id}`)
|
ctx.set('sessionId', `${consumer.id}:${deployment.id}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For unauthenticated requests, default to a free pricing plan if available.
|
|
||||||
pricingPlan = deployment.pricingPlans.find((plan) => plan.slug === 'free')
|
|
||||||
|
|
||||||
if (!ctx.get('sessionId')) {
|
if (!ctx.get('sessionId')) {
|
||||||
assert(ip, 500, 'IP address is required for unauthenticated requests')
|
assert(ip, 500, 'IP address is required for unauthenticated requests')
|
||||||
ctx.set('sessionId', `${ip}:${deployment.projectId}`)
|
ctx.set('sessionId', `${ip}:${deployment.projectId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(ctx.get('sessionId'), 500, 'Internal error: sessionId should be set')
|
// Parse tool call arguments from the request body.
|
||||||
|
|
||||||
// Parse tool call args from the request body.
|
|
||||||
const toolCallArgs = await getToolArgsFromRequest(ctx, { tool, deployment })
|
const toolCallArgs = await getToolArgsFromRequest(ctx, { tool, deployment })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deployment,
|
...resolvedEdgeRequest,
|
||||||
consumer,
|
consumer,
|
||||||
pricingPlan,
|
pricingPlan,
|
||||||
tool,
|
tool,
|
||||||
|
|
|
@ -1,71 +1,27 @@
|
||||||
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
import type {
|
||||||
import { assert } from '@agentic/platform-core'
|
GatewayHonoContext,
|
||||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
ResolvedEdgeRequest,
|
||||||
|
ResolvedMcpEdgeRequest
|
||||||
import type { AdminConsumer, GatewayHonoContext } from './types'
|
} from './types'
|
||||||
import { getAdminConsumer } from './get-admin-consumer'
|
import { resolveConsumerForEdgeRequest } from './resolve-edge-request'
|
||||||
import { getAdminDeployment } from './get-admin-deployment'
|
|
||||||
|
|
||||||
export type ResolvedMcpEdgeRequest = {
|
|
||||||
deployment: AdminDeployment
|
|
||||||
consumer?: AdminConsumer
|
|
||||||
pricingPlan?: PricingPlan
|
|
||||||
ip?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveMcpEdgeRequest(
|
export async function resolveMcpEdgeRequest(
|
||||||
ctx: GatewayHonoContext
|
ctx: GatewayHonoContext,
|
||||||
|
resolvedEdgeRequest: ResolvedEdgeRequest
|
||||||
): Promise<ResolvedMcpEdgeRequest> {
|
): Promise<ResolvedMcpEdgeRequest> {
|
||||||
const requestUrl = new URL(ctx.req.url)
|
const { deployment } = resolvedEdgeRequest
|
||||||
const { pathname } = requestUrl
|
|
||||||
const requestedDeploymentIdentifier = pathname
|
|
||||||
.replace(/^\//, '')
|
|
||||||
.replace(/\/$/, '')
|
|
||||||
const { deploymentIdentifier } = parseToolIdentifier(
|
|
||||||
requestedDeploymentIdentifier
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployment = await getAdminDeployment(ctx, deploymentIdentifier)
|
|
||||||
|
|
||||||
|
// TODO: Should MCP edge requests also support Authorization header?
|
||||||
const apiKey = ctx.req.query('apiKey')?.trim()
|
const apiKey = ctx.req.query('apiKey')?.trim()
|
||||||
let consumer: AdminConsumer | undefined
|
|
||||||
let pricingPlan: PricingPlan | undefined
|
|
||||||
|
|
||||||
if (apiKey) {
|
const { consumer, pricingPlan } = await resolveConsumerForEdgeRequest(ctx, {
|
||||||
consumer = await getAdminConsumer(ctx, apiKey)
|
deployment,
|
||||||
assert(consumer, 401, `Invalid api key "${apiKey}"`)
|
apiKey
|
||||||
assert(
|
})
|
||||||
consumer.isStripeSubscriptionActive,
|
|
||||||
402,
|
|
||||||
`API key "${apiKey}" subscription is not active`
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
consumer.projectId === deployment.projectId,
|
|
||||||
403,
|
|
||||||
`API key "${apiKey}" is not authorized for project "${deployment.projectId}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Ensure that consumer.plan is compatible with the target deployment?
|
|
||||||
// TODO: This could definitely cause issues when changing pricing plans.
|
|
||||||
|
|
||||||
pricingPlan = deployment.pricingPlans.find(
|
|
||||||
(pricingPlan) => consumer!.plan === pricingPlan.slug
|
|
||||||
)
|
|
||||||
|
|
||||||
// assert(
|
|
||||||
// pricingPlan,
|
|
||||||
// 403,
|
|
||||||
// `API key "${apiKey}" unable to find matching pricing plan for project "${deployment.project}"`
|
|
||||||
// )
|
|
||||||
} else {
|
|
||||||
// For unauthenticated requests, default to a free pricing plan if available.
|
|
||||||
pricingPlan = deployment.pricingPlans.find((plan) => plan.slug === 'free')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deployment,
|
...resolvedEdgeRequest,
|
||||||
consumer,
|
consumer,
|
||||||
pricingPlan,
|
pricingPlan
|
||||||
ip: ctx.get('ip')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ export async function resolveOriginToolCall({
|
||||||
const pricingPlanToolOverride = pricingPlan
|
const pricingPlanToolOverride = pricingPlan
|
||||||
? toolConfig.pricingPlanOverridesMap?.[pricingPlan.slug]
|
? toolConfig.pricingPlanOverridesMap?.[pricingPlan.slug]
|
||||||
: undefined
|
: undefined
|
||||||
const isToolConfigEnabled = toolConfig.enabled ?? true
|
const isToolEnabled = toolConfig.enabled ?? true
|
||||||
|
|
||||||
// Check if this tool is configured for pricing-plan-specific overrides
|
// Check if this tool is configured for pricing-plan-specific overrides
|
||||||
// which take precedence over the tool's default behavior.
|
// which take precedence over the tool's default behavior.
|
||||||
|
@ -131,11 +131,11 @@ export async function resolveOriginToolCall({
|
||||||
if (pricingPlanToolOverride.enabled !== undefined) {
|
if (pricingPlanToolOverride.enabled !== undefined) {
|
||||||
assert(
|
assert(
|
||||||
pricingPlanToolOverride.enabled,
|
pricingPlanToolOverride.enabled,
|
||||||
isToolConfigEnabled ? 403 : 404,
|
isToolEnabled ? 403 : 404,
|
||||||
`Tool "${tool.name}" is disabled for pricing plan "${pricingPlan.slug}"`
|
`Tool "${tool.name}" is disabled for pricing plan "${pricingPlan.slug}"`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
assert(isToolConfigEnabled, 404, `Tool "${tool.name}" is disabled`)
|
assert(isToolEnabled, 404, `Tool "${tool.name}" is disabled`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pricingPlanToolOverride.reportUsage !== undefined) {
|
if (pricingPlanToolOverride.reportUsage !== undefined) {
|
||||||
|
@ -146,7 +146,7 @@ export async function resolveOriginToolCall({
|
||||||
rateLimit = pricingPlanToolOverride.rateLimit
|
rateLimit = pricingPlanToolOverride.rateLimit
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert(isToolConfigEnabled, 404, `Tool "${tool.name}" is disabled`)
|
assert(isToolEnabled, 404, `Tool "${tool.name}" is disabled`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cacheControl) {
|
if (cacheControl) {
|
||||||
|
|
|
@ -6,10 +6,14 @@ import type {
|
||||||
} from '@agentic/platform-hono'
|
} from '@agentic/platform-hono'
|
||||||
import type {
|
import type {
|
||||||
AdminConsumer as AdminConsumerImpl,
|
AdminConsumer as AdminConsumerImpl,
|
||||||
|
AdminDeployment,
|
||||||
|
PricingPlan,
|
||||||
RateLimit,
|
RateLimit,
|
||||||
|
Tool,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
User
|
User
|
||||||
} from '@agentic/platform-types'
|
} from '@agentic/platform-types'
|
||||||
|
import type { ParsedToolIdentifier } from '@agentic/platform-validators'
|
||||||
import type { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
import type { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
import type { Simplify } from 'type-fest'
|
import type { Simplify } from 'type-fest'
|
||||||
|
@ -57,10 +61,30 @@ export type RateLimitState = {
|
||||||
export type RateLimitCache = Map<string, RateLimitState>
|
export type RateLimitCache = Map<string, RateLimitState>
|
||||||
|
|
||||||
export type CacheStatus = 'HIT' | 'MISS' | 'BYPASS' | 'DYNAMIC'
|
export type CacheStatus = 'HIT' | 'MISS' | 'BYPASS' | 'DYNAMIC'
|
||||||
export type RequestMode = 'mcp' | 'http'
|
export type EdgeRequestMode = 'mcp' | 'http'
|
||||||
|
|
||||||
export type WaitUntil = (promise: Promise<any>) => void
|
export type WaitUntil = (promise: Promise<any>) => void
|
||||||
|
|
||||||
|
export interface ResolvedEdgeRequest extends Record<string, unknown> {
|
||||||
|
parsedToolIdentifier: ParsedToolIdentifier
|
||||||
|
deployment: AdminDeployment
|
||||||
|
requestId: string
|
||||||
|
ip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedMcpEdgeRequest extends ResolvedEdgeRequest {
|
||||||
|
consumer?: AdminConsumer
|
||||||
|
pricingPlan?: PricingPlan
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedHttpEdgeRequest extends ResolvedEdgeRequest {
|
||||||
|
consumer?: AdminConsumer
|
||||||
|
pricingPlan?: PricingPlan
|
||||||
|
tool: Tool
|
||||||
|
toolCallArgs: ToolCallArgs
|
||||||
|
cacheControl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ResolvedOriginToolCallResult = {
|
export type ResolvedOriginToolCallResult = {
|
||||||
toolCallArgs: ToolCallArgs
|
toolCallArgs: ToolCallArgs
|
||||||
originRequest?: Request
|
originRequest?: Request
|
||||||
|
|
|
@ -463,6 +463,9 @@ importers:
|
||||||
'@agentic/platform-validators':
|
'@agentic/platform-validators':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/validators
|
version: link:../../packages/validators
|
||||||
|
'@cloudflare/workers-oauth-provider':
|
||||||
|
specifier: ^0.0.5
|
||||||
|
version: 0.0.5
|
||||||
'@hono/zod-validator':
|
'@hono/zod-validator':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.7.0(hono@4.7.11)(zod@3.25.62)
|
version: 0.7.0(hono@4.7.11)(zod@3.25.62)
|
||||||
|
@ -969,6 +972,9 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@cloudflare/workers-oauth-provider@0.0.5':
|
||||||
|
resolution: {integrity: sha512-t1x5KAzsubCvb4APnJ93z407X1x7SGj/ga5ziRnwIb/iLy4PMkT/hgd1y5z7Bbsdy5Fy6mywhCP4lym24bX66w==}
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250610.0':
|
'@cloudflare/workers-types@4.20250610.0':
|
||||||
resolution: {integrity: sha512-HxnUoey3QxCEfy07pUm7J42jBi9YPHq/hA3fw6JmOqYLHdviHI28OA8lup+2RUaHwDzh6q1DSfrBvvDqde645A==}
|
resolution: {integrity: sha512-HxnUoey3QxCEfy07pUm7J42jBi9YPHq/hA3fw6JmOqYLHdviHI28OA8lup+2RUaHwDzh6q1DSfrBvvDqde645A==}
|
||||||
|
|
||||||
|
@ -5986,6 +5992,10 @@ snapshots:
|
||||||
'@cloudflare/workerd-windows-64@1.20250604.0':
|
'@cloudflare/workerd-windows-64@1.20250604.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workers-oauth-provider@0.0.5':
|
||||||
|
dependencies:
|
||||||
|
'@cloudflare/workers-types': 4.20250610.0
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250610.0': {}
|
'@cloudflare/workers-types@4.20250610.0': {}
|
||||||
|
|
||||||
'@commander-js/extra-typings@14.0.0(commander@14.0.0)':
|
'@commander-js/extra-typings@14.0.0(commander@14.0.0)':
|
||||||
|
|
Ładowanie…
Reference in New Issue