kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: fix and add e2e tests for rate-limits
rodzic
8a8cb58267
commit
d4d15dc920
|
@ -16,9 +16,9 @@ const fixtures = [
|
|||
// 'pricing-3-plans',
|
||||
// 'pricing-monthly-annual',
|
||||
// 'pricing-custom-0',
|
||||
'basic-openapi',
|
||||
'basic-mcp',
|
||||
'everything-openapi'
|
||||
'basic-openapi'
|
||||
// 'basic-mcp',
|
||||
// 'everything-openapi'
|
||||
]
|
||||
|
||||
const fixturesDir = path.join(
|
||||
|
|
|
@ -21,14 +21,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"ky": "catalog:",
|
||||
"p-map": "catalog:"
|
||||
"p-map": "catalog:",
|
||||
"p-times": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@agentic/platform": "workspace:*",
|
||||
"@agentic/platform-api-client": "workspace:*",
|
||||
"@agentic/platform-core": "workspace:*",
|
||||
"@agentic/platform-fixtures": "workspace:*",
|
||||
"fast-content-type-parse": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "catalog:"
|
||||
"@modelcontextprotocol/sdk": "catalog:",
|
||||
"fast-content-type-parse": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,8 +250,6 @@ exports[`HTTP => OpenAPI origin everything "echo" tool with empty body > 9.0: PO
|
|||
|
||||
exports[`HTTP => OpenAPI origin everything "echo" tool with empty body > 9.1: POST @dev/test-everything-openapi/echo 1`] = `{}`;
|
||||
|
||||
exports[`HTTP => OpenAPI origin everything "echo_headers" tool > 12.0: GET @dev/test-everything-openapi@e738c8aa/custom_rate_limit_tool 1`] = `{}`;
|
||||
|
||||
exports[`HTTP => OpenAPI origin everything "pure" tool > 7.0: POST @dev/test-everything-openapi/pure 1`] = `
|
||||
{
|
||||
"foo": "bar",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MCP => OpenAPI origin basic @ 010332cf get_post success > 2.0: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic @ 726d9f61 get_post success > 2.0: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -48,7 +48,7 @@ nostrum rerum est autem sunt rem eveniet architecto",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.0: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.0: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -64,7 +64,7 @@ nostrum rerum est autem sunt rem eveniet architecto",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.1: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.1: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -80,7 +80,7 @@ nostrum rerum est autem sunt rem eveniet architecto",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.2: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic caching > 7.2: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -128,7 +128,7 @@ nostrum rerum est autem sunt rem eveniet architecto",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin basic normalized caching > 8.0: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic normalized caching > 8.0: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -144,7 +144,7 @@ nostrum rerum est autem sunt rem eveniet architecto",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin basic normalized caching > 8.1: @dev/test-basic-openapi@010332cf/mcp get_post 1`] = `
|
||||
exports[`MCP => OpenAPI origin basic normalized caching > 8.1: @dev/test-basic-openapi@726d9f61/mcp get_post 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
|
@ -190,28 +190,6 @@ exports[`MCP => OpenAPI origin everything "pure" tool > 11.1: @dev/test-everythi
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin everything "unpure_marked_pure" tool > 14.0: @dev/test-everything-openapi/mcp unpure_marked_pure 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
"structuredContent": {
|
||||
"nala": "cat",
|
||||
"now": 1749674829923,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin everything "unpure_marked_pure" tool > 14.1: @dev/test-everything-openapi/mcp unpure_marked_pure 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
"isError": false,
|
||||
"structuredContent": {
|
||||
"nala": "cat",
|
||||
"now": 1749674829923,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`MCP => OpenAPI origin everything errors > 5.0: @dev/test-everything-openapi/mcp strict_additional_properties 1`] = `
|
||||
{
|
||||
"content": [],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import contentType from 'fast-content-type-parse'
|
||||
import defaultKy from 'ky'
|
||||
import pTimes from 'p-times'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { env } from './env'
|
||||
|
@ -20,7 +21,8 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
title,
|
||||
fixtures,
|
||||
compareResponseBodies = false,
|
||||
repeat = 1,
|
||||
repeat,
|
||||
repeatConcurrency = 1,
|
||||
repeatSuccessCriteria = 'all'
|
||||
} = fixtureSuite
|
||||
|
||||
|
@ -28,6 +30,10 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
describeFn(title, () => {
|
||||
let fixtureResponseBody: any | undefined
|
||||
|
||||
if (repeat) {
|
||||
expect(repeat).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
for (const [j, fixture] of fixtures.entries()) {
|
||||
const method = fixture.request?.method ?? 'GET'
|
||||
const timeout = fixture.timeout ?? 30_000
|
||||
|
@ -63,115 +69,117 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
// eslint-disable-next-line no-loop-func
|
||||
async () => {
|
||||
const numIterations = repeat ?? 1
|
||||
let numRepeatSuccesses = 0
|
||||
let numSuccessCases = 0
|
||||
|
||||
for (let iteration = 0; iteration < numIterations; ++iteration) {
|
||||
const repeatIterationPrefix = repeat
|
||||
? `[${iteration}/${numIterations}] `
|
||||
: ''
|
||||
await pTimes(
|
||||
numIterations,
|
||||
async (iteration: number) => {
|
||||
const repeatIterationPrefix = repeat
|
||||
? `[${iteration}/${numIterations}] `
|
||||
: ''
|
||||
|
||||
const res = await ky(fixture.path, {
|
||||
timeout,
|
||||
...fixture.request
|
||||
})
|
||||
const res = await ky(fixture.path, {
|
||||
timeout,
|
||||
...fixture.request
|
||||
})
|
||||
|
||||
if (res.status !== status && res.status >= 500) {
|
||||
let body: any
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch {}
|
||||
if (res.status !== status && res.status >= 500) {
|
||||
let body: any
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch {}
|
||||
|
||||
console.error(
|
||||
`${repeatIterationPrefix}${fixtureName} => UNEXPECTED ERROR ${res.status}`,
|
||||
{
|
||||
console.error(
|
||||
`${repeatIterationPrefix}${fixtureName} => UNEXPECTED ERROR ${res.status}`,
|
||||
body
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (repeat) {
|
||||
if (res.status === status) {
|
||||
++numRepeatSuccesses
|
||||
if (repeat) {
|
||||
if (res.status === status) {
|
||||
++numSuccessCases
|
||||
} else {
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} => ${res.status} (invalid sample; expected ${status})`,
|
||||
{
|
||||
headers: Object.fromEntries(res.headers.entries())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} => ${res.status} (invalid; expected ${status})`,
|
||||
{
|
||||
headers: Object.fromEntries(res.headers.entries())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
expect(res.status).toBe(status)
|
||||
}
|
||||
} else {
|
||||
expect(res.status).toBe(status)
|
||||
}
|
||||
|
||||
const { type } = contentType.safeParse(
|
||||
res.headers.get('content-type') ?? ''
|
||||
)
|
||||
expect(type).toBe(expectedContentType)
|
||||
|
||||
let body: any
|
||||
|
||||
if (type.includes('json')) {
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch (err) {
|
||||
console.error('json error', err)
|
||||
throw err
|
||||
}
|
||||
} else if (type.includes('text')) {
|
||||
body = await res.text()
|
||||
} else {
|
||||
body = await res.arrayBuffer()
|
||||
}
|
||||
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} => ${res.status}`,
|
||||
{
|
||||
body,
|
||||
headers: Object.fromEntries(res.headers.entries())
|
||||
}
|
||||
const { type } = contentType.safeParse(
|
||||
res.headers.get('content-type') ?? ''
|
||||
)
|
||||
}
|
||||
expect(type).toBe(expectedContentType)
|
||||
|
||||
if (expectedBody) {
|
||||
expect(body).toEqual(expectedBody)
|
||||
}
|
||||
let body: any
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(body))
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
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 (!fixtureResponseBody) {
|
||||
fixtureResponseBody = body
|
||||
if (type.includes('json')) {
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch (err) {
|
||||
console.error('json error', err)
|
||||
throw err
|
||||
}
|
||||
} else if (type.includes('text')) {
|
||||
body = await res.text()
|
||||
} else {
|
||||
expect(body).toEqual(fixtureResponseBody)
|
||||
body = await res.arrayBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} => ${res.status}`,
|
||||
{
|
||||
body,
|
||||
headers: Object.fromEntries(res.headers.entries())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (expectedBody) {
|
||||
expect(body).toEqual(expectedBody)
|
||||
}
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(body))
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
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 (!fixtureResponseBody) {
|
||||
fixtureResponseBody = body
|
||||
} else {
|
||||
expect(body).toEqual(fixtureResponseBody)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ concurrency: repeatConcurrency, stopOnError: true }
|
||||
)
|
||||
|
||||
if (repeat) {
|
||||
if (repeatSuccessCriteria === 'all') {
|
||||
expect(numRepeatSuccesses).toBe(numIterations)
|
||||
expect(numSuccessCases).toBe(numIterations)
|
||||
} else if (repeatSuccessCriteria === 'some') {
|
||||
expect(numRepeatSuccesses).toBeGreaterThan(0)
|
||||
expect(numSuccessCases).toBeGreaterThan(0)
|
||||
} else if (typeof repeatSuccessCriteria === 'function') {
|
||||
expect(repeatSuccessCriteria(numRepeatSuccesses)).toBe(true)
|
||||
await Promise.resolve(repeatSuccessCriteria(numSuccessCases))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,11 +56,14 @@ export type E2ETestFixtureSuite = {
|
|||
/** @default undefined */
|
||||
repeat?: number
|
||||
|
||||
/** @default 1 */
|
||||
repeatConcurrency?: number
|
||||
|
||||
/** @default 'all' */
|
||||
repeatSuccessCriteria?:
|
||||
| 'all'
|
||||
| 'some'
|
||||
| ((numRepeatSuccesses: number) => boolean)
|
||||
| ((numRepeatSuccesses: number) => void | Promise<void>)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
@ -641,17 +644,17 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
|
|||
snapshot: false,
|
||||
fixtures: [
|
||||
{
|
||||
path: '@dev/test-everything-openapi@c8c25547/echo_headers',
|
||||
path: '@dev/test-everything-openapi@707562a9/echo_headers',
|
||||
response: {
|
||||
validate: (body) => {
|
||||
expect(body['x-agentic-proxy-secret']).toEqual(
|
||||
'f279280a67a15df6e0245511bdeb11854fc8f6f702c49d028431bb1dbc03bfdc'
|
||||
)
|
||||
expect(body['x-agentic-deployment-id']).toEqual(
|
||||
'depl_kb0jszdetahn52ospawj3reu'
|
||||
'depl_tj03dd941xfrcd8cjqhg1b9w'
|
||||
)
|
||||
expect(body['x-agentic-deployment-identifier']).toEqual(
|
||||
'@dev/test-everything-openapi@c8c25547'
|
||||
'@dev/test-everything-openapi@707562a9'
|
||||
)
|
||||
expect(body['x-agentic-is-customer-subscription-active']).toEqual(
|
||||
'false'
|
||||
|
@ -664,10 +667,15 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
|
|||
]
|
||||
},
|
||||
{
|
||||
title: 'HTTP => OpenAPI origin everything "custom_rate_limit_tool"',
|
||||
only: true,
|
||||
repeat: 1,
|
||||
repeatSuccessCriteria: (numRepeatSuccesses) => numRepeatSuccesses <= 2,
|
||||
title:
|
||||
'HTTP => OpenAPI origin everything "custom_rate_limit_tool" (strict mode)',
|
||||
repeat: 5,
|
||||
repeatSuccessCriteria: (numRepeatSuccesses) => {
|
||||
expect(
|
||||
numRepeatSuccesses,
|
||||
'should have at least three 429 responses out of 5 requests with a strict rate limit of 2 requests per 30s'
|
||||
).toBeGreaterThanOrEqual(3)
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
path: '@dev/test-everything-openapi/custom_rate_limit_tool',
|
||||
|
@ -680,5 +688,29 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title:
|
||||
'HTTP => OpenAPI origin everything "custom_rate_limit_approximate_tool" (approximate mode)',
|
||||
repeat: 16,
|
||||
repeatConcurrency: 8,
|
||||
repeatSuccessCriteria: (numRepeatSuccesses) => {
|
||||
expect(
|
||||
numRepeatSuccesses,
|
||||
'should have at least one 429 response'
|
||||
).toBeGreaterThan(0)
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
path: '@dev/test-everything-openapi/custom_rate_limit_approximate_tool',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'ratelimit-policy': '2;w=30',
|
||||
'ratelimit-limit': '2'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,19 +1,31 @@
|
|||
import { pick } from '@agentic/platform-core'
|
||||
import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import pTimes from 'p-times'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { env } from './env'
|
||||
import { fixtureSuites } from './mcp-fixtures'
|
||||
|
||||
for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
||||
const { title, fixtures, compareResponseBodies = false } = fixtureSuite
|
||||
const {
|
||||
title,
|
||||
fixtures,
|
||||
compareResponseBodies = false,
|
||||
repeat,
|
||||
repeatConcurrency = 1,
|
||||
repeatSuccessCriteria = 'all'
|
||||
} = fixtureSuite
|
||||
|
||||
const describeFn = fixtureSuite.only ? describe.only : describe
|
||||
describeFn(title, () => {
|
||||
let fixtureResult: any | undefined
|
||||
|
||||
let client: McpClient
|
||||
|
||||
if (repeat) {
|
||||
expect(repeat).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
client = new McpClient({
|
||||
name: fixtureSuite.path,
|
||||
|
@ -82,94 +94,141 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
},
|
||||
// eslint-disable-next-line no-loop-func
|
||||
async () => {
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: fixture.request.args,
|
||||
_meta: fixture.request._meta
|
||||
})
|
||||
const numIterations = repeat ?? 1
|
||||
let numSuccessCases = 0
|
||||
|
||||
if (debugFixture) {
|
||||
console.log(fixtureName, '=>', result)
|
||||
}
|
||||
await pTimes(
|
||||
numIterations,
|
||||
async (iteration: number) => {
|
||||
const repeatIterationPrefix = repeat
|
||||
? `[${iteration}/${numIterations}] `
|
||||
: ''
|
||||
|
||||
if (isError) {
|
||||
expect(result.isError).toBeTruthy()
|
||||
} else {
|
||||
expect(result.isError).toBeFalsy()
|
||||
}
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: fixture.request.args,
|
||||
_meta: fixture.request._meta
|
||||
})
|
||||
|
||||
if (expectedResult) {
|
||||
expect(result).toEqual(expectedResult)
|
||||
}
|
||||
if (repeat) {
|
||||
if (result.isError === isError) {
|
||||
++numSuccessCases
|
||||
} else {
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} => (invalid sample; expected ${result.isError ? 'error' : 'no error'})`,
|
||||
JSON.stringify(result, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
if (expectedContent) {
|
||||
expect(result.content).toEqual(expectedContent)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedStructuredContent) {
|
||||
expect(result.structuredContent).toEqual(expectedStructuredContent)
|
||||
}
|
||||
if (debugFixture) {
|
||||
console.log(
|
||||
`${repeatIterationPrefix}${fixtureName} =>`,
|
||||
JSON.stringify(result, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
if (expectedMeta) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(typeof result._meta).toEqual('object')
|
||||
expect(!Array.isArray(result._meta)).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(expectedMeta)) {
|
||||
expect(result._meta![key]).toEqual(value)
|
||||
}
|
||||
}
|
||||
if (expectedAgenticMeta) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(result._meta?.agentic).toBeDefined()
|
||||
expect(typeof result._meta?.agentic).toEqual('object')
|
||||
expect(!Array.isArray(result._meta?.agentic)).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(expectedAgenticMeta)) {
|
||||
expect((result._meta!.agentic as any)[key]).toEqual(value)
|
||||
}
|
||||
}
|
||||
if (isError) {
|
||||
expect(result.isError).toBeTruthy()
|
||||
} else {
|
||||
expect(result.isError).toBeFalsy()
|
||||
}
|
||||
|
||||
if (expectedAgenticMetaHeaders) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(result._meta?.agentic).toBeDefined()
|
||||
expect(typeof result._meta?.agentic).toEqual('object')
|
||||
expect(!Array.isArray(result._meta?.agentic)).toBeTruthy()
|
||||
expect(typeof (result._meta?.agentic as any)?.headers).toEqual(
|
||||
'object'
|
||||
)
|
||||
expect(
|
||||
!Array.isArray((result._meta?.agentic as any)?.headers)
|
||||
).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(
|
||||
expectedAgenticMetaHeaders
|
||||
)) {
|
||||
expect((result._meta!.agentic as any).headers[key]).toEqual(value)
|
||||
}
|
||||
}
|
||||
if (expectedResult) {
|
||||
expect(result).toEqual(expectedResult)
|
||||
}
|
||||
|
||||
if (expectedSnapshot) {
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
if (expectedContent) {
|
||||
expect(result.content).toEqual(expectedContent)
|
||||
}
|
||||
|
||||
const stableResult = pick(
|
||||
result,
|
||||
'content',
|
||||
'structuredContent',
|
||||
'isError'
|
||||
if (expectedStructuredContent) {
|
||||
expect(result.structuredContent).toEqual(
|
||||
expectedStructuredContent
|
||||
)
|
||||
}
|
||||
|
||||
if (expectedMeta) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(typeof result._meta).toEqual('object')
|
||||
expect(!Array.isArray(result._meta)).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(expectedMeta)) {
|
||||
expect(result._meta![key]).toEqual(value)
|
||||
}
|
||||
}
|
||||
if (expectedAgenticMeta) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(result._meta?.agentic).toBeDefined()
|
||||
expect(typeof result._meta?.agentic).toEqual('object')
|
||||
expect(!Array.isArray(result._meta?.agentic)).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(
|
||||
expectedAgenticMeta
|
||||
)) {
|
||||
expect((result._meta!.agentic as any)[key]).toEqual(value)
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedAgenticMetaHeaders) {
|
||||
expect(result._meta).toBeDefined()
|
||||
expect(result._meta?.agentic).toBeDefined()
|
||||
expect(typeof result._meta?.agentic).toEqual('object')
|
||||
expect(!Array.isArray(result._meta?.agentic)).toBeTruthy()
|
||||
expect(typeof (result._meta?.agentic as any)?.headers).toEqual(
|
||||
'object'
|
||||
)
|
||||
expect(
|
||||
!Array.isArray((result._meta?.agentic as any)?.headers)
|
||||
).toBeTruthy()
|
||||
for (const [key, value] of Object.entries(
|
||||
expectedAgenticMetaHeaders
|
||||
)) {
|
||||
expect((result._meta!.agentic as any).headers[key]).toEqual(
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedSnapshot) {
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
|
||||
const stableResult = pick(
|
||||
result,
|
||||
'content',
|
||||
'structuredContent',
|
||||
'isError'
|
||||
)
|
||||
|
||||
if (expectedStableSnapshot) {
|
||||
expect(stableResult).toMatchSnapshot()
|
||||
}
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(result))
|
||||
}
|
||||
|
||||
if (compareResponseBodies && !isError) {
|
||||
if (!fixtureResult) {
|
||||
fixtureResult = stableResult
|
||||
} else {
|
||||
expect(stableResult).toEqual(fixtureResult)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ concurrency: repeatConcurrency, stopOnError: true }
|
||||
)
|
||||
|
||||
if (expectedStableSnapshot) {
|
||||
expect(stableResult).toMatchSnapshot()
|
||||
}
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(result))
|
||||
}
|
||||
|
||||
if (compareResponseBodies && !isError) {
|
||||
if (!fixtureResult) {
|
||||
fixtureResult = stableResult
|
||||
} else {
|
||||
expect(stableResult).toEqual(fixtureResult)
|
||||
if (repeat) {
|
||||
if (repeatSuccessCriteria === 'all') {
|
||||
expect(numSuccessCases).toBe(numIterations)
|
||||
} else if (repeatSuccessCriteria === 'some') {
|
||||
expect(numSuccessCases).toBeGreaterThan(0)
|
||||
} else if (typeof repeatSuccessCriteria === 'function') {
|
||||
await Promise.resolve(repeatSuccessCriteria(numSuccessCases))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,18 @@ export type MCPE2ETestFixtureSuite = {
|
|||
|
||||
/** @default undefined */
|
||||
stableSnapshot?: boolean
|
||||
|
||||
/** @default undefined */
|
||||
repeat?: number
|
||||
|
||||
/** @default 1 */
|
||||
repeatConcurrency?: number
|
||||
|
||||
/** @default 'all' */
|
||||
repeatSuccessCriteria?:
|
||||
| 'all'
|
||||
| 'some'
|
||||
| ((numRepeatSuccesses: number) => void | Promise<void>)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
@ -95,8 +107,8 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
]
|
||||
},
|
||||
{
|
||||
title: 'MCP => OpenAPI origin basic @ 010332cf get_post success ',
|
||||
path: '@dev/test-basic-openapi@010332cf/mcp',
|
||||
title: 'MCP => OpenAPI origin basic @ 726d9f61 get_post success ',
|
||||
path: '@dev/test-basic-openapi@726d9f61/mcp',
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
|
@ -286,7 +298,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
},
|
||||
{
|
||||
title: 'MCP => OpenAPI origin basic caching',
|
||||
path: '@dev/test-basic-openapi@010332cf/mcp',
|
||||
path: '@dev/test-basic-openapi@726d9f61/mcp',
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
|
@ -340,7 +352,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
},
|
||||
{
|
||||
title: 'MCP => OpenAPI origin basic normalized caching',
|
||||
path: '@dev/test-basic-openapi@010332cf/mcp',
|
||||
path: '@dev/test-basic-openapi@726d9f61/mcp',
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
|
@ -521,6 +533,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
title: 'MCP => OpenAPI origin everything "unpure_marked_pure" tool',
|
||||
path: '@dev/test-everything-openapi/mcp',
|
||||
compareResponseBodies: true,
|
||||
stableSnapshot: false,
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
|
@ -560,7 +573,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
},
|
||||
{
|
||||
title: 'MCP => OpenAPI origin everything "echo_headers" tool',
|
||||
path: '@dev/test-everything-openapi/mcp',
|
||||
path: '@dev/test-everything-openapi@707562a9/mcp',
|
||||
stableSnapshot: false,
|
||||
fixtures: [
|
||||
{
|
||||
|
@ -573,6 +586,84 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
|||
expect(result.structuredContent['x-agentic-proxy-secret']).toEqual(
|
||||
'f279280a67a15df6e0245511bdeb11854fc8f6f702c49d028431bb1dbc03bfdc'
|
||||
)
|
||||
expect(result.structuredContent['x-agentic-deployment-id']).toEqual(
|
||||
'depl_tj03dd941xfrcd8cjqhg1b9w'
|
||||
)
|
||||
expect(
|
||||
result.structuredContent['x-agentic-deployment-identifier']
|
||||
).toEqual('@dev/test-everything-openapi@707562a9')
|
||||
expect(
|
||||
result.structuredContent[
|
||||
'x-agentic-is-customer-subscription-active'
|
||||
]
|
||||
).toEqual('false')
|
||||
expect(
|
||||
result.structuredContent['x-agentic-user-id']
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
result.structuredContent['x-agentic-customer-id']
|
||||
).toBeUndefined()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title:
|
||||
'MCP => OpenAPI origin everything "custom_rate_limit_tool" (strict mode)',
|
||||
path: '@dev/test-everything-openapi/mcp',
|
||||
repeat: 5,
|
||||
repeatSuccessCriteria: (numRepeatSuccesses) => {
|
||||
expect(
|
||||
numRepeatSuccesses,
|
||||
'should have at least three 429 responses out of 5 requests with a strict rate limit of 2 requests per 30s'
|
||||
).toBeGreaterThanOrEqual(3)
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
name: 'custom_rate_limit_tool',
|
||||
args: {}
|
||||
},
|
||||
response: {
|
||||
isError: true,
|
||||
_agenticMeta: {
|
||||
status: 429
|
||||
},
|
||||
_agenticMetaHeaders: {
|
||||
'ratelimit-policy': '2;w=30',
|
||||
'ratelimit-limit': '2'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title:
|
||||
'MCP => OpenAPI origin everything "custom_rate_limit_approximate_tool" (approximate mode)',
|
||||
path: '@dev/test-everything-openapi/mcp',
|
||||
repeat: 16,
|
||||
repeatConcurrency: 8,
|
||||
repeatSuccessCriteria: (numRepeatSuccesses) => {
|
||||
expect(
|
||||
numRepeatSuccesses,
|
||||
'should have at least one 429 response'
|
||||
).toBeGreaterThan(0)
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
name: 'custom_rate_limit_approximate_tool',
|
||||
args: {}
|
||||
},
|
||||
response: {
|
||||
isError: true,
|
||||
_agenticMeta: {
|
||||
status: 429
|
||||
},
|
||||
_agenticMetaHeaders: {
|
||||
'ratelimit-policy': '2;w=30',
|
||||
'ratelimit-limit': '2'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,30 +17,38 @@ export class DurableRateLimiterBase extends DurableObject<RawEnv> {
|
|||
intervalMs: number
|
||||
cost?: number
|
||||
}): Promise<RateLimitState> {
|
||||
const existingState =
|
||||
(await this.ctx.storage.get<RateLimitState>('value')) || initialState
|
||||
const existingState = await this.ctx.storage.get<RateLimitState>('value')
|
||||
const currentAlarm = await this.ctx.storage.getAlarm()
|
||||
|
||||
const now = Date.now()
|
||||
const updatedResetTimeMs = now + intervalMs
|
||||
|
||||
// Update the payload
|
||||
const resetTimeMs = existingState.resetTimeMs ?? Date.now() + intervalMs
|
||||
|
||||
const state: RateLimitState = {
|
||||
current: existingState.current + cost,
|
||||
resetTimeMs
|
||||
}
|
||||
const state =
|
||||
existingState && currentAlarm && currentAlarm > now
|
||||
? existingState
|
||||
: {
|
||||
current: 0,
|
||||
resetTimeMs: updatedResetTimeMs
|
||||
}
|
||||
state.current += cost
|
||||
|
||||
// Update the alarm
|
||||
const currentAlarm = await this.ctx.storage.getAlarm()
|
||||
if (!currentAlarm) {
|
||||
await this.ctx.storage.setAlarm(resetTimeMs)
|
||||
if (!currentAlarm || currentAlarm <= now) {
|
||||
await this.ctx.storage.setAlarm(state.resetTimeMs)
|
||||
}
|
||||
|
||||
await this.ctx.storage.put('value', state)
|
||||
|
||||
// const updatedState = await this.ctx.storage.get<RateLimitState>('value')
|
||||
// console.log('update', this.ctx.id, {
|
||||
// console.log('DurableRateLimiter.update', this.ctx.id.toString(), {
|
||||
// existingState,
|
||||
// state,
|
||||
// updatedState
|
||||
// updatedState,
|
||||
// now,
|
||||
// intervalMs,
|
||||
// updatedResetTimeMs,
|
||||
// currentAlarm
|
||||
// })
|
||||
|
||||
return state
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { RateLimit } from '@agentic/platform-types'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import type { RawEnv } from '../env'
|
||||
|
@ -17,30 +18,23 @@ import type { DurableRateLimiterBase } from './durable-rate-limiter'
|
|||
const globalRateLimitCache: RateLimitCache = new Map()
|
||||
|
||||
export async function enforceRateLimit({
|
||||
rateLimit,
|
||||
id,
|
||||
interval,
|
||||
limit,
|
||||
cost = 1,
|
||||
async: _async = true,
|
||||
env,
|
||||
cache = globalRateLimitCache,
|
||||
waitUntil
|
||||
}: {
|
||||
/**
|
||||
* The rate limit to enforce.
|
||||
*/
|
||||
rateLimit: RateLimit
|
||||
|
||||
/**
|
||||
* The identifier used to uniquely track this rate limit.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Interval in seconds over which the rate limit is enforced.
|
||||
*/
|
||||
interval: number
|
||||
|
||||
/**
|
||||
* Maximum number of requests that can be made per interval.
|
||||
*/
|
||||
limit: number
|
||||
|
||||
/**
|
||||
* The cost of the request.
|
||||
*
|
||||
|
@ -48,21 +42,17 @@ export async function enforceRateLimit({
|
|||
*/
|
||||
cost?: number
|
||||
|
||||
/**
|
||||
* Whether to enforce the rate limit synchronously or asynchronously.
|
||||
*
|
||||
* @default true (asynchronous)
|
||||
*/
|
||||
async?: boolean
|
||||
|
||||
env: RawEnv
|
||||
cache?: RateLimitCache
|
||||
waitUntil: WaitUntil
|
||||
}): Promise<RateLimitResult> {
|
||||
}): Promise<RateLimitResult | undefined> {
|
||||
if (rateLimit.enabled === false) {
|
||||
return
|
||||
}
|
||||
|
||||
assert(id, 400, 'Unauthenticated requests must have a valid IP address')
|
||||
|
||||
const async = false
|
||||
|
||||
const { interval, limit, mode } = rateLimit
|
||||
const intervalMs = interval * 1000
|
||||
const now = Date.now()
|
||||
|
||||
|
@ -103,7 +93,10 @@ export async function enforceRateLimit({
|
|||
|
||||
const updatedRateLimitStateP = durableRateLimiter.update({ cost, intervalMs })
|
||||
|
||||
if (async) {
|
||||
if (mode === 'strict') {
|
||||
const updatedRateLimitState = await updatedRateLimitStateP
|
||||
updateCache(updatedRateLimitState)
|
||||
} else {
|
||||
waitUntil(
|
||||
updatedRateLimitStateP
|
||||
.then((updatedRateLimitState: RateLimitState) => {
|
||||
|
@ -119,25 +112,23 @@ export async function enforceRateLimit({
|
|||
|
||||
rateLimitState.current += cost
|
||||
updateCache(rateLimitState)
|
||||
} else {
|
||||
const updatedRateLimitState = await updatedRateLimitStateP
|
||||
updateCache(updatedRateLimitState)
|
||||
}
|
||||
|
||||
// console.log('rateLimit', {
|
||||
// id,
|
||||
// initial: initialRateLimitState,
|
||||
// current: rateLimitState,
|
||||
// mode,
|
||||
// cost
|
||||
// })
|
||||
|
||||
return {
|
||||
id,
|
||||
passed: rateLimitState.current <= limit,
|
||||
current: rateLimitState.current,
|
||||
limit,
|
||||
current: rateLimitState.current,
|
||||
remaining: Math.max(0, limit - rateLimitState.current),
|
||||
resetTimeMs: rateLimitState.resetTimeMs,
|
||||
intervalMs,
|
||||
remaining: Math.max(0, limit - rateLimitState.current)
|
||||
intervalMs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,20 +164,18 @@ export async function resolveOriginToolCall({
|
|||
}
|
||||
}
|
||||
|
||||
if (rateLimit && rateLimit.enabled !== false) {
|
||||
if (rateLimit) {
|
||||
// TODO: Consider decrementing rate limit if the response is cached or
|
||||
// errors? this doesn't seem too important, so will leave as-is for now.
|
||||
rateLimitResult = await enforceRateLimit({
|
||||
rateLimit,
|
||||
id: consumer?.id ?? ip ?? sessionId,
|
||||
interval: rateLimit.interval,
|
||||
limit: rateLimit.limit,
|
||||
async: rateLimit.async,
|
||||
cost: numRequestsCost,
|
||||
env,
|
||||
waitUntil
|
||||
})
|
||||
|
||||
if (!rateLimitResult.passed) {
|
||||
if (rateLimitResult && !rateLimitResult.passed) {
|
||||
throw new RateLimitError({ rateLimitResult })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ export function createAgenticMcpMetadata(
|
|||
existingMetadata?: Record<string, any>
|
||||
): Record<string, any> {
|
||||
const rawAgenticMcpMetadata = pruneEmpty({
|
||||
status: 200,
|
||||
...existingMetadata?.agentic,
|
||||
...metadata,
|
||||
headers: {
|
||||
|
|
|
@ -63,6 +63,14 @@ export default defineConfig({
|
|||
mode: 'strict'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'custom_rate_limit_approximate_tool',
|
||||
rateLimit: {
|
||||
interval: '30s',
|
||||
limit: 2,
|
||||
mode: 'approximate'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'disabled_rate_limit_tool',
|
||||
rateLimit: { enabled: false }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -487,10 +487,10 @@ export interface components {
|
|||
/** @description Maximum number of operations per interval (unitless). */
|
||||
limit: number;
|
||||
/**
|
||||
* @description Whether to enforce the rate limit synchronously (strict but slower) or asynchronously (approximate and faster, the default).
|
||||
* @default true
|
||||
* @description How to enforce the rate limit: "strict" (more precise but slower) or "approximate" (the default; faster and asynchronous but less precise).
|
||||
* @default approximate
|
||||
*/
|
||||
async: boolean;
|
||||
mode: "strict" | "approximate";
|
||||
/** @default true */
|
||||
enabled: boolean;
|
||||
};
|
||||
|
@ -685,10 +685,10 @@ export interface components {
|
|||
* }
|
||||
* ],
|
||||
* "rateLimit": {
|
||||
* "enabled": true,
|
||||
* "interval": 60,
|
||||
* "limit": 1000,
|
||||
* "async": true,
|
||||
* "enabled": true
|
||||
* "mode": "approximate"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
|
@ -1668,10 +1668,10 @@ export interface operations {
|
|||
* }
|
||||
* ],
|
||||
* "rateLimit": {
|
||||
* "enabled": true,
|
||||
* "interval": 60,
|
||||
* "limit": 1000,
|
||||
* "async": true,
|
||||
* "enabled": true
|
||||
* "mode": "approximate"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
|
|
|
@ -12,6 +12,7 @@ export const rateLimitModeSchema = z.union([
|
|||
z.literal('strict'),
|
||||
z.literal('approximate')
|
||||
])
|
||||
export type RateLimitMode = z.infer<typeof rateLimitModeSchema>
|
||||
|
||||
/**
|
||||
* Rate limit config for metered LineItems.
|
||||
|
|
101
pnpm-lock.yaml
101
pnpm-lock.yaml
|
@ -417,6 +417,9 @@ importers:
|
|||
p-map:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.3
|
||||
p-times:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
devDependencies:
|
||||
'@agentic/platform':
|
||||
specifier: workspace:*
|
||||
|
@ -462,7 +465,7 @@ importers:
|
|||
version: link:../../packages/validators
|
||||
'@hono/zod-validator':
|
||||
specifier: 'catalog:'
|
||||
version: 0.7.0(hono@4.7.11)(zod@3.25.51)
|
||||
version: 0.7.0(hono@4.7.11)(zod@3.25.62)
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.12.1
|
||||
|
@ -880,10 +883,6 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.20.2
|
||||
|
||||
'@babel/code-frame@7.26.2':
|
||||
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -896,10 +895,6 @@ packages:
|
|||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9':
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1':
|
||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -2783,6 +2778,10 @@ packages:
|
|||
peerDependencies:
|
||||
react: '*'
|
||||
|
||||
aggregate-error@4.0.1:
|
||||
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ai@4.3.16:
|
||||
resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -3029,6 +3028,10 @@ packages:
|
|||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
clean-stack@4.2.0:
|
||||
resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -3486,6 +3489,10 @@ packages:
|
|||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
escape-string-regexp@5.0.0:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
eslint-config-prettier@10.1.5:
|
||||
resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==}
|
||||
hasBin: true
|
||||
|
@ -4637,6 +4644,10 @@ packages:
|
|||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-map@5.5.0:
|
||||
resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
p-map@6.0.0:
|
||||
resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==}
|
||||
engines: {node: '>=16'}
|
||||
|
@ -4645,6 +4656,10 @@ packages:
|
|||
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
p-times@4.0.0:
|
||||
resolution: {integrity: sha512-KqBkcxIZ2EZHHkt8lbNORmRofFLrLlHQh0ObFO3moOlrdN/v+sbJ2ssZspP5GN7E7zwvcqiqV0xnJydkQuoNyw==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
|
@ -5846,33 +5861,33 @@ snapshots:
|
|||
hono: 4.7.11
|
||||
jose: 5.9.6
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.8(zod@3.25.51)':
|
||||
'@ai-sdk/provider-utils@2.2.8(zod@3.25.62)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
nanoid: 3.3.11
|
||||
secure-json-parse: 2.7.0
|
||||
zod: 3.25.51
|
||||
zod: 3.25.62
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.51)':
|
||||
'@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.62)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.51)
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.62)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.62)
|
||||
react: 19.1.0
|
||||
swr: 2.3.3(react@19.1.0)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 3.25.51
|
||||
zod: 3.25.62
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.11(zod@3.25.51)':
|
||||
'@ai-sdk/ui-utils@1.2.11(zod@3.25.62)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
|
||||
zod: 3.25.51
|
||||
zod-to-json-schema: 3.24.5(zod@3.25.51)
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.62)
|
||||
zod: 3.25.62
|
||||
zod-to-json-schema: 3.24.5(zod@3.25.62)
|
||||
|
||||
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
|
||||
dependencies:
|
||||
|
@ -5886,12 +5901,6 @@ snapshots:
|
|||
openapi3-ts: 4.4.0
|
||||
zod: 3.25.62
|
||||
|
||||
'@babel/code-frame@7.26.2':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
@ -5908,8 +5917,6 @@ snapshots:
|
|||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1': {}
|
||||
|
||||
'@babel/parser@7.27.5':
|
||||
|
@ -6276,11 +6283,6 @@ snapshots:
|
|||
hono: 4.7.11
|
||||
zod: 3.25.62
|
||||
|
||||
'@hono/zod-validator@0.7.0(hono@4.7.11)(zod@3.25.51)':
|
||||
dependencies:
|
||||
hono: 4.7.11
|
||||
zod: 3.25.51
|
||||
|
||||
'@hono/zod-validator@0.7.0(hono@4.7.11)(zod@3.25.62)':
|
||||
dependencies:
|
||||
hono: 4.7.11
|
||||
|
@ -7604,26 +7606,31 @@ snapshots:
|
|||
agents@0.0.95(@cloudflare/workers-types@4.20250610.0)(react@19.1.0):
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk': 1.12.1
|
||||
ai: 4.3.16(react@19.1.0)(zod@3.25.51)
|
||||
ai: 4.3.16(react@19.1.0)(zod@3.25.62)
|
||||
cron-schedule: 5.0.4
|
||||
nanoid: 5.1.5
|
||||
partyserver: 0.0.71(@cloudflare/workers-types@4.20250610.0)
|
||||
partysocket: 1.1.4
|
||||
react: 19.1.0
|
||||
zod: 3.25.51
|
||||
zod: 3.25.62
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
- supports-color
|
||||
|
||||
ai@4.3.16(react@19.1.0)(zod@3.25.51):
|
||||
aggregate-error@4.0.1:
|
||||
dependencies:
|
||||
clean-stack: 4.2.0
|
||||
indent-string: 5.0.0
|
||||
|
||||
ai@4.3.16(react@19.1.0)(zod@3.25.62):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
|
||||
'@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.51)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.51)
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.62)
|
||||
'@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.62)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.62)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
zod: 3.25.51
|
||||
zod: 3.25.62
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
|
||||
|
@ -7882,6 +7889,10 @@ snapshots:
|
|||
dependencies:
|
||||
escape-string-regexp: 1.0.5
|
||||
|
||||
clean-stack@4.2.0:
|
||||
dependencies:
|
||||
escape-string-regexp: 5.0.0
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
|
@ -8336,6 +8347,8 @@ snapshots:
|
|||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-config-prettier@10.1.5(eslint@9.28.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
eslint: 9.28.0(jiti@2.4.2)
|
||||
|
@ -9645,10 +9658,18 @@ snapshots:
|
|||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
p-map@5.5.0:
|
||||
dependencies:
|
||||
aggregate-error: 4.0.1
|
||||
|
||||
p-map@6.0.0: {}
|
||||
|
||||
p-map@7.0.3: {}
|
||||
|
||||
p-times@4.0.0:
|
||||
dependencies:
|
||||
p-map: 5.5.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
|
@ -9657,7 +9678,7 @@ snapshots:
|
|||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
'@babel/code-frame': 7.27.1
|
||||
index-to-position: 1.1.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@
|
|||
- how to handle binary bodies and responses?
|
||||
- improve logger vs console for non-hono path and util methods
|
||||
- default rate limits?
|
||||
- **test rate limiting**
|
||||
- **test usage tracking and reporting**
|
||||
- disallow `mcp` as a tool name or figure out another workaround
|
||||
- disallow `mcp` as a tool name or figure out a different workaround
|
||||
- **Public MCP server interface**
|
||||
- how does oauth work with this flow?
|
||||
- pass requestId to DurableMcpServer somehow on a per-request basis
|
||||
|
|
Ładowanie…
Reference in New Issue