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.
- package/CHANGELOG.md +26 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +9 -0
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +157 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +2 -1
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.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/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- 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 +23 -2
- 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 +20 -84
- 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/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +35 -17
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +1 -1
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +1 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -5
- 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.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +2 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +1 -0
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +28 -40
- package/src/Expires.test.ts +2 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.test.ts +1 -1
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +2 -1
- package/src/Store.test.ts +57 -7
- package/src/Store.ts +25 -0
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +215 -2
- package/src/cli/cli.ts +166 -1
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +4 -1
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +2 -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 +2 -1
- package/src/client/internal/Fetch.test-d.ts +2 -1
- package/src/client/internal/Fetch.test.ts +3 -1
- package/src/client/internal/Fetch.ts +1 -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 +2 -1
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
- package/src/mcp-sdk/client/McpClient.test.ts +2 -1
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +2 -1
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +28 -2
- package/src/middlewares/elysia.ts +36 -1
- package/src/middlewares/express.test.ts +95 -7
- package/src/middlewares/express.ts +40 -2
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +74 -1
- package/src/middlewares/internal/mppx.test.ts +2 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +32 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +55 -270
- package/src/proxy/Proxy.ts +73 -93
- package/src/proxy/Service.test.ts +24 -1
- package/src/proxy/Service.ts +48 -88
- package/src/proxy/internal/Headers.test.ts +2 -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 +2 -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 +194 -1
- package/src/server/Mppx.ts +38 -19
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +2 -1
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +2 -1
- package/src/server/Transport.test.ts +2 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +2 -1
- package/src/stripe/server/Charge.test.ts +2 -1
- package/src/tempo/Attribution.test.ts +2 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +7 -3
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +6 -2
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +29 -1
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/auto-swap.test.ts +2 -1
- package/src/tempo/internal/auto-swap.ts +1 -0
- package/src/tempo/internal/defaults.test.ts +2 -1
- package/src/tempo/internal/fee-payer.test.ts +2 -1
- package/src/tempo/internal/fee-payer.ts +1 -0
- package/src/tempo/server/Charge.test.ts +2 -1
- package/src/tempo/server/Charge.ts +1 -0
- package/src/tempo/server/Session.test.ts +88 -37
- package/src/tempo/server/Session.ts +26 -8
- package/src/tempo/server/Sse.test.ts +2 -1
- package/src/tempo/server/internal/transport.test.ts +25 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +6 -2
- package/src/tempo/session/Chain.ts +2 -1
- package/src/tempo/session/Channel.test.ts +2 -1
- package/src/tempo/session/ChannelStore.test.ts +2 -1
- package/src/tempo/session/ChannelStore.ts +1 -0
- package/src/tempo/session/Receipt.test.ts +2 -1
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +2 -1
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +2 -1
- package/src/tempo/session/Voucher.ts +1 -0
- package/src/viem/Account.test.ts +2 -1
- package/src/viem/Client.test.ts +2 -1
- package/src/viem/Client.ts +1 -0
- package/src/zod.test.ts +147 -0
package/src/proxy/Proxy.test.ts
CHANGED
|
@@ -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 '
|
|
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 /
|
|
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}/
|
|
95
|
+
const res = await fetch(`${proxyServer.url}/openapi.json`)
|
|
87
96
|
expect(res.status).toBe(200)
|
|
88
|
-
expect(
|
|
89
|
-
|
|
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
|
-
"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 /
|
|
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}/
|
|
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
|
-
-
|
|
201
|
-
-
|
|
174
|
+
- OpenAI: Chat completions, embeddings, image generation, and audio transcription.
|
|
175
|
+
- Anthropic: Claude language models for messages and completions.
|
|
202
176
|
|
|
203
|
-
[
|
|
177
|
+
[OpenAPI discovery](/openapi.json)"
|
|
204
178
|
`)
|
|
205
179
|
})
|
|
206
180
|
|
|
207
|
-
test('behavior: GET /
|
|
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}/
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 () => {
|
package/src/proxy/Proxy.ts
CHANGED
|
@@ -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 === '/
|
|
86
|
+
(pathname === '/openapi.json' || pathname === '/openapi.json/')
|
|
96
87
|
) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
if (
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
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 '
|
|
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 () => ({
|