agentic-server 0.25.1 → 0.26.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 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 entityId = req.get('X-Entity-Id');
62
- const actorId = req.get('X-Actor-Id');
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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 entityId = req.get('X-Entity-Id');
217
- const actorId = req.get('X-Actor-Id');
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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 entityId = req.get('X-Entity-Id');
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
- databaseId,
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 (private network). */
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.25.1",
3
+ "version": "0.26.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,13 +29,13 @@
29
29
  "test:watch": "jest --watch"
30
30
  },
31
31
  "dependencies": {
32
- "@pgpmjs/logger": "^2.25.0"
32
+ "@pgpmjs/logger": "^2.25.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/express": "^5.0.6",
36
36
  "@types/node": "^22.19.11",
37
37
  "express": "^5.2.1",
38
- "makage": "^0.3.0"
38
+ "makage": "^0.8.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "express": "^5.0.0"
@@ -51,5 +51,5 @@
51
51
  "metering",
52
52
  "constructive"
53
53
  ],
54
- "gitHead": "f9611d3a1ba8bdb50a39b5bca7c9a7115df96e34"
54
+ "gitHead": "cc30249ddf84bc8c410d9681adb15f98228ab591"
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 entityId = req.get('X-Entity-Id');
65
- const actorId = req.get('X-Actor-Id');
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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 entityId = req.get('X-Entity-Id');
220
- const actorId = req.get('X-Actor-Id');
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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
- databaseId, entityId, actorId,
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 entityId = req.get('X-Entity-Id');
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
- databaseId,
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 (private network). */
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';