@stigg/agent-sdk-js 0.0.0-stage → 1.0.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 +1147 -2
- package/dist/bindings/client/core/registry.d.ts +12 -0
- package/dist/bindings/client/core/registry.js +22 -0
- package/dist/bindings/client/core/types.d.ts +117 -0
- package/dist/bindings/client/core/types.js +13 -0
- package/dist/bindings/client/core/wrap-client.d.ts +6 -0
- package/dist/bindings/client/core/wrap-client.js +258 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
- package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
- package/dist/bindings/client/gateways/litellm/types.js +2 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +15 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +141 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +29 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +30 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +79 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +43 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +80 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +33 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +73 -0
- package/dist/bindings/proxy/headers.d.ts +6 -0
- package/dist/bindings/proxy/headers.js +12 -0
- package/dist/bindings/proxy/index.d.ts +21 -0
- package/dist/bindings/proxy/index.js +18 -0
- package/dist/bindings/proxy/types.d.ts +8 -0
- package/dist/bindings/proxy/types.js +2 -0
- package/dist/bindings/shared/estimate.d.ts +27 -0
- package/dist/bindings/shared/estimate.js +38 -0
- package/dist/bindings/shared/meter-stream.d.ts +18 -0
- package/dist/bindings/shared/meter-stream.js +151 -0
- package/dist/bindings/shared/token-usage.d.ts +148 -0
- package/dist/bindings/shared/token-usage.js +414 -0
- package/dist/bindings/shared/types.d.ts +79 -0
- package/dist/bindings/shared/types.js +2 -0
- package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
- package/dist/bindings/shared/weighted-metrics.js +44 -0
- package/dist/config.d.ts +136 -0
- package/dist/config.js +97 -0
- package/dist/core/context.d.ts +151 -0
- package/dist/core/context.js +154 -0
- package/dist/core/dimensions.d.ts +22 -0
- package/dist/core/dimensions.js +47 -0
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +47 -0
- package/dist/core/session.d.ts +161 -0
- package/dist/core/session.js +403 -0
- package/dist/decision-text.d.ts +15 -0
- package/dist/decision-text.js +54 -0
- package/dist/engine/composite-engine.d.ts +72 -0
- package/dist/engine/composite-engine.js +135 -0
- package/dist/engine/create-engine.d.ts +16 -0
- package/dist/engine/create-engine.js +93 -0
- package/dist/engine/decision.d.ts +37 -0
- package/dist/engine/decision.js +128 -0
- package/dist/engine/engine.d.ts +166 -0
- package/dist/engine/engine.js +11 -0
- package/dist/engine/node-sdk-engine.d.ts +125 -0
- package/dist/engine/node-sdk-engine.js +262 -0
- package/dist/engine/report.d.ts +106 -0
- package/dist/engine/report.js +185 -0
- package/dist/engine/rest-governance-engine.d.ts +47 -0
- package/dist/engine/rest-governance-engine.js +220 -0
- package/dist/errors.d.ts +78 -0
- package/dist/errors.js +151 -0
- package/dist/headers.d.ts +27 -0
- package/dist/headers.js +63 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +85 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +81 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +20 -0
- package/dist/sdk-identity.d.ts +36 -0
- package/dist/sdk-identity.js +43 -0
- package/dist/stigg-agent-sdk.d.ts +242 -0
- package/dist/stigg-agent-sdk.js +594 -0
- package/dist/types.d.ts +531 -0
- package/dist/types.js +97 -0
- package/package.json +80 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine half that runs on Stigg's own Node server SDK: cached entitlement reads and usage
|
|
3
|
+
* ingest. This is the only file in the package that imports it at runtime.
|
|
4
|
+
*
|
|
5
|
+
* It knows nothing about entities or chains - the GraphQL surface has no governance on it. What
|
|
6
|
+
* it brings is the cache, which is the reason the composite keeps ingest (and customer-level
|
|
7
|
+
* checks) here rather than sending everything to REST - and the reason nothing above this file
|
|
8
|
+
* caches anything. That cache writes reported usage back from the server's own response,
|
|
9
|
+
* invalidates on realtime updates, resets usage periods at read time, falls back when Stigg is
|
|
10
|
+
* unreachable, and optionally lives in Redis so sibling processes share one view.
|
|
11
|
+
*
|
|
12
|
+
* All of that is configured through {@link CacheOptions}, which is the Node SDK's own
|
|
13
|
+
* configuration passed straight through rather than re-described here.
|
|
14
|
+
*/
|
|
15
|
+
import Stigg, { type ClientConfiguration } from '@stigg/node-server-sdk';
|
|
16
|
+
import type { EngineCapabilities, EngineCheckParams, EngineCheckResult, EngineReportParams, EngineReportResult, GovernanceEngine } from './engine';
|
|
17
|
+
/**
|
|
18
|
+
* Caching - all of it, and none of it implemented here.
|
|
19
|
+
*
|
|
20
|
+
* This SDK holds no cache. Checks are served by the Node server SDK's entitlement cache, which
|
|
21
|
+
* already does the hard parts: it writes the server's own measurement responses back into cached
|
|
22
|
+
* usage, invalidates on realtime updates, applies usage-period resets at read time, and can be
|
|
23
|
+
* backed by Redis so sibling processes share one view instead of each holding their own.
|
|
24
|
+
*
|
|
25
|
+
* So this is a passthrough rather than a parallel design - the field names are the Node SDK's own,
|
|
26
|
+
* and anything it learns to do lands here without a change. The one thing that does *not* cache is
|
|
27
|
+
* an attributed governance check: it resolves an entity, which only the governance API can do, so
|
|
28
|
+
* it is a live call every time. That is the stricter answer, and it is where per-agent budgets are
|
|
29
|
+
* enforced.
|
|
30
|
+
*/
|
|
31
|
+
export type CacheOptions = Pick<ClientConfiguration, 'redis' | 'memoryCacheMaxSizeBytes' | 'entitlementsFallback' | 'creditEntitlementsFallback' | 'realtimeUpdatesEnabled' | 'enableEdge' | 'offline'>;
|
|
32
|
+
/**
|
|
33
|
+
* The Node server SDK client itself, for an application that already has one.
|
|
34
|
+
*
|
|
35
|
+
* Re-exported from `config.ts` as the public name, so the vendor client stays behind this file.
|
|
36
|
+
*/
|
|
37
|
+
export type NodeSdkClient = Stigg;
|
|
38
|
+
export type NodeSdkEngineOptions = {
|
|
39
|
+
apiKey: string;
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Everything about caching, passed through to the Node SDK verbatim. Anything set here wins
|
|
44
|
+
* over the localhost-aware defaults below - including `enableEdge` and `realtimeUpdatesEnabled`,
|
|
45
|
+
* which are cache configuration in all but name: the socket is what keeps a cached entitlement
|
|
46
|
+
* from going stale.
|
|
47
|
+
*/
|
|
48
|
+
cache?: CacheOptions;
|
|
49
|
+
/**
|
|
50
|
+
* An existing client to use instead of building one. Borrowed, never closed.
|
|
51
|
+
*
|
|
52
|
+
* This is the only way to be *sure* who owns the client, which matters because
|
|
53
|
+
* `Stigg.initialize` keeps one global instance per process and hands it back when the API key
|
|
54
|
+
* matches. Closing a client the application is still using would take its cache and its socket
|
|
55
|
+
* with it, and nothing from out here can tell a client that was just built from one that was
|
|
56
|
+
* handed back: the instance exposes neither its configuration nor its identity.
|
|
57
|
+
*/
|
|
58
|
+
client?: NodeSdkClient;
|
|
59
|
+
};
|
|
60
|
+
export declare class NodeSdkEngine implements GovernanceEngine {
|
|
61
|
+
readonly capabilities: EngineCapabilities;
|
|
62
|
+
/**
|
|
63
|
+
* Whether this engine built the client, and may therefore close it.
|
|
64
|
+
*
|
|
65
|
+
* False for a client that was handed in: it belongs to whoever built it, and its lifecycle is
|
|
66
|
+
* theirs. See `close`.
|
|
67
|
+
*/
|
|
68
|
+
readonly ownsClient: boolean;
|
|
69
|
+
private readonly client;
|
|
70
|
+
constructor(options: NodeSdkEngineOptions);
|
|
71
|
+
/**
|
|
72
|
+
* Waits for the cache to be usable.
|
|
73
|
+
*
|
|
74
|
+
* Not cosmetic: until the runtime reports itself initialized, every read is treated as a miss
|
|
75
|
+
* and refetched, so a client that starts serving immediately pays a round trip per check for as
|
|
76
|
+
* long as initialization takes. Awaited once at startup instead.
|
|
77
|
+
*/
|
|
78
|
+
ready(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Loads this customer's entitlements into the cache, so the checks that follow are local.
|
|
81
|
+
*
|
|
82
|
+
* The whole map, not one decision: a later check against a different entitlement is warm too,
|
|
83
|
+
* and each is evaluated against the amount it actually asks for rather than against whatever
|
|
84
|
+
* amount happened to warm it.
|
|
85
|
+
*/
|
|
86
|
+
preload(customerId: string): Promise<void>;
|
|
87
|
+
upsertEntity(): Promise<void>;
|
|
88
|
+
archiveEntity(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* The two kinds of entitlement are two different endpoints with two different answers, so each
|
|
91
|
+
* gets its own method and this one only picks. Narrowing on `currencyId` rather than through
|
|
92
|
+
* `entitlementKind` is what lets each branch read its own id without an assertion: a function
|
|
93
|
+
* call cannot narrow a union.
|
|
94
|
+
*/
|
|
95
|
+
check(customerId: string, { entitlement, requestedUsage }: EngineCheckParams): Promise<EngineCheckResult>;
|
|
96
|
+
/**
|
|
97
|
+
* `getCreditEntitlement` takes no `requestedUsage` - `CreditEntitlementOptions` has no such
|
|
98
|
+
* field - so unlike the metered path, nobody at Stigg is asked whether this particular amount is
|
|
99
|
+
* affordable. Dropping the comparison is not an option: a check for five units against a balance
|
|
100
|
+
* of two would come back granted and the run would overspend. So the SDK answers it, and says so
|
|
101
|
+
* in `locallyDerived` rather than letting a verdict it reached by subtraction read like one the
|
|
102
|
+
* server handed down.
|
|
103
|
+
*/
|
|
104
|
+
private checkCredit;
|
|
105
|
+
/** The metered path, where Stigg is asked about this amount and answers for it. */
|
|
106
|
+
private checkFeature;
|
|
107
|
+
/** The four calls this client makes; `reportMetrics` decides which of them each metric is. */
|
|
108
|
+
report(customerId: string, params: EngineReportParams): Promise<EngineReportResult>;
|
|
109
|
+
/** The feature path: bulk, because a turn reports several metrics and this client has one call. */
|
|
110
|
+
private reportUsage;
|
|
111
|
+
/**
|
|
112
|
+
* The credit path: the currency is drawn from the customer's wallet with no feature mapping,
|
|
113
|
+
* so none of the feature-side setup (a metered feature, a credit rate, an entitling
|
|
114
|
+
* subscription) is involved. Stigg forwards the consumption to governance itself, so per-entity
|
|
115
|
+
* budgets still decrement.
|
|
116
|
+
*
|
|
117
|
+
* `replayed` is always false here: the response carries the wallet's running balance but not
|
|
118
|
+
* this call's own deduction, so a replayed idempotency key is indistinguishable from a fresh
|
|
119
|
+
* one. The server still deduplicates on the key - only our ability to report it is lost.
|
|
120
|
+
*/
|
|
121
|
+
private consumeCredits;
|
|
122
|
+
/** The raw-events path: one event for the turn, matched by name, and no balance in the answer. */
|
|
123
|
+
private reportAsEvent;
|
|
124
|
+
close(): Promise<void>;
|
|
125
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.NodeSdkEngine = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* The engine half that runs on Stigg's own Node server SDK: cached entitlement reads and usage
|
|
39
|
+
* ingest. This is the only file in the package that imports it at runtime.
|
|
40
|
+
*
|
|
41
|
+
* It knows nothing about entities or chains - the GraphQL surface has no governance on it. What
|
|
42
|
+
* it brings is the cache, which is the reason the composite keeps ingest (and customer-level
|
|
43
|
+
* checks) here rather than sending everything to REST - and the reason nothing above this file
|
|
44
|
+
* caches anything. That cache writes reported usage back from the server's own response,
|
|
45
|
+
* invalidates on realtime updates, resets usage periods at read time, falls back when Stigg is
|
|
46
|
+
* unreachable, and optionally lives in Redis so sibling processes share one view.
|
|
47
|
+
*
|
|
48
|
+
* All of that is configured through {@link CacheOptions}, which is the Node SDK's own
|
|
49
|
+
* configuration passed straight through rather than re-described here.
|
|
50
|
+
*/
|
|
51
|
+
const node_server_sdk_1 = __importStar(require("@stigg/node-server-sdk"));
|
|
52
|
+
const errors_1 = require("../errors");
|
|
53
|
+
const sdk_identity_1 = require("../sdk-identity");
|
|
54
|
+
const report_1 = require("./report");
|
|
55
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
56
|
+
/**
|
|
57
|
+
* Drops keys that were never set, so a spread cannot un-set a default.
|
|
58
|
+
*
|
|
59
|
+
* The Node SDK merges its configuration with lodash, which skips an `undefined` source value and
|
|
60
|
+
* keeps its own default - so `{ enableEdge: false, ...cache }` with an unset `cache.enableEdge`
|
|
61
|
+
* would land back on `true` and point a local run at an edge service that was never started.
|
|
62
|
+
*/
|
|
63
|
+
const defined = (options) => Object.fromEntries(Object.entries(options ?? {}).filter(([, value]) => value !== undefined));
|
|
64
|
+
function isLocalhost(baseUrl) {
|
|
65
|
+
if (!baseUrl)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
return LOCAL_HOSTS.has(new URL(baseUrl).hostname);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
class NodeSdkEngine {
|
|
75
|
+
capabilities = {
|
|
76
|
+
// The SDK serves reads from its own cache, so two processes can each approve the same budget.
|
|
77
|
+
strictCheck: false,
|
|
78
|
+
chains: false,
|
|
79
|
+
provisioning: false,
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Whether this engine built the client, and may therefore close it.
|
|
83
|
+
*
|
|
84
|
+
* False for a client that was handed in: it belongs to whoever built it, and its lifecycle is
|
|
85
|
+
* theirs. See `close`.
|
|
86
|
+
*/
|
|
87
|
+
ownsClient;
|
|
88
|
+
client;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
if (options.client) {
|
|
91
|
+
this.client = options.client;
|
|
92
|
+
this.ownsClient = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.ownsClient = true;
|
|
96
|
+
const local = isLocalhost(options.baseUrl);
|
|
97
|
+
this.client = node_server_sdk_1.default.initialize({
|
|
98
|
+
apiKey: options.apiKey,
|
|
99
|
+
// So this traffic is attributable to the agent SDK rather than reading as a plain
|
|
100
|
+
// node-server-sdk client. Only applies to a client built here: `Stigg.initialize` returns
|
|
101
|
+
// its existing global instance when the API key matches, and a client handed in through
|
|
102
|
+
// `options.client` keeps whatever name the application gave it.
|
|
103
|
+
clientName: sdk_identity_1.AGENT_SDK_CLIENT_NAME,
|
|
104
|
+
clientVersion: sdk_identity_1.AGENT_SDK_VERSION,
|
|
105
|
+
...(options.baseUrl ? { baseUri: options.baseUrl } : {}),
|
|
106
|
+
...(options.timeoutMs ? { entitlementsTimeout: options.timeoutMs } : {}),
|
|
107
|
+
// The edge cache and the realtime socket are separate services from the API, and a Stigg
|
|
108
|
+
// running on localhost usually serves neither - without this a local run fails on
|
|
109
|
+
// ECONNREFUSED against services that were never started.
|
|
110
|
+
enableEdge: !local,
|
|
111
|
+
realtimeUpdatesEnabled: !local,
|
|
112
|
+
// Last, so an explicit choice wins over the defaults above.
|
|
113
|
+
...defined(options.cache),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Waits for the cache to be usable.
|
|
118
|
+
*
|
|
119
|
+
* Not cosmetic: until the runtime reports itself initialized, every read is treated as a miss
|
|
120
|
+
* and refetched, so a client that starts serving immediately pays a round trip per check for as
|
|
121
|
+
* long as initialization takes. Awaited once at startup instead.
|
|
122
|
+
*/
|
|
123
|
+
async ready() {
|
|
124
|
+
await this.client.waitForInitialization();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Loads this customer's entitlements into the cache, so the checks that follow are local.
|
|
128
|
+
*
|
|
129
|
+
* The whole map, not one decision: a later check against a different entitlement is warm too,
|
|
130
|
+
* and each is evaluated against the amount it actually asks for rather than against whatever
|
|
131
|
+
* amount happened to warm it.
|
|
132
|
+
*/
|
|
133
|
+
async preload(customerId) {
|
|
134
|
+
await this.client.reloadEntitlements(customerId);
|
|
135
|
+
}
|
|
136
|
+
async upsertEntity() {
|
|
137
|
+
throw new errors_1.GovernanceNotEnabledError('The Node server SDK has no governance surface; entities need the REST client.');
|
|
138
|
+
}
|
|
139
|
+
async archiveEntity() {
|
|
140
|
+
throw new errors_1.GovernanceNotEnabledError('The Node server SDK has no governance surface; entities need the REST client.');
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The two kinds of entitlement are two different endpoints with two different answers, so each
|
|
144
|
+
* gets its own method and this one only picks. Narrowing on `currencyId` rather than through
|
|
145
|
+
* `entitlementKind` is what lets each branch read its own id without an assertion: a function
|
|
146
|
+
* call cannot narrow a union.
|
|
147
|
+
*/
|
|
148
|
+
async check(customerId, { entitlement, requestedUsage }) {
|
|
149
|
+
try {
|
|
150
|
+
return entitlement.currencyId !== undefined
|
|
151
|
+
? await this.checkCredit(customerId, entitlement.currencyId, requestedUsage)
|
|
152
|
+
: await this.checkFeature(customerId, entitlement.featureId, requestedUsage);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw (0, errors_1.asEngineError)('check', err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* `getCreditEntitlement` takes no `requestedUsage` - `CreditEntitlementOptions` has no such
|
|
160
|
+
* field - so unlike the metered path, nobody at Stigg is asked whether this particular amount is
|
|
161
|
+
* affordable. Dropping the comparison is not an option: a check for five units against a balance
|
|
162
|
+
* of two would come back granted and the run would overspend. So the SDK answers it, and says so
|
|
163
|
+
* in `locallyDerived` rather than letting a verdict it reached by subtraction read like one the
|
|
164
|
+
* server handed down.
|
|
165
|
+
*/
|
|
166
|
+
async checkCredit(customerId, currencyId, requestedUsage) {
|
|
167
|
+
const state = await this.client.getCreditEntitlement({ customerId, currencyId });
|
|
168
|
+
const affordable = state.remaining >= requestedUsage;
|
|
169
|
+
const denial = state.hasAccess ? node_server_sdk_1.AccessDeniedReason.InsufficientCredits : state.accessDeniedReason;
|
|
170
|
+
return {
|
|
171
|
+
isGranted: state.hasAccess && affordable,
|
|
172
|
+
accessDeniedReason: affordable && state.hasAccess ? null : (denial ?? null),
|
|
173
|
+
locallyDerived: state.hasAccess,
|
|
174
|
+
usageLimit: state.usageLimit,
|
|
175
|
+
currentUsage: state.currentUsage,
|
|
176
|
+
isFallback: state.isFallback,
|
|
177
|
+
source: 'node-sdk',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** The metered path, where Stigg is asked about this amount and answers for it. */
|
|
181
|
+
async checkFeature(customerId, featureId, requestedUsage) {
|
|
182
|
+
const state = await this.client.getMeteredEntitlement({ customerId, featureId, options: { requestedUsage } });
|
|
183
|
+
return {
|
|
184
|
+
isGranted: state.hasAccess,
|
|
185
|
+
accessDeniedReason: state.accessDeniedReason ?? null,
|
|
186
|
+
usageLimit: state.isUnlimited ? null : (state.usageLimit ?? null),
|
|
187
|
+
currentUsage: state.currentUsage,
|
|
188
|
+
isFallback: state.isFallback,
|
|
189
|
+
source: 'node-sdk',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** The four calls this client makes; `reportMetrics` decides which of them each metric is. */
|
|
193
|
+
async report(customerId, params) {
|
|
194
|
+
return (0, report_1.reportMetrics)(params, {
|
|
195
|
+
credits: (featureId, value) => this.consumeCredits(customerId, params, featureId, value),
|
|
196
|
+
creditsAsync: (featureId, value) => this.consumeCredits(customerId, params, featureId, value, 'async'),
|
|
197
|
+
event: (metrics, dimensions) => this.reportAsEvent(customerId, params, metrics, dimensions),
|
|
198
|
+
usage: (metrics) => this.reportUsage(customerId, params, metrics),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** The feature path: bulk, because a turn reports several metrics and this client has one call. */
|
|
202
|
+
async reportUsage(customerId, params, metrics) {
|
|
203
|
+
const acks = await this.client.reportUsageBulk({ usages: (0, report_1.usageEntries)(customerId, params, metrics) });
|
|
204
|
+
return (0, report_1.usageResult)(acks, metrics, 'node-sdk');
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The credit path: the currency is drawn from the customer's wallet with no feature mapping,
|
|
208
|
+
* so none of the feature-side setup (a metered feature, a credit rate, an entitling
|
|
209
|
+
* subscription) is involved. Stigg forwards the consumption to governance itself, so per-entity
|
|
210
|
+
* budgets still decrement.
|
|
211
|
+
*
|
|
212
|
+
* `replayed` is always false here: the response carries the wallet's running balance but not
|
|
213
|
+
* this call's own deduction, so a replayed idempotency key is indistinguishable from a fresh
|
|
214
|
+
* one. The server still deduplicates on the key - only our ability to report it is lost.
|
|
215
|
+
*/
|
|
216
|
+
async consumeCredits(customerId, params, featureId, value, mode = 'sync') {
|
|
217
|
+
// The wallet's own id. A metric only reaches this path when it names the currency the run
|
|
218
|
+
// checks, so the metric name and the currency are the same string.
|
|
219
|
+
const consumption = (0, report_1.creditConsumption)(customerId, params, featureId, value);
|
|
220
|
+
try {
|
|
221
|
+
// The async form takes the same consumption and reconciles it on Stigg's side, so the
|
|
222
|
+
// amount is still ours to state - which is why a run reporting to its own currency keeps
|
|
223
|
+
// an exact local budget in either mode. Only the balance is lost.
|
|
224
|
+
const ack = mode === 'async' ? undefined : await this.client.consumeCredits(consumption);
|
|
225
|
+
if (mode === 'async')
|
|
226
|
+
await this.client.consumeCreditsAsync({ consumptions: [consumption] });
|
|
227
|
+
return (0, report_1.creditResult)(featureId, value, ack?.credit?.currentUsage, mode, 'node-sdk');
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
throw (0, errors_1.asEngineError)('ingest', err);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** The raw-events path: one event for the turn, matched by name, and no balance in the answer. */
|
|
234
|
+
async reportAsEvent(customerId, params, metrics, dimensions) {
|
|
235
|
+
const event = params.event;
|
|
236
|
+
if (!event)
|
|
237
|
+
throw (0, report_1.missingEventError)();
|
|
238
|
+
try {
|
|
239
|
+
await this.client.reportEvent({
|
|
240
|
+
customerId,
|
|
241
|
+
eventName: event.name,
|
|
242
|
+
// The turn's own key: one event, so no per-feature suffix to keep them apart.
|
|
243
|
+
idempotencyKey: params.idempotencyKey,
|
|
244
|
+
dimensions,
|
|
245
|
+
});
|
|
246
|
+
// No balance comes back, and aggregation lags by seconds - so `replayed` stays unknown.
|
|
247
|
+
return { reported: metrics, mode: 'async', source: 'node-sdk' };
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
throw (0, errors_1.asEngineError)('ingest', err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async close() {
|
|
254
|
+
// The SDK keeps a background cache and sockets alive; without this a CLI never exits. But
|
|
255
|
+
// only for a client this engine built: `close` nulls the Node SDK's global instance and tears
|
|
256
|
+
// down the runtime, so closing a borrowed one would take the application's cache and socket
|
|
257
|
+
// with it.
|
|
258
|
+
if (this.ownsClient)
|
|
259
|
+
await this.client.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
exports.NodeSdkEngine = NodeSdkEngine;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of an ingest, which both transports share even though none of their calls do.
|
|
3
|
+
*
|
|
4
|
+
* Stigg has two ingest APIs and they are not interchangeable. The usage report takes a fixed DTO -
|
|
5
|
+
* a `featureId` and a whole-number `value` per metric - and aggregates on arrival, so the next
|
|
6
|
+
* check sees the spend. The event report takes the call as one event and lets Stigg's own meters
|
|
7
|
+
* turn it into usage seconds later, for a fraction of the latency and a far higher rate limit.
|
|
8
|
+
* Which one a run wants is a product decision - immediacy against throughput - so the caller
|
|
9
|
+
* chooses it and this file only carries the choice out.
|
|
10
|
+
*
|
|
11
|
+
* What that costs each metric:
|
|
12
|
+
*
|
|
13
|
+
* - **`'sync'`** sends one usage report per metric, batched into the one bulk call both clients
|
|
14
|
+
* have, because a chat turn otherwise costs four round trips.
|
|
15
|
+
* - **`'async'`** sends **one event for the whole turn**, every metric on its own dimension. One
|
|
16
|
+
* event can feed any number of meters - each picks its number with `aggregation.field` - so
|
|
17
|
+
* fanning out per metric would buy nothing and cost a request each.
|
|
18
|
+
*
|
|
19
|
+
* A metric named after the run's own credit currency is neither: it draws the wallet directly,
|
|
20
|
+
* because a usage report takes only a `featureId` and there is no event path from a raw event to
|
|
21
|
+
* a currency. It follows the mode only in how it waits - `consumeCredits` under `'sync'`, which
|
|
22
|
+
* answers with a balance, and `consumeCreditsAsync` under `'async'`, which does not.
|
|
23
|
+
*
|
|
24
|
+
* That orchestration lives here, and with it the parts of an ingest that are the same whichever
|
|
25
|
+
* client makes it: the consumption payload, the per-metric idempotency key, and how a bulk ack is
|
|
26
|
+
* read. Each engine still passes its own calls, so a vendor method is only ever named by the file
|
|
27
|
+
* that owns that client - but the split between wallet and features, and what a turn is taken to
|
|
28
|
+
* have cost, cannot come to mean one thing over REST and another over the SDK.
|
|
29
|
+
*/
|
|
30
|
+
import { EngineError } from '../errors';
|
|
31
|
+
import type { IngestMode, Metrics } from '../types';
|
|
32
|
+
import type { EngineOpSource, EngineReportParams, EngineReportResult } from './engine';
|
|
33
|
+
/** What one transport can do with an ingest, as the calls it already knows how to make. */
|
|
34
|
+
export type ReportCalls = {
|
|
35
|
+
/** Draw the wallet and wait for the balance, for a metric that names the run's currency. */
|
|
36
|
+
credits: (featureId: string, value: number) => Promise<EngineReportResult>;
|
|
37
|
+
/** The same draw, reconciled on Stigg's side. No balance comes back. */
|
|
38
|
+
creditsAsync: (featureId: string, value: number) => Promise<EngineReportResult>;
|
|
39
|
+
/** One bulk usage report, one entry per metric. */
|
|
40
|
+
usage: (metrics: Metrics) => Promise<EngineReportResult>;
|
|
41
|
+
/** One event for the whole turn, carrying every metric as a dimension. */
|
|
42
|
+
event: (metrics: Metrics, dimensions: Record<string, string | number | boolean>) => Promise<EngineReportResult>;
|
|
43
|
+
};
|
|
44
|
+
/** A draw on the wallet. `consume` and `consumeAsync` take it unchanged, on both transports. */
|
|
45
|
+
export declare const creditConsumption: (customerId: string, params: EngineReportParams, currencyId: string, amount: number) => {
|
|
46
|
+
customerId: string;
|
|
47
|
+
currencyId: string;
|
|
48
|
+
amount: number;
|
|
49
|
+
idempotencyKey: string;
|
|
50
|
+
dimensions: Record<string, string>;
|
|
51
|
+
};
|
|
52
|
+
/** One bulk usage report, one entry per metric. REST adds its own `updateBehavior` on top. */
|
|
53
|
+
export declare const usageEntries: (customerId: string, params: EngineReportParams, metrics: Metrics) => {
|
|
54
|
+
customerId: string;
|
|
55
|
+
featureId: string;
|
|
56
|
+
value: number;
|
|
57
|
+
dimensions: Record<string, string>;
|
|
58
|
+
idempotencyKey: string;
|
|
59
|
+
}[];
|
|
60
|
+
/**
|
|
61
|
+
* What a wallet draw amounts to, read the same way whichever client made it.
|
|
62
|
+
*
|
|
63
|
+
* Only the call and where the balance sits on its response differ between the transports; this is
|
|
64
|
+
* everything after that. `creditsConsumed` is the metric's own value because a draw is spent at
|
|
65
|
+
* par - no feature mapping, no rate - and `replayed` is `false` under `'sync'` rather than
|
|
66
|
+
* unknown: the response carries the wallet's running balance but not this call's own deduction,
|
|
67
|
+
* so a replayed key is indistinguishable from a fresh one. Under `'async'` nothing comes back at
|
|
68
|
+
* all, so it stays undefined.
|
|
69
|
+
*/
|
|
70
|
+
export declare function creditResult(currencyId: string, amount: number, currentUsage: number | null | undefined, mode: IngestMode, source: EngineOpSource): EngineReportResult;
|
|
71
|
+
/** As much of a bulk ack as either client returns: the balance, and what each entry cost. */
|
|
72
|
+
export type UsageAck = {
|
|
73
|
+
currentUsage?: number | null;
|
|
74
|
+
credit?: {
|
|
75
|
+
currentUsage?: number | null;
|
|
76
|
+
consumed?: number | null;
|
|
77
|
+
} | null;
|
|
78
|
+
};
|
|
79
|
+
/** What a bulk usage report amounts to, read the same way whichever client answered. */
|
|
80
|
+
export declare function usageResult(acks: readonly UsageAck[], metrics: Metrics, source: EngineOpSource): EngineReportResult;
|
|
81
|
+
export declare function reportMetrics(params: EngineReportParams, calls: ReportCalls): Promise<EngineReportResult>;
|
|
82
|
+
/**
|
|
83
|
+
* What one event carries, and in which order.
|
|
84
|
+
*
|
|
85
|
+
* Three namespaces flatten into one map, so the order is the whole design. Attribution is applied
|
|
86
|
+
* last and wins outright: a gateway passes vendor fields through, and a vendor field that happened
|
|
87
|
+
* to be called `agentId` would otherwise re-attribute the bill to an entity that never ran. The
|
|
88
|
+
* raw block sits under its own prefix beneath both, where it can be metered deliberately but
|
|
89
|
+
* cannot displace anything.
|
|
90
|
+
*/
|
|
91
|
+
export declare function eventDimensions(params: EngineReportParams, metrics: Metrics): Record<string, string | number | boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* The one thing the caller has to fix themselves, so it reads the same whichever client found it.
|
|
94
|
+
*
|
|
95
|
+
* An in-process run derives the name from the vendor call it wrapped, so this is reachable only
|
|
96
|
+
* out of process, where there is no recipe to derive it from.
|
|
97
|
+
*/
|
|
98
|
+
export declare function missingEventError(): EngineError;
|
|
99
|
+
/**
|
|
100
|
+
* A sync report to a feature only a meter can fill.
|
|
101
|
+
*
|
|
102
|
+
* The rejection used to be a routing signal - the engines retried it as an event and remembered.
|
|
103
|
+
* That made the transport a property of the catalog, discovered one rejection at a time, and
|
|
104
|
+
* silently moved a run onto an eventually-consistent path it never asked for. Now it says so.
|
|
105
|
+
*/
|
|
106
|
+
export declare function meterMismatchError(metrics: Metrics): EngineError;
|