mppx 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +155 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/discovery/Discovery.d.ts +146 -0
  6. package/dist/discovery/Discovery.d.ts.map +1 -0
  7. package/dist/discovery/Discovery.js +60 -0
  8. package/dist/discovery/Discovery.js.map +1 -0
  9. package/dist/discovery/OpenApi.d.ts +61 -0
  10. package/dist/discovery/OpenApi.d.ts.map +1 -0
  11. package/dist/discovery/OpenApi.js +139 -0
  12. package/dist/discovery/OpenApi.js.map +1 -0
  13. package/dist/discovery/Validate.d.ts +10 -0
  14. package/dist/discovery/Validate.d.ts.map +1 -0
  15. package/dist/discovery/Validate.js +63 -0
  16. package/dist/discovery/Validate.js.map +1 -0
  17. package/dist/discovery/index.d.ts +4 -0
  18. package/dist/discovery/index.d.ts.map +1 -0
  19. package/dist/discovery/index.js +4 -0
  20. package/dist/discovery/index.js.map +1 -0
  21. package/dist/middlewares/elysia.d.ts +52 -1
  22. package/dist/middlewares/elysia.d.ts.map +1 -1
  23. package/dist/middlewares/elysia.js +17 -0
  24. package/dist/middlewares/elysia.js.map +1 -1
  25. package/dist/middlewares/express.d.ts +13 -1
  26. package/dist/middlewares/express.d.ts.map +1 -1
  27. package/dist/middlewares/express.js +18 -0
  28. package/dist/middlewares/express.js.map +1 -1
  29. package/dist/middlewares/hono.d.ts +19 -1
  30. package/dist/middlewares/hono.d.ts.map +1 -1
  31. package/dist/middlewares/hono.js +51 -0
  32. package/dist/middlewares/hono.js.map +1 -1
  33. package/dist/middlewares/internal/mppx.d.ts +4 -2
  34. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  35. package/dist/middlewares/internal/mppx.js +10 -3
  36. package/dist/middlewares/internal/mppx.js.map +1 -1
  37. package/dist/middlewares/nextjs.d.ts +11 -0
  38. package/dist/middlewares/nextjs.d.ts.map +1 -1
  39. package/dist/middlewares/nextjs.js +15 -0
  40. package/dist/middlewares/nextjs.js.map +1 -1
  41. package/dist/proxy/Proxy.d.ts +6 -0
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +56 -80
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts +16 -23
  46. package/dist/proxy/Service.d.ts.map +1 -1
  47. package/dist/proxy/Service.js +19 -83
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/proxy/internal/Route.js +1 -1
  50. package/dist/proxy/internal/Route.js.map +1 -1
  51. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  52. package/dist/proxy/services/anthropic.js +5 -0
  53. package/dist/proxy/services/anthropic.js.map +1 -1
  54. package/dist/proxy/services/openai.d.ts.map +1 -1
  55. package/dist/proxy/services/openai.js +6 -3
  56. package/dist/proxy/services/openai.js.map +1 -1
  57. package/dist/proxy/services/stripe.d.ts.map +1 -1
  58. package/dist/proxy/services/stripe.js +6 -3
  59. package/dist/proxy/services/stripe.js.map +1 -1
  60. package/dist/tempo/server/Session.d.ts.map +1 -1
  61. package/dist/tempo/server/Session.js +18 -5
  62. package/dist/tempo/server/Session.js.map +1 -1
  63. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  64. package/dist/tempo/server/internal/transport.js +8 -0
  65. package/dist/tempo/server/internal/transport.js.map +1 -1
  66. package/dist/tempo/session/Chain.js +1 -1
  67. package/dist/tempo/session/Chain.js.map +1 -1
  68. package/package.json +6 -1
  69. package/src/BodyDigest.test.ts +1 -1
  70. package/src/Challenge.fuzz.test.ts +121 -0
  71. package/src/Challenge.test-d.ts +1 -1
  72. package/src/Challenge.test.ts +1 -1
  73. package/src/Credential.fuzz.test.ts +62 -0
  74. package/src/Credential.test.ts +1 -1
  75. package/src/Errors.test.ts +1 -1
  76. package/src/Expires.test.ts +1 -1
  77. package/src/Method.test.ts +1 -1
  78. package/src/PaymentRequest.test.ts +1 -1
  79. package/src/Receipt.test.ts +1 -1
  80. package/src/Store.test-d.ts +1 -1
  81. package/src/Store.test.ts +1 -1
  82. package/src/cli/cli.test.ts +212 -1
  83. package/src/cli/cli.ts +162 -0
  84. package/src/client/Mppx.test-d.ts +1 -1
  85. package/src/client/Mppx.test.ts +1 -1
  86. package/src/client/Transport.test.ts +1 -1
  87. package/src/client/internal/Fetch.browser.test.ts +1 -1
  88. package/src/client/internal/Fetch.test-d.ts +1 -1
  89. package/src/client/internal/Fetch.test.ts +2 -1
  90. package/src/discovery/Discovery.test.ts +152 -0
  91. package/src/discovery/Discovery.ts +72 -0
  92. package/src/discovery/OpenApi.test.ts +425 -0
  93. package/src/discovery/OpenApi.ts +224 -0
  94. package/src/discovery/Validate.test.ts +188 -0
  95. package/src/discovery/Validate.ts +76 -0
  96. package/src/discovery/index.ts +3 -0
  97. package/src/internal/constantTimeEqual.test.ts +1 -1
  98. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
  99. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  100. package/src/mcp-sdk/server/Transport.test.ts +1 -1
  101. package/src/middlewares/elysia.test.ts +27 -2
  102. package/src/middlewares/elysia.ts +35 -1
  103. package/src/middlewares/express.test.ts +35 -7
  104. package/src/middlewares/express.ts +34 -0
  105. package/src/middlewares/hono.test.ts +28 -6
  106. package/src/middlewares/hono.ts +73 -1
  107. package/src/middlewares/internal/mppx.test.ts +1 -1
  108. package/src/middlewares/internal/mppx.ts +14 -6
  109. package/src/middlewares/nextjs.test.ts +31 -6
  110. package/src/middlewares/nextjs.ts +28 -0
  111. package/src/proxy/Proxy.test.ts +54 -270
  112. package/src/proxy/Proxy.ts +71 -93
  113. package/src/proxy/Service.test.ts +23 -1
  114. package/src/proxy/Service.ts +40 -86
  115. package/src/proxy/internal/Headers.test.ts +1 -1
  116. package/src/proxy/internal/Route.test.ts +9 -1
  117. package/src/proxy/internal/Route.ts +1 -1
  118. package/src/proxy/services/anthropic.test.ts +132 -0
  119. package/src/proxy/services/anthropic.ts +5 -0
  120. package/src/proxy/services/openai.test.ts +1 -1
  121. package/src/proxy/services/openai.ts +6 -4
  122. package/src/proxy/services/stripe.test.ts +132 -0
  123. package/src/proxy/services/stripe.ts +6 -4
  124. package/src/server/Mppx.test-d.ts +1 -1
  125. package/src/server/Mppx.test.ts +2 -1
  126. package/src/server/NodeListener.test.ts +1 -1
  127. package/src/server/Request.test.ts +1 -1
  128. package/src/server/Response.test.ts +1 -1
  129. package/src/server/Transport.test.ts +1 -1
  130. package/src/stripe/Charge.integration.test.ts +1 -1
  131. package/src/stripe/Methods.test.ts +1 -1
  132. package/src/stripe/client/Charge.test.ts +1 -1
  133. package/src/stripe/server/Charge.test.ts +1 -1
  134. package/src/tempo/Attribution.test.ts +1 -1
  135. package/src/tempo/Methods.test.ts +1 -1
  136. package/src/tempo/client/ChannelOps.test.ts +6 -3
  137. package/src/tempo/client/Session.test.ts +5 -2
  138. package/src/tempo/client/SessionManager.test.ts +1 -1
  139. package/src/tempo/internal/auto-swap.test.ts +1 -1
  140. package/src/tempo/internal/defaults.test.ts +1 -1
  141. package/src/tempo/internal/fee-payer.test.ts +1 -1
  142. package/src/tempo/server/Charge.test.ts +1 -1
  143. package/src/tempo/server/Session.test.ts +87 -37
  144. package/src/tempo/server/Session.ts +25 -8
  145. package/src/tempo/server/Sse.test.ts +1 -1
  146. package/src/tempo/server/internal/transport.test.ts +24 -1
  147. package/src/tempo/server/internal/transport.ts +11 -0
  148. package/src/tempo/session/Chain.test.ts +5 -2
  149. package/src/tempo/session/Chain.ts +1 -1
  150. package/src/tempo/session/Channel.test.ts +1 -1
  151. package/src/tempo/session/ChannelStore.test.ts +1 -1
  152. package/src/tempo/session/Receipt.test.ts +1 -1
  153. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  154. package/src/tempo/session/Sse.test.ts +1 -1
  155. package/src/tempo/session/Voucher.test.ts +1 -1
  156. package/src/viem/Account.test.ts +1 -1
  157. package/src/viem/Client.test.ts +1 -1
  158. package/src/zod.test.ts +147 -0
