mppx 0.4.9 → 0.4.11
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.
- package/CHANGELOG.md +25 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +155 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +18 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +19 -83
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +3 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +25 -8
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Errors.test.ts +1 -1
- package/src/Expires.test.ts +1 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/Receipt.test.ts +1 -1
- package/src/Store.test-d.ts +1 -1
- package/src/Store.test.ts +1 -1
- package/src/cli/cli.test.ts +212 -1
- package/src/cli/cli.ts +162 -0
- package/src/client/Mppx.test-d.ts +1 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +1 -1
- package/src/client/internal/Fetch.test-d.ts +1 -1
- package/src/client/internal/Fetch.test.ts +2 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/mcp-sdk/server/Transport.test.ts +1 -1
- package/src/middlewares/elysia.test.ts +27 -2
- package/src/middlewares/elysia.ts +35 -1
- package/src/middlewares/express.test.ts +35 -7
- package/src/middlewares/express.ts +34 -0
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +73 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +31 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +54 -270
- package/src/proxy/Proxy.ts +71 -93
- package/src/proxy/Service.test.ts +23 -1
- package/src/proxy/Service.ts +40 -86
- package/src/proxy/internal/Headers.test.ts +1 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +2 -1
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +1 -1
- package/src/server/Response.test.ts +1 -1
- package/src/server/Transport.test.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/internal/types.ts +5 -1
- package/src/stripe/server/Charge.test.ts +53 -2
- package/src/stripe/server/Charge.ts +12 -4
- package/src/tempo/Attribution.test.ts +1 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +6 -3
- package/src/tempo/client/Session.test.ts +5 -2
- package/src/tempo/client/SessionManager.test.ts +1 -1
- package/src/tempo/internal/auto-swap.test.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +1 -1
- package/src/tempo/server/Charge.test.ts +1 -1
- package/src/tempo/server/Session.test.ts +116 -37
- package/src/tempo/server/Session.ts +32 -11
- package/src/tempo/server/Sse.test.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +24 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +5 -2
- package/src/tempo/session/Chain.ts +1 -1
- package/src/tempo/session/Channel.test.ts +1 -1
- package/src/tempo/session/ChannelStore.test.ts +1 -1
- package/src/tempo/session/Receipt.test.ts +1 -1
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +1 -1
- package/src/tempo/session/Voucher.test.ts +1 -1
- package/src/viem/Account.test.ts +1 -1
- package/src/viem/Client.test.ts +1 -1
- package/src/zod.test.ts +147 -0
package/src/proxy/Proxy.test.ts
CHANGED
|
@@ -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 '
|
|
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 /
|
|
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}/
|
|
95
|
+
const res = await fetch(`${proxyServer.url}/openapi.json`)
|
|
88
96
|
expect(res.status).toBe(200)
|
|
89
|
-
expect(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 /
|
|
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}/
|
|
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
|
-
-
|
|
202
|
-
-
|
|
174
|
+
- OpenAI: Chat completions, embeddings, image generation, and audio transcription.
|
|
175
|
+
- Anthropic: Claude language models for messages and completions.
|
|
203
176
|
|
|
204
|
-
[
|
|
177
|
+
[OpenAPI discovery](/openapi.json)"
|
|
205
178
|
`)
|
|
206
179
|
})
|
|
207
180
|
|
|
208
|
-
test('behavior: GET /
|
|
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}/
|
|
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
|
-
|
|
282
|
-
expect(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 () => {
|
package/src/proxy/Proxy.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type * as http from 'node:http'
|
|
|
2
2
|
|
|
3
3
|
import { createFetchProxy } from '@remix-run/fetch-proxy'
|
|
4
4
|
|
|
5
|
+
import { generateProxy } from '../discovery/OpenApi.js'
|
|
5
6
|
import * as Request from '../server/Request.js'
|
|
6
7
|
import * as Headers from './internal/Headers.js'
|
|
7
8
|
import * as Route from './internal/Route.js'
|
|
@@ -55,6 +56,24 @@ export function create(config: create.Config): Proxy {
|
|
|
55
56
|
}),
|
|
56
57
|
)
|
|
57
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
|
+
|
|
58
77
|
async function handle(request: globalThis.Request): Promise<Response> {
|
|
59
78
|
const url = new URL(request.url)
|
|
60
79
|
|
|
@@ -62,68 +81,22 @@ export function create(config: create.Config): Proxy {
|
|
|
62
81
|
|
|
63
82
|
if (!pathname) return new Response('Not Found', { status: 404 })
|
|
64
83
|
|
|
65
|
-
if (request.method === 'GET' && pathname === '/llms.txt')
|
|
66
|
-
return new Response(
|
|
67
|
-
Service.toLlmsTxt(config.services, {
|
|
68
|
-
title: config.title,
|
|
69
|
-
description: config.description,
|
|
70
|
-
}),
|
|
71
|
-
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if (request.method === 'GET' && pathname === '/discover.md')
|
|
75
|
-
return new Response(
|
|
76
|
-
Service.toLlmsTxt(config.services, {
|
|
77
|
-
title: config.title,
|
|
78
|
-
description: config.description,
|
|
79
|
-
}),
|
|
80
|
-
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
if (request.method === 'GET' && (pathname === '/discover' || pathname === '/discover/')) {
|
|
84
|
-
if (wantsMarkdown(request))
|
|
85
|
-
return new Response(
|
|
86
|
-
Service.toLlmsTxt(config.services, {
|
|
87
|
-
title: config.title,
|
|
88
|
-
description: config.description,
|
|
89
|
-
}),
|
|
90
|
-
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
|
|
91
|
-
)
|
|
92
|
-
return Response.json(config.services.map(Service.serialize))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
84
|
if (
|
|
96
85
|
request.method === 'GET' &&
|
|
97
|
-
(pathname === '/
|
|
86
|
+
(pathname === '/openapi.json' || pathname === '/openapi.json/')
|
|
98
87
|
) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (request.method === 'GET' && pathname === '/discover/all.md')
|
|
107
|
-
return new Response(Service.toServicesMarkdown(config.services), {
|
|
108
|
-
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
|
+
},
|
|
109
93
|
})
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
// List service
|
|
113
|
-
const match =
|
|
114
|
-
pathname.match(/^\/discover\/([^/]+)\.md$/) ?? pathname.match(/^\/discover\/([^/]+)\/?$/)
|
|
115
|
-
if (request.method === 'GET' && match) {
|
|
116
|
-
const service = config.services.find((s) => s.id === match[1])
|
|
117
|
-
if (!service) return new Response('Not Found', { status: 404 })
|
|
118
|
-
const wantsText = pathname.endsWith('.md') || wantsMarkdown(request)
|
|
119
|
-
if (wantsText)
|
|
120
|
-
return new Response(Service.toMarkdown(service), {
|
|
121
|
-
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
122
|
-
})
|
|
123
|
-
return Response.json(Service.serialize(service))
|
|
124
|
-
}
|
|
125
94
|
}
|
|
126
95
|
|
|
96
|
+
if (request.method === 'GET' && pathname === '/llms.txt')
|
|
97
|
+
return new Response(llmsTxt, {
|
|
98
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
99
|
+
})
|
|
127
100
|
const parsed = Route.parse(pathname)
|
|
128
101
|
if (!parsed) return new Response('Not Found', { status: 404 })
|
|
129
102
|
|
|
@@ -177,14 +150,20 @@ export declare namespace create {
|
|
|
177
150
|
export type Config = {
|
|
178
151
|
/** Base path prefix to strip before routing (e.g. `'/api/proxy'`). */
|
|
179
152
|
basePath?: string | undefined
|
|
153
|
+
/** Free-form categories for root discovery metadata. */
|
|
154
|
+
categories?: string[] | undefined
|
|
180
155
|
/** Short description of the proxy shown in `llms.txt`. */
|
|
181
156
|
description?: string | undefined
|
|
157
|
+
/** Structured documentation links for root discovery metadata. */
|
|
158
|
+
docs?: Service.Docs | undefined
|
|
182
159
|
/** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
|
|
183
160
|
fetch?: typeof globalThis.fetch | undefined
|
|
184
161
|
/** Services to proxy. Each service is mounted at `/{serviceId}/`. */
|
|
185
162
|
services: Service.Service[]
|
|
186
163
|
/** Human-readable title for the proxy shown in `llms.txt`. */
|
|
187
164
|
title?: string | undefined
|
|
165
|
+
/** Version to include in the generated OpenAPI document. */
|
|
166
|
+
version?: string | undefined
|
|
188
167
|
}
|
|
189
168
|
}
|
|
190
169
|
|
|
@@ -230,41 +209,40 @@ async function proxyUpstream(options: proxyUpstream.Options): Promise<Response>
|
|
|
230
209
|
return upstreamRes
|
|
231
210
|
}
|
|
232
211
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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}`
|
|
270
248
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test } from '
|
|
1
|
+
import { describe, expect, test } from 'vp/test'
|
|
2
2
|
|
|
3
3
|
import * as Service from './Service.js'
|
|
4
4
|
|
|
@@ -102,6 +102,28 @@ describe('custom', () => {
|
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
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
|
+
|
|
105
127
|
describe('getOptions', () => {
|
|
106
128
|
test('behavior: returns options from endpoint object', () => {
|
|
107
129
|
const handler: Service.IntentHandler = async () => ({
|