@agentic-kit/metering 0.3.0 → 0.4.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/gateway.d.ts +17 -3
- package/esm/gateway.js +20 -9
- package/esm/identity.d.ts +19 -5
- package/esm/identity.js +29 -5
- package/esm/index.d.ts +2 -2
- package/esm/index.js +2 -2
- package/gateway.d.ts +17 -3
- package/gateway.js +21 -9
- package/identity.d.ts +19 -5
- package/identity.js +30 -6
- package/index.d.ts +2 -2
- package/index.js +6 -1
- package/package.json +2 -2
package/esm/gateway.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `agentic-server` speaks OpenAI's `/v1/chat/completions`, so routing a harness
|
|
5
5
|
* through it needs no custom streaming code — only an endpoint whose `baseUrl` is
|
|
6
|
-
* the gateway and whose headers carry the run's identity. Every harness worth
|
|
6
|
+
* the gateway's OpenAI api root and whose headers carry the run's identity. Every harness worth
|
|
7
7
|
* adapting can already talk to an OpenAI-compatible endpoint, which is why this
|
|
8
8
|
* resolution is neutral and only the registration is vendor-specific: an adapter
|
|
9
9
|
* turns `MeteredGateway` into whatever its harness calls a provider.
|
|
@@ -35,7 +35,7 @@ export interface MeteredModelSpec {
|
|
|
35
35
|
cost?: MeteredModelCost;
|
|
36
36
|
}
|
|
37
37
|
export interface MeteredGatewayOptions {
|
|
38
|
-
/** Gateway root, e.g. `https://agentic.example.com
|
|
38
|
+
/** Gateway root, e.g. `https://agentic.example.com`; a trailing `/v1` is tolerated. */
|
|
39
39
|
gatewayUrl: string;
|
|
40
40
|
identity: MeteredIdentity;
|
|
41
41
|
models: readonly MeteredModelSpec[];
|
|
@@ -72,8 +72,22 @@ export interface MeteredModel {
|
|
|
72
72
|
contextWindow: number;
|
|
73
73
|
maxTokens: number;
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Normalize the gateway root: absolute http(s), no trailing slash, no `/v1`.
|
|
77
|
+
*
|
|
78
|
+
* A caller who names the api root instead is answered the root it hangs under,
|
|
79
|
+
* so a deployment that spells its gateway either way reaches the same routes
|
|
80
|
+
* rather than one of them 404ing on the first model turn.
|
|
81
|
+
*/
|
|
76
82
|
export declare function normalizeGatewayUrl(gatewayUrl: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The OpenAI api root under a gateway root.
|
|
85
|
+
*
|
|
86
|
+
* An `openai-completions` client (pi-ai, the OpenAI SDKs, any harness built on
|
|
87
|
+
* either) appends `/chat/completions` to its `baseUrl` and nothing else, so the
|
|
88
|
+
* `/v1` belongs here rather than in the caller's environment.
|
|
89
|
+
*/
|
|
90
|
+
export declare function completionsBaseUrl(gatewayUrl: string): string;
|
|
77
91
|
export declare function resolveMeteredModel(spec: MeteredModelSpec): MeteredModel;
|
|
78
92
|
/**
|
|
79
93
|
* Resolve the gateway endpoint for a run.
|
package/esm/gateway.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `agentic-server` speaks OpenAI's `/v1/chat/completions`, so routing a harness
|
|
5
5
|
* through it needs no custom streaming code — only an endpoint whose `baseUrl` is
|
|
6
|
-
* the gateway and whose headers carry the run's identity. Every harness worth
|
|
6
|
+
* the gateway's OpenAI api root and whose headers carry the run's identity. Every harness worth
|
|
7
7
|
* adapting can already talk to an OpenAI-compatible endpoint, which is why this
|
|
8
8
|
* resolution is neutral and only the registration is vendor-specific: an adapter
|
|
9
9
|
* turns `MeteredGateway` into whatever its harness calls a provider.
|
|
@@ -14,7 +14,13 @@ export const GATEWAY_API = 'openai-completions';
|
|
|
14
14
|
/** Default provider name; also the prefix a model picker shows. */
|
|
15
15
|
export const DEFAULT_PROVIDER_NAME = 'constructive-gateway';
|
|
16
16
|
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Normalize the gateway root: absolute http(s), no trailing slash, no `/v1`.
|
|
19
|
+
*
|
|
20
|
+
* A caller who names the api root instead is answered the root it hangs under,
|
|
21
|
+
* so a deployment that spells its gateway either way reaches the same routes
|
|
22
|
+
* rather than one of them 404ing on the first model turn.
|
|
23
|
+
*/
|
|
18
24
|
export function normalizeGatewayUrl(gatewayUrl) {
|
|
19
25
|
const raw = gatewayUrl?.trim();
|
|
20
26
|
if (!raw)
|
|
@@ -29,14 +35,19 @@ export function normalizeGatewayUrl(gatewayUrl) {
|
|
|
29
35
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
30
36
|
throw new Error(`metered model: gatewayUrl must be http(s), got ${parsed.protocol}`);
|
|
31
37
|
}
|
|
32
|
-
const path = parsed.pathname.replace(/\/+$/, '');
|
|
33
|
-
// An OpenAI-compatible client appends `/v1/chat/completions`, so a baseUrl that
|
|
34
|
-
// already ends in `/v1` would request `/v1/v1/chat/completions` and 404 at the
|
|
35
|
-
// first model turn.
|
|
36
|
-
if (/\/v1$/.test(path))
|
|
37
|
-
throw new Error(`metered model: gatewayUrl must be the gateway root, not ${raw} (drop the /v1)`);
|
|
38
|
+
const path = parsed.pathname.replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
38
39
|
return `${parsed.origin}${path}`;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The OpenAI api root under a gateway root.
|
|
43
|
+
*
|
|
44
|
+
* An `openai-completions` client (pi-ai, the OpenAI SDKs, any harness built on
|
|
45
|
+
* either) appends `/chat/completions` to its `baseUrl` and nothing else, so the
|
|
46
|
+
* `/v1` belongs here rather than in the caller's environment.
|
|
47
|
+
*/
|
|
48
|
+
export function completionsBaseUrl(gatewayUrl) {
|
|
49
|
+
return `${normalizeGatewayUrl(gatewayUrl)}/v1`;
|
|
50
|
+
}
|
|
40
51
|
export function resolveMeteredModel(spec) {
|
|
41
52
|
if (!spec.id?.trim())
|
|
42
53
|
throw new Error('metered model: model id is required');
|
|
@@ -64,7 +75,7 @@ export function resolveMeteredGateway(options) {
|
|
|
64
75
|
return {
|
|
65
76
|
providerName: options.providerName ?? DEFAULT_PROVIDER_NAME,
|
|
66
77
|
displayName: options.displayName ?? 'Constructive (metered)',
|
|
67
|
-
baseUrl:
|
|
78
|
+
baseUrl: completionsBaseUrl(options.gatewayUrl),
|
|
68
79
|
api: GATEWAY_API,
|
|
69
80
|
headers,
|
|
70
81
|
// Harnesses typically require an apiKey once models are declared; the gateway
|
package/esm/identity.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Who a metered request belongs to.
|
|
2
|
+
* Who a metered request belongs to, and which task it is a cost of.
|
|
3
3
|
*
|
|
4
|
-
* These are exactly the headers `agentic-server` reads
|
|
5
|
-
* `X-Entity-Id`, `X-Actor-Id`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These are exactly the headers `agentic-server` reads — identity
|
|
5
|
+
* (`X-Database-Id`, `X-Entity-Id`, `X-Actor-Id`) plus task correlation
|
|
6
|
+
* (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`) — the same lane the
|
|
7
|
+
* platform's own clients use, so a pi session lands in `inference_log` beside
|
|
8
|
+
* every other metered call rather than in a parallel accounting scheme, and its
|
|
9
|
+
* tokens join the invocation the customer was actually charged for.
|
|
8
10
|
*
|
|
9
11
|
* Headers are only trustworthy where the gateway is not reachable by the
|
|
10
12
|
* untrusted party: in-cluster, or behind an authenticated ingress that pins the
|
|
@@ -18,6 +20,14 @@ export interface MeteredIdentity {
|
|
|
18
20
|
entityId?: string;
|
|
19
21
|
/** The actor on whose behalf the run executes. */
|
|
20
22
|
actorId?: string;
|
|
23
|
+
/** Invocation the run is executing, when the platform dispatched it. */
|
|
24
|
+
invocationId?: string;
|
|
25
|
+
/** Queue job id for that invocation; decimal digits only. */
|
|
26
|
+
jobId?: string;
|
|
27
|
+
/** Attempt number of that job; a non-negative integer. */
|
|
28
|
+
attempt?: number;
|
|
29
|
+
/** The agent run itself. */
|
|
30
|
+
runId?: string;
|
|
21
31
|
/**
|
|
22
32
|
* Bearer token for the gateway's ingress — run-scoped, not an account token.
|
|
23
33
|
* Sent as `Authorization: Bearer <token>`.
|
|
@@ -27,6 +37,10 @@ export interface MeteredIdentity {
|
|
|
27
37
|
export declare const DATABASE_ID_HEADER = "X-Database-Id";
|
|
28
38
|
export declare const ENTITY_ID_HEADER = "X-Entity-Id";
|
|
29
39
|
export declare const ACTOR_ID_HEADER = "X-Actor-Id";
|
|
40
|
+
export declare const INVOCATION_ID_HEADER = "X-Invocation-Id";
|
|
41
|
+
export declare const JOB_ID_HEADER = "X-Job-Id";
|
|
42
|
+
export declare const ATTEMPT_HEADER = "X-Attempt";
|
|
43
|
+
export declare const RUN_ID_HEADER = "X-Run-Id";
|
|
30
44
|
/**
|
|
31
45
|
* Build the identity headers for a metered request.
|
|
32
46
|
*
|
package/esm/identity.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Who a metered request belongs to.
|
|
2
|
+
* Who a metered request belongs to, and which task it is a cost of.
|
|
3
3
|
*
|
|
4
|
-
* These are exactly the headers `agentic-server` reads
|
|
5
|
-
* `X-Entity-Id`, `X-Actor-Id`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These are exactly the headers `agentic-server` reads — identity
|
|
5
|
+
* (`X-Database-Id`, `X-Entity-Id`, `X-Actor-Id`) plus task correlation
|
|
6
|
+
* (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`) — the same lane the
|
|
7
|
+
* platform's own clients use, so a pi session lands in `inference_log` beside
|
|
8
|
+
* every other metered call rather than in a parallel accounting scheme, and its
|
|
9
|
+
* tokens join the invocation the customer was actually charged for.
|
|
8
10
|
*
|
|
9
11
|
* Headers are only trustworthy where the gateway is not reachable by the
|
|
10
12
|
* untrusted party: in-cluster, or behind an authenticated ingress that pins the
|
|
@@ -14,6 +16,10 @@
|
|
|
14
16
|
export const DATABASE_ID_HEADER = 'X-Database-Id';
|
|
15
17
|
export const ENTITY_ID_HEADER = 'X-Entity-Id';
|
|
16
18
|
export const ACTOR_ID_HEADER = 'X-Actor-Id';
|
|
19
|
+
export const INVOCATION_ID_HEADER = 'X-Invocation-Id';
|
|
20
|
+
export const JOB_ID_HEADER = 'X-Job-Id';
|
|
21
|
+
export const ATTEMPT_HEADER = 'X-Attempt';
|
|
22
|
+
export const RUN_ID_HEADER = 'X-Run-Id';
|
|
17
23
|
/**
|
|
18
24
|
* Build the identity headers for a metered request.
|
|
19
25
|
*
|
|
@@ -32,6 +38,24 @@ export function buildIdentityHeaders(identity) {
|
|
|
32
38
|
const actorId = identity.actorId?.trim();
|
|
33
39
|
if (actorId)
|
|
34
40
|
headers[ACTOR_ID_HEADER] = actorId;
|
|
41
|
+
const invocationId = identity.invocationId?.trim();
|
|
42
|
+
if (invocationId)
|
|
43
|
+
headers[INVOCATION_ID_HEADER] = invocationId;
|
|
44
|
+
const jobId = identity.jobId?.trim();
|
|
45
|
+
if (jobId) {
|
|
46
|
+
if (!/^\d+$/.test(jobId))
|
|
47
|
+
throw new Error(`metered model: identity.jobId must be decimal digits, got "${jobId}"`);
|
|
48
|
+
headers[JOB_ID_HEADER] = jobId;
|
|
49
|
+
}
|
|
50
|
+
if (identity.attempt !== undefined) {
|
|
51
|
+
if (!Number.isInteger(identity.attempt) || identity.attempt < 0) {
|
|
52
|
+
throw new Error(`metered model: identity.attempt must be a non-negative integer, got ${identity.attempt}`);
|
|
53
|
+
}
|
|
54
|
+
headers[ATTEMPT_HEADER] = String(identity.attempt);
|
|
55
|
+
}
|
|
56
|
+
const runId = identity.runId?.trim();
|
|
57
|
+
if (runId)
|
|
58
|
+
headers[RUN_ID_HEADER] = runId;
|
|
35
59
|
const runToken = identity.runToken?.trim();
|
|
36
60
|
if (runToken)
|
|
37
61
|
headers.Authorization = `Bearer ${runToken}`;
|
package/esm/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No harness SDK is imported here. An adapter registers `MeteredGateway` with
|
|
8
8
|
* its own harness and feeds `toUsageReport()` from its own message events.
|
|
9
9
|
*/
|
|
10
|
-
export { DEFAULT_PROVIDER_NAME, GATEWAY_API, type MeteredGateway, type MeteredGatewayOptions, type MeteredModel, type MeteredModelCost, type MeteredModelSpec, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
-
export { ACTOR_ID_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER, type MeteredIdentity } from './identity';
|
|
10
|
+
export { completionsBaseUrl, DEFAULT_PROVIDER_NAME, GATEWAY_API, type MeteredGateway, type MeteredGatewayOptions, type MeteredModel, type MeteredModelCost, type MeteredModelSpec, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
+
export { ACTOR_ID_HEADER, ATTEMPT_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER, INVOCATION_ID_HEADER, JOB_ID_HEADER, type MeteredIdentity, RUN_ID_HEADER } from './identity';
|
|
12
12
|
export { httpUsageSink, type HttpUsageSinkOptions, UsageReporter, type UsageReporterOptions, type UsageSink } from './reporter';
|
|
13
13
|
export { type AssistantUsageMessage, isAssistantMessage, toUsageReport, type ToUsageReportOptions, type UsageReport } from './usage-report';
|
package/esm/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No harness SDK is imported here. An adapter registers `MeteredGateway` with
|
|
8
8
|
* its own harness and feeds `toUsageReport()` from its own message events.
|
|
9
9
|
*/
|
|
10
|
-
export { DEFAULT_PROVIDER_NAME, GATEWAY_API, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
-
export { ACTOR_ID_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER } from './identity';
|
|
10
|
+
export { completionsBaseUrl, DEFAULT_PROVIDER_NAME, GATEWAY_API, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
+
export { ACTOR_ID_HEADER, ATTEMPT_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER, INVOCATION_ID_HEADER, JOB_ID_HEADER, RUN_ID_HEADER } from './identity';
|
|
12
12
|
export { httpUsageSink, UsageReporter } from './reporter';
|
|
13
13
|
export { isAssistantMessage, toUsageReport } from './usage-report';
|
package/gateway.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `agentic-server` speaks OpenAI's `/v1/chat/completions`, so routing a harness
|
|
5
5
|
* through it needs no custom streaming code — only an endpoint whose `baseUrl` is
|
|
6
|
-
* the gateway and whose headers carry the run's identity. Every harness worth
|
|
6
|
+
* the gateway's OpenAI api root and whose headers carry the run's identity. Every harness worth
|
|
7
7
|
* adapting can already talk to an OpenAI-compatible endpoint, which is why this
|
|
8
8
|
* resolution is neutral and only the registration is vendor-specific: an adapter
|
|
9
9
|
* turns `MeteredGateway` into whatever its harness calls a provider.
|
|
@@ -35,7 +35,7 @@ export interface MeteredModelSpec {
|
|
|
35
35
|
cost?: MeteredModelCost;
|
|
36
36
|
}
|
|
37
37
|
export interface MeteredGatewayOptions {
|
|
38
|
-
/** Gateway root, e.g. `https://agentic.example.com
|
|
38
|
+
/** Gateway root, e.g. `https://agentic.example.com`; a trailing `/v1` is tolerated. */
|
|
39
39
|
gatewayUrl: string;
|
|
40
40
|
identity: MeteredIdentity;
|
|
41
41
|
models: readonly MeteredModelSpec[];
|
|
@@ -72,8 +72,22 @@ export interface MeteredModel {
|
|
|
72
72
|
contextWindow: number;
|
|
73
73
|
maxTokens: number;
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Normalize the gateway root: absolute http(s), no trailing slash, no `/v1`.
|
|
77
|
+
*
|
|
78
|
+
* A caller who names the api root instead is answered the root it hangs under,
|
|
79
|
+
* so a deployment that spells its gateway either way reaches the same routes
|
|
80
|
+
* rather than one of them 404ing on the first model turn.
|
|
81
|
+
*/
|
|
76
82
|
export declare function normalizeGatewayUrl(gatewayUrl: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The OpenAI api root under a gateway root.
|
|
85
|
+
*
|
|
86
|
+
* An `openai-completions` client (pi-ai, the OpenAI SDKs, any harness built on
|
|
87
|
+
* either) appends `/chat/completions` to its `baseUrl` and nothing else, so the
|
|
88
|
+
* `/v1` belongs here rather than in the caller's environment.
|
|
89
|
+
*/
|
|
90
|
+
export declare function completionsBaseUrl(gatewayUrl: string): string;
|
|
77
91
|
export declare function resolveMeteredModel(spec: MeteredModelSpec): MeteredModel;
|
|
78
92
|
/**
|
|
79
93
|
* Resolve the gateway endpoint for a run.
|
package/gateway.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `agentic-server` speaks OpenAI's `/v1/chat/completions`, so routing a harness
|
|
6
6
|
* through it needs no custom streaming code — only an endpoint whose `baseUrl` is
|
|
7
|
-
* the gateway and whose headers carry the run's identity. Every harness worth
|
|
7
|
+
* the gateway's OpenAI api root and whose headers carry the run's identity. Every harness worth
|
|
8
8
|
* adapting can already talk to an OpenAI-compatible endpoint, which is why this
|
|
9
9
|
* resolution is neutral and only the registration is vendor-specific: an adapter
|
|
10
10
|
* turns `MeteredGateway` into whatever its harness calls a provider.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.DEFAULT_PROVIDER_NAME = exports.GATEWAY_API = void 0;
|
|
14
14
|
exports.normalizeGatewayUrl = normalizeGatewayUrl;
|
|
15
|
+
exports.completionsBaseUrl = completionsBaseUrl;
|
|
15
16
|
exports.resolveMeteredModel = resolveMeteredModel;
|
|
16
17
|
exports.resolveMeteredGateway = resolveMeteredGateway;
|
|
17
18
|
const identity_1 = require("./identity");
|
|
@@ -20,7 +21,13 @@ exports.GATEWAY_API = 'openai-completions';
|
|
|
20
21
|
/** Default provider name; also the prefix a model picker shows. */
|
|
21
22
|
exports.DEFAULT_PROVIDER_NAME = 'constructive-gateway';
|
|
22
23
|
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Normalize the gateway root: absolute http(s), no trailing slash, no `/v1`.
|
|
26
|
+
*
|
|
27
|
+
* A caller who names the api root instead is answered the root it hangs under,
|
|
28
|
+
* so a deployment that spells its gateway either way reaches the same routes
|
|
29
|
+
* rather than one of them 404ing on the first model turn.
|
|
30
|
+
*/
|
|
24
31
|
function normalizeGatewayUrl(gatewayUrl) {
|
|
25
32
|
const raw = gatewayUrl?.trim();
|
|
26
33
|
if (!raw)
|
|
@@ -35,14 +42,19 @@ function normalizeGatewayUrl(gatewayUrl) {
|
|
|
35
42
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
36
43
|
throw new Error(`metered model: gatewayUrl must be http(s), got ${parsed.protocol}`);
|
|
37
44
|
}
|
|
38
|
-
const path = parsed.pathname.replace(/\/+$/, '');
|
|
39
|
-
// An OpenAI-compatible client appends `/v1/chat/completions`, so a baseUrl that
|
|
40
|
-
// already ends in `/v1` would request `/v1/v1/chat/completions` and 404 at the
|
|
41
|
-
// first model turn.
|
|
42
|
-
if (/\/v1$/.test(path))
|
|
43
|
-
throw new Error(`metered model: gatewayUrl must be the gateway root, not ${raw} (drop the /v1)`);
|
|
45
|
+
const path = parsed.pathname.replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
44
46
|
return `${parsed.origin}${path}`;
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The OpenAI api root under a gateway root.
|
|
50
|
+
*
|
|
51
|
+
* An `openai-completions` client (pi-ai, the OpenAI SDKs, any harness built on
|
|
52
|
+
* either) appends `/chat/completions` to its `baseUrl` and nothing else, so the
|
|
53
|
+
* `/v1` belongs here rather than in the caller's environment.
|
|
54
|
+
*/
|
|
55
|
+
function completionsBaseUrl(gatewayUrl) {
|
|
56
|
+
return `${normalizeGatewayUrl(gatewayUrl)}/v1`;
|
|
57
|
+
}
|
|
46
58
|
function resolveMeteredModel(spec) {
|
|
47
59
|
if (!spec.id?.trim())
|
|
48
60
|
throw new Error('metered model: model id is required');
|
|
@@ -70,7 +82,7 @@ function resolveMeteredGateway(options) {
|
|
|
70
82
|
return {
|
|
71
83
|
providerName: options.providerName ?? exports.DEFAULT_PROVIDER_NAME,
|
|
72
84
|
displayName: options.displayName ?? 'Constructive (metered)',
|
|
73
|
-
baseUrl:
|
|
85
|
+
baseUrl: completionsBaseUrl(options.gatewayUrl),
|
|
74
86
|
api: exports.GATEWAY_API,
|
|
75
87
|
headers,
|
|
76
88
|
// Harnesses typically require an apiKey once models are declared; the gateway
|
package/identity.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Who a metered request belongs to.
|
|
2
|
+
* Who a metered request belongs to, and which task it is a cost of.
|
|
3
3
|
*
|
|
4
|
-
* These are exactly the headers `agentic-server` reads
|
|
5
|
-
* `X-Entity-Id`, `X-Actor-Id`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These are exactly the headers `agentic-server` reads — identity
|
|
5
|
+
* (`X-Database-Id`, `X-Entity-Id`, `X-Actor-Id`) plus task correlation
|
|
6
|
+
* (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`) — the same lane the
|
|
7
|
+
* platform's own clients use, so a pi session lands in `inference_log` beside
|
|
8
|
+
* every other metered call rather than in a parallel accounting scheme, and its
|
|
9
|
+
* tokens join the invocation the customer was actually charged for.
|
|
8
10
|
*
|
|
9
11
|
* Headers are only trustworthy where the gateway is not reachable by the
|
|
10
12
|
* untrusted party: in-cluster, or behind an authenticated ingress that pins the
|
|
@@ -18,6 +20,14 @@ export interface MeteredIdentity {
|
|
|
18
20
|
entityId?: string;
|
|
19
21
|
/** The actor on whose behalf the run executes. */
|
|
20
22
|
actorId?: string;
|
|
23
|
+
/** Invocation the run is executing, when the platform dispatched it. */
|
|
24
|
+
invocationId?: string;
|
|
25
|
+
/** Queue job id for that invocation; decimal digits only. */
|
|
26
|
+
jobId?: string;
|
|
27
|
+
/** Attempt number of that job; a non-negative integer. */
|
|
28
|
+
attempt?: number;
|
|
29
|
+
/** The agent run itself. */
|
|
30
|
+
runId?: string;
|
|
21
31
|
/**
|
|
22
32
|
* Bearer token for the gateway's ingress — run-scoped, not an account token.
|
|
23
33
|
* Sent as `Authorization: Bearer <token>`.
|
|
@@ -27,6 +37,10 @@ export interface MeteredIdentity {
|
|
|
27
37
|
export declare const DATABASE_ID_HEADER = "X-Database-Id";
|
|
28
38
|
export declare const ENTITY_ID_HEADER = "X-Entity-Id";
|
|
29
39
|
export declare const ACTOR_ID_HEADER = "X-Actor-Id";
|
|
40
|
+
export declare const INVOCATION_ID_HEADER = "X-Invocation-Id";
|
|
41
|
+
export declare const JOB_ID_HEADER = "X-Job-Id";
|
|
42
|
+
export declare const ATTEMPT_HEADER = "X-Attempt";
|
|
43
|
+
export declare const RUN_ID_HEADER = "X-Run-Id";
|
|
30
44
|
/**
|
|
31
45
|
* Build the identity headers for a metered request.
|
|
32
46
|
*
|
package/identity.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Who a metered request belongs to.
|
|
3
|
+
* Who a metered request belongs to, and which task it is a cost of.
|
|
4
4
|
*
|
|
5
|
-
* These are exactly the headers `agentic-server` reads
|
|
6
|
-
* `X-Entity-Id`, `X-Actor-Id`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* These are exactly the headers `agentic-server` reads — identity
|
|
6
|
+
* (`X-Database-Id`, `X-Entity-Id`, `X-Actor-Id`) plus task correlation
|
|
7
|
+
* (`X-Invocation-Id`, `X-Job-Id`, `X-Attempt`, `X-Run-Id`) — the same lane the
|
|
8
|
+
* platform's own clients use, so a pi session lands in `inference_log` beside
|
|
9
|
+
* every other metered call rather than in a parallel accounting scheme, and its
|
|
10
|
+
* tokens join the invocation the customer was actually charged for.
|
|
9
11
|
*
|
|
10
12
|
* Headers are only trustworthy where the gateway is not reachable by the
|
|
11
13
|
* untrusted party: in-cluster, or behind an authenticated ingress that pins the
|
|
@@ -13,11 +15,15 @@
|
|
|
13
15
|
* ingress do the pinning.
|
|
14
16
|
*/
|
|
15
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.ACTOR_ID_HEADER = exports.ENTITY_ID_HEADER = exports.DATABASE_ID_HEADER = void 0;
|
|
18
|
+
exports.RUN_ID_HEADER = exports.ATTEMPT_HEADER = exports.JOB_ID_HEADER = exports.INVOCATION_ID_HEADER = exports.ACTOR_ID_HEADER = exports.ENTITY_ID_HEADER = exports.DATABASE_ID_HEADER = void 0;
|
|
17
19
|
exports.buildIdentityHeaders = buildIdentityHeaders;
|
|
18
20
|
exports.DATABASE_ID_HEADER = 'X-Database-Id';
|
|
19
21
|
exports.ENTITY_ID_HEADER = 'X-Entity-Id';
|
|
20
22
|
exports.ACTOR_ID_HEADER = 'X-Actor-Id';
|
|
23
|
+
exports.INVOCATION_ID_HEADER = 'X-Invocation-Id';
|
|
24
|
+
exports.JOB_ID_HEADER = 'X-Job-Id';
|
|
25
|
+
exports.ATTEMPT_HEADER = 'X-Attempt';
|
|
26
|
+
exports.RUN_ID_HEADER = 'X-Run-Id';
|
|
21
27
|
/**
|
|
22
28
|
* Build the identity headers for a metered request.
|
|
23
29
|
*
|
|
@@ -36,6 +42,24 @@ function buildIdentityHeaders(identity) {
|
|
|
36
42
|
const actorId = identity.actorId?.trim();
|
|
37
43
|
if (actorId)
|
|
38
44
|
headers[exports.ACTOR_ID_HEADER] = actorId;
|
|
45
|
+
const invocationId = identity.invocationId?.trim();
|
|
46
|
+
if (invocationId)
|
|
47
|
+
headers[exports.INVOCATION_ID_HEADER] = invocationId;
|
|
48
|
+
const jobId = identity.jobId?.trim();
|
|
49
|
+
if (jobId) {
|
|
50
|
+
if (!/^\d+$/.test(jobId))
|
|
51
|
+
throw new Error(`metered model: identity.jobId must be decimal digits, got "${jobId}"`);
|
|
52
|
+
headers[exports.JOB_ID_HEADER] = jobId;
|
|
53
|
+
}
|
|
54
|
+
if (identity.attempt !== undefined) {
|
|
55
|
+
if (!Number.isInteger(identity.attempt) || identity.attempt < 0) {
|
|
56
|
+
throw new Error(`metered model: identity.attempt must be a non-negative integer, got ${identity.attempt}`);
|
|
57
|
+
}
|
|
58
|
+
headers[exports.ATTEMPT_HEADER] = String(identity.attempt);
|
|
59
|
+
}
|
|
60
|
+
const runId = identity.runId?.trim();
|
|
61
|
+
if (runId)
|
|
62
|
+
headers[exports.RUN_ID_HEADER] = runId;
|
|
39
63
|
const runToken = identity.runToken?.trim();
|
|
40
64
|
if (runToken)
|
|
41
65
|
headers.Authorization = `Bearer ${runToken}`;
|
package/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No harness SDK is imported here. An adapter registers `MeteredGateway` with
|
|
8
8
|
* its own harness and feeds `toUsageReport()` from its own message events.
|
|
9
9
|
*/
|
|
10
|
-
export { DEFAULT_PROVIDER_NAME, GATEWAY_API, type MeteredGateway, type MeteredGatewayOptions, type MeteredModel, type MeteredModelCost, type MeteredModelSpec, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
-
export { ACTOR_ID_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER, type MeteredIdentity } from './identity';
|
|
10
|
+
export { completionsBaseUrl, DEFAULT_PROVIDER_NAME, GATEWAY_API, type MeteredGateway, type MeteredGatewayOptions, type MeteredModel, type MeteredModelCost, type MeteredModelSpec, normalizeGatewayUrl, resolveMeteredGateway, resolveMeteredModel } from './gateway';
|
|
11
|
+
export { ACTOR_ID_HEADER, ATTEMPT_HEADER, buildIdentityHeaders, DATABASE_ID_HEADER, ENTITY_ID_HEADER, INVOCATION_ID_HEADER, JOB_ID_HEADER, type MeteredIdentity, RUN_ID_HEADER } from './identity';
|
|
12
12
|
export { httpUsageSink, type HttpUsageSinkOptions, UsageReporter, type UsageReporterOptions, type UsageSink } from './reporter';
|
|
13
13
|
export { type AssistantUsageMessage, isAssistantMessage, toUsageReport, type ToUsageReportOptions, type UsageReport } from './usage-report';
|
package/index.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* its own harness and feeds `toUsageReport()` from its own message events.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.toUsageReport = exports.isAssistantMessage = exports.UsageReporter = exports.httpUsageSink = exports.ENTITY_ID_HEADER = exports.DATABASE_ID_HEADER = exports.buildIdentityHeaders = exports.ACTOR_ID_HEADER = exports.resolveMeteredModel = exports.resolveMeteredGateway = exports.normalizeGatewayUrl = exports.GATEWAY_API = exports.DEFAULT_PROVIDER_NAME = void 0;
|
|
12
|
+
exports.toUsageReport = exports.isAssistantMessage = exports.UsageReporter = exports.httpUsageSink = exports.RUN_ID_HEADER = exports.JOB_ID_HEADER = exports.INVOCATION_ID_HEADER = exports.ENTITY_ID_HEADER = exports.DATABASE_ID_HEADER = exports.buildIdentityHeaders = exports.ATTEMPT_HEADER = exports.ACTOR_ID_HEADER = exports.resolveMeteredModel = exports.resolveMeteredGateway = exports.normalizeGatewayUrl = exports.GATEWAY_API = exports.DEFAULT_PROVIDER_NAME = exports.completionsBaseUrl = void 0;
|
|
13
13
|
var gateway_1 = require("./gateway");
|
|
14
|
+
Object.defineProperty(exports, "completionsBaseUrl", { enumerable: true, get: function () { return gateway_1.completionsBaseUrl; } });
|
|
14
15
|
Object.defineProperty(exports, "DEFAULT_PROVIDER_NAME", { enumerable: true, get: function () { return gateway_1.DEFAULT_PROVIDER_NAME; } });
|
|
15
16
|
Object.defineProperty(exports, "GATEWAY_API", { enumerable: true, get: function () { return gateway_1.GATEWAY_API; } });
|
|
16
17
|
Object.defineProperty(exports, "normalizeGatewayUrl", { enumerable: true, get: function () { return gateway_1.normalizeGatewayUrl; } });
|
|
@@ -18,9 +19,13 @@ Object.defineProperty(exports, "resolveMeteredGateway", { enumerable: true, get:
|
|
|
18
19
|
Object.defineProperty(exports, "resolveMeteredModel", { enumerable: true, get: function () { return gateway_1.resolveMeteredModel; } });
|
|
19
20
|
var identity_1 = require("./identity");
|
|
20
21
|
Object.defineProperty(exports, "ACTOR_ID_HEADER", { enumerable: true, get: function () { return identity_1.ACTOR_ID_HEADER; } });
|
|
22
|
+
Object.defineProperty(exports, "ATTEMPT_HEADER", { enumerable: true, get: function () { return identity_1.ATTEMPT_HEADER; } });
|
|
21
23
|
Object.defineProperty(exports, "buildIdentityHeaders", { enumerable: true, get: function () { return identity_1.buildIdentityHeaders; } });
|
|
22
24
|
Object.defineProperty(exports, "DATABASE_ID_HEADER", { enumerable: true, get: function () { return identity_1.DATABASE_ID_HEADER; } });
|
|
23
25
|
Object.defineProperty(exports, "ENTITY_ID_HEADER", { enumerable: true, get: function () { return identity_1.ENTITY_ID_HEADER; } });
|
|
26
|
+
Object.defineProperty(exports, "INVOCATION_ID_HEADER", { enumerable: true, get: function () { return identity_1.INVOCATION_ID_HEADER; } });
|
|
27
|
+
Object.defineProperty(exports, "JOB_ID_HEADER", { enumerable: true, get: function () { return identity_1.JOB_ID_HEADER; } });
|
|
28
|
+
Object.defineProperty(exports, "RUN_ID_HEADER", { enumerable: true, get: function () { return identity_1.RUN_ID_HEADER; } });
|
|
24
29
|
var reporter_1 = require("./reporter");
|
|
25
30
|
Object.defineProperty(exports, "httpUsageSink", { enumerable: true, get: function () { return reporter_1.httpUsageSink; } });
|
|
26
31
|
Object.defineProperty(exports, "UsageReporter", { enumerable: true, get: function () { return reporter_1.UsageReporter; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentic-kit/metering",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "harness-neutral metering for an agent run — the Constructive gateway as a model endpoint, run identity headers, and usage reporting",
|
|
6
6
|
"main": "index.js",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"gateway",
|
|
36
36
|
"constructive"
|
|
37
37
|
],
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "cc30249ddf84bc8c410d9681adb15f98228ab591"
|
|
39
39
|
}
|