@tangle-network/agent-gateway 0.6.0 → 0.7.1
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/README.md +31 -6
- package/dist/chunk-Q4YAIEZY.js +1763 -0
- package/dist/chunk-Q4YAIEZY.js.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.js +112 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +1 -1
- package/dist/types-DEsMmS-X.d.ts +875 -0
- package/dist/types.d.ts +1 -1
- package/package.json +14 -10
- package/src/a2a/agent-card.ts +55 -0
- package/src/a2a/handler.ts +797 -0
- package/src/a2a/jsonrpc.ts +65 -0
- package/src/a2a/push-notifications.ts +299 -0
- package/src/a2a/task-store-sql.ts +189 -0
- package/src/a2a/task-store.ts +53 -0
- package/src/a2a/translate.ts +77 -0
- package/src/a2a/types.ts +217 -0
- package/src/dispatch.ts +486 -0
- package/src/index.ts +58 -1
- package/src/middleware.ts +139 -294
- package/src/types.ts +76 -2
- package/src/verify.ts +93 -26
- package/dist/chunk-373QHRKV.js +0 -635
- package/dist/chunk-373QHRKV.js.map +0 -1
- package/dist/types-C_L7yXXI.d.ts +0 -362
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export { createAgentGateway } from './middleware'
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
verifyX402,
|
|
4
|
+
verifyMpp,
|
|
5
|
+
defaultVerifyApiKey,
|
|
6
|
+
isApiKeyAuthEnabled,
|
|
7
|
+
isMppAuthEnabled,
|
|
8
|
+
} from './verify'
|
|
3
9
|
export {
|
|
4
10
|
filterConsumerMessages,
|
|
5
11
|
filterConsumerMessagesStrict,
|
|
@@ -57,3 +63,54 @@ export type {
|
|
|
57
63
|
ChatCompletionRequest,
|
|
58
64
|
ChatCompletionChunk,
|
|
59
65
|
} from './types'
|
|
66
|
+
|
|
67
|
+
// --- A2A protocol surface (Google Agent-to-Agent) ---
|
|
68
|
+
// Types + task-store adapter. Handlers are wired automatically by
|
|
69
|
+
// createAgentGateway when `GatewayConfig.a2a` (or its default) is honored;
|
|
70
|
+
// consumers only import these to BYO a durable TaskStore (D1, postgres, DO)
|
|
71
|
+
// or to declare richer AgentMeta.skills for the Agent Card.
|
|
72
|
+
export { InMemoryTaskStore, type TaskStore } from './a2a/task-store'
|
|
73
|
+
export {
|
|
74
|
+
type D1DatabaseLike,
|
|
75
|
+
type D1StmtLike,
|
|
76
|
+
d1ToSqlAdapter,
|
|
77
|
+
type SqlAdapter,
|
|
78
|
+
SqlTaskStore,
|
|
79
|
+
} from './a2a/task-store-sql'
|
|
80
|
+
export {
|
|
81
|
+
deliverPushNotifications,
|
|
82
|
+
InMemoryPushNotificationStore,
|
|
83
|
+
type PushDeliveryResult,
|
|
84
|
+
type PushNotificationAuthentication,
|
|
85
|
+
type PushNotificationConfig,
|
|
86
|
+
type PushNotificationStore,
|
|
87
|
+
SqlPushNotificationStore,
|
|
88
|
+
type TaskPushNotificationConfig,
|
|
89
|
+
} from './a2a/push-notifications'
|
|
90
|
+
export type {
|
|
91
|
+
AgentCard,
|
|
92
|
+
AgentCapabilities,
|
|
93
|
+
AgentCardAuthentication,
|
|
94
|
+
AgentProvider,
|
|
95
|
+
AgentSkill,
|
|
96
|
+
Artifact,
|
|
97
|
+
DataPart,
|
|
98
|
+
FilePart,
|
|
99
|
+
JSONRPCErrorResponse,
|
|
100
|
+
JSONRPCRequest,
|
|
101
|
+
JSONRPCResponse,
|
|
102
|
+
JSONRPCSuccessResponse,
|
|
103
|
+
Message,
|
|
104
|
+
MessageSendParams,
|
|
105
|
+
Part,
|
|
106
|
+
StreamingEvent,
|
|
107
|
+
Task,
|
|
108
|
+
TaskArtifactUpdateEvent,
|
|
109
|
+
TaskIdParams,
|
|
110
|
+
TaskPushNotificationConfigGetParams,
|
|
111
|
+
TaskState,
|
|
112
|
+
TaskStatus,
|
|
113
|
+
TaskStatusUpdateEvent,
|
|
114
|
+
TextPart,
|
|
115
|
+
} from './a2a/types'
|
|
116
|
+
export { A2A_ERROR_CODES } from './a2a/types'
|
package/src/middleware.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
|
|
3
|
+
import { createA2AHandlers } from './a2a/handler'
|
|
4
|
+
import { InMemoryTaskStore } from './a2a/task-store'
|
|
5
|
+
import {
|
|
6
|
+
type AuthorizedRequest,
|
|
7
|
+
type GatewayState,
|
|
8
|
+
authenticateAndGuard,
|
|
9
|
+
dispatchSandboxStream,
|
|
10
|
+
estimateTokens,
|
|
11
|
+
settleAndRecord,
|
|
12
|
+
} from './dispatch'
|
|
12
13
|
import { MemoryNonceStore } from './nonce-store'
|
|
13
|
-
import {
|
|
14
|
+
import { type GatewayObserver, type RequestContext, generateRequestId } from './observer'
|
|
15
|
+
import { MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
|
|
16
|
+
import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from './types'
|
|
17
|
+
import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Create a Hono router that serves the agent gateway.
|
|
@@ -19,7 +23,7 @@ import { generateRequestId, type GatewayObserver, type RequestContext } from './
|
|
|
19
23
|
* app.route('/v1/agents', createAgentGateway(config))
|
|
20
24
|
*
|
|
21
25
|
* Exposes:
|
|
22
|
-
* GET /:slug/chat/completions — agent discovery metadata
|
|
26
|
+
* GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
|
|
23
27
|
* POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
|
|
24
28
|
*/
|
|
25
29
|
export function createAgentGateway(config: GatewayConfig) {
|
|
@@ -32,19 +36,23 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
32
36
|
)
|
|
33
37
|
}
|
|
34
38
|
const gw = new Hono()
|
|
35
|
-
const maxLen = config.maxMessageLength ?? 8000
|
|
36
39
|
const rateLimitStore: RateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore()
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const state: GatewayState = {
|
|
41
|
+
rateLimitStore,
|
|
42
|
+
nonceStore: config.nonceStore ?? new MemoryNonceStore(),
|
|
43
|
+
globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
|
|
44
|
+
requiredScope: config.requiredScope ?? 'chat',
|
|
45
|
+
maxLen: config.maxMessageLength ?? 8000,
|
|
46
|
+
obs: config.observer,
|
|
47
|
+
}
|
|
48
|
+
const obs: GatewayObserver | undefined = state.obs
|
|
41
49
|
|
|
42
50
|
// --- Discovery endpoint (no auth) ---
|
|
43
51
|
|
|
44
52
|
gw.get('/:slug/chat/completions', async (c) => {
|
|
45
53
|
const slug = c.req.param('slug')
|
|
46
54
|
const agent = await config.resolveAgent(slug)
|
|
47
|
-
if (!agent) return c.json({ error: 'Agent not found or not published' }, 404)
|
|
55
|
+
if (!agent || !agent.enabled) return c.json({ error: 'Agent not found or not published' }, 404)
|
|
48
56
|
|
|
49
57
|
const paymentMethods: Array<Record<string, unknown>> = [
|
|
50
58
|
{
|
|
@@ -54,14 +62,14 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
54
62
|
credits_contract: config.x402.creditsAddress,
|
|
55
63
|
},
|
|
56
64
|
]
|
|
57
|
-
if (config
|
|
65
|
+
if (isMppAuthEnabled(config)) {
|
|
58
66
|
paymentMethods.push({
|
|
59
67
|
type: 'mpp',
|
|
60
|
-
realm: config.mpp
|
|
61
|
-
method: config.mpp
|
|
68
|
+
realm: config.mpp!.realm,
|
|
69
|
+
method: config.mpp!.method ?? 'blueprintevm',
|
|
62
70
|
})
|
|
63
71
|
}
|
|
64
|
-
paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
|
|
72
|
+
if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
|
|
65
73
|
|
|
66
74
|
return c.json({
|
|
67
75
|
slug: agent.slug,
|
|
@@ -84,24 +92,22 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
84
92
|
|
|
85
93
|
gw.post('/:slug/chat/completions', async (c) => {
|
|
86
94
|
const slug = c.req.param('slug')
|
|
87
|
-
const startMs = Date.now()
|
|
88
|
-
const requestId = generateRequestId()
|
|
89
|
-
const ctx: RequestContext = { requestId, agentSlug: slug, startMs }
|
|
90
|
-
|
|
91
|
-
await obs?.onRequestStart?.(ctx)
|
|
92
95
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
if (!agent) {
|
|
96
|
-
return c.json({ error: { message: 'Agent not found', type: 'not_found' } }, 404)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 2. Body size limit (before parsing — DoS prevention)
|
|
100
|
-
const contentLength = parseInt(c.req.header('Content-Length') ?? '0', 10)
|
|
96
|
+
// Body size limit (before parsing — DoS prevention).
|
|
97
|
+
const contentLength = Number.parseInt(c.req.header('Content-Length') ?? '0', 10)
|
|
101
98
|
if (contentLength > 65536) {
|
|
102
|
-
|
|
99
|
+
const requestId = generateRequestId()
|
|
100
|
+
await obs?.onBodyTooLarge?.(
|
|
101
|
+
{ requestId, agentSlug: slug, startMs: Date.now() },
|
|
102
|
+
contentLength,
|
|
103
|
+
)
|
|
103
104
|
return c.json(
|
|
104
|
-
{
|
|
105
|
+
{
|
|
106
|
+
error: {
|
|
107
|
+
message: 'Request body too large (max 64KB)',
|
|
108
|
+
type: 'invalid_request',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
105
111
|
413,
|
|
106
112
|
)
|
|
107
113
|
}
|
|
@@ -113,277 +119,116 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
113
119
|
return c.json({ error: { message: 'Invalid JSON', type: 'invalid_request' } }, 400)
|
|
114
120
|
}
|
|
115
121
|
if (!body.messages?.length) {
|
|
116
|
-
return c.json({ error: { message: 'messages array required', type: 'invalid_request' } }, 400)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 3. Authenticate — x402 SpendAuth, MPP, or API key
|
|
120
|
-
const spendAuthHeader = c.req.header('X-Payment-Signature')
|
|
121
|
-
const authHeader = c.req.header('Authorization') ?? ''
|
|
122
|
-
let consumerId: string | null = null
|
|
123
|
-
let paymentMethod: PaymentMethod = 'none'
|
|
124
|
-
let keyInfo: ApiKeyInfo | null = null
|
|
125
|
-
|
|
126
|
-
if (spendAuthHeader) {
|
|
127
|
-
const signer = await verifyX402(spendAuthHeader, config.x402, nonceStore)
|
|
128
|
-
if (!signer) {
|
|
129
|
-
await obs?.onAuthFailure?.(ctx, { method: 'x402', code: 'invalid_spend_auth', httpStatus: 402 })
|
|
130
|
-
return c.json(
|
|
131
|
-
{ error: { message: 'Invalid X-Payment-Signature', type: 'authentication_error', code: 'invalid_spend_auth' } },
|
|
132
|
-
{ status: 402, headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': requestId } },
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
consumerId = signer
|
|
136
|
-
paymentMethod = 'x402'
|
|
137
|
-
} else if (config.mpp && authHeader.toLowerCase().startsWith('payment ')) {
|
|
138
|
-
const signer = await verifyMpp(authHeader, config.mpp, config.x402)
|
|
139
|
-
if (!signer) {
|
|
140
|
-
const realm = config.mpp.realm
|
|
141
|
-
const method = config.mpp.method ?? 'blueprintevm'
|
|
142
|
-
await obs?.onAuthFailure?.(ctx, { method: 'mpp', code: 'invalid_mpp_credential', httpStatus: 401 })
|
|
143
|
-
return c.json(
|
|
144
|
-
{ error: { message: 'Invalid Payment credential', type: 'authentication_error', code: 'invalid_mpp_credential' } },
|
|
145
|
-
{ status: 401, headers: { 'WWW-Authenticate': `Payment realm="${realm}", method="${method}"`, 'X-Request-Id': requestId } },
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
consumerId = signer
|
|
149
|
-
paymentMethod = 'mpp'
|
|
150
|
-
} else if (authHeader.startsWith('Bearer ')) {
|
|
151
|
-
const verify = config.verifyApiKey ?? defaultVerifyApiKey
|
|
152
|
-
const key = await verify(authHeader)
|
|
153
|
-
if (!key) {
|
|
154
|
-
await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'invalid_api_key', httpStatus: 401 })
|
|
155
|
-
return c.json(
|
|
156
|
-
{ error: { message: 'Invalid API key', type: 'authentication_error' } },
|
|
157
|
-
{ status: 401, headers: { 'X-Request-Id': requestId } },
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Scope enforcement — API key must include the required scope
|
|
162
|
-
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(requiredScope)) {
|
|
163
|
-
await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'insufficient_scope', httpStatus: 403 })
|
|
164
|
-
return c.json(
|
|
165
|
-
{ error: { message: `API key missing required scope: ${requiredScope}`, type: 'forbidden', code: 'insufficient_scope' } },
|
|
166
|
-
{ status: 403, headers: { 'X-Request-Id': requestId } },
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
consumerId = key.consumerId
|
|
171
|
-
paymentMethod = 'apikey'
|
|
172
|
-
keyInfo = key
|
|
173
|
-
} else {
|
|
174
|
-
// No payment — return 402 with instructions
|
|
175
|
-
await obs?.onAuthFailure?.(ctx, { method: 'none', code: 'payment_required', httpStatus: 402 })
|
|
176
|
-
const methods: string[] = ['x402']
|
|
177
|
-
if (config.mpp) methods.push('mpp')
|
|
178
|
-
methods.push('api_key')
|
|
179
|
-
|
|
180
|
-
const headers: Record<string, string> = {
|
|
181
|
-
'X-Payment-Required': methods.join(', '),
|
|
182
|
-
'X-Request-Id': requestId,
|
|
183
|
-
}
|
|
184
|
-
if (config.mpp) {
|
|
185
|
-
headers['WWW-Authenticate'] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? 'blueprintevm'}"`
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return c.json({
|
|
189
|
-
error: {
|
|
190
|
-
message: 'Payment required',
|
|
191
|
-
type: 'payment_required',
|
|
192
|
-
payment_methods: methods,
|
|
193
|
-
x402: {
|
|
194
|
-
operator: config.x402.operatorAddress,
|
|
195
|
-
chain_id: config.x402.chainId,
|
|
196
|
-
credits_address: config.x402.creditsAddress,
|
|
197
|
-
estimated_amount_per_request: '20000',
|
|
198
|
-
},
|
|
199
|
-
...(config.mpp ? {
|
|
200
|
-
mpp: { realm: config.mpp.realm, method: config.mpp.method ?? 'blueprintevm' },
|
|
201
|
-
} : {}),
|
|
202
|
-
api_key: {
|
|
203
|
-
purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : undefined,
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
}, { status: 402, headers })
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
await obs?.onPaymentVerified?.(ctx, { method: paymentMethod, consumerId: consumerId!, keyId: keyInfo?.keyId })
|
|
210
|
-
|
|
211
|
-
// 4. Rate limit — per-key override or global
|
|
212
|
-
const effectiveRateLimit = keyInfo?.rateLimitPerMinute
|
|
213
|
-
? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 }
|
|
214
|
-
: globalRateLimit
|
|
215
|
-
|
|
216
|
-
const rl = await checkRateLimit(consumerId!, effectiveRateLimit, rateLimitStore)
|
|
217
|
-
if (!rl.allowed) {
|
|
218
|
-
await obs?.onRateLimited?.(ctx, { consumerId: consumerId!, retryAfterSeconds: rl.retryAfterSeconds ?? 60 })
|
|
219
122
|
return c.json(
|
|
220
|
-
{ error: { message: '
|
|
221
|
-
|
|
123
|
+
{ error: { message: 'messages array required', type: 'invalid_request' } },
|
|
124
|
+
400,
|
|
222
125
|
)
|
|
223
126
|
}
|
|
224
127
|
|
|
225
|
-
|
|
226
|
-
|
|
128
|
+
const guard = await authenticateAndGuard(c, slug, body.messages, config, state)
|
|
129
|
+
if (guard instanceof Response) return guard
|
|
130
|
+
const authz = guard
|
|
227
131
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
consumerId: consumerId!,
|
|
231
|
-
patterns: injectionWarnings,
|
|
232
|
-
blocked: !!config.blockInjection,
|
|
233
|
-
})
|
|
132
|
+
return streamChatCompletions(c, authz, config, obs)
|
|
133
|
+
})
|
|
234
134
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
135
|
+
// --- A2A protocol surface (Google Agent-to-Agent, JSON-RPC 2.0 + AgentCard) ---
|
|
136
|
+
// Mounted alongside the OpenAI-compat routes so a single agent speaks both.
|
|
137
|
+
// Both surfaces share authenticateAndGuard + dispatchSandboxStream +
|
|
138
|
+
// settleAndRecord, so every security and billing guarantee applies uniformly
|
|
139
|
+
// regardless of which protocol the caller used.
|
|
140
|
+
const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore()
|
|
141
|
+
const pushStore = config.a2a?.pushStore
|
|
142
|
+
const a2a = createA2AHandlers({ config, state, taskStore, pushStore })
|
|
143
|
+
gw.get('/:slug/.well-known/agent.json', a2a.handleAgentCard)
|
|
144
|
+
gw.post('/:slug', a2a.handleJsonRpc)
|
|
243
145
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.map((m) => m.content)
|
|
247
|
-
.join('\n\n')
|
|
146
|
+
return gw
|
|
147
|
+
}
|
|
248
148
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Drain the sandbox stream into an OpenAI-shaped SSE response, settle the
|
|
151
|
+
* payment, fire observer hooks. Identical pre-refactor behavior, just lifted
|
|
152
|
+
* out of the handler so the A2A wrapper can reach the same dispatch path
|
|
153
|
+
* without duplicating it.
|
|
154
|
+
*/
|
|
155
|
+
function streamChatCompletions(
|
|
156
|
+
c: import('hono').Context,
|
|
157
|
+
authz: AuthorizedRequest,
|
|
158
|
+
config: GatewayConfig,
|
|
159
|
+
obs: GatewayObserver | undefined,
|
|
160
|
+
): Response {
|
|
161
|
+
const { agent, consumerId, paymentMethod, requestId, userMessage, rateLimitRemaining } = authz
|
|
162
|
+
const inputTokens = estimateTokens(userMessage)
|
|
163
|
+
let outputTokens = 0
|
|
164
|
+
const ctx: RequestContext = {
|
|
165
|
+
requestId,
|
|
166
|
+
agentSlug: agent.slug,
|
|
167
|
+
startMs: authz.startMs,
|
|
168
|
+
}
|
|
252
169
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
message: authz.reason,
|
|
267
|
-
type: 'authorization_denied',
|
|
268
|
-
code: authz.code,
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
{ status: 403, headers: { 'X-Request-Id': requestId } },
|
|
272
|
-
)
|
|
170
|
+
const stream = new ReadableStream({
|
|
171
|
+
async start(controller) {
|
|
172
|
+
const encoder = new TextEncoder()
|
|
173
|
+
const sendChunk = (delta: string) => {
|
|
174
|
+
outputTokens += estimateTokens(delta)
|
|
175
|
+
const chunk: ChatCompletionChunk = {
|
|
176
|
+
id: `chatcmpl-${Date.now()}`,
|
|
177
|
+
object: 'chat.completion.chunk',
|
|
178
|
+
created: Math.floor(Date.now() / 1000),
|
|
179
|
+
model: agent.slug,
|
|
180
|
+
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],
|
|
181
|
+
}
|
|
182
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`))
|
|
273
183
|
}
|
|
274
|
-
}
|
|
275
184
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const stream = new ReadableStream({
|
|
281
|
-
async start(controller) {
|
|
282
|
-
const encoder = new TextEncoder()
|
|
283
|
-
const sendChunk = (rawDelta: string) => {
|
|
284
|
-
// Redact system prompt leakage from output
|
|
285
|
-
const delta = redactSystemPromptFromOutput(rawDelta, agent.systemPrompt)
|
|
286
|
-
outputTokens += Math.ceil(delta.length / 4)
|
|
287
|
-
const chunk: ChatCompletionChunk = {
|
|
288
|
-
id: `chatcmpl-${Date.now()}`,
|
|
289
|
-
object: 'chat.completion.chunk',
|
|
290
|
-
created: Math.floor(Date.now() / 1000),
|
|
291
|
-
model: agent.slug,
|
|
292
|
-
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],
|
|
293
|
-
}
|
|
294
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`))
|
|
185
|
+
try {
|
|
186
|
+
for await (const delta of dispatchSandboxStream(agent, userMessage, consumerId, config)) {
|
|
187
|
+
sendChunk(delta)
|
|
295
188
|
}
|
|
296
189
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for await (const event of promptStream) {
|
|
305
|
-
if (
|
|
306
|
-
event.type === 'message.part.updated' &&
|
|
307
|
-
event.data?.part?.type === 'text' &&
|
|
308
|
-
event.data.delta
|
|
309
|
-
) {
|
|
310
|
-
sendChunk(event.data.delta)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Final chunk
|
|
315
|
-
const done: ChatCompletionChunk = {
|
|
316
|
-
id: `chatcmpl-${Date.now()}`,
|
|
317
|
-
object: 'chat.completion.chunk',
|
|
318
|
-
created: Math.floor(Date.now() / 1000),
|
|
319
|
-
model: agent.slug,
|
|
320
|
-
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
321
|
-
}
|
|
322
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
|
|
323
|
-
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
324
|
-
|
|
325
|
-
// 7. Record usage + settle payment
|
|
326
|
-
const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd
|
|
327
|
-
const ownerEarned = totalCost * (1 - agent.platformFeePercent)
|
|
328
|
-
const platformFee = totalCost * agent.platformFeePercent
|
|
329
|
-
|
|
330
|
-
const usageEvent = {
|
|
331
|
-
requestId: ctx.requestId,
|
|
332
|
-
agentId: agent.id,
|
|
333
|
-
agentSlug: agent.slug,
|
|
334
|
-
consumerId: consumerId!,
|
|
335
|
-
paymentMethod,
|
|
336
|
-
inputTokens,
|
|
337
|
-
outputTokens,
|
|
338
|
-
totalCostUsd: totalCost,
|
|
339
|
-
ownerEarnedUsd: ownerEarned,
|
|
340
|
-
platformFeeUsd: platformFee,
|
|
341
|
-
durationMs: Date.now() - startMs,
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
await config.recordUsage(usageEvent)
|
|
345
|
-
await obs?.onRequestComplete?.(ctx, usageEvent)
|
|
346
|
-
|
|
347
|
-
if (config.settlePayment) {
|
|
348
|
-
await config.settlePayment(
|
|
349
|
-
{ method: paymentMethod, consumerId: consumerId!, requestId: ctx.requestId },
|
|
350
|
-
totalCost,
|
|
351
|
-
).catch(async err => {
|
|
352
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
353
|
-
console.error(`[agent-gateway] settlement failed for ${consumerId}: ${msg}`)
|
|
354
|
-
await obs?.onSettlementError?.(ctx, { consumerId: consumerId!, method: paymentMethod, errorMessage: msg })
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
// Sanitize error — never expose stack traces or internal paths
|
|
359
|
-
const rawMessage = err instanceof Error ? err.message : String(err)
|
|
360
|
-
const safeMessage =
|
|
361
|
-
rawMessage.includes('/') || rawMessage.includes('\\')
|
|
362
|
-
? 'Internal agent error'
|
|
363
|
-
: rawMessage
|
|
364
|
-
await obs?.onStreamError?.(ctx, { consumerId: consumerId!, errorMessage: rawMessage })
|
|
365
|
-
controller.enqueue(
|
|
366
|
-
encoder.encode(`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`),
|
|
367
|
-
)
|
|
368
|
-
} finally {
|
|
369
|
-
controller.close()
|
|
190
|
+
const done: ChatCompletionChunk = {
|
|
191
|
+
id: `chatcmpl-${Date.now()}`,
|
|
192
|
+
object: 'chat.completion.chunk',
|
|
193
|
+
created: Math.floor(Date.now() / 1000),
|
|
194
|
+
model: agent.slug,
|
|
195
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
370
196
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
197
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
|
|
198
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
199
|
+
|
|
200
|
+
await settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const rawMessage = err instanceof Error ? err.message : String(err)
|
|
203
|
+
// Never expose stack traces / absolute paths from sandbox internals.
|
|
204
|
+
const safeMessage =
|
|
205
|
+
rawMessage.includes('/') || rawMessage.includes('\\')
|
|
206
|
+
? 'Internal agent error'
|
|
207
|
+
: rawMessage
|
|
208
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage })
|
|
209
|
+
controller.enqueue(
|
|
210
|
+
encoder.encode(
|
|
211
|
+
`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
} finally {
|
|
215
|
+
controller.close()
|
|
216
|
+
}
|
|
217
|
+
},
|
|
386
218
|
})
|
|
387
219
|
|
|
388
|
-
return
|
|
220
|
+
return new Response(stream, {
|
|
221
|
+
headers: {
|
|
222
|
+
'Content-Type': 'text/event-stream',
|
|
223
|
+
'Cache-Control': 'no-cache',
|
|
224
|
+
'X-Request-Id': requestId,
|
|
225
|
+
'X-Agent-Slug': agent.slug,
|
|
226
|
+
'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
|
|
227
|
+
'X-Payment-Method': paymentMethod,
|
|
228
|
+
'X-Payment-Settled': paymentMethod === 'x402' ? 'pending' : 'true',
|
|
229
|
+
...(rateLimitRemaining !== undefined
|
|
230
|
+
? { 'X-RateLimit-Remaining': String(rateLimitRemaining) }
|
|
231
|
+
: {}),
|
|
232
|
+
},
|
|
233
|
+
})
|
|
389
234
|
}
|