mppx 0.4.8 → 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 (267) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +9 -0
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +157 -1
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +2 -1
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/discovery/Discovery.d.ts +146 -0
  39. package/dist/discovery/Discovery.d.ts.map +1 -0
  40. package/dist/discovery/Discovery.js +60 -0
  41. package/dist/discovery/Discovery.js.map +1 -0
  42. package/dist/discovery/OpenApi.d.ts +61 -0
  43. package/dist/discovery/OpenApi.d.ts.map +1 -0
  44. package/dist/discovery/OpenApi.js +139 -0
  45. package/dist/discovery/OpenApi.js.map +1 -0
  46. package/dist/discovery/Validate.d.ts +10 -0
  47. package/dist/discovery/Validate.d.ts.map +1 -0
  48. package/dist/discovery/Validate.js +63 -0
  49. package/dist/discovery/Validate.js.map +1 -0
  50. package/dist/discovery/index.d.ts +4 -0
  51. package/dist/discovery/index.d.ts.map +1 -0
  52. package/dist/discovery/index.js +4 -0
  53. package/dist/discovery/index.js.map +1 -0
  54. package/dist/internal/types.d.ts.map +1 -1
  55. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  56. package/dist/mcp-sdk/client/McpClient.js +1 -1
  57. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  58. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  59. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  60. package/dist/middlewares/elysia.d.ts +52 -1
  61. package/dist/middlewares/elysia.d.ts.map +1 -1
  62. package/dist/middlewares/elysia.js +17 -0
  63. package/dist/middlewares/elysia.js.map +1 -1
  64. package/dist/middlewares/express.d.ts +13 -1
  65. package/dist/middlewares/express.d.ts.map +1 -1
  66. package/dist/middlewares/express.js +23 -2
  67. package/dist/middlewares/express.js.map +1 -1
  68. package/dist/middlewares/hono.d.ts +19 -1
  69. package/dist/middlewares/hono.d.ts.map +1 -1
  70. package/dist/middlewares/hono.js +51 -0
  71. package/dist/middlewares/hono.js.map +1 -1
  72. package/dist/middlewares/internal/mppx.d.ts +4 -2
  73. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  74. package/dist/middlewares/internal/mppx.js +10 -3
  75. package/dist/middlewares/internal/mppx.js.map +1 -1
  76. package/dist/middlewares/nextjs.d.ts +11 -0
  77. package/dist/middlewares/nextjs.d.ts.map +1 -1
  78. package/dist/middlewares/nextjs.js +15 -0
  79. package/dist/middlewares/nextjs.js.map +1 -1
  80. package/dist/proxy/Proxy.d.ts +6 -0
  81. package/dist/proxy/Proxy.d.ts.map +1 -1
  82. package/dist/proxy/Proxy.js +56 -80
  83. package/dist/proxy/Proxy.js.map +1 -1
  84. package/dist/proxy/Service.d.ts +16 -23
  85. package/dist/proxy/Service.d.ts.map +1 -1
  86. package/dist/proxy/Service.js +20 -84
  87. package/dist/proxy/Service.js.map +1 -1
  88. package/dist/proxy/internal/Route.js +1 -1
  89. package/dist/proxy/internal/Route.js.map +1 -1
  90. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  91. package/dist/proxy/services/anthropic.js +5 -0
  92. package/dist/proxy/services/anthropic.js.map +1 -1
  93. package/dist/proxy/services/openai.d.ts.map +1 -1
  94. package/dist/proxy/services/openai.js +6 -3
  95. package/dist/proxy/services/openai.js.map +1 -1
  96. package/dist/proxy/services/stripe.d.ts.map +1 -1
  97. package/dist/proxy/services/stripe.js +6 -3
  98. package/dist/proxy/services/stripe.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +35 -17
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Request.d.ts.map +1 -1
  103. package/dist/server/Request.js.map +1 -1
  104. package/dist/stripe/Methods.d.ts.map +1 -1
  105. package/dist/stripe/Methods.js.map +1 -1
  106. package/dist/tempo/Methods.d.ts.map +1 -1
  107. package/dist/tempo/Methods.js.map +1 -1
  108. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  109. package/dist/tempo/client/ChannelOps.js.map +1 -1
  110. package/dist/tempo/client/Charge.d.ts.map +1 -1
  111. package/dist/tempo/client/Charge.js.map +1 -1
  112. package/dist/tempo/client/Session.d.ts.map +1 -1
  113. package/dist/tempo/client/Session.js.map +1 -1
  114. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  115. package/dist/tempo/client/SessionManager.js +1 -1
  116. package/dist/tempo/client/SessionManager.js.map +1 -1
  117. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  118. package/dist/tempo/internal/auto-swap.js +1 -1
  119. package/dist/tempo/internal/auto-swap.js.map +1 -1
  120. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  121. package/dist/tempo/internal/fee-payer.js +1 -1
  122. package/dist/tempo/internal/fee-payer.js.map +1 -1
  123. package/dist/tempo/server/Charge.d.ts.map +1 -1
  124. package/dist/tempo/server/Charge.js +1 -1
  125. package/dist/tempo/server/Charge.js.map +1 -1
  126. package/dist/tempo/server/Session.d.ts.map +1 -1
  127. package/dist/tempo/server/Session.js +18 -5
  128. package/dist/tempo/server/Session.js.map +1 -1
  129. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  130. package/dist/tempo/server/internal/transport.js +8 -0
  131. package/dist/tempo/server/internal/transport.js.map +1 -1
  132. package/dist/tempo/session/Chain.d.ts.map +1 -1
  133. package/dist/tempo/session/Chain.js +1 -1
  134. package/dist/tempo/session/Chain.js.map +1 -1
  135. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  136. package/dist/tempo/session/ChannelStore.js.map +1 -1
  137. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  138. package/dist/tempo/session/Receipt.js.map +1 -1
  139. package/dist/tempo/session/Sse.d.ts.map +1 -1
  140. package/dist/tempo/session/Sse.js.map +1 -1
  141. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  142. package/dist/tempo/session/Voucher.js.map +1 -1
  143. package/dist/viem/Client.d.ts.map +1 -1
  144. package/dist/viem/Client.js.map +1 -1
  145. package/package.json +6 -1
  146. package/src/BodyDigest.test.ts +1 -1
  147. package/src/BodyDigest.ts +1 -0
  148. package/src/Challenge.fuzz.test.ts +121 -0
  149. package/src/Challenge.test-d.ts +2 -1
  150. package/src/Challenge.test.ts +1 -1
  151. package/src/Challenge.ts +1 -0
  152. package/src/Credential.fuzz.test.ts +62 -0
  153. package/src/Credential.test.ts +1 -1
  154. package/src/Credential.ts +1 -0
  155. package/src/Errors.test.ts +28 -40
  156. package/src/Expires.test.ts +2 -1
  157. package/src/Method.test.ts +1 -1
  158. package/src/PaymentRequest.test.ts +1 -1
  159. package/src/PaymentRequest.ts +1 -0
  160. package/src/Receipt.test.ts +1 -1
  161. package/src/Receipt.ts +1 -0
  162. package/src/Store.test-d.ts +2 -1
  163. package/src/Store.test.ts +57 -7
  164. package/src/Store.ts +25 -0
  165. package/src/cli/account.ts +65 -30
  166. package/src/cli/cli.test.ts +215 -2
  167. package/src/cli/cli.ts +166 -1
  168. package/src/cli/config.test.ts +1 -0
  169. package/src/cli/internal.ts +1 -0
  170. package/src/cli/plugins/stripe.ts +1 -0
  171. package/src/cli/plugins/tempo.ts +4 -1
  172. package/src/cli/utils.ts +1 -0
  173. package/src/client/Mppx.test-d.ts +2 -1
  174. package/src/client/Mppx.test.ts +1 -1
  175. package/src/client/Transport.test.ts +1 -1
  176. package/src/client/internal/Fetch.browser.test.ts +2 -1
  177. package/src/client/internal/Fetch.test-d.ts +2 -1
  178. package/src/client/internal/Fetch.test.ts +3 -1
  179. package/src/client/internal/Fetch.ts +1 -1
  180. package/src/discovery/Discovery.test.ts +152 -0
  181. package/src/discovery/Discovery.ts +72 -0
  182. package/src/discovery/OpenApi.test.ts +425 -0
  183. package/src/discovery/OpenApi.ts +224 -0
  184. package/src/discovery/Validate.test.ts +188 -0
  185. package/src/discovery/Validate.ts +76 -0
  186. package/src/discovery/index.ts +3 -0
  187. package/src/internal/constantTimeEqual.test.ts +2 -1
  188. package/src/internal/types.ts +1 -3
  189. package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
  190. package/src/mcp-sdk/client/McpClient.test.ts +2 -1
  191. package/src/mcp-sdk/client/McpClient.ts +2 -0
  192. package/src/mcp-sdk/server/Transport.test.ts +2 -1
  193. package/src/mcp-sdk/server/Transport.ts +1 -0
  194. package/src/middlewares/elysia.test.ts +28 -2
  195. package/src/middlewares/elysia.ts +36 -1
  196. package/src/middlewares/express.test.ts +95 -7
  197. package/src/middlewares/express.ts +40 -2
  198. package/src/middlewares/hono.test.ts +28 -6
  199. package/src/middlewares/hono.ts +74 -1
  200. package/src/middlewares/internal/mppx.test.ts +2 -1
  201. package/src/middlewares/internal/mppx.ts +14 -6
  202. package/src/middlewares/nextjs.test.ts +32 -6
  203. package/src/middlewares/nextjs.ts +28 -0
  204. package/src/proxy/Proxy.test.ts +55 -270
  205. package/src/proxy/Proxy.ts +73 -93
  206. package/src/proxy/Service.test.ts +24 -1
  207. package/src/proxy/Service.ts +48 -88
  208. package/src/proxy/internal/Headers.test.ts +2 -1
  209. package/src/proxy/internal/Route.test.ts +9 -1
  210. package/src/proxy/internal/Route.ts +1 -1
  211. package/src/proxy/services/anthropic.test.ts +132 -0
  212. package/src/proxy/services/anthropic.ts +5 -0
  213. package/src/proxy/services/openai.test.ts +2 -1
  214. package/src/proxy/services/openai.ts +6 -4
  215. package/src/proxy/services/stripe.test.ts +132 -0
  216. package/src/proxy/services/stripe.ts +6 -4
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +194 -1
  219. package/src/server/Mppx.ts +38 -19
  220. package/src/server/NodeListener.test.ts +1 -1
  221. package/src/server/Request.test.ts +2 -1
  222. package/src/server/Request.ts +1 -0
  223. package/src/server/Response.test.ts +2 -1
  224. package/src/server/Transport.test.ts +2 -1
  225. package/src/stripe/Charge.integration.test.ts +1 -1
  226. package/src/stripe/Methods.test.ts +1 -1
  227. package/src/stripe/Methods.ts +1 -0
  228. package/src/stripe/client/Charge.test.ts +2 -1
  229. package/src/stripe/server/Charge.test.ts +2 -1
  230. package/src/tempo/Attribution.test.ts +2 -1
  231. package/src/tempo/Methods.test.ts +1 -1
  232. package/src/tempo/Methods.ts +1 -0
  233. package/src/tempo/client/ChannelOps.test.ts +7 -3
  234. package/src/tempo/client/ChannelOps.ts +1 -0
  235. package/src/tempo/client/Charge.ts +1 -0
  236. package/src/tempo/client/Session.test.ts +6 -2
  237. package/src/tempo/client/Session.ts +1 -0
  238. package/src/tempo/client/SessionManager.test.ts +29 -1
  239. package/src/tempo/client/SessionManager.ts +2 -1
  240. package/src/tempo/internal/auto-swap.test.ts +2 -1
  241. package/src/tempo/internal/auto-swap.ts +1 -0
  242. package/src/tempo/internal/defaults.test.ts +2 -1
  243. package/src/tempo/internal/fee-payer.test.ts +2 -1
  244. package/src/tempo/internal/fee-payer.ts +1 -0
  245. package/src/tempo/server/Charge.test.ts +2 -1
  246. package/src/tempo/server/Charge.ts +1 -0
  247. package/src/tempo/server/Session.test.ts +88 -37
  248. package/src/tempo/server/Session.ts +26 -8
  249. package/src/tempo/server/Sse.test.ts +2 -1
  250. package/src/tempo/server/internal/transport.test.ts +25 -1
  251. package/src/tempo/server/internal/transport.ts +11 -0
  252. package/src/tempo/session/Chain.test.ts +6 -2
  253. package/src/tempo/session/Chain.ts +2 -1
  254. package/src/tempo/session/Channel.test.ts +2 -1
  255. package/src/tempo/session/ChannelStore.test.ts +2 -1
  256. package/src/tempo/session/ChannelStore.ts +1 -0
  257. package/src/tempo/session/Receipt.test.ts +2 -1
  258. package/src/tempo/session/Receipt.ts +1 -0
  259. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  260. package/src/tempo/session/Sse.test.ts +2 -1
  261. package/src/tempo/session/Sse.ts +1 -0
  262. package/src/tempo/session/Voucher.test.ts +2 -1
  263. package/src/tempo/session/Voucher.ts +1 -0
  264. package/src/viem/Account.test.ts +2 -1
  265. package/src/viem/Client.test.ts +2 -1
  266. package/src/viem/Client.ts +1 -0
  267. package/src/zod.test.ts +147 -0
