agentic-server 0.25.2 → 0.27.0
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/esm/router.js +32 -16
- package/esm/server.js +9 -2
- package/index.d.ts +1 -1
- package/package.json +3 -3
- package/router.d.ts +8 -1
- package/router.js +34 -17
- package/server.js +9 -2
- package/types.d.ts +12 -0
package/esm/router.js
CHANGED
|
@@ -46,6 +46,25 @@ function upstreamErrorMessage(provider, status, body) {
|
|
|
46
46
|
? `${provider.type} provider error ${status}: ${reason}`
|
|
47
47
|
: `${provider.type} provider error ${status}`;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Who the call is for and which task it is a cost of. Identity comes from the
|
|
51
|
+
* caller's identity headers; task linkage from the correlation headers the
|
|
52
|
+
* runtimes forward (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`).
|
|
53
|
+
* `X-Attempt` must be a non-negative integer or it is dropped.
|
|
54
|
+
*/
|
|
55
|
+
export const readAttribution = (req) => {
|
|
56
|
+
const rawAttempt = req.get('X-Attempt');
|
|
57
|
+
const attempt = typeof rawAttempt === 'string' && /^\d+$/.test(rawAttempt) ? Number(rawAttempt) : undefined;
|
|
58
|
+
return {
|
|
59
|
+
databaseId: req.get('X-Database-Id'),
|
|
60
|
+
entityId: req.get('X-Entity-Id'),
|
|
61
|
+
actorId: req.get('X-Actor-Id'),
|
|
62
|
+
invocationId: req.get('X-Invocation-Id'),
|
|
63
|
+
jobId: req.get('X-Job-Id'),
|
|
64
|
+
attempt,
|
|
65
|
+
runId: req.get('X-Run-Id')
|
|
66
|
+
};
|
|
67
|
+
};
|
|
49
68
|
export const createRouter = (options) => {
|
|
50
69
|
const router = Router();
|
|
51
70
|
// Metering is backend-agnostic: the caller injects an InferenceSink. When
|
|
@@ -58,8 +77,8 @@ export const createRouter = (options) => {
|
|
|
58
77
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
59
78
|
return;
|
|
60
79
|
}
|
|
61
|
-
const
|
|
62
|
-
const
|
|
80
|
+
const attribution = readAttribution(req);
|
|
81
|
+
const { entityId } = attribution;
|
|
63
82
|
const requestProvider = req.get('X-LLM-Provider');
|
|
64
83
|
const startTime = process.hrtime.bigint();
|
|
65
84
|
const provider = resolveProvider(options, req.body?.model, requestProvider);
|
|
@@ -109,7 +128,7 @@ export const createRouter = (options) => {
|
|
|
109
128
|
log.error('upstream error', { status: upstream.status, provider: provider.type, body: text });
|
|
110
129
|
if (sink) {
|
|
111
130
|
sink.logInference({
|
|
112
|
-
|
|
131
|
+
...attribution,
|
|
113
132
|
model: String(req.body?.model || body.model || ''),
|
|
114
133
|
provider: provider.type,
|
|
115
134
|
service: 'chat',
|
|
@@ -138,7 +157,7 @@ export const createRouter = (options) => {
|
|
|
138
157
|
});
|
|
139
158
|
if (sink) {
|
|
140
159
|
sink.logInference({
|
|
141
|
-
|
|
160
|
+
...attribution,
|
|
142
161
|
model: String(req.body?.model || body.model || ''),
|
|
143
162
|
provider: provider.type,
|
|
144
163
|
service: 'chat',
|
|
@@ -164,7 +183,7 @@ export const createRouter = (options) => {
|
|
|
164
183
|
});
|
|
165
184
|
if (sink) {
|
|
166
185
|
sink.logInference({
|
|
167
|
-
|
|
186
|
+
...attribution,
|
|
168
187
|
model: String(req.body?.model || body.model || ''),
|
|
169
188
|
provider: provider.type,
|
|
170
189
|
service: 'chat',
|
|
@@ -184,7 +203,7 @@ export const createRouter = (options) => {
|
|
|
184
203
|
log.error('chat/completions error', err);
|
|
185
204
|
if (sink) {
|
|
186
205
|
sink.logInference({
|
|
187
|
-
|
|
206
|
+
...attribution,
|
|
188
207
|
model: String(req.body?.model || ''),
|
|
189
208
|
provider: provider.type,
|
|
190
209
|
service: 'chat',
|
|
@@ -213,8 +232,8 @@ export const createRouter = (options) => {
|
|
|
213
232
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
214
233
|
return;
|
|
215
234
|
}
|
|
216
|
-
const
|
|
217
|
-
const
|
|
235
|
+
const attribution = readAttribution(req);
|
|
236
|
+
const { entityId } = attribution;
|
|
218
237
|
const requestProvider = req.get('X-LLM-Provider');
|
|
219
238
|
const startTime = process.hrtime.bigint();
|
|
220
239
|
const provider = resolveProvider(options, req.body?.model, requestProvider);
|
|
@@ -234,7 +253,7 @@ export const createRouter = (options) => {
|
|
|
234
253
|
log.error('upstream embed error', { status: upstream.status, provider: provider.type });
|
|
235
254
|
if (sink) {
|
|
236
255
|
sink.logInference({
|
|
237
|
-
|
|
256
|
+
...attribution,
|
|
238
257
|
model: String(req.body?.model || body.model || ''),
|
|
239
258
|
provider: provider.type,
|
|
240
259
|
service: 'embed',
|
|
@@ -255,7 +274,7 @@ export const createRouter = (options) => {
|
|
|
255
274
|
log.info('embed complete', { databaseId, entityId, provider: provider.type });
|
|
256
275
|
if (sink) {
|
|
257
276
|
sink.logInference({
|
|
258
|
-
|
|
277
|
+
...attribution,
|
|
259
278
|
model: String(req.body?.model || body.model || ''),
|
|
260
279
|
provider: provider.type,
|
|
261
280
|
service: 'embed',
|
|
@@ -275,7 +294,7 @@ export const createRouter = (options) => {
|
|
|
275
294
|
log.error('embeddings error', err);
|
|
276
295
|
if (sink) {
|
|
277
296
|
sink.logInference({
|
|
278
|
-
|
|
297
|
+
...attribution,
|
|
279
298
|
model: String(req.body?.model || ''),
|
|
280
299
|
provider: provider.type,
|
|
281
300
|
service: 'embed',
|
|
@@ -298,8 +317,7 @@ export const createRouter = (options) => {
|
|
|
298
317
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
299
318
|
return;
|
|
300
319
|
}
|
|
301
|
-
const
|
|
302
|
-
const actorId = req.get('X-Actor-Id');
|
|
320
|
+
const attribution = readAttribution(req);
|
|
303
321
|
const { model, provider: reportedProvider, service, operation, input_tokens, output_tokens, total_tokens, latency_ms, status, error_type, raw_usage } = req.body || {};
|
|
304
322
|
if (!model) {
|
|
305
323
|
res.status(400).json({ error: { message: 'model is required' } });
|
|
@@ -307,9 +325,7 @@ export const createRouter = (options) => {
|
|
|
307
325
|
}
|
|
308
326
|
if (sink) {
|
|
309
327
|
sink.logInference({
|
|
310
|
-
|
|
311
|
-
entityId,
|
|
312
|
-
actorId,
|
|
328
|
+
...attribution,
|
|
313
329
|
model: String(model),
|
|
314
330
|
provider: String(reportedProvider || 'unknown'),
|
|
315
331
|
service: (service === 'embed' ? 'embed' : 'chat'),
|
package/esm/server.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createRouter } from './router';
|
|
3
|
-
/** Identity headers trusted only when isPublic === false
|
|
3
|
+
/** Identity + task-correlation headers trusted only when isPublic === false
|
|
4
|
+
* (private network). The correlation headers say which invocation/job/attempt/
|
|
5
|
+
* run a model call is a cost of; an external client must not be able to pin
|
|
6
|
+
* its usage onto someone else's task. */
|
|
4
7
|
const IDENTITY_HEADERS = [
|
|
5
8
|
'x-database-id',
|
|
6
9
|
'x-entity-id',
|
|
7
|
-
'x-actor-id'
|
|
10
|
+
'x-actor-id',
|
|
11
|
+
'x-invocation-id',
|
|
12
|
+
'x-job-id',
|
|
13
|
+
'x-attempt',
|
|
14
|
+
'x-run-id'
|
|
8
15
|
];
|
|
9
16
|
/**
|
|
10
17
|
* Create a standalone Express app for the agentic server.
|
package/index.d.ts
CHANGED
|
@@ -25,4 +25,4 @@ export { buildProviderHeaders, resolveProvider, resolveUpstreamUrl } from './pro
|
|
|
25
25
|
export { createRouter } from './router';
|
|
26
26
|
export type { AgenticServerStartOptions } from './server';
|
|
27
27
|
export { createAgenticServer } from './server';
|
|
28
|
-
export type { AgenticServerOptions, InferenceEntry, InferenceSink, ProviderConfig, ResolvedProvider, UsageResult } from './types';
|
|
28
|
+
export type { AgenticServerOptions, InferenceAttribution, InferenceEntry, InferenceSink, ProviderConfig, ResolvedProvider, UsageResult } from './types';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Standalone OpenAI-compatible LLM gateway — multi-provider proxy with pluggable inference metering",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "esm/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"test:watch": "jest --watch"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pgpmjs/logger": "^2.
|
|
32
|
+
"@pgpmjs/logger": "^2.26.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/express": "^5.0.6",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"metering",
|
|
52
52
|
"constructive"
|
|
53
53
|
],
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "baa84094b8541eb5000701b0060aa5b298032dc7"
|
|
55
55
|
}
|
package/router.d.ts
CHANGED
|
@@ -10,5 +10,12 @@
|
|
|
10
10
|
* - /v1/usage reporting endpoint for external usage submission
|
|
11
11
|
*/
|
|
12
12
|
import { Router } from 'express';
|
|
13
|
-
import type { AgenticServerOptions } from './types';
|
|
13
|
+
import type { AgenticServerOptions, InferenceAttribution } from './types';
|
|
14
|
+
/**
|
|
15
|
+
* Who the call is for and which task it is a cost of. Identity comes from the
|
|
16
|
+
* caller's identity headers; task linkage from the correlation headers the
|
|
17
|
+
* runtimes forward (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`).
|
|
18
|
+
* `X-Attempt` must be a non-negative integer or it is dropped.
|
|
19
|
+
*/
|
|
20
|
+
export declare const readAttribution: (req: any) => InferenceAttribution;
|
|
14
21
|
export declare const createRouter: (options: AgenticServerOptions) => Router;
|
package/router.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - /v1/usage reporting endpoint for external usage submission
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.createRouter = void 0;
|
|
14
|
+
exports.createRouter = exports.readAttribution = void 0;
|
|
15
15
|
const logger_1 = require("@pgpmjs/logger");
|
|
16
16
|
const express_1 = require("express");
|
|
17
17
|
const providers_1 = require("./providers");
|
|
@@ -49,6 +49,26 @@ function upstreamErrorMessage(provider, status, body) {
|
|
|
49
49
|
? `${provider.type} provider error ${status}: ${reason}`
|
|
50
50
|
: `${provider.type} provider error ${status}`;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Who the call is for and which task it is a cost of. Identity comes from the
|
|
54
|
+
* caller's identity headers; task linkage from the correlation headers the
|
|
55
|
+
* runtimes forward (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`).
|
|
56
|
+
* `X-Attempt` must be a non-negative integer or it is dropped.
|
|
57
|
+
*/
|
|
58
|
+
const readAttribution = (req) => {
|
|
59
|
+
const rawAttempt = req.get('X-Attempt');
|
|
60
|
+
const attempt = typeof rawAttempt === 'string' && /^\d+$/.test(rawAttempt) ? Number(rawAttempt) : undefined;
|
|
61
|
+
return {
|
|
62
|
+
databaseId: req.get('X-Database-Id'),
|
|
63
|
+
entityId: req.get('X-Entity-Id'),
|
|
64
|
+
actorId: req.get('X-Actor-Id'),
|
|
65
|
+
invocationId: req.get('X-Invocation-Id'),
|
|
66
|
+
jobId: req.get('X-Job-Id'),
|
|
67
|
+
attempt,
|
|
68
|
+
runId: req.get('X-Run-Id')
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
exports.readAttribution = readAttribution;
|
|
52
72
|
const createRouter = (options) => {
|
|
53
73
|
const router = (0, express_1.Router)();
|
|
54
74
|
// Metering is backend-agnostic: the caller injects an InferenceSink. When
|
|
@@ -61,8 +81,8 @@ const createRouter = (options) => {
|
|
|
61
81
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
62
82
|
return;
|
|
63
83
|
}
|
|
64
|
-
const
|
|
65
|
-
const
|
|
84
|
+
const attribution = (0, exports.readAttribution)(req);
|
|
85
|
+
const { entityId } = attribution;
|
|
66
86
|
const requestProvider = req.get('X-LLM-Provider');
|
|
67
87
|
const startTime = process.hrtime.bigint();
|
|
68
88
|
const provider = (0, providers_1.resolveProvider)(options, req.body?.model, requestProvider);
|
|
@@ -112,7 +132,7 @@ const createRouter = (options) => {
|
|
|
112
132
|
log.error('upstream error', { status: upstream.status, provider: provider.type, body: text });
|
|
113
133
|
if (sink) {
|
|
114
134
|
sink.logInference({
|
|
115
|
-
|
|
135
|
+
...attribution,
|
|
116
136
|
model: String(req.body?.model || body.model || ''),
|
|
117
137
|
provider: provider.type,
|
|
118
138
|
service: 'chat',
|
|
@@ -141,7 +161,7 @@ const createRouter = (options) => {
|
|
|
141
161
|
});
|
|
142
162
|
if (sink) {
|
|
143
163
|
sink.logInference({
|
|
144
|
-
|
|
164
|
+
...attribution,
|
|
145
165
|
model: String(req.body?.model || body.model || ''),
|
|
146
166
|
provider: provider.type,
|
|
147
167
|
service: 'chat',
|
|
@@ -167,7 +187,7 @@ const createRouter = (options) => {
|
|
|
167
187
|
});
|
|
168
188
|
if (sink) {
|
|
169
189
|
sink.logInference({
|
|
170
|
-
|
|
190
|
+
...attribution,
|
|
171
191
|
model: String(req.body?.model || body.model || ''),
|
|
172
192
|
provider: provider.type,
|
|
173
193
|
service: 'chat',
|
|
@@ -187,7 +207,7 @@ const createRouter = (options) => {
|
|
|
187
207
|
log.error('chat/completions error', err);
|
|
188
208
|
if (sink) {
|
|
189
209
|
sink.logInference({
|
|
190
|
-
|
|
210
|
+
...attribution,
|
|
191
211
|
model: String(req.body?.model || ''),
|
|
192
212
|
provider: provider.type,
|
|
193
213
|
service: 'chat',
|
|
@@ -216,8 +236,8 @@ const createRouter = (options) => {
|
|
|
216
236
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
217
237
|
return;
|
|
218
238
|
}
|
|
219
|
-
const
|
|
220
|
-
const
|
|
239
|
+
const attribution = (0, exports.readAttribution)(req);
|
|
240
|
+
const { entityId } = attribution;
|
|
221
241
|
const requestProvider = req.get('X-LLM-Provider');
|
|
222
242
|
const startTime = process.hrtime.bigint();
|
|
223
243
|
const provider = (0, providers_1.resolveProvider)(options, req.body?.model, requestProvider);
|
|
@@ -237,7 +257,7 @@ const createRouter = (options) => {
|
|
|
237
257
|
log.error('upstream embed error', { status: upstream.status, provider: provider.type });
|
|
238
258
|
if (sink) {
|
|
239
259
|
sink.logInference({
|
|
240
|
-
|
|
260
|
+
...attribution,
|
|
241
261
|
model: String(req.body?.model || body.model || ''),
|
|
242
262
|
provider: provider.type,
|
|
243
263
|
service: 'embed',
|
|
@@ -258,7 +278,7 @@ const createRouter = (options) => {
|
|
|
258
278
|
log.info('embed complete', { databaseId, entityId, provider: provider.type });
|
|
259
279
|
if (sink) {
|
|
260
280
|
sink.logInference({
|
|
261
|
-
|
|
281
|
+
...attribution,
|
|
262
282
|
model: String(req.body?.model || body.model || ''),
|
|
263
283
|
provider: provider.type,
|
|
264
284
|
service: 'embed',
|
|
@@ -278,7 +298,7 @@ const createRouter = (options) => {
|
|
|
278
298
|
log.error('embeddings error', err);
|
|
279
299
|
if (sink) {
|
|
280
300
|
sink.logInference({
|
|
281
|
-
|
|
301
|
+
...attribution,
|
|
282
302
|
model: String(req.body?.model || ''),
|
|
283
303
|
provider: provider.type,
|
|
284
304
|
service: 'embed',
|
|
@@ -301,8 +321,7 @@ const createRouter = (options) => {
|
|
|
301
321
|
res.status(400).json({ error: { message: 'X-Database-Id is required' } });
|
|
302
322
|
return;
|
|
303
323
|
}
|
|
304
|
-
const
|
|
305
|
-
const actorId = req.get('X-Actor-Id');
|
|
324
|
+
const attribution = (0, exports.readAttribution)(req);
|
|
306
325
|
const { model, provider: reportedProvider, service, operation, input_tokens, output_tokens, total_tokens, latency_ms, status, error_type, raw_usage } = req.body || {};
|
|
307
326
|
if (!model) {
|
|
308
327
|
res.status(400).json({ error: { message: 'model is required' } });
|
|
@@ -310,9 +329,7 @@ const createRouter = (options) => {
|
|
|
310
329
|
}
|
|
311
330
|
if (sink) {
|
|
312
331
|
sink.logInference({
|
|
313
|
-
|
|
314
|
-
entityId,
|
|
315
|
-
actorId,
|
|
332
|
+
...attribution,
|
|
316
333
|
model: String(model),
|
|
317
334
|
provider: String(reportedProvider || 'unknown'),
|
|
318
335
|
service: (service === 'embed' ? 'embed' : 'chat'),
|
package/server.js
CHANGED
|
@@ -6,11 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createAgenticServer = void 0;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
8
|
const router_1 = require("./router");
|
|
9
|
-
/** Identity headers trusted only when isPublic === false
|
|
9
|
+
/** Identity + task-correlation headers trusted only when isPublic === false
|
|
10
|
+
* (private network). The correlation headers say which invocation/job/attempt/
|
|
11
|
+
* run a model call is a cost of; an external client must not be able to pin
|
|
12
|
+
* its usage onto someone else's task. */
|
|
10
13
|
const IDENTITY_HEADERS = [
|
|
11
14
|
'x-database-id',
|
|
12
15
|
'x-entity-id',
|
|
13
|
-
'x-actor-id'
|
|
16
|
+
'x-actor-id',
|
|
17
|
+
'x-invocation-id',
|
|
18
|
+
'x-job-id',
|
|
19
|
+
'x-attempt',
|
|
20
|
+
'x-run-id'
|
|
14
21
|
];
|
|
15
22
|
/**
|
|
16
23
|
* Create a standalone Express app for the agentic server.
|
package/types.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface AgenticServerOptions {
|
|
|
23
23
|
* When false (default), the server is deployed behind a private network
|
|
24
24
|
* (Docker network, K8s service mesh) and trusts identity headers directly:
|
|
25
25
|
* X-Database-Id, X-Entity-Id, X-Actor-Id
|
|
26
|
+
* and the task-correlation headers
|
|
27
|
+
* X-Invocation-Id, X-Job-Id, X-Attempt, X-Run-Id
|
|
26
28
|
* When true, identity headers are stripped from incoming requests
|
|
27
29
|
* (external clients cannot set tenant context). */
|
|
28
30
|
isPublic?: boolean;
|
|
@@ -46,10 +48,20 @@ export interface UsageResult {
|
|
|
46
48
|
export interface InferenceSink {
|
|
47
49
|
logInference(entry: InferenceEntry): void;
|
|
48
50
|
}
|
|
51
|
+
/** The identity + task-linkage slice of an InferenceEntry, read from headers. */
|
|
52
|
+
export type InferenceAttribution = Pick<InferenceEntry, 'databaseId' | 'entityId' | 'actorId' | 'invocationId' | 'jobId' | 'attempt' | 'runId'>;
|
|
49
53
|
export interface InferenceEntry {
|
|
50
54
|
databaseId: string;
|
|
51
55
|
entityId?: string;
|
|
52
56
|
actorId?: string;
|
|
57
|
+
/** Task linkage — the invocation / job / attempt / agent run this call is a
|
|
58
|
+
* cost of. Inference is COGS attributed to the task the customer was
|
|
59
|
+
* charged for; it is never a customer meter of its own. Absent when the
|
|
60
|
+
* call was served outside an invocation. */
|
|
61
|
+
invocationId?: string;
|
|
62
|
+
jobId?: string;
|
|
63
|
+
attempt?: number;
|
|
64
|
+
runId?: string;
|
|
53
65
|
model: string;
|
|
54
66
|
provider: string;
|
|
55
67
|
service: 'chat' | 'embed';
|