@@ -1,13 +1,16 @@
1
+ import type { DiscoveryHandler } from '../../discovery/OpenApi.js'
1
2
  import type * as Method from '../../Method.js'
2
3
  import type * as Mppx from '../../server/Mppx.js'
3
4
 
4
5
  export type AnyMethodFn = Mppx.AnyMethodFn
5
6
  export type AnyServer = Method.AnyServer
6
7
 
8
+ type DiscoveryMeta = Pick<DiscoveryHandler, '_internal'>
9
+
7
10
  /** Recursively wraps nested handler objects one level deep. */
8
11
  type WrapNested<obj, handler> = {
9
12
  [key in keyof obj]: obj[key] extends (options: infer options) => any
10
- ? (o: options) => handler
13
+ ? (o: options) => handler & DiscoveryMeta
11
14
  : obj[key]
12
15
  }
13
16
 
@@ -21,7 +24,7 @@ export type Wrap<mppx, handler> = {
21
24
  | 'transport'
22
25
  ? mppx[key]
23
26
  : mppx[key] extends (options: infer options) => any
24
- ? (o: options) => handler
27
+ ? (o: options) => handler & DiscoveryMeta
25
28
  : mppx[key] extends Record<string, (options: any) => any>
26
29
  ? WrapNested<mppx[key], handler>
27
30
  : mppx[key]
@@ -43,14 +46,19 @@ export function wrap<mppx extends Mppx.Mppx<any, any>, handler>(
43
46
  for (const mi of mppx.methods as readonly Method.AnyServer[]) {
44
47
  const key = `${mi.name}/${mi.intent}`
45
48
  const methodFn = (mppx as any)[key]
46
- result[key] = (options: any) => wrapper(methodFn, options)
49
+ const wrapWithMeta = (options: any) => {
50
+ const configured = methodFn(options)
51
+ const handler = wrapper(methodFn, options) as any
52
+ if (configured._internal) handler._internal = configured._internal
53
+ return handler
54
+ }
55
+ result[key] = wrapWithMeta
47
56
  // Also set shorthand intent key if Mppx registered it (no collision)
48
- if ((mppx as any)[mi.intent]) result[mi.intent] = (options: any) => wrapper(methodFn, options)
57
+ if ((mppx as any)[mi.intent]) result[mi.intent] = wrapWithMeta
49
58
  // Build nested handlers: wrapped.tempo.charge(...)
50
59
  if (!result[mi.name] || typeof result[mi.name] !== 'object')
51
60
  result[mi.name] = {} as Record<string, unknown>
52
- ;(result[mi.name] as Record<string, unknown>)[mi.intent] = (options: any) =>
53
- wrapper(methodFn, options)
61
+ ;(result[mi.name] as Record<string, unknown>)[mi.intent] = wrapWithMeta
54
62
  }
55
63
  return result as never
56
64
  }
@@ -2,11 +2,11 @@ import * as http from 'node:http'
2
2
 
3
3
  import { Receipt } from 'mppx'
4
4
  import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
5
- import { Mppx } from 'mppx/nextjs'
5
+ import { Mppx, discovery } from 'mppx/nextjs'
6
6
  import { tempo as tempo_server } from 'mppx/server'
7
7
  import type { Address } from 'viem'
8
8
  import { Addresses } from 'viem/tempo'
9
- import { beforeAll, describe, expect, test } from 'vitest'
9
+ import { beforeAll, describe, expect, test } from 'vp/test'
10
10
  import { deployEscrow } from '~test/tempo/session.js'
11
11
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
12
12
 
@@ -42,7 +42,7 @@ describe('charge', () => {
42
42
  tempo_server.charge({
43
43
  getClient: () => client,
44
44
  currency: asset,
45
- recipient: accounts[0].address,
45
+ account: accounts[0],
46
46
  }),
47
47
  ],
48
48
  secretKey,
@@ -89,6 +89,31 @@ describe('charge', () => {
89
89
 
90
90
  server.close()
91
91
  })
92
+
93
+ test('serves /openapi.json from a handler-derived route config', async () => {
94
+ const pay = mppx.charge({ amount: '1' })
95
+ const server = await createServer(
96
+ discovery(mppx, {
97
+ info: { title: 'Next API', version: '3.0.0' },
98
+ routes: [{ handler: pay, method: 'get', path: '/' }],
99
+ }),
100
+ )
101
+
102
+ const response = await globalThis.fetch(server.url)
103
+ expect(response.status).toBe(200)
104
+ expect(response.headers.get('cache-control')).toBe('public, max-age=300')
105
+
106
+ const body = (await response.json()) as Record<string, any>
107
+ expect(body.info).toEqual({ title: 'Next API', version: '3.0.0' })
108
+ expect(body.paths['/'].get['x-payment-info']).toMatchObject({
109
+ amount: '1000000',
110
+ currency: asset,
111
+ intent: 'charge',
112
+ method: 'tempo',
113
+ })
114
+
115
+ server.close()
116
+ })
92
117
  })
