kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add support for ToolConfig.additionalProperties
rodzic
eb2932b799
commit
1884597812
|
@ -16,8 +16,8 @@ const fixtures = [
|
||||||
// 'pricing-3-plans',
|
// 'pricing-3-plans',
|
||||||
// 'pricing-monthly-annual',
|
// 'pricing-monthly-annual',
|
||||||
// 'pricing-custom-0',
|
// 'pricing-custom-0',
|
||||||
// 'basic-openapi',
|
'basic-openapi',
|
||||||
// 'basic-mcp',
|
'basic-mcp',
|
||||||
'everything-openapi'
|
'everything-openapi'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -221,3 +221,17 @@ et est aut quod aut provident voluptas autem voluptas",
|
||||||
"userId": 1,
|
"userId": 1,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`OpenAPI kitchen sink pure tool > 7.0: POST @dev/test-everything-openapi@390e70bf/pure 1`] = `
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"nala": "kitten",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`OpenAPI kitchen sink pure tool > 7.1: POST @dev/test-everything-openapi@390e70bf/pure 1`] = `
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"nala": "kitten",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -74,12 +74,6 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
||||||
)
|
)
|
||||||
expect(type).toBe(expectedContentType)
|
expect(type).toBe(expectedContentType)
|
||||||
|
|
||||||
if (expectedHeaders) {
|
|
||||||
for (const [key, value] of Object.entries(expectedHeaders)) {
|
|
||||||
expect(res.headers.get(key)).toBe(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: any
|
let body: any
|
||||||
|
|
||||||
if (type.includes('json')) {
|
if (type.includes('json')) {
|
||||||
|
@ -107,6 +101,12 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
||||||
expect(body).toMatchSnapshot()
|
expect(body).toMatchSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expectedHeaders) {
|
||||||
|
for (const [key, value] of Object.entries(expectedHeaders)) {
|
||||||
|
expect(res.headers.get(key)).toBe(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (compareResponseBodies && status >= 200 && status < 300) {
|
if (compareResponseBodies && status >= 200 && status < 300) {
|
||||||
if (!fixtureResponseBody) {
|
if (!fixtureResponseBody) {
|
||||||
fixtureResponseBody = body
|
fixtureResponseBody = body
|
||||||
|
|
|
@ -475,5 +475,54 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'OpenAPI kitchen sink pure tool',
|
||||||
|
sequential: true,
|
||||||
|
compareResponseBodies: true,
|
||||||
|
fixtures: [
|
||||||
|
{
|
||||||
|
path: '@dev/test-everything-openapi@390e70bf/pure',
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
nala: 'kitten',
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
headers: {
|
||||||
|
'cache-control':
|
||||||
|
'public, max-age=31560000, s-maxage=31560000, stale-while-revalidate=3600'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
nala: 'kitten',
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// second request should hit the cache
|
||||||
|
path: '@dev/test-everything-openapi@390e70bf/pure',
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
nala: 'kitten',
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
headers: {
|
||||||
|
'cf-cache-status': 'HIT',
|
||||||
|
'cache-control':
|
||||||
|
'public, max-age=31560000, s-maxage=31560000, stale-while-revalidate=3600'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
nala: 'kitten',
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -53,13 +53,14 @@ app.use(responseTime)
|
||||||
|
|
||||||
app.all(async (ctx) => {
|
app.all(async (ctx) => {
|
||||||
const waitUntil = ctx.executionCtx.waitUntil.bind(ctx.executionCtx)
|
const waitUntil = ctx.executionCtx.waitUntil.bind(ctx.executionCtx)
|
||||||
|
const isCachingEnabled = isRequestPubliclyCacheable(ctx.req.raw)
|
||||||
ctx.set('cache', caches.default)
|
ctx.set('cache', caches.default)
|
||||||
ctx.set(
|
ctx.set(
|
||||||
'client',
|
'client',
|
||||||
createAgenticClient({
|
createAgenticClient({
|
||||||
env: ctx.env,
|
env: ctx.env,
|
||||||
cache: caches.default,
|
cache: caches.default,
|
||||||
isCachingEnabled: isRequestPubliclyCacheable(ctx.req.raw),
|
isCachingEnabled,
|
||||||
waitUntil
|
waitUntil
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,10 @@ export async function createHttpRequestForOpenAPIOperation({
|
||||||
`Internal logic error for origin adapter type "${deployment.originAdapter.type}"`
|
`Internal logic error for origin adapter type "${deployment.originAdapter.type}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { method } = operation
|
||||||
|
const methodHasBody =
|
||||||
|
method === 'post' || method === 'put' || method === 'patch'
|
||||||
|
|
||||||
// TODO: Make this more efficient by changing the `parameterSources` data structure
|
// TODO: Make this more efficient by changing the `parameterSources` data structure
|
||||||
const params = Object.entries(operation.parameterSources)
|
const params = Object.entries(operation.parameterSources)
|
||||||
const bodyParams = params.filter(([_key, source]) => source === 'body')
|
const bodyParams = params.filter(([_key, source]) => source === 'body')
|
||||||
|
@ -40,6 +44,20 @@ export async function createHttpRequestForOpenAPIOperation({
|
||||||
'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.'
|
'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Make this more efficient...
|
||||||
|
const extraArgs = Object.keys(toolCallArgs).filter((key) => {
|
||||||
|
if (bodyParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
if (formDataParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
if (headerParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
if (queryParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
if (pathParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
if (cookieParams.some(([paramKey]) => paramKey === key)) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const extraArgsEntries = extraArgs
|
||||||
|
.map((key) => [key, toolCallArgs[key]])
|
||||||
|
.filter(([, value]) => value !== undefined)
|
||||||
|
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
if (request) {
|
if (request) {
|
||||||
// TODO: do we want to expose these? especially authorization?
|
// TODO: do we want to expose these? especially authorization?
|
||||||
|
@ -59,31 +77,40 @@ export async function createHttpRequestForOpenAPIOperation({
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: string | undefined
|
let body: string | undefined
|
||||||
if (bodyParams.length > 0) {
|
if (methodHasBody) {
|
||||||
body = JSON.stringify(
|
if (bodyParams.length > 0 || !formDataParams.length) {
|
||||||
Object.fromEntries(
|
const bodyJson = Object.fromEntries(
|
||||||
bodyParams
|
bodyParams
|
||||||
.map(([key]) => [key, toolCallArgs[key]])
|
.map(([key]) => [key, toolCallArgs[key]])
|
||||||
|
.concat(extraArgsEntries)
|
||||||
// Prune undefined values. We know these aren't required fields,
|
// Prune undefined values. We know these aren't required fields,
|
||||||
// because the incoming request params have already been validated
|
// because the incoming request params have already been validated
|
||||||
// against the tool's input schema.
|
// against the tool's input schema.
|
||||||
.filter(([, value]) => value !== undefined)
|
.filter(([, value]) => value !== undefined)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
headers['content-type'] ??= 'application/json'
|
body = JSON.stringify(bodyJson)
|
||||||
} else if (formDataParams.length > 0) {
|
headers['content-type'] = 'application/json'
|
||||||
// TODO: Double-check FormData usage.
|
headers['content-length'] = body.length.toString()
|
||||||
const formData = new FormData()
|
} else if (formDataParams.length > 0) {
|
||||||
for (const [key] of formDataParams) {
|
// TODO: Double-check FormData usage.
|
||||||
const value = toolCallArgs[key]
|
const bodyFormData = new FormData()
|
||||||
if (value !== undefined) {
|
|
||||||
formData.append(key, value)
|
for (const [key] of formDataParams) {
|
||||||
|
const value = toolCallArgs[key]
|
||||||
|
if (value !== undefined) {
|
||||||
|
bodyFormData.append(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
body = formData.toString()
|
for (const [key, value] of extraArgsEntries) {
|
||||||
headers['content-type'] ??= 'application/x-www-form-urlencoded'
|
bodyFormData.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
body = bodyFormData.toString()
|
||||||
|
headers['content-type'] = 'application/x-www-form-urlencoded'
|
||||||
|
headers['content-length'] = body.length.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = operation.path
|
let path = operation.path
|
||||||
|
@ -112,13 +139,20 @@ export async function createHttpRequestForOpenAPIOperation({
|
||||||
for (const [key] of queryParams) {
|
for (const [key] of queryParams) {
|
||||||
query.set(key, toolCallArgs[key] as string)
|
query.set(key, toolCallArgs[key] as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!methodHasBody) {
|
||||||
|
for (const [key, value] of extraArgsEntries) {
|
||||||
|
query.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queryString = query.toString()
|
const queryString = query.toString()
|
||||||
const originRequestUrl = `${deployment.originUrl}${path}${
|
const originRequestUrl = `${deployment.originUrl}${path}${
|
||||||
queryString ? `?${queryString}` : ''
|
queryString ? `?${queryString}` : ''
|
||||||
}`
|
}`
|
||||||
|
|
||||||
return new Request(originRequestUrl, {
|
return new Request(originRequestUrl, {
|
||||||
method: operation.method,
|
method: method.toUpperCase(),
|
||||||
body,
|
body,
|
||||||
headers
|
headers
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,26 +15,32 @@ export async function getRequestCacheKey(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.method === 'POST' || request.method === 'PUT') {
|
if (
|
||||||
|
request.method === 'POST' ||
|
||||||
|
request.method === 'PUT' ||
|
||||||
|
request.method === 'PATCH'
|
||||||
|
) {
|
||||||
const contentLength = Number.parseInt(
|
const contentLength = Number.parseInt(
|
||||||
request.headers.get('content-length') ?? '0'
|
request.headers.get('content-length') ?? '0'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (contentLength && contentLength < MAX_POST_BODY_SIZE_BYTES) {
|
if (contentLength < MAX_POST_BODY_SIZE_BYTES) {
|
||||||
const { type } = contentType.safeParse(
|
const { type } = contentType.safeParse(
|
||||||
request.headers.get('content-type') || 'application/octet-stream'
|
request.headers.get('content-type') || 'application/octet-stream'
|
||||||
)
|
)
|
||||||
let hash: string
|
let hash = '___AGENTIC_CACHE_KEY_EMPTY_BODY___'
|
||||||
|
|
||||||
if (type.includes('json')) {
|
if (contentLength > 0) {
|
||||||
const bodyJson: any = await request.clone().json()
|
if (type.includes('json')) {
|
||||||
hash = hashObject(bodyJson)
|
const bodyJson: any = await request.clone().json()
|
||||||
} else if (type.includes('text/')) {
|
hash = hashObject(bodyJson)
|
||||||
const bodyString = await request.clone().text()
|
} else if (type.includes('text/')) {
|
||||||
hash = await sha256(bodyString)
|
const bodyString = await request.clone().text()
|
||||||
} else {
|
hash = await sha256(bodyString)
|
||||||
const bodyBuffer = await request.clone().arrayBuffer()
|
} else {
|
||||||
hash = await sha256(bodyBuffer)
|
const bodyBuffer = await request.clone().arrayBuffer()
|
||||||
|
hash = await sha256(bodyBuffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheUrl = new URL(request.url)
|
const cacheUrl = new URL(request.url)
|
||||||
|
@ -56,8 +62,6 @@ export async function getRequestCacheKey(
|
||||||
|
|
||||||
return newReq
|
return newReq
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
} else if (request.method === 'GET' || request.method === 'HEAD') {
|
} else if (request.method === 'GET' || request.method === 'HEAD') {
|
||||||
const url = request.url
|
const url = request.url
|
||||||
const normalizedUrl = normalizeUrl(url)
|
const normalizedUrl = normalizeUrl(url)
|
||||||
|
@ -80,11 +84,14 @@ export async function getRequestCacheKey(
|
||||||
request.url,
|
request.url,
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestHeaderWhitelist = new Set(['cache-control', 'mcp-session-id'])
|
const requestHeaderWhitelist = new Set([
|
||||||
|
'cache-control',
|
||||||
|
'content-type',
|
||||||
|
'mcp-session-id'
|
||||||
|
])
|
||||||
|
|
||||||
function normalizeRequestHeaders(request: Request) {
|
function normalizeRequestHeaders(request: Request) {
|
||||||
const headers = Object.fromEntries(request.headers.entries())
|
const headers = Object.fromEntries(request.headers.entries())
|
||||||
|
|
|
@ -34,7 +34,7 @@ export async function getToolArgsFromRequest(
|
||||||
data: incomingRequestArgsRaw,
|
data: incomingRequestArgsRaw,
|
||||||
errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
|
errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
|
||||||
coerce: true,
|
coerce: true,
|
||||||
strictAdditionalProperties: true
|
strictAdditionalProperties: false
|
||||||
})
|
})
|
||||||
|
|
||||||
return incomingRequestArgs
|
return incomingRequestArgs
|
||||||
|
|
|
@ -26,7 +26,10 @@ import { fetchCache } from './fetch-cache'
|
||||||
import { getRequestCacheKey } from './get-request-cache-key'
|
import { getRequestCacheKey } from './get-request-cache-key'
|
||||||
import { enforceRateLimit } from './rate-limits/enforce-rate-limit'
|
import { enforceRateLimit } from './rate-limits/enforce-rate-limit'
|
||||||
import { updateOriginRequest } from './update-origin-request'
|
import { updateOriginRequest } from './update-origin-request'
|
||||||
import { isCacheControlPubliclyCacheable } from './utils'
|
import {
|
||||||
|
isCacheControlPubliclyCacheable,
|
||||||
|
isResponsePubliclyCacheable
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
export async function resolveOriginToolCall({
|
export async function resolveOriginToolCall({
|
||||||
tool,
|
tool,
|
||||||
|
@ -173,7 +176,7 @@ export async function resolveOriginToolCall({
|
||||||
schema: tool.inputSchema,
|
schema: tool.inputSchema,
|
||||||
data: args,
|
data: args,
|
||||||
errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
|
errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
|
||||||
strictAdditionalProperties: true
|
strictAdditionalProperties: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const originStartTimeMs = Date.now()
|
const originStartTimeMs = Date.now()
|
||||||
|
@ -196,9 +199,17 @@ export async function resolveOriginToolCall({
|
||||||
// TODO: transform origin 5XX errors to 502 errors...
|
// TODO: transform origin 5XX errors to 502 errors...
|
||||||
const originResponse = await fetchCache({
|
const originResponse = await fetchCache({
|
||||||
cacheKey,
|
cacheKey,
|
||||||
fetchResponse: () => fetch(originRequest),
|
fetchResponse: async () => {
|
||||||
|
let response = await fetch(originRequest)
|
||||||
|
if (cacheControl && isResponsePubliclyCacheable(response)) {
|
||||||
|
response = new Response(response.body, response)
|
||||||
|
response.headers.set('cache-control', cacheControl)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
waitUntil
|
waitUntil
|
||||||
})
|
})
|
||||||
|
// const originResponse = await fetch(originRequest)
|
||||||
|
|
||||||
const cacheStatus =
|
const cacheStatus =
|
||||||
(originResponse.headers.get('cf-cache-status') as CacheStatus) ??
|
(originResponse.headers.get('cf-cache-status') as CacheStatus) ??
|
||||||
|
|
|
@ -64,6 +64,7 @@ test('isCacheControlPubliclyCacheable false', () => {
|
||||||
expect(isCacheControlPubliclyCacheable('no-store')).toBe(false)
|
expect(isCacheControlPubliclyCacheable('no-store')).toBe(false)
|
||||||
expect(isCacheControlPubliclyCacheable('no-cache')).toBe(false)
|
expect(isCacheControlPubliclyCacheable('no-cache')).toBe(false)
|
||||||
expect(isCacheControlPubliclyCacheable('private')).toBe(false)
|
expect(isCacheControlPubliclyCacheable('private')).toBe(false)
|
||||||
|
expect(isCacheControlPubliclyCacheable('max-age=0')).toBe(false)
|
||||||
expect(isCacheControlPubliclyCacheable('private, max-age=3600')).toBe(false)
|
expect(isCacheControlPubliclyCacheable('private, max-age=3600')).toBe(false)
|
||||||
expect(isCacheControlPubliclyCacheable('private, s-maxage=3600')).toBe(false)
|
expect(isCacheControlPubliclyCacheable('private, s-maxage=3600')).toBe(false)
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -7,6 +7,15 @@ export function isRequestPubliclyCacheable(request: Request): boolean {
|
||||||
return isCacheControlPubliclyCacheable(request.headers.get('cache-control'))
|
return isCacheControlPubliclyCacheable(request.headers.get('cache-control'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isResponsePubliclyCacheable(response: Response): boolean {
|
||||||
|
const pragma = response.headers.get('pragma')
|
||||||
|
if (pragma === 'no-cache') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCacheControlPubliclyCacheable(response.headers.get('cache-control'))
|
||||||
|
}
|
||||||
|
|
||||||
export function isCacheControlPubliclyCacheable(
|
export function isCacheControlPubliclyCacheable(
|
||||||
cacheControl?: string | null
|
cacheControl?: string | null
|
||||||
): boolean {
|
): boolean {
|
||||||
|
@ -19,7 +28,8 @@ export function isCacheControlPubliclyCacheable(
|
||||||
if (
|
if (
|
||||||
directives.has('no-store') ||
|
directives.has('no-store') ||
|
||||||
directives.has('no-cache') ||
|
directives.has('no-cache') ||
|
||||||
directives.has('private')
|
directives.has('private') ||
|
||||||
|
directives.has('max-age=0')
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,10 @@ export default defineConfig({
|
||||||
{
|
{
|
||||||
name: 'disabled_rate_limit_tool',
|
name: 'disabled_rate_limit_tool',
|
||||||
rateLimit: null
|
rateLimit: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'strict_additional_properties',
|
||||||
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Echoes the request body only allowing a single "foo" field.',
|
||||||
|
operationId: 'strictAdditionalProperties',
|
||||||
|
method: 'post',
|
||||||
|
path: '/strict-additional-properties',
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Echoed request body',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerStrictAdditionalProperties(app: OpenAPIHono) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
return c.json(c.req.valid('json'))
|
||||||
|
})
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { registerHealthCheck } from './routes/health-check'
|
||||||
import { registerNoCacheCacheControlTool } from './routes/no-cache-cache-control-tool'
|
import { registerNoCacheCacheControlTool } from './routes/no-cache-cache-control-tool'
|
||||||
import { registerNoStoreCacheControlTool } from './routes/no-store-cache-control-tool'
|
import { registerNoStoreCacheControlTool } from './routes/no-store-cache-control-tool'
|
||||||
import { registerPure } from './routes/pure'
|
import { registerPure } from './routes/pure'
|
||||||
|
import { registerStrictAdditionalProperties } from './routes/strict-additional-properties'
|
||||||
import { registerUnpureMarkedPure } from './routes/unpure-marked-pure'
|
import { registerUnpureMarkedPure } from './routes/unpure-marked-pure'
|
||||||
|
|
||||||
export const app = new OpenAPIHono()
|
export const app = new OpenAPIHono()
|
||||||
|
@ -32,6 +33,7 @@ registerNoStoreCacheControlTool(app)
|
||||||
registerNoCacheCacheControlTool(app)
|
registerNoCacheCacheControlTool(app)
|
||||||
registerCustomRateLimitTool(app)
|
registerCustomRateLimitTool(app)
|
||||||
registerDisabledRateLimitTool(app)
|
registerDisabledRateLimitTool(app)
|
||||||
|
registerStrictAdditionalProperties(app)
|
||||||
|
|
||||||
app.doc31('/docs', {
|
app.doc31('/docs', {
|
||||||
openapi: '3.1.0',
|
openapi: '3.1.0',
|
||||||
|
|
|
@ -1672,6 +1672,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
|
||||||
"path": "/health",
|
"path": "/health",
|
||||||
"tags": undefined,
|
"tags": undefined,
|
||||||
},
|
},
|
||||||
|
"no_cache_cache_control_tool": {
|
||||||
|
"method": "post",
|
||||||
|
"operationId": "noCacheCacheControlTool",
|
||||||
|
"parameterSources": {},
|
||||||
|
"path": "/no-cache-cache-control-tool",
|
||||||
|
"tags": undefined,
|
||||||
|
},
|
||||||
"no_store_cache_control_tool": {
|
"no_store_cache_control_tool": {
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"operationId": "noStoreCacheControlTool",
|
"operationId": "noStoreCacheControlTool",
|
||||||
|
@ -1686,6 +1693,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
|
||||||
"path": "/pure",
|
"path": "/pure",
|
||||||
"tags": undefined,
|
"tags": undefined,
|
||||||
},
|
},
|
||||||
|
"unpure_marked_pure": {
|
||||||
|
"method": "post",
|
||||||
|
"operationId": "unpure_marked_pure",
|
||||||
|
"parameterSources": {},
|
||||||
|
"path": "/unpure-marked-pure",
|
||||||
|
"tags": undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"tools": [
|
"tools": [
|
||||||
{
|
{
|
||||||
|
@ -1809,6 +1823,26 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Unpure tool marked pure",
|
||||||
|
"inputSchema": {
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"name": "unpure_marked_pure",
|
||||||
|
"outputSchema": {
|
||||||
|
"properties": {
|
||||||
|
"now": {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"now",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Custom cache control tool",
|
"description": "Custom cache control tool",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
|
@ -1835,6 +1869,19 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "No cache cache control tool",
|
||||||
|
"inputSchema": {
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"name": "no_cache_cache_control_tool",
|
||||||
|
"outputSchema": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Custom rate limit tool",
|
"description": "Custom rate limit tool",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
|
|
|
@ -22533,7 +22533,8 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"title": "OpenAPI server to test everything",
|
"description": "OpenAPI kitchen sink server meant for testing Agentic's origin OpenAPI adapter and ToolConfig features.",
|
||||||
|
"title": "OpenAPI server everything",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
},
|
},
|
||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
|
@ -22732,6 +22733,35 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/no-cache-cache-control-tool": {
|
||||||
|
"post": {
|
||||||
|
"description": "No cache cache control tool",
|
||||||
|
"operationId": "noCacheCacheControlTool",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "Echoed request body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"/no-store-cache-control-tool": {
|
"/no-store-cache-control-tool": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "No store cache control tool",
|
"description": "No store cache control tool",
|
||||||
|
@ -22790,6 +22820,42 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/unpure-marked-pure": {
|
||||||
|
"post": {
|
||||||
|
"description": "Unpure tool marked pure",
|
||||||
|
"operationId": "unpure_marked_pure",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"now": {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"now",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "Echoed request body with current timestamp to not be pure",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"/users/{userId}": {
|
"/users/{userId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Gets a user",
|
"description": "Gets a user",
|
||||||
|
|
|
@ -135,7 +135,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
"name": "test-everything-openapi",
|
"name": "test-everything-openapi",
|
||||||
"originAdapter": {
|
"originAdapter": {
|
||||||
"location": "external",
|
"location": "external",
|
||||||
"spec": "{"openapi":"3.1.0","info":{"title":"OpenAPI server to test everything","version":"0.1.0"},"components":{"schemas":{"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"]}},"parameters":{}},"paths":{"/health":{"get":{"description":"Check if the server is healthy","operationId":"healthCheck","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/users/{userId}":{"get":{"description":"Gets a user","tags":["users"],"operationId":"getUser","parameters":[{"schema":{"type":"string"},"required":true,"description":"User ID","name":"userId","in":"path"}],"responses":{"200":{"description":"A user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/disabled-tool":{"get":{"description":"Disabled tool","operationId":"disabledTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/disabled-for-free-plan-tool":{"get":{"description":"Disabled for free plan tool","operationId":"disabledForFreePlanTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/echo":{"post":{"description":"Echoes the request body","operationId":"echo","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/pure":{"post":{"description":"Pure tool","operationId":"pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-cache-control-tool":{"post":{"description":"Custom cache control tool","operationId":"customCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-store-cache-control-tool":{"post":{"description":"No store cache control tool","operationId":"noStoreCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-rate-limit-tool":{"post":{"description":"Custom rate limit tool","operationId":"customRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/disabled-rate-limit-tool":{"post":{"description":"Disabled rate limit tool","operationId":"disabledRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}},"webhooks":{}}",
|
"spec": "{"openapi":"3.1.0","info":{"title":"OpenAPI server everything","description":"OpenAPI kitchen sink server meant for testing Agentic's origin OpenAPI adapter and ToolConfig features.","version":"0.1.0"},"components":{"schemas":{"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"]}},"parameters":{}},"paths":{"/health":{"get":{"description":"Check if the server is healthy","operationId":"healthCheck","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/users/{userId}":{"get":{"description":"Gets a user","tags":["users"],"operationId":"getUser","parameters":[{"schema":{"type":"string"},"required":true,"description":"User ID","name":"userId","in":"path"}],"responses":{"200":{"description":"A user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/disabled-tool":{"get":{"description":"Disabled tool","operationId":"disabledTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/disabled-for-free-plan-tool":{"get":{"description":"Disabled for free plan tool","operationId":"disabledForFreePlanTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/echo":{"post":{"description":"Echoes the request body","operationId":"echo","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/pure":{"post":{"description":"Pure tool","operationId":"pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/unpure-marked-pure":{"post":{"description":"Unpure tool marked pure","operationId":"unpure_marked_pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body with current timestamp to not be pure","content":{"application/json":{"schema":{"type":"object","properties":{"now":{"type":"number"}},"required":["now"]}}}}}}},"/custom-cache-control-tool":{"post":{"description":"Custom cache control tool","operationId":"customCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-store-cache-control-tool":{"post":{"description":"No store cache control tool","operationId":"noStoreCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-cache-cache-control-tool":{"post":{"description":"No cache cache control tool","operationId":"noCacheCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-rate-limit-tool":{"post":{"description":"Custom rate limit tool","operationId":"customRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/disabled-rate-limit-tool":{"post":{"description":"Disabled rate limit tool","operationId":"disabledRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}},"webhooks":{}}",
|
||||||
"type": "openapi",
|
"type": "openapi",
|
||||||
},
|
},
|
||||||
"originUrl": "https://agentic-platform-fixtures-everything.onrender.com",
|
"originUrl": "https://agentic-platform-fixtures-everything.onrender.com",
|
||||||
|
@ -158,7 +158,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
"toolConfigs": [
|
"toolConfigs": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "getUser",
|
"name": "get_user",
|
||||||
"pricingPlanOverridesMap": {
|
"pricingPlanOverridesMap": {
|
||||||
"free": {
|
"free": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -171,13 +171,13 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"name": "disabledTool",
|
"name": "disabled_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "disabledForFreePlanTool",
|
"name": "disabled_for_free_plan_tool",
|
||||||
"pricingPlanOverridesMap": {
|
"pricingPlanOverridesMap": {
|
||||||
"free": {
|
"free": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -188,40 +188,40 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "pureTool",
|
"name": "pure",
|
||||||
"pure": true,
|
"pure": true,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "unpureToolMarkedPure",
|
"name": "unpure_marked_pure",
|
||||||
"pure": true,
|
"pure": true,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cacheControl": "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600",
|
"cacheControl": "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "customCacheControlTool",
|
"name": "custom_cache_control_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cacheControl": "no-cache",
|
"cacheControl": "no-cache",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "noCacheCacheControlTool",
|
"name": "no_cache_cache_control_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cacheControl": "no-store",
|
"cacheControl": "no-store",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "noStoreCacheControlTool",
|
"name": "no_store_cache_control_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "customRateLimitTool",
|
"name": "custom_rate_limit_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"rateLimit": {
|
"rateLimit": {
|
||||||
"async": true,
|
"async": true,
|
||||||
|
@ -232,7 +232,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "disabledRateLimitTool",
|
"name": "disabled_rate_limit_tool",
|
||||||
"pure": false,
|
"pure": false,
|
||||||
"rateLimit": null,
|
"rateLimit": null,
|
||||||
"reportUsage": true,
|
"reportUsage": true,
|
||||||
|
|
|
@ -460,6 +460,9 @@ export interface components {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
required?: string[];
|
required?: string[];
|
||||||
|
additionalProperties?: boolean | {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
Tool: {
|
Tool: {
|
||||||
/** @description Agentic tool name */
|
/** @description Agentic tool name */
|
||||||
|
@ -502,6 +505,8 @@ export interface components {
|
||||||
/** @default true */
|
/** @default true */
|
||||||
reportUsage: boolean;
|
reportUsage: boolean;
|
||||||
rateLimit?: components["schemas"]["RateLimit"] | null;
|
rateLimit?: components["schemas"]["RateLimit"] | null;
|
||||||
|
/** @default true */
|
||||||
|
additionalProperties: boolean;
|
||||||
/** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */
|
/** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */
|
||||||
pricingPlanOverridesMap?: {
|
pricingPlanOverridesMap?: {
|
||||||
[key: string]: components["schemas"]["PricingPlanToolOverride"];
|
[key: string]: components["schemas"]["PricingPlanToolOverride"];
|
||||||
|
|
|
@ -36,7 +36,10 @@ export const jsonSchemaObjectSchema = z
|
||||||
type: z.literal('object'),
|
type: z.literal('object'),
|
||||||
// TODO: improve this schema
|
// TODO: improve this schema
|
||||||
properties: z.record(z.string(), z.any()).optional(),
|
properties: z.record(z.string(), z.any()).optional(),
|
||||||
required: z.array(z.string()).optional()
|
required: z.array(z.string()).optional(),
|
||||||
|
additionalProperties: z
|
||||||
|
.union([z.boolean(), z.record(z.string(), z.any())])
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
.openapi('JsonSchemaObject')
|
.openapi('JsonSchemaObject')
|
||||||
|
@ -157,6 +160,16 @@ export const toolConfigSchema = z
|
||||||
*/
|
*/
|
||||||
rateLimit: z.union([rateLimitSchema, z.null()]).optional(),
|
rateLimit: z.union([rateLimitSchema, z.null()]).optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to allow additional properties in the tool's input schema.
|
||||||
|
*
|
||||||
|
* The default MCP spec allows additional properties. Set this to `false` if
|
||||||
|
* you want your tool to be more strict.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
additionalProperties: z.boolean().optional().default(true),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows you to override this tool's behavior or disable it entirely for
|
* Allows you to override this tool's behavior or disable it entirely for
|
||||||
* different pricing plans.
|
* different pricing plans.
|
||||||
|
|
Ładowanie…
Reference in New Issue