@@ -1,9 +1,10 @@
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
8
  import * as ApiProxy from './Proxy.js'
8
9
  import * as Service from './Service.js'
9
10
  import { anthropic } from './services/anthropic.js'
@@ -64,11 +65,19 @@ function createUpstream(handler: (req: Request) => Response | Promise<Response>)
64
65
  }
65
66
 
66
67
  describe('create', () => {
67
- test('behavior: GET /discover/all returns service discovery JSON', async () => {
68
+ test('behavior: GET /openapi.json returns discovery JSON', async () => {
68
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',
69
77
  services: [
70
78
  Service.from('api', {
71
79
  baseUrl: 'https://api.example.com',
80
+ categories: ['compute'],
72
81
  routes: {
73
82
  'GET /v1/models': true,
74
83
  'POST /v1/generate': mppx_server.charge({ amount: '1', description: 'Generate text' }),
@@ -83,73 +92,40 @@ describe('create', () => {
83
92
  })
84
93
  proxyServer = await Http.createServer(proxy.listener)
85
94
 
86
- const res = await fetch(`${proxyServer.url}/discover/all`)
95
+ const res = await fetch(`${proxyServer.url}/openapi.json`)
87
96
  expect(res.status).toBe(200)
88
- expect(await res.json()).toMatchInlineSnapshot(`
89
- [
90
- {
91
- "id": "api",
92
- "routes": [
93
- {
94
- "method": "GET",
95
- "path": "/api/v1/models",
96
- "pattern": "GET /api/v1/models",
97
- "payment": null,
98
- },
99
- {
100
- "method": "POST",
101
- "path": "/api/v1/generate",
102
- "pattern": "POST /api/v1/generate",
103
- "payment": {
104
- "amount": "1000000",
105
- "currency": "0x20c0000000000000000000000000000000000001",
106
- "decimals": 6,
107
- "description": "Generate text",
108
- "intent": "charge",
109
- "method": "tempo",
110
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
111
- },
112
- },
113
- {
114
- "method": "POST",
115
- "path": "/api/v1/stream",
116
- "pattern": "POST /api/v1/stream",
117
- "payment": {
118
- "amount": "1000000",
119
- "currency": "0x20c0000000000000000000000000000000000001",
120
- "decimals": 6,
121
- "description": "Stream text",
122
- "intent": "session",
123
- "method": "tempo",
124
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
125
- "unitType": "token",
126
- },
127
- },
128
- ],
129
- },
130
- ]
131
- `)
132
- })
133
-
134
- test('behavior: GET /discover returns JSON by default', async () => {
135
- const proxy = ApiProxy.create({
136
- services: [
137
- Service.from('api', {
138
- baseUrl: 'https://api.example.com',
139
- routes: {
140
- 'GET /v1/models': true,
141
- },
142
- }),
143
- ],
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',
144
125
  })
145
- proxyServer = await Http.createServer(proxy.listener)
146
-
147
- const res = await fetch(`${proxyServer.url}/discover`)
148
- expect(res.status).toBe(200)
149
- expect(res.headers.get('content-type')).toMatchInlineSnapshot(`"application/json"`)
150
126
  })
151
127
 
152
- 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 () => {
153
129
  const proxy = ApiProxy.create({
154
130
  title: 'My AI Gateway',
155
131
  description: 'A paid proxy for LLM and AI services.',
@@ -185,9 +161,7 @@ describe('create', () => {
185
161
  })
186
162
  proxyServer = await Http.createServer(proxy.listener)
187
163
 
188
- const res = await fetch(`${proxyServer.url}/discover`, {
189
- headers: { Accept: 'text/plain' },
190
- })
164
+ const res = await fetch(`${proxyServer.url}/llms.txt`)
191
165
  expect(res.status).toBe(200)
192
166
  expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8')
193
167
  expect(await res.text()).toMatchInlineSnapshot(`
@@ -197,20 +171,20 @@ describe('create', () => {
197
171
 
198
172
  ## Services
199
173
 
200
- - [OpenAI](/discover/openai.md): Chat completions, embeddings, image generation, and audio transcription.
201
- - [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.
202
176
 
203
- [See all service definitions](/discover/all.md)"
177
+ [OpenAPI discovery](/openapi.json)"
204
178
  `)
205
179
  })
206
180
 
207
- test('behavior: GET /discover/:id returns single service', async () => {
181
+ test('behavior: GET /openapi.json respects basePath', async () => {
208
182
  const proxy = ApiProxy.create({
183
+ basePath: '/proxy',
209
184
  services: [
210
185
  Service.from('api', {
211
186
  baseUrl: 'https://api.example.com',
212
187
  routes: {
213
- 'GET /v1/models': true,
214
188
  'POST /v1/generate': mppx_server.charge({ amount: '1', description: 'Generate text' }),
215
189
  },
216
190
  }),
@@ -218,205 +192,16 @@ describe('create', () => {
218
192
  })
219
193
  proxyServer = await Http.createServer(proxy.listener)
220
194
 
221
- const res = await fetch(`${proxyServer.url}/discover/api`)
222
- expect(res.status).toBe(200)
223
- expect(await res.json()).toMatchInlineSnapshot(`
224
- {
225
- "id": "api",
226
- "routes": [
227
- {
228
- "method": "GET",
229
- "path": "/api/v1/models",
230
- "pattern": "GET /api/v1/models",
231
- "payment": null,
232
- },
233
- {
234
- "method": "POST",
235
- "path": "/api/v1/generate",
236
- "pattern": "POST /api/v1/generate",
237
- "payment": {
238
- "amount": "1000000",
239
- "currency": "0x20c0000000000000000000000000000000000001",
240
- "decimals": 6,
241
- "description": "Generate text",
242
- "intent": "charge",
243
- "method": "tempo",
244
- "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
245
- },
246
- },
247
- ],
248
- }
249
- `)
250
- })
251
-
252
- test('behavior: GET /discover/all.md returns full markdown with routes', async () => {
253
- const proxy = ApiProxy.create({
254
- services: [
255
- openai({
256
- apiKey: 'sk-test',
257
- routes: {
258
- 'POST /v1/chat/completions': mppx_server.charge({
259
- amount: '0.05',
260
- description: 'Chat completion',
261
- }),
262
- 'GET /v1/models': true,
263
- },
264
- }),
265
- anthropic({
266
- apiKey: 'sk-ant-test',
267
- routes: {
268
- 'POST /v1/messages': mppx_server.charge({
269
- amount: '0.03',
270
- description: 'Send message',
271
- }),
272
- },
273
- }),
274
- ],
275
- })
276
- proxyServer = await Http.createServer(proxy.listener)
277
-
278
- const res = await fetch(`${proxyServer.url}/discover/all.md`)
279
- expect(res.status).toBe(200)
280
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
281
- expect(await res.text()).toMatchInlineSnapshot(`
282
- "# Services
283
-
284
- ## [OpenAI](/discover/openai.md)
285
-
286
- Chat completions, embeddings, image generation, and audio transcription.
287
-
288
- ### Routes
289
-
290
- - \`POST /openai/v1/chat/completions\`: Chat completion
291
- - Type: charge
292
- - Price: 0.05 (50000 units, 6 decimals)
293
- - Currency: 0x20c0000000000000000000000000000000000001
294
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=POST%20%2Fv1%2Fchat%2Fcompletions
295
-
296
- - \`GET /openai/v1/models\`
297
- - Type: free
298
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=GET%20%2Fv1%2Fmodels
299
-
300
- ## [Anthropic](/discover/anthropic.md)
301
-
302
- Claude language models for messages and completions.
303
-
304
- ### Routes
305
-
306
- - \`POST /anthropic/v1/messages\`: Send message
307
- - Type: charge
308
- - Price: 0.03 (30000 units, 6 decimals)
309
- - Currency: 0x20c0000000000000000000000000000000000001
310
- "
311
- `)
312
- })
313
-
314
- test('behavior: GET /discover/:id.md returns markdown', async () => {
315
- const proxy = ApiProxy.create({
316
- services: [
317
- openai({
318
- apiKey: 'sk-test',
319
- routes: {
320
- 'POST /v1/chat/completions': mppx_server.charge({
321
- amount: '0.05',
322
- description: 'Chat completion',
323
- }),
324
- 'GET /v1/models': true,
325
- },
326
- }),
327
- anthropic({
328
- apiKey: 'sk-ant-test',
329
- routes: {
330
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
331
- },
332
- }),
333
- ],
334
- })
335
- proxyServer = await Http.createServer(proxy.listener)
336
-
337
- const res = await fetch(`${proxyServer.url}/discover/openai.md`)
338
- expect(res.status).toBe(200)
339
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
340
- expect(await res.text()).toMatchInlineSnapshot(`
341
- "# OpenAI
342
-
343
- > Documentation: https://context7.com/websites/platform_openai/llms.txt
344
-
345
- Chat completions, embeddings, image generation, and audio transcription.
346
-
347
- ## Routes
348
-
349
- - \`POST /openai/v1/chat/completions\`: Chat completion
350
- - Type: charge
351
- - Price: 0.05 (50000 units, 6 decimals)
352
- - Currency: 0x20c0000000000000000000000000000000000001
353
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=POST%20%2Fv1%2Fchat%2Fcompletions
354
-
355
- - \`GET /openai/v1/models\`
356
- - Type: free
357
- - Docs: https://context7.com/websites/platform_openai/llms.txt?topic=GET%20%2Fv1%2Fmodels
358
- "
359
- `)
360
- })
361
-
362
- test('behavior: GET /discover/:id with Accept: text/markdown returns markdown', async () => {
363
- const proxy = ApiProxy.create({
364
- services: [
365
- openai({
366
- apiKey: 'sk-test',
367
- routes: { 'GET /v1/models': true },
368
- }),
369
- anthropic({
370
- apiKey: 'sk-ant-test',
371
- routes: {
372
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
373
- },
374
- }),
375
- ],
376
- })
377
- proxyServer = await Http.createServer(proxy.listener)
378
-
379
- const res = await fetch(`${proxyServer.url}/discover/anthropic`, {
380
- headers: { Accept: 'text/markdown' },
381
- })
195
+ const res = await fetch(`${proxyServer.url}/proxy/openapi.json`)
382
196
  expect(res.status).toBe(200)
383
- expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8')
384
- })
385
-
386
- test('behavior: GET /discover/:id without Accept returns JSON', async () => {
387
- const proxy = ApiProxy.create({
388
- services: [
389
- openai({
390
- apiKey: 'sk-test',
391
- routes: { 'GET /v1/models': true },
392
- }),
393
- anthropic({
394
- apiKey: 'sk-ant-test',
395
- routes: {
396
- 'POST /v1/messages': mppx_server.charge({ amount: '0.03' }),
397
- },
398
- }),
399
- ],
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',
400
204
  })
401
- proxyServer = await Http.createServer(proxy.listener)
402
-
403
- const res = await fetch(`${proxyServer.url}/discover/openai`)
404
- expect(res.status).toBe(200)
405
- expect(res.headers.get('content-type')).toMatchInlineSnapshot(`"application/json"`)
406
- })
407
-
408
- test('behavior: GET /discover/:id.md returns 404 for unknown', async () => {
409
- const proxy = ApiProxy.create({ services: [] })
410
- proxyServer = await Http.createServer(proxy.listener)
411
- const res = await fetch(`${proxyServer.url}/discover/unknown.md`)
412
- expect(res.status).toBe(404)
413
- })
414
-
415
- test('behavior: GET /discover/:id returns 404 for unknown', async () => {
416
- const proxy = ApiProxy.create({ services: [] })
417
- proxyServer = await Http.createServer(proxy.listener)
418
- const res = await fetch(`${proxyServer.url}/discover/unknown`)
419
- expect(res.status).toBe(404)
420
205
  })
421
206
 
422
207
  test('behavior: returns 404 for unknown service', async () => {
@@ -1,5 +1,8 @@
1
1
  import type * as http from 'node:http'
2
+
2
3
  import { createFetchProxy } from '@remix-run/fetch-proxy'
4
+
5
+ import { generateProxy } from '../discovery/OpenApi.js'
3
6
  import * as Request from '../server/Request.js'
4
7
  import * as Headers from './internal/Headers.js'
5
8
  import * as Route from './internal/Route.js'
@@ -53,6 +56,24 @@ export function create(config: create.Config): Proxy {
53
56
  }),
54
57
  )
55
58
 
59
+ // Pre-generate static discovery responses once at startup.
60
+ const openApiJson = JSON.stringify(
61
+ generateProxy({
62
+ basePath: config.basePath,
63
+ info: {
64
+ title: config.title ?? 'API Proxy',
65
+ version: config.version ?? '1.0.0',
66
+ },
67
+ routes: buildDiscoveryRoutes(config.services),
68
+ serviceInfo: buildServiceInfo(config),
69
+ }),
70
+ )
71
+ const llmsTxt = Service.toLlmsTxt(config.services, {
72
+ title: config.title,
73
+ description: config.description,
74
+ openApiPath: withBasePath(config.basePath, '/openapi.json'),
75
+ })
76
+
56
77
  async function handle(request: globalThis.Request): Promise<Response> {
57
78
  const url = new URL(request.url)
58
79
 
@@ -60,68 +81,22 @@ export function create(config: create.Config): Proxy {
60
81
 
61
82
  if (!pathname) return new Response('Not Found', { status: 404 })
62
83
 
63
- if (request.method === 'GET' && pathname === '/llms.txt')
64
- return new Response(
65
- Service.toLlmsTxt(config.services, {
66
- title: config.title,
67
- description: config.description,
68
- }),
69
- { headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
70
- )
71
-
72
- if (request.method === 'GET' && pathname === '/discover.md')
73
- return new Response(
74
- Service.toLlmsTxt(config.services, {
75
- title: config.title,
76
- description: config.description,
77
- }),
78
- { headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
79
- )
80
-
81
- if (request.method === 'GET' && (pathname === '/discover' || pathname === '/discover/')) {
82
- if (wantsMarkdown(request))
83
- return new Response(
84
- Service.toLlmsTxt(config.services, {
85
- title: config.title,
86
- description: config.description,
87
- }),
88
- { headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
89
- )
90
- return Response.json(config.services.map(Service.serialize))
91
- }
92
-
93
84
  if (
94
85
  request.method === 'GET' &&
95
- (pathname === '/discover/all' || pathname === '/discover/all/')
86
+ (pathname === '/openapi.json' || pathname === '/openapi.json/')
96
87
  ) {
97
- if (wantsMarkdown(request))
98
- return new Response(Service.toServicesMarkdown(config.services), {
99
- headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
100
- })
101
- return Response.json(config.services.map(Service.serialize))
102
- }
103
-
104
- if (request.method === 'GET' && pathname === '/discover/all.md')
105
- return new Response(Service.toServicesMarkdown(config.services), {
106
- headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
88
+ return new Response(openApiJson, {
89
+ headers: {
90
+ 'Cache-Control': 'public, max-age=300',
91
+ 'Content-Type': 'application/json',
92
+ },
107
93
  })
108
-
109
- {
110
- // List service
111
- const match =
112
- pathname.match(/^\/discover\/([^/]+)\.md$/) ?? pathname.match(/^\/discover\/([^/]+)\/?$/)
113
- if (request.method === 'GET' && match) {
114
- const service = config.services.find((s) => s.id === match[1])
115
- if (!service) return new Response('Not Found', { status: 404 })
116
- const wantsText = pathname.endsWith('.md') || wantsMarkdown(request)
117
- if (wantsText)
118
- return new Response(Service.toMarkdown(service), {
119
- headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
120
- })
121
- return Response.json(Service.serialize(service))
122
- }
123
94
  }
124
95
 
96
+ if (request.method === 'GET' && pathname === '/llms.txt')
97
+ return new Response(llmsTxt, {
98
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
99
+ })
125
100
  const parsed = Route.parse(pathname)
126
101
  if (!parsed) return new Response('Not Found', { status: 404 })
127
102
 
@@ -175,14 +150,20 @@ export declare namespace create {
175
150
  export type Config = {
176
151
  /** Base path prefix to strip before routing (e.g. `'/api/proxy'`). */
177
152
  basePath?: string | undefined
153
+ /** Free-form categories for root discovery metadata. */
154
+ categories?: string[] | undefined
178
155
  /** Short description of the proxy shown in `llms.txt`. */
179
156
  description?: string | undefined
157
+ /** Structured documentation links for root discovery metadata. */
158
+ docs?: Service.Docs | undefined
180
159
  /** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
181
160
  fetch?: typeof globalThis.fetch | undefined
182
161
  /** Services to proxy. Each service is mounted at `/{serviceId}/`. */
183
162
  services: Service.Service[]
184
163
  /** Human-readable title for the proxy shown in `llms.txt`. */
185
164
  title?: string | undefined
165
+ /** Version to include in the generated OpenAPI document. */
166
+ version?: string | undefined
186
167
  }
187
168
  }
188
169
 
@@ -228,41 +209,40 @@ async function proxyUpstream(options: proxyUpstream.Options): Promise<Response>
228
209
  return upstreamRes
229
210
  }
230
211
 
231
- const aiUserAgents = [
232
- 'GPTBot',
233
- 'OAI-SearchBot',
234
- 'ChatGPT-User',
235
- 'anthropic-ai',
236
- 'ClaudeBot',
237
- 'claude-web',
238
- 'PerplexityBot',
239
- 'Perplexity-User',
240
- 'Google-Extended',
241
- 'Googlebot',
242
- 'Bingbot',
243
- 'Amazonbot',
244
- 'Applebot',
245
- 'Applebot-Extended',
246
- 'FacebookBot',
247
- 'meta-externalagent',
248
- 'Bytespider',
249
- 'DuckAssistBot',
250
- 'cohere-ai',
251
- 'AI2Bot',
252
- 'CCBot',
253
- 'Diffbot',
254
- 'YouBot',
255
- 'MistralAI-User',
256
- 'GoogleAgent-Mariner',
257
- ]
258
-
259
- const terminalUserAgents = ['curl', 'Wget', 'HTTPie', 'httpie-go', 'mppx', 'presto', 'xh']
260
-
261
- function wantsMarkdown(request: globalThis.Request): boolean {
262
- const accept = request.headers.get('accept')
263
- if (accept && (accept.includes('text/markdown') || accept.includes('text/plain'))) return true
264
- const ua = request.headers.get('user-agent') ?? ''
265
- if (aiUserAgents.some((agent) => ua.includes(agent))) return true
266
- if (terminalUserAgents.some((agent) => ua.includes(agent))) return true
267
- return false
212
+ function buildDiscoveryRoutes(services: Service.Service[]) {
213
+ return services.flatMap((service) =>
214
+ Object.entries(service.routes).map(([pattern, endpoint]) => {
215
+ const tokens = pattern.trim().split(/\s+/)
216
+ const hasMethod = tokens.length >= 2
217
+ const path = hasMethod ? tokens.slice(1).join(' ') : tokens[0]
218
+ return {
219
+ method: hasMethod ? tokens[0]! : 'GET',
220
+ path: `/${service.id}${path}`,
221
+ payment: endpoint ? Service.paymentOf(endpoint) : null,
222
+ }
223
+ }),
224
+ )
225
+ }
226
+
227
+ function buildServiceInfo(config: create.Config): { categories?: string[]; docs?: Service.Docs } {
228
+ const categories =
229
+ config.categories ??
230
+ Array.from(new Set(config.services.flatMap((service) => service.categories ?? [])))
231
+
232
+ const docs = {
233
+ ...(config.docs ?? {}),
234
+ llms: config.docs?.llms ?? withBasePath(config.basePath, '/llms.txt'),
235
+ }
236
+
237
+ return {
238
+ ...(categories.length > 0 ? { categories } : {}),
239
+ docs,
240
+ }
241
+ }
242
+
243
+ function withBasePath(basePath: string | undefined, path: string) {
244
+ if (!basePath) return path
245
+ const normalized = basePath.startsWith('/') ? basePath : `/${basePath}`
246
+ const trimmed = normalized.endsWith('/') ? normalized.slice(0, -1) : normalized
247
+ return `${trimmed}${path}`
268
248
  }
@@ -1,4 +1,5 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
+
2
3
  import * as Service from './Service.js'
3
4
 
4
5
  describe('from', () => {
@@ -101,6 +102,28 @@ describe('custom', () => {
101
102
  })
102
103
  })
103
104
 
105
+ describe('paymentOf', () => {
106
+ test('behavior: returns null for free passthrough endpoint', () => {
107
+ expect(Service.paymentOf(true)).toBeNull()
108
+ })
109
+
110
+ test('behavior: returns null for paid handler without _internal metadata', () => {
111
+ const handler: Service.IntentHandler = async () => ({
112
+ status: 200 as const,
113
+ withReceipt: <T>(r: T) => r,
114
+ })
115
+ expect(Service.paymentOf(handler)).toBeNull()
116
+ })
117
+
118
+ test('behavior: returns null for paid endpoint object without _internal metadata', () => {
119
+ const handler: Service.IntentHandler = async () => ({
120
+ status: 200 as const,
121
+ withReceipt: <T>(r: T) => r,
122
+ })
123
+ expect(Service.paymentOf({ pay: handler, options: {} })).toBeNull()
124
+ })
125
+ })
126
+
104
127
  describe('getOptions', () => {
105
128
  test('behavior: returns options from endpoint object', () => {
106
129
  const handler: Service.IntentHandler = async () => ({