kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
2e458b226f
commit
449bb4a926
|
@ -0,0 +1,103 @@
|
|||
import type {
|
||||
AdminDeployment,
|
||||
OpenAPIToolOperation
|
||||
} from '@agentic/platform-types'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
export async function createRequestForOpenAPIOperation({
|
||||
request,
|
||||
operation,
|
||||
deployment
|
||||
}: {
|
||||
request: Request
|
||||
operation: OpenAPIToolOperation
|
||||
deployment: AdminDeployment
|
||||
}): Promise<Request> {
|
||||
const tempInitialRequest = request.clone()
|
||||
const tempInitialRequestBody: any = await tempInitialRequest.json()
|
||||
|
||||
const params = Object.entries(operation.parameterSources)
|
||||
|
||||
// TODO: Make this more efficient by changing the `parameterSources` data structure
|
||||
const bodyParams = params.filter(([_key, source]) => source === 'body')
|
||||
const formDataParams = params.filter(
|
||||
([_key, source]) => source === 'formData'
|
||||
)
|
||||
const headerParams = params.filter(([_key, source]) => source === 'header')
|
||||
const pathParams = params.filter(([_key, source]) => source === 'path')
|
||||
const queryParams = params.filter(([_key, source]) => source === 'query')
|
||||
const cookieParams = params.filter(([_key, source]) => source === 'cookie')
|
||||
assert(
|
||||
!cookieParams.length,
|
||||
500,
|
||||
'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.'
|
||||
)
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (headerParams.length > 0) {
|
||||
for (const [key] of headerParams) {
|
||||
headers[key] = tempInitialRequest.headers.get(key) as string
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key] of cookieParams) {
|
||||
headers[key] = tempInitialRequestBody[key] as string
|
||||
}
|
||||
|
||||
let body: string | undefined
|
||||
if (bodyParams.length > 0) {
|
||||
body = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
bodyParams.map(([key]) => [key, tempInitialRequestBody[key] as any])
|
||||
)
|
||||
)
|
||||
|
||||
headers['content-type'] ??= 'application/json'
|
||||
} else if (formDataParams.length > 0) {
|
||||
body = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
formDataParams.map(([key]) => [key, tempInitialRequestBody[key] as any])
|
||||
)
|
||||
)
|
||||
|
||||
headers['content-type'] ??= 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
let path = operation.path
|
||||
if (pathParams.length > 0) {
|
||||
for (const [key] of pathParams) {
|
||||
const value: string = tempInitialRequestBody[key]
|
||||
assert(value, 400, `Missing required parameter "${key}"`)
|
||||
|
||||
const pathParamPlaceholder = `{${key}}`
|
||||
assert(
|
||||
path.includes(pathParamPlaceholder),
|
||||
500,
|
||||
`Misconfigured OpenAPI deployment "${deployment.id}": invalid path "${operation.path}" missing required path parameter "${key}"`
|
||||
)
|
||||
|
||||
path = path.replaceAll(pathParamPlaceholder, value)
|
||||
}
|
||||
}
|
||||
assert(
|
||||
!/\{\w+\}/.test(path),
|
||||
500,
|
||||
`Misconfigured OpenAPI deployment "${deployment.id}": invalid path "${operation.path}"`
|
||||
)
|
||||
|
||||
const query = new URLSearchParams()
|
||||
for (const [key] of queryParams) {
|
||||
query.set(key, tempInitialRequestBody[key] as string)
|
||||
}
|
||||
|
||||
const queryString = query.toString()
|
||||
const originRequestUrl = `${deployment.originUrl}${path}${
|
||||
queryString ? `?${queryString}` : ''
|
||||
}`
|
||||
|
||||
return new Request(originRequestUrl, {
|
||||
method: operation.method,
|
||||
body,
|
||||
headers
|
||||
})
|
||||
}
|
|
@ -2,6 +2,7 @@ import type { PricingPlan, RateLimit } from '@agentic/platform-types'
|
|||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import type { AdminConsumer, Context, ResolvedOriginRequest } from './types'
|
||||
import { createRequestForOpenAPIOperation } from './create-request-for-openapi-operation'
|
||||
import { enforceRateLimit } from './enforce-rate-limit'
|
||||
import { getAdminConsumer } from './get-admin-consumer'
|
||||
import { getAdminDeployment } from './get-admin-deployment'
|
||||
|
@ -84,18 +85,6 @@ export async function resolveOriginRequest(
|
|||
} else {
|
||||
// For unauthenticated requests, default to a free pricing plan if available.
|
||||
pricingPlan = deployment.pricingPlans.find((plan) => plan.slug === 'free')
|
||||
|
||||
// assert(
|
||||
// pricingPlan,
|
||||
// 403,
|
||||
// `Auth error, unable to find matching pricing plan for project "${deployment.project}"`
|
||||
// )
|
||||
|
||||
// assert(
|
||||
// !pricingPlan.auth,
|
||||
// 403,
|
||||
// `Auth error, encountered invalid pricing plan "${pricingPlan.slug}" for project "${deployment.project}"`
|
||||
// )
|
||||
}
|
||||
|
||||
let rateLimit: RateLimit | undefined | null
|
||||
|
@ -175,97 +164,14 @@ export async function resolveOriginRequest(
|
|||
let originRequest: Request | undefined
|
||||
|
||||
if (originAdapter.type === 'openapi' || originAdapter.type === 'raw') {
|
||||
// TODO: For OpenAPI, we need to convert from POST to the correct operation?
|
||||
// Or, do we only support a single public MCP interface?
|
||||
if (originAdapter.type === 'openapi') {
|
||||
const operation = originAdapter.toolToOperationMap[tool.name]
|
||||
assert(operation, 404, `Tool "${tool.name}" not found in OpenAPI spec`)
|
||||
|
||||
const tempInitialRequest = ctx.req.clone()
|
||||
const tempInitialRequestBody: any = await tempInitialRequest.json()
|
||||
|
||||
const params = Object.entries(operation.parameterSources)
|
||||
const bodyParams = params.filter(([_key, source]) => source === 'body')
|
||||
const formDataParams = params.filter(
|
||||
([_key, source]) => source === 'formData'
|
||||
)
|
||||
const headerParams = params.filter(
|
||||
([_key, source]) => source === 'header'
|
||||
)
|
||||
const pathParams = params.filter(([_key, source]) => source === 'path')
|
||||
const queryParams = params.filter(([_key, source]) => source === 'query')
|
||||
const cookieParams = params.filter(
|
||||
([_key, source]) => source === 'cookie'
|
||||
)
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (headerParams.length > 0) {
|
||||
for (const [key] of headerParams) {
|
||||
headers[key] = tempInitialRequest.headers.get(key) as string
|
||||
}
|
||||
}
|
||||
|
||||
let body: string | undefined
|
||||
if (bodyParams.length > 0) {
|
||||
body = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
bodyParams.map(([key]) => [key, tempInitialRequestBody[key] as any])
|
||||
)
|
||||
)
|
||||
|
||||
headers['content-type'] ??= 'application/json'
|
||||
} else if (formDataParams.length > 0) {
|
||||
body = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
formDataParams.map(([key]) => [
|
||||
key,
|
||||
tempInitialRequestBody[key] as any
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
headers['content-type'] ??= 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
let path = operation.path
|
||||
if (pathParams.length > 0) {
|
||||
for (const [key] of pathParams) {
|
||||
const value: string = tempInitialRequestBody[key]
|
||||
assert(value, 400, `Missing required parameter "${key}"`)
|
||||
|
||||
const pathParamPlaceholder = `{${key}}`
|
||||
assert(
|
||||
path.includes(pathParamPlaceholder),
|
||||
500,
|
||||
`Misconfigured OpenAPI deployment "${deployment.id}": invalid path "${operation.path}" missing required path parameter "${key}"`
|
||||
)
|
||||
|
||||
path = path.replaceAll(pathParamPlaceholder, value)
|
||||
}
|
||||
}
|
||||
assert(
|
||||
!/\{\w+\}/.test(path),
|
||||
500,
|
||||
`Misconfigured OpenAPI deployment "${deployment.id}": invalid path "${operation.path}"`
|
||||
)
|
||||
|
||||
const query = new URLSearchParams()
|
||||
for (const [key] of queryParams) {
|
||||
query.set(key, tempInitialRequestBody[key] as string)
|
||||
}
|
||||
|
||||
for (const [key] of cookieParams) {
|
||||
headers[key] = tempInitialRequestBody[key] as string
|
||||
}
|
||||
|
||||
const queryString = query.toString()
|
||||
const originRequestUrl = `${deployment.originUrl}${path}${
|
||||
queryString ? `?${queryString}` : ''
|
||||
}`
|
||||
originRequest = new Request(originRequestUrl, {
|
||||
method: operation.method,
|
||||
body,
|
||||
headers
|
||||
originRequest = await createRequestForOpenAPIOperation({
|
||||
request: req,
|
||||
operation,
|
||||
deployment
|
||||
})
|
||||
} else {
|
||||
const originRequestUrl = `${deployment.originUrl}${toolPath}${search}`
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
|
||||
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
// import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
|
||||
|
||||
import type { Context, ResolvedOriginRequest } from './lib/types'
|
||||
|
||||
// TODO: https://github.com/modelcontextprotocol/servers/blob/8fb7bbdab73eddb42aba72e8eab81102efe1d544/src/everything/sse.ts
|
||||
// TODO: https://github.com/cloudflare/agents
|
||||
|
||||
const transports: Map<string, SSEServerTransport> = new Map<
|
||||
string,
|
||||
SSEServerTransport
|
||||
>()
|
||||
// const transports: Map<string, SSEServerTransport> = new Map<
|
||||
// string,
|
||||
// SSEServerTransport
|
||||
// >()
|
||||
|
||||
export async function handleMCPRequest(
|
||||
ctx: Context,
|
||||
resolvedOriginRequest: ResolvedOriginRequest
|
||||
_ctx: Context,
|
||||
_resolvedOriginRequest: ResolvedOriginRequest
|
||||
) {
|
||||
const serverTransport = new SSEServerTransport()
|
||||
const server = new McpServer({
|
||||
name: 'weather',
|
||||
version: '1.0.0',
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {}
|
||||
}
|
||||
})
|
||||
// const serverTransport = new SSEServerTransport()
|
||||
// const server = new McpServer({
|
||||
// name: 'weather',
|
||||
// version: '1.0.0',
|
||||
// capabilities: {
|
||||
// resources: {},
|
||||
// tools: {}
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
declare namespace Cloudflare {
|
||||
interface Env {
|
||||
ENVIRONMENT: 'development' | 'production'
|
||||
AGENTIC_API_BASE_URL: 'http://localhost:3000' | 'https://api.agentic.ai'
|
||||
AGENTIC_API_BASE_URL: 'http://localhost:3000' | 'https://api.agentic.so'
|
||||
DO_RATE_LIMITER: DurableObjectNamespace<
|
||||
import('./worker').DurableObjectRateLimiter
|
||||
>
|
||||
|
|
|
@ -63,18 +63,22 @@ export default {
|
|||
|
||||
try {
|
||||
originStartTime = Date.now()
|
||||
let originResponse: Response | undefined
|
||||
|
||||
switch (resolvedOriginRequest.deployment.originAdapter.type) {
|
||||
case 'openapi':
|
||||
return fetch(resolvedOriginRequest.originRequest!)
|
||||
originResponse = await fetch(resolvedOriginRequest.originRequest!)
|
||||
break
|
||||
|
||||
case 'raw':
|
||||
return fetch(resolvedOriginRequest.originRequest!)
|
||||
originResponse = await fetch(resolvedOriginRequest.originRequest!)
|
||||
break
|
||||
|
||||
case 'mcp':
|
||||
break
|
||||
throw new Error('MCP not yet supported')
|
||||
}
|
||||
|
||||
const res = new Response(originResponse.body, originResponse)
|
||||
recordTimespans()
|
||||
|
||||
// Record the time it took for both the origin and gateway proxy to respond
|
||||
|
@ -101,28 +105,28 @@ export default {
|
|||
type: err.type,
|
||||
code: err.code
|
||||
}),
|
||||
{ status: 500, headers: globalResHeaders }
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
return res
|
||||
} finally {
|
||||
const now = Date.now()
|
||||
|
||||
// const now = Date.now()
|
||||
// Report usage.
|
||||
// Note that we are not awaiting the results of this on purpose so we can
|
||||
// return the response to the client immediately.
|
||||
ctx.waitUntil(
|
||||
reportUsage(ctx, {
|
||||
...call,
|
||||
cache: res!.headers.get('cf-cache-status'),
|
||||
status: res!.status,
|
||||
timestamp: Math.ceil(now / 1000),
|
||||
computeTime: originTimespan!,
|
||||
gatewayTime: gatewayTimespan!,
|
||||
// TODO: record correct bandwidth of request + response content-length
|
||||
bandwidth: 0
|
||||
})
|
||||
)
|
||||
// TODO
|
||||
// ctx.waitUntil(
|
||||
// reportUsage(ctx, {
|
||||
// ...call,
|
||||
// cache: res!.headers.get('cf-cache-status'),
|
||||
// status: res!.status,
|
||||
// timestamp: Math.ceil(now / 1000),
|
||||
// computeTime: originTimespan!,
|
||||
// gatewayTime: gatewayTimespan!,
|
||||
// // TODO: record correct bandwidth of request + response content-length
|
||||
// bandwidth: 0
|
||||
// })
|
||||
// )
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
|
@ -139,8 +143,7 @@ export default {
|
|||
code: err.code
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: globalResHeaders
|
||||
status: 500
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"prod": {
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production",
|
||||
"AGENTIC_API_BASE_URL": "https://api.agentic.ai"
|
||||
"AGENTIC_API_BASE_URL": "https://api.agentic.so"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,12 @@ export default [
|
|||
'unicorn/no-process-exit': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['apps/gateway/src/**/*.ts'],
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['apps/api/src/**/*.ts'],
|
||||
plugins: {
|
||||
|
|
|
@ -21,6 +21,11 @@ export function mergeJsonSchemaObjects(
|
|||
label: string
|
||||
}
|
||||
) {
|
||||
assert(
|
||||
source !== 'cookie',
|
||||
'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.'
|
||||
)
|
||||
|
||||
if (schema1.type === 'object' && schema1.properties) {
|
||||
schema0.properties = {
|
||||
...schema0.properties,
|
||||
|
|
Ładowanie…
Reference in New Issue