mppx 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- 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/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.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/server/Charge.test.ts +1 -1
- 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 +87 -37
- package/src/tempo/server/Session.ts +25 -8
- 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
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import type { DiscoveryHandler } from '../../discovery/OpenApi.js'
|
|
1
2
|
import type * as Method from '../../Method.js'
|
|
2
3
|
import type * as Mppx from '../../server/Mppx.js'
|
|
3
4
|
|
|
4
5
|
export type AnyMethodFn = Mppx.AnyMethodFn
|
|
5
6
|
export type AnyServer = Method.AnyServer
|
|
6
7
|
|
|
8
|
+
type DiscoveryMeta = Pick<DiscoveryHandler, '_internal'>
|
|
9
|
+
|
|
7
10
|
/** Recursively wraps nested handler objects one level deep. */
|
|
8
11
|
type WrapNested<obj, handler> = {
|
|
9
12
|
[key in keyof obj]: obj[key] extends (options: infer options) => any
|
|
10
|
-
? (o: options) => handler
|
|
13
|
+
? (o: options) => handler & DiscoveryMeta
|
|
11
14
|
: obj[key]
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -21,7 +24,7 @@ export type Wrap<mppx, handler> = {
|
|
|
21
24
|
| 'transport'
|
|
22
25
|
? mppx[key]
|
|
23
26
|
: mppx[key] extends (options: infer options) => any
|
|
24
|
-
? (o: options) => handler
|
|
27
|
+
? (o: options) => handler & DiscoveryMeta
|
|
25
28
|
: mppx[key] extends Record<string, (options: any) => any>
|
|
26
29
|
? WrapNested<mppx[key], handler>
|
|
27
30
|
: mppx[key]
|
|
@@ -43,14 +46,19 @@ export function wrap<mppx extends Mppx.Mppx<any, any>, handler>(
|
|
|
43
46
|
for (const mi of mppx.methods as readonly Method.AnyServer[]) {
|
|
44
47
|
const key = `${mi.name}/${mi.intent}`
|
|
45
48
|
const methodFn = (mppx as any)[key]
|
|
46
|
-
|
|
49
|
+
const wrapWithMeta = (options: any) => {
|
|
50
|
+
const configured = methodFn(options)
|
|
51
|
+
const handler = wrapper(methodFn, options) as any
|
|
52
|
+
if (configured._internal) handler._internal = configured._internal
|
|
53
|
+
return handler
|
|
54
|
+
}
|
|
55
|
+
result[key] = wrapWithMeta
|
|
47
56
|
// Also set shorthand intent key if Mppx registered it (no collision)
|
|
48
|
-
if ((mppx as any)[mi.intent]) result[mi.intent] =
|
|
57
|
+
if ((mppx as any)[mi.intent]) result[mi.intent] = wrapWithMeta
|
|
49
58
|
// Build nested handlers: wrapped.tempo.charge(...)
|
|
50
59
|
if (!result[mi.name] || typeof result[mi.name] !== 'object')
|
|
51
60
|
result[mi.name] = {} as Record<string, unknown>
|
|
52
|
-
;(result[mi.name] as Record<string, unknown>)[mi.intent] =
|
|
53
|
-
wrapper(methodFn, options)
|
|
61
|
+
;(result[mi.name] as Record<string, unknown>)[mi.intent] = wrapWithMeta
|
|
54
62
|
}
|
|
55
63
|
return result as never
|
|
56
64
|
}
|
|
@@ -2,11 +2,11 @@ import * as http from 'node:http'
|
|
|
2
2
|
|
|
3
3
|
import { Receipt } from 'mppx'
|
|
4
4
|
import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
|
|
5
|
-
import { Mppx } from 'mppx/nextjs'
|
|
5
|
+
import { Mppx, discovery } from 'mppx/nextjs'
|
|
6
6
|
import { tempo as tempo_server } from 'mppx/server'
|
|
7
7
|
import type { Address } from 'viem'
|
|
8
8
|
import { Addresses } from 'viem/tempo'
|
|
9
|
-
import { beforeAll, describe, expect, test } from '
|
|
9
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
10
10
|
import { deployEscrow } from '~test/tempo/session.js'
|
|
11
11
|
import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ describe('charge', () => {
|
|
|
42
42
|
tempo_server.charge({
|
|
43
43
|
getClient: () => client,
|
|
44
44
|
currency: asset,
|
|
45
|
-
|
|
45
|
+
account: accounts[0],
|
|
46
46
|
}),
|
|
47
47
|
],
|
|
48
48
|
secretKey,
|
|
@@ -89,6 +89,31 @@ describe('charge', () => {
|
|
|
89
89
|
|
|
90
90
|
server.close()
|
|
91
91
|
})
|
|
92
|
+
|
|
93
|
+
test('serves /openapi.json from a handler-derived route config', async () => {
|
|
94
|
+
const pay = mppx.charge({ amount: '1' })
|
|
95
|
+
const server = await createServer(
|
|
96
|
+
discovery(mppx, {
|
|
97
|
+
info: { title: 'Next API', version: '3.0.0' },
|
|
98
|
+
routes: [{ handler: pay, method: 'get', path: '/' }],
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const response = await globalThis.fetch(server.url)
|
|
103
|
+
expect(response.status).toBe(200)
|
|
104
|
+
expect(response.headers.get('cache-control')).toBe('public, max-age=300')
|
|
105
|
+
|
|
106
|
+
const body = (await response.json()) as Record<string, any>
|
|
107
|
+
expect(body.info).toEqual({ title: 'Next API', version: '3.0.0' })
|
|
108
|
+
expect(body.paths['/'].get['x-payment-info']).toMatchObject({
|
|
109
|
+
amount: '1000000',
|
|
110
|
+
currency: asset,
|
|
111
|
+
intent: 'charge',
|
|
112
|
+
method: 'tempo',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
server.close()
|
|
116
|
+
})
|
|
92
117
|
})
|
|
93
118
|
|
|
94
119
|
describe('session', () => {
|
|
@@ -105,7 +130,7 @@ describe('session', () => {
|
|
|
105
130
|
methods: [
|
|
106
131
|
tempo_server.session({
|
|
107
132
|
getClient: () => client,
|
|
108
|
-
|
|
133
|
+
account: accounts[0],
|
|
109
134
|
currency: asset,
|
|
110
135
|
escrowContract,
|
|
111
136
|
}),
|
|
@@ -130,10 +155,10 @@ describe('session', () => {
|
|
|
130
155
|
methods: [
|
|
131
156
|
tempo_server.session({
|
|
132
157
|
getClient: () => client,
|
|
133
|
-
|
|
158
|
+
account: accounts[0],
|
|
134
159
|
currency: asset,
|
|
135
160
|
escrowContract,
|
|
136
|
-
feePayer:
|
|
161
|
+
feePayer: true,
|
|
137
162
|
}),
|
|
138
163
|
],
|
|
139
164
|
secretKey,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
|
|
1
2
|
import * as Mppx_core from '../server/Mppx.js'
|
|
2
3
|
import * as Mppx_internal from './internal/mppx.js'
|
|
3
4
|
|
|
@@ -64,3 +65,30 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
64
65
|
return result.withReceipt(response)
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
|
|
70
|
+
routes?: RouteConfig[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a route handler that serves an OpenAPI discovery document.
|
|
77
|
+
*/
|
|
78
|
+
export function discovery(
|
|
79
|
+
mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
|
|
80
|
+
config: DiscoveryConfig = {},
|
|
81
|
+
): RouteHandler {
|
|
82
|
+
const cached = JSON.stringify(
|
|
83
|
+
generate(mppx, {
|
|
84
|
+
...(config.info ? { info: config.info } : {}),
|
|
85
|
+
routes: config.routes ?? [],
|
|
86
|
+
...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return () =>
|
|
91
|
+
new Response(cached, {
|
|
92
|
+
headers: { ...discoveryHeaders, 'Content-Type': 'application/json' },
|
|
93
|
+
})
|
|
94
|
+
}
|
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 () => {
|