93
118
 
94
119
  describe('session', () => {
@@ -105,7 +130,7 @@ describe('session', () => {
105
130
  methods: [
106
131
  tempo_server.session({
107
132
  getClient: () => client,
108
- recipient: accounts[0].address,
133
+ account: accounts[0],
109
134
  currency: asset,
110
135
  escrowContract,
111
136
  }),
@@ -130,10 +155,10 @@ describe('session', () => {
130
155
  methods: [
131
156
  tempo_server.session({
132
157
  getClient: () => client,
133
- recipient: accounts[0].address,
158
+ account: accounts[0],
134
159
  currency: asset,
135
160
  escrowContract,
136
- feePayer: accounts[0],
161
+ feePayer: true,
137
162
  }),
138
163
  ],
139
164
  secretKey,
@@ -1,3 +1,4 @@
1
+ import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
1
2
  import * as Mppx_core from '../server/Mppx.js'
2
3
  import * as Mppx_internal from './internal/mppx.js'
3
4
 
@@ -64,3 +65,30 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
64
65
  return result.withReceipt(response)
65
66
  }
66
67
  }
68
+
69
+ export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
70
+ routes?: RouteConfig[]
71
+ }
72
+
73
+ const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
74
+
75
+ /**
76
+ * Creates a route handler that serves an OpenAPI discovery document.
77
+ */
78
+ export function discovery(
79
+ mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
80
+ config: DiscoveryConfig = {},
81
+ ): RouteHandler {
82
+ const cached = JSON.stringify(
83
+ generate(mppx, {
84
+ ...(config.info ? { info: config.info } : {}),
85
+ routes: config.routes ?? [],
86
+ ...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
87
+ }),
88
+ )
89
+
90
+ return () =>
91
+ new Response(cached, {
92
+ headers: { ...discoveryHeaders, 'Content-Type': 'application/json' },
93
+ })
94
+ }
@@ -1,7 +1,7 @@
1
1
  import { Receipt } from 'mppx'
2
2
  import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
- import { afterEach, describe, expect, test } from 'vitest'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
5
  import * as Http from '~test/Http.js'
6
6
  import { accounts, asset, client } from '~test/tempo/viem.js'
7
7
 
@@ -65,11 +65,19 @@ function createUpstream(handler: (req: Request) => Response | Promise<Response>)
65
65
  }
66
66
 
67
67
  describe('create', () => {
68
- test('behavior: GET /discover/all returns service discovery JSON', async () => {
68
+ test('behavior: GET /openapi.json returns discovery JSON', async () => {
69
69
  const proxy = ApiProxy.create({
70
+ categories: ['gateway'],
71
+ docs: {
72
+ apiReference: 'https://gateway.example.com/reference',
73
+ homepage: 'https://gateway.example.com',
74
+ },
75
+ title: 'My AI Gateway',
76
+ version: '2.0.0',
70
77
  services: [
71
78
  Service.from('api', {
72
79
  baseUrl: 'https://api.example.com',
80
+ categories: ['compute'],
73
81
  routes: {
74
82
  'GET /v1/models': true,
75
83
  'POST /v1/generate': mppx_server.charge({ amount: '1', description: 'Generate text' }),
@@ -84,73 +92,40 @@ describe('create', () => {
84
92
  })
85
93
  proxyServer = await Http.createServer(proxy.listener)
86
94
 
87
- const res = await fetch(`${proxyServer.url}/discover/all`)
95
+ const res = await fetch(`${proxyServer.url}/openapi.json`)
88
96
  expect(res.status).toBe(200)
89
- expect(await res.json()).toMatchInlineSnapshot(`
90
- [
91
- {
92
- "id": "api",
93
- "routes": [
94
- {
95
- "method": "GET",
96
- "path": "/api/v1/models",
97
- "pattern": "GET /api/v1/models",
98
- "payment": null,
99
- },
100
- {
101
- "method": "POST",
102
- "path": "/api/v1/generate",
103
- "pattern": "POST /api/v1/generate",
104
- "payment": {
105
- "amount": "1000000",
106
- "currency": "0x20c0000000000000000000000000000000000001",
107
- "decimals": 6,
108
- "description": "Generate text",
109
- "intent": "charge",
110
- "method": "tempo",
111
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
112
- },
113
- },
114
- {
115
- "method": "POST",
116
- "path": "/api/v1/stream",
117
- "pattern": "POST /api/v1/stream",
118
- "payment": {
119
- "amount": "1000000",
120
- "currency": "0x20c0000000000000000000000000000000000001",
121
- "decimals": 6,
122
- "description": "Stream text",
123
- "intent": "session",
124
- "method": "tempo",
125
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
126
- "unitType": "token",
127
- },
128
- },
129
- ],
130
- },
131
- ]
132
- `)
133
- })
134
-
135
- test('behavior: GET /discover returns JSON by default', async () => {
136
- const proxy = ApiProxy.create({
137
- services: [
138
- Service.from('api', {
139
- baseUrl: 'https://api.example.com',
140
- routes: {
141
- 'GET /v1/models': true,
142
- },
143
- }),
144
- ],
97
+ expect(res.headers.get('cache-control')).toBe('public, max-age=300')
98
+ const body = (await res.json()) as Record<string, any>
99
+ expect(body.openapi).toBe('3.1.0')
100
+ expect(body.info).toEqual({ title: 'My AI Gateway', version: '2.0.0' })
101
+ expect(body['x-service-info']).toEqual({
102
+ categories: ['gateway'],
103
+ docs: {
104
+ apiReference: 'https://gateway.example.com/reference',
105
+ homepage: 'https://gateway.example.com',
106
+ llms: '/llms.txt',
107
+ },
108
+ })
109
+ expect(body.paths['/api/v1/models'].get.responses['200']).toEqual({
110
+ description: 'Successful response',
111
+ })
112
+ expect(body.paths['/api/v1/generate'].post['x-payment-info']).toMatchObject({
113
+ amount: '1000000',
114
+ currency: asset,
115
+ description: 'Generate text',
116
+ intent: 'charge',
117
+ method: 'tempo',
118
+ })
119
+ expect(body.paths['/api/v1/stream'].post['x-payment-info']).toMatchObject({
120
+ amount: '1000000',
121
+ currency: asset,
122
+ description: 'Stream text',
123
+ intent: 'session',
124
+ method: 'tempo',
145
125
  })
146
- proxyServer = await Http.createServer(proxy.listener)
147
-
148
- const res = await fetch(`${proxyServer.url}/discover`)
149
- expect(res.status).toBe(200)
150
- expect(res.headers.get('content-type')).toMatchInlineSnapshot(`"application/json"`)
151
126
  })
152
127
 
153
- test('behavior: GET /discover returns llms.txt for markdown clients', async () => {
128
+ test('behavior: GET /llms.txt returns text docs linked to OpenAPI discovery', async () => {
154
129
  const proxy = ApiProxy.create({
155
130
  title: 'My AI Gateway',
156
131
  description: 'A paid proxy for LLM and AI services.',
@@ -186,9 +161,7 @@ describe('create', () => {
186
161
  })
187
162
  proxyServer = await Http.createServer(proxy.listener)
188
163
 
189
- const res = await fetch(`${proxyServer.url}/discover`, {
190
- headers: { Accept: 'text/plain' },
191
- })
164
+ const res = await fetch(`${proxyServer.url}/llms.txt`)
192
165
  expect(res.status).toBe(200)
193
166
  expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8')
194
167
  expect(await res.text()).toMatchInlineSnapshot(`
@@ -198,20 +171,20 @@ describe('create', () => {
198
171
 
199
172
  ## Services
200
173
 
201
- - [OpenAI](/discover/openai.md): Chat completions, embeddings, image generation, and audio transcription.
202
- - [Anthropic](/discover/anthropic.md): Claude language models for messages and completions.
174
+ - OpenAI: Chat completions, embeddings, image generation, and audio transcription.
175
+ - Anthropic: Claude language models for messages and completions.
203
176
 
204
- [See all service definitions](/discover/all.md)"
177
+ [OpenAPI discovery](/openapi.json)"
205
178
  `)
206
179
  })
207
180
 
208
- test('behavior: GET /discover/:id returns single service', async () => {
181
+ test('behavior: GET /openapi.json respects basePath', async () => {
209
182
  const proxy = ApiProxy.create({
183
+ basePath: '/proxy',
210
184
  services: [
211
185
  Service.from('api', {
212
186
  baseUrl: 'https://api.example.com',
213
187
  routes: {
214
- 'GET /v1/models': true,
215
188
  'POST /v1/generate': mppx_server.charge({ amount: '1', description: 'Generate text' }),
216
189
  },
217
190
  }),
@@ -219,205 +192,16 @@ describe('create', () => {
219
192
  })
220
193
  proxyServer = await Http.createServer(proxy.listener)
221
194
 
222
- const res = await fetch(`${proxyServer.url}/discover/api`)
223
- expect(res.status).toBe(200)
224
- expect(await res.json()).toMatchInlineSnapshot(`
225
- {
226
- "id": "api",
227
- "routes": [
228
- {
229
- "method": "GET",
230
- "path": "/api/v1/models",
231
- "pattern": "GET /api/v1/models",
232
- "payment": null,
233
- },
234
- {
235
- "method": "POST",
236
- "path": "/api/v1/generate",
237
- "pattern": "POST /api/v1/generate",
238
- "payment": {
239
- "amount": "1000000",
240
- "currency": "0x20c0000000000000000000000000000000000001",
241
- "decimals": 6,
242
- "description": "Generate text",
243
- "intent": "charge",
244
- "method": "tempo",
245
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
246
- },
247
- },
248
- ],
249
- }
250
- `)
251
- })
252
-
253
- test('behavior: GET /discover/all.md returns full markdown with routes', async () => {
254
- const proxy = ApiProxy.create({
255
- services: [
256
- openai({
257
- apiKey: 'sk-test',
258
- routes: {
259
- 'POST /v1/chat/completions': mppx_server.charge({
260
- amount: '0.05',
261
- description: 'Chat completion',
262
- }),
263
- 'GET /v1/models': true,
264
- },
265
- }),
266
- anthropic({
267
- apiKey: 'sk-ant-test',
268
- routes: {
269
- 'POST /v1/messages': mppx_server.charge({
270
- amount: '0.03',
271
- description: 'Send message',
272
- }),
273
- },
274
- }),
275
- ],
276
- })
277
- proxyServer = await Http.createServer(proxy.listener)
278
-
279
- const res = await fetch(`${proxyServer.url}/discover/all.md`)
195
+ const res = await fetch(`${proxyServer.url}/proxy/openapi.json`)
280
196
  expect(res.status).toBe(200)
281
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
282
- expect(await res.text()).toMatchInlineSnapshot(`
283
- "# Services
284
-
285
- ## [OpenAI](/discover/openai.md)
286
-
287
- Chat completions, embeddings, image generation, and audio transcription.
288
-
289
- ### Routes
290
-
291
- - \`POST /openai/v1/chat/completions\`: Chat completion
292
- - Type: charge
293
- - Price: 0.05 (50000 units, 6 decimals)
294
- - Currency: 0x20c0000000000000000000000000000000000001
295
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=POST%20%2Fv1%2Fchat%2Fcompletions
296
-
297
- - \`GET /openai/v1/models\`
298
- - Type: free
299
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=GET%20%2Fv1%2Fmodels
300
-
301
- ## [Anthropic](/discover/anthropic.md)
302
-
303
- Claude language models for messages and completions.
304
-
305
- ### Routes
306
-
307
- - \`POST /anthropic/v1/messages\`: Send message
308
- - Type: charge
309
- - Price: 0.03 (30000 units, 6 decimals)
310
- - Currency: 0x20c0000000000000000000000000000000000001
311
- "
312
- `)
313
- })
314
-
315
- test('behavior: GET /discover/:id.md returns markdown', async () => {
316
- const proxy = ApiProxy.create({
317
- services: [
318
- openai({
319
- apiKey: 'sk-test',
320
- routes: {
321
- 'POST /v1/chat/completions': mppx_server.charge({
322
- amount: '0.05',
323
- description: 'Chat completion',
324
- }),
325
- 'GET /v1/models': true,
326
- },
327
- }),
328
- anthropic({
329
- apiKey: 'sk-ant-test',
330
- routes: {
331
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
332
- },
333
- }),
334
- ],
335
- })
336
- proxyServer = await Http.createServer(proxy.listener)
337
-
338
- const res = await fetch(`${proxyServer.url}/discover/openai.md`)
339
- expect(res.status).toBe(200)
340
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
341
- expect(await res.text()).toMatchInlineSnapshot(`
342
- "# OpenAI
343
-
344
- > Documentation: https://context7.com/websites/platform_openai/llms.txt
345
-
346
- Chat completions, embeddings, image generation, and audio transcription.
347
-
348
- ## Routes
349
-
350
- - \`POST /openai/v1/chat/completions\`: Chat completion
351
- - Type: charge
352
- - Price: 0.05 (50000 units, 6 decimals)
353
- - Currency: 0x20c0000000000000000000000000000000000001
354
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=POST%20%2Fv1%2Fchat%2Fcompletions
355
-
356
- - \`GET /openai/v1/models\`
357
- - Type: free
358
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=GET%20%2Fv1%2Fmodels
359
- "
360
- `)
361
- })
362
-
363
- test('behavior: GET /discover/:id with Accept: text/markdown returns markdown', async () => {
364
- const proxy = ApiProxy.create({
365
- services: [
366
- openai({
367
- apiKey: 'sk-test',
368
- routes: { 'GET /v1/models': true },
369
- }),
370
- anthropic({
371
- apiKey: 'sk-ant-test',
372
- routes: {
373
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
374
- },
375
- }),
376
- ],
377
- })
378
- proxyServer = await Http.createServer(proxy.listener)
379
-
380
- const res = await fetch(`${proxyServer.url}/discover/anthropic`, {
381
- headers: { Accept: 'text/markdown' },
382
- })
383
- expect(res.status).toBe(200)
384
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
385
- })
386
-
387
- test('behavior: GET /discover/:id without Accept returns JSON', async () => {
388
- const proxy = ApiProxy.create({
389
- services: [
390
- openai({
391
- apiKey: 'sk-test',
392
- routes: { 'GET /v1/models': true },
393
- }),
394
- anthropic({
395
- apiKey: 'sk-ant-test',
396
- routes: {
397
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
398
- },
399
- }),
400
- ],
197
+ const body = (await res.json()) as Record<string, any>
198
+ expect(body.paths['/proxy/api/v1/generate'].post['x-payment-info']).toMatchObject({
199
+ amount: '1000000',
200
+ currency: asset,
201
+ description: 'Generate text',
202
+ intent: 'charge',
203
+ method: 'tempo',
401
204
  })
402
- proxyServer = await Http.createServer(proxy.listener)
403
-
404
- const res = await fetch(`${proxyServer.url}/discover/openai`)
405
- expect(res.status).toBe(200)
406
- expect(res.headers.get('content-type')).toMatchInlineSnapshot(`"application/json"`)
407
- })
408
-
409
- test('behavior: GET /discover/:id.md returns 404 for unknown', async () => {
410
- const proxy = ApiProxy.create({ services: [] })
411
- proxyServer = await Http.createServer(proxy.listener)
412
- const res = await fetch(`${proxyServer.url}/discover/unknown.md`)
413
- expect(res.status).toBe(404)
414
- })
415
-
416
- test('behavior: GET /discover/:id returns 404 for unknown', async () => {
417
- const proxy = ApiProxy.create({ services: [] })
418
- proxyServer = await Http.createServer(proxy.listener)
419
- const res = await fetch(`${proxyServer.url}/discover/unknown`)
420
- expect(res.status).toBe(404)
421
205
  })
422
206
 
423
207
  test('behavior: returns 404 for unknown service', async () => {