@stigg/agent-sdk-js 0.0.0-stage → 0.1.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/README.md +1296 -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/bedrock/bedrock-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
- package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
- package/dist/bindings/client/gateways/bedrock/types.js +2 -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 +20 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +244 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -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/groq/groq-recipe.d.ts +20 -0
- package/dist/bindings/client/providers/groq/groq-recipe.js +101 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
- package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
- package/dist/bindings/client/providers/mistral/types.js +2 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
- package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
- package/dist/bindings/client/providers/ollama/types.js +2 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -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 +240 -0
- package/dist/bindings/shared/token-usage.js +574 -0
- package/dist/bindings/shared/types.d.ts +129 -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 +47 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +99 -0
- package/dist/core/context.d.ts +160 -0
- package/dist/core/context.js +221 -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 +190 -0
- package/dist/core/session.js +448 -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 +42 -0
- package/dist/engine/decision.js +135 -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 +188 -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 +86 -0
- package/dist/errors.js +161 -0
- package/dist/headers.d.ts +40 -0
- package/dist/headers.js +82 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +106 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +87 -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 +300 -0
- package/dist/stigg-agent-sdk.js +656 -0
- package/dist/types.d.ts +642 -0
- package/dist/types.js +147 -0
- package/package.json +89 -3
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vendor response shapes these converters read.
|
|
3
|
+
*
|
|
4
|
+
* Typed structurally rather than against each vendor's SDK, so the same handful of functions cover
|
|
5
|
+
* a direct client, a gateway response and an orchestrator callback - and adding a provider whose
|
|
6
|
+
* response happens to match costs nothing.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A flat `usage` object whose cache counters sit BESIDE the input count rather than inside it.
|
|
10
|
+
*
|
|
11
|
+
* Named for the shape, not for a vendor, because more than one reports it: Anthropic's Messages
|
|
12
|
+
* API and TypeSafe's System One, which carries only the first two. The distinction from the two
|
|
13
|
+
* shapes below is the whole reason this is a separate reader - they spell the same two counters
|
|
14
|
+
* the same way but count their cached tokens *within* the input total, so reading one as the
|
|
15
|
+
* other double-counts in one direction or overstates in the other.
|
|
16
|
+
*/
|
|
17
|
+
export type FlatTokenUsage = {
|
|
18
|
+
input_tokens?: number | null;
|
|
19
|
+
output_tokens?: number | null;
|
|
20
|
+
cache_read_input_tokens?: number | null;
|
|
21
|
+
cache_creation_input_tokens?: number | null;
|
|
22
|
+
};
|
|
23
|
+
/** OpenAI-compatible `usage` - also what OpenRouter and most gateways return. */
|
|
24
|
+
export type OpenAILikeUsage = {
|
|
25
|
+
prompt_tokens?: number | null;
|
|
26
|
+
completion_tokens?: number | null;
|
|
27
|
+
/** The vendor's own total. `prompt_tokens + completion_tokens`; cached tokens are inside the first. */
|
|
28
|
+
total_tokens?: number | null;
|
|
29
|
+
prompt_tokens_details?: {
|
|
30
|
+
cached_tokens?: number | null;
|
|
31
|
+
cache_write_tokens?: number | null;
|
|
32
|
+
} | null;
|
|
33
|
+
};
|
|
34
|
+
/** OpenAI's Responses API `usage` - the same counts as above under different names. */
|
|
35
|
+
export type ResponsesLikeUsage = {
|
|
36
|
+
input_tokens?: number | null;
|
|
37
|
+
output_tokens?: number | null;
|
|
38
|
+
/** The vendor's own total, under the same name the Chat Completions shape uses. */
|
|
39
|
+
total_tokens?: number | null;
|
|
40
|
+
input_tokens_details?: {
|
|
41
|
+
cached_tokens?: number | null;
|
|
42
|
+
cache_write_tokens?: number | null;
|
|
43
|
+
} | null;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Bedrock's Converse `usage`. Flat, camelCased, and **disjoint**.
|
|
47
|
+
*
|
|
48
|
+
* AWS is explicit that `inputTokens` counts only the tokens that were neither read from nor
|
|
49
|
+
* written to the cache - "total input tokens = inputTokens + cacheReadInputTokens +
|
|
50
|
+
* cacheWriteInputTokens" - so this shape behaves like Anthropic's and not like OpenAI's, where the
|
|
51
|
+
* cached count nests inside the prompt count. Subtracting here would undercount the call.
|
|
52
|
+
*/
|
|
53
|
+
export type BedrockLikeUsage = {
|
|
54
|
+
inputTokens?: number | null;
|
|
55
|
+
outputTokens?: number | null;
|
|
56
|
+
totalTokens?: number | null;
|
|
57
|
+
cacheReadInputTokens?: number | null;
|
|
58
|
+
cacheWriteInputTokens?: number | null;
|
|
59
|
+
};
|
|
60
|
+
/** LangChain's `usage_metadata`, as carried on an AIMessage. */
|
|
61
|
+
export type LangChainLikeUsage = {
|
|
62
|
+
input_tokens?: number | null;
|
|
63
|
+
output_tokens?: number | null;
|
|
64
|
+
input_token_details?: {
|
|
65
|
+
cache_read?: number | null;
|
|
66
|
+
cache_creation?: number | null;
|
|
67
|
+
} | null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Gemini's `usageMetadata`, which counts in its own names and overlaps in two directions at once.
|
|
71
|
+
*
|
|
72
|
+
* Both overlaps are quoted from the vendor's own field docs, because each one is a doubled or
|
|
73
|
+
* halved bill if read the other way round:
|
|
74
|
+
*
|
|
75
|
+
* - `promptTokenCount` - "when `cached_content` is set, this **also includes** the number of
|
|
76
|
+
* tokens in the cached content", so `cachedContentTokenCount` is counted *inside* it, the way
|
|
77
|
+
* OpenAI nests `cached_tokens` inside `prompt_tokens`.
|
|
78
|
+
* - `totalTokenCount` - "the sum of `prompt_token_count`, `candidates_token_count`,
|
|
79
|
+
* `tool_use_prompt_token_count`, and `thoughts_token_count`", so the last two sit *beside* the
|
|
80
|
+
* first two rather than within them. Thinking tokens in particular are billed as output and
|
|
81
|
+
* are **not** inside `candidatesTokenCount`: a reader that ignores them undercounts the output
|
|
82
|
+
* of every reasoning model this vendor sells.
|
|
83
|
+
*/
|
|
84
|
+
export type GeminiLikeUsage = {
|
|
85
|
+
/**
|
|
86
|
+
* The vendor's own total: `promptTokenCount + candidatesTokenCount + toolUsePromptTokenCount +
|
|
87
|
+
* thoughtsTokenCount`, with the cached count already inside the first. Read, never recomputed.
|
|
88
|
+
*/
|
|
89
|
+
totalTokenCount?: number | null;
|
|
90
|
+
promptTokenCount?: number | null;
|
|
91
|
+
candidatesTokenCount?: number | null;
|
|
92
|
+
cachedContentTokenCount?: number | null;
|
|
93
|
+
thoughtsTokenCount?: number | null;
|
|
94
|
+
toolUsePromptTokenCount?: number | null;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Mistral's `UsageInfo`, which counts the OpenAI three under camelCase names.
|
|
98
|
+
*
|
|
99
|
+
* The *wire* is snake_case - `prompt_tokens` and the rest - and the generated SDK renames it on
|
|
100
|
+
* the way in, so what a recipe reads here and what it reports are deliberately different strings.
|
|
101
|
+
* See `MISTRAL_METRIC_NAME_MAP` in `token-usage.ts` for the mapping and why it runs in that direction.
|
|
102
|
+
*
|
|
103
|
+
* Every counter is optional because the SDK declares them so: the inbound schema defaults each to
|
|
104
|
+
* zero, which means an absent counter and a genuine zero arrive looking identical. Read them as
|
|
105
|
+
* possibly-missing rather than trusting the declared `number`.
|
|
106
|
+
*/
|
|
107
|
+
export type MistralLikeUsage = {
|
|
108
|
+
promptTokens?: number | null;
|
|
109
|
+
completionTokens?: number | null;
|
|
110
|
+
/** The vendor's own total. Stated, so it is read rather than recomputed from the two above. */
|
|
111
|
+
totalTokens?: number | null;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Ollama's counters, which sit **flat on the response root** rather than inside a `usage` object.
|
|
115
|
+
*
|
|
116
|
+
* Every field is optional here even though `ollama`'s own `ChatResponse` declares
|
|
117
|
+
* `prompt_eval_count: number` and `eval_count: number` as required. The server marks the whole
|
|
118
|
+
* `Metrics` struct `omitempty`, so an intermediate stream chunk carries none of them at all and
|
|
119
|
+
* the declared type is simply wrong about it. Trusting it substitutes zeroes for counters that
|
|
120
|
+
* were never sent, which bills a real call at nothing.
|
|
121
|
+
*
|
|
122
|
+
* `prompt_eval_cached_count` exists on the wire and is deliberately **not** read: the SDK does not
|
|
123
|
+
* declare it, and whether it sits inside `prompt_eval_count` or beside it is undocumented. A
|
|
124
|
+
* counter whose overlap direction is unknown cannot be reported without risking a double count.
|
|
125
|
+
*/
|
|
126
|
+
export type OllamaLikeUsage = {
|
|
127
|
+
prompt_eval_count?: number | null;
|
|
128
|
+
eval_count?: number | null;
|
|
129
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A price per metric, as the mapper a run reports through.
|
|
3
|
+
*
|
|
4
|
+
* Writing one by hand is a reduce over a record and three details that are easy to miss: Stigg
|
|
5
|
+
* rejects fractions, a turn that measured nothing must report nothing rather than a zero, and a
|
|
6
|
+
* counter the response omitted is absent rather than zero. This gets all three right once.
|
|
7
|
+
*/
|
|
8
|
+
import type { MetricsMapper } from '../../types';
|
|
9
|
+
import type { ClientRecipe } from '../client/core/types';
|
|
10
|
+
export type WeightedMetricsOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* How many weighted units make one of `target`. Defaults to 1, so a weight reads as the price
|
|
13
|
+
* of one unit of that metric: `output_tokens: 0.5` is half a credit per output token.
|
|
14
|
+
*
|
|
15
|
+
* Set it when the weights are easier to write per thousand than per one - `per: 1000` with
|
|
16
|
+
* `output_tokens: 5` prices five credits per thousand output tokens.
|
|
17
|
+
*/
|
|
18
|
+
per?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Prices what a vendor reported into one metric of your own.
|
|
22
|
+
*
|
|
23
|
+
* The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
|
|
24
|
+
* is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* // Half a credit per output token, a whole one per input token, cache reads free.
|
|
28
|
+
* metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
|
|
29
|
+
* input_tokens: 1,
|
|
30
|
+
* output_tokens: 0.5,
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Prices what the vendor actually said, under its own names. For one vocabulary across several
|
|
35
|
+
* vendors, `normalizedMetrics` reports the four neutral counters instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare function weightedMetrics<M extends string>(recipe: ClientRecipe<M>, target: string, weights: Partial<Record<M, number>>, options?: WeightedMetricsOptions): MetricsMapper;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.weightedMetrics = weightedMetrics;
|
|
4
|
+
/**
|
|
5
|
+
* Prices what a vendor reported into one metric of your own.
|
|
6
|
+
*
|
|
7
|
+
* The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
|
|
8
|
+
* is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Half a credit per output token, a whole one per input token, cache reads free.
|
|
12
|
+
* metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
|
|
13
|
+
* input_tokens: 1,
|
|
14
|
+
* output_tokens: 0.5,
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Prices what the vendor actually said, under its own names. For one vocabulary across several
|
|
19
|
+
* vendors, `normalizedMetrics` reports the four neutral counters instead.
|
|
20
|
+
*/
|
|
21
|
+
function weightedMetrics(recipe, target, weights, options = {}) {
|
|
22
|
+
const per = options.per && options.per > 0 ? options.per : 1;
|
|
23
|
+
// The one thing this cannot report its way out of: an empty target is a feature id of `''`,
|
|
24
|
+
// which nothing downstream rejects - the value is validated, the key is not - so the turn
|
|
25
|
+
// reports to a feature that does not exist and the budget never moves.
|
|
26
|
+
if (!target.trim()) {
|
|
27
|
+
throw new Error('weightedMetrics needs a target to price into - an empty one reports to no feature.');
|
|
28
|
+
}
|
|
29
|
+
// Deduplicated, because the reducer walks the declaration: a recipe listing a name twice would
|
|
30
|
+
// charge for it twice, which is the one direction a meter must not be wrong in.
|
|
31
|
+
const priced = [...new Set(recipe.metrics)];
|
|
32
|
+
return ({ metrics }) => {
|
|
33
|
+
if (!metrics)
|
|
34
|
+
return {};
|
|
35
|
+
// An unpriced metric is free rather than an error, so a table names only what it charges for.
|
|
36
|
+
const weighted = priced.reduce((sum, name) => sum + (metrics[name] ?? 0) * (weights[name] ?? 0), 0);
|
|
37
|
+
// Nothing measured is not the same as nothing owed: report no metric at all, which the run
|
|
38
|
+
// counts as the no-op ingest it is rather than as a report of zero.
|
|
39
|
+
if (weighted <= 0)
|
|
40
|
+
return {};
|
|
41
|
+
// Reported as it came out of the weights. A credit currency takes fractions, so a turn worth
|
|
42
|
+
// a fifth of a unit costs a fifth - rounding here would be this SDK's arithmetic on someone
|
|
43
|
+
// else's price. A feature is the exception, since `POST /api/v1/usage` takes integers: choose
|
|
44
|
+
// `per` so the weights divide, or round in a mapper of your own.
|
|
45
|
+
return { [target]: weighted / per };
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client configuration - everything that is true of one Stigg *environment*, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A Stigg API key is environment-bound, so the key identifies the environment: one instance per
|
|
5
|
+
* environment, like the Node server SDK. That draws the line for what belongs here. Anything that
|
|
6
|
+
* varies within an environment - which customer, which agent, what it bills against, who wants to
|
|
7
|
+
* watch it happen - is context for the call that needs it, not configuration for the process.
|
|
8
|
+
*
|
|
9
|
+
* The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
|
|
10
|
+
* a readable message instead of as a 404 three calls later. The collaborator half (logger,
|
|
11
|
+
* onError) is injected as-is - those are substitution seams, not data to validate.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { type Logger } from './logger';
|
|
15
|
+
import type { CacheOptions, NodeSdkClient } from './engine/node-sdk-engine';
|
|
16
|
+
import type { CheckErrorAction, IngestMode } from './types';
|
|
17
|
+
import type { EngineError } from './errors';
|
|
18
|
+
declare const dataSchema: z.ZodObject<{
|
|
19
|
+
apiKey: z.ZodString;
|
|
20
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
21
|
+
checkErrorAction: z.ZodDefault<z.ZodEnum<{
|
|
22
|
+
deny: "deny";
|
|
23
|
+
allow: "allow";
|
|
24
|
+
}>>;
|
|
25
|
+
ingest: z.ZodDefault<z.ZodEnum<{
|
|
26
|
+
sync: "sync";
|
|
27
|
+
async: "async";
|
|
28
|
+
}>>;
|
|
29
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export type { CacheOptions, NodeSdkClient };
|
|
32
|
+
/** Reported to `onError` alongside the failure, so a swallowed ingest is still diagnosable. */
|
|
33
|
+
export type ErrorContext = {
|
|
34
|
+
operation: string;
|
|
35
|
+
runId?: string;
|
|
36
|
+
idempotencyKey?: string;
|
|
37
|
+
customerId?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Everything the SDK can be told, in one object.
|
|
41
|
+
*
|
|
42
|
+
* All of it optional and all of it with a working default, including where to send traffic: the
|
|
43
|
+
* key falls back to `STIGG_SERVER_API_KEY` and the URL to the hosted API, so the smallest working
|
|
44
|
+
* program is `new StiggAgentSDK()`.
|
|
45
|
+
*
|
|
46
|
+
* One object rather than positional arguments so that each answer has exactly one place to go -
|
|
47
|
+
* a URL given in two places needs a runtime check to refuse both, and an optional argument in
|
|
48
|
+
* front of a required one forces `undefined` placeholders at the call site.
|
|
49
|
+
*/
|
|
50
|
+
export type StiggAgentSDKOptions = {
|
|
51
|
+
/**
|
|
52
|
+
* The server key, which names the environment. Falls back to `STIGG_SERVER_API_KEY`, the way
|
|
53
|
+
* every vendor SDK does - **including when `baseUrl` is set**, since a URL says where traffic
|
|
54
|
+
* goes and nothing about who is sending it.
|
|
55
|
+
*
|
|
56
|
+
* So a shell with that variable exported sends it to whatever `baseUrl` names, a local mock
|
|
57
|
+
* included. To run keyless on such a machine, unset the variable for the process rather than
|
|
58
|
+
* relying on `baseUrl` to suppress it: a deployment that genuinely needs no key is the case
|
|
59
|
+
* where nothing is configured anywhere, and that is the one the fallback below serves.
|
|
60
|
+
*/
|
|
61
|
+
apiKey?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Where to send traffic, when it is not the hosted API. Pass it alongside `apiKey` for a
|
|
64
|
+
* self-hosted deployment that still wants a key, or on its own for one that needs none.
|
|
65
|
+
*/
|
|
66
|
+
baseUrl?: string;
|
|
67
|
+
/**
|
|
68
|
+
* How checks are cached. See {@link CacheOptions} - it is the Node server SDK's own cache
|
|
69
|
+
* configuration, passed through, because that is where caching actually happens.
|
|
70
|
+
*/
|
|
71
|
+
cache?: CacheOptions;
|
|
72
|
+
/**
|
|
73
|
+
* A Node server SDK client you already have, used instead of building one.
|
|
74
|
+
*
|
|
75
|
+
* Pass it when the application uses `@stigg/node-server-sdk` directly. It is the only way to
|
|
76
|
+
* settle ownership: `Stigg.initialize` keeps one global instance per process and hands it back
|
|
77
|
+
* when the API key matches, so without this the SDK may be handed your client, believe it built
|
|
78
|
+
* it, and close it out from under you on `stigg.close()` - taking its cache and socket along.
|
|
79
|
+
* A client passed here is borrowed and never closed.
|
|
80
|
+
*
|
|
81
|
+
* It replaces only the cached half. The governance REST client is still built from the API key,
|
|
82
|
+
* because the Node SDK has no governance surface. Mutually exclusive with
|
|
83
|
+
* {@link StiggAgentSDKOptions.cache}, which configures a client this SDK is no longer building.
|
|
84
|
+
*/
|
|
85
|
+
client?: NodeSdkClient;
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Ignored. There is no local decision cache to put a TTL on any more - the Node
|
|
88
|
+
* server SDK's entitlement cache serves these checks and keeps itself fresh, so there is no
|
|
89
|
+
* window to size. Configure caching with {@link StiggAgentSDKOptions.cache}; for a hard limit
|
|
90
|
+
* with no cached state at all, there is nothing to turn off on the attributed governance path.
|
|
91
|
+
*/
|
|
92
|
+
cacheTtlMs?: number;
|
|
93
|
+
/**
|
|
94
|
+
* What happens to a call when the check itself fails - Stigg unreachable, a timeout, a 5xx.
|
|
95
|
+
*
|
|
96
|
+
* Defaults to `'allow'`: a soft limit, so an outage does not take the application down with it.
|
|
97
|
+
* Pass `'deny'` to mirror governance's own fail-closed behaviour. Overridable per call through
|
|
98
|
+
* `stiggContext`, and by `wrap`'s own context for every call through one client.
|
|
99
|
+
*/
|
|
100
|
+
checkErrorAction?: CheckErrorAction;
|
|
101
|
+
/**
|
|
102
|
+
* Which of Stigg's ingest APIs a run reports through. Defaults to `'async'`: the event API, which
|
|
103
|
+
* costs a fraction of the latency and takes a far higher rate. Overridable per run, and see
|
|
104
|
+
* {@link IngestMode} for what the two trade against each other.
|
|
105
|
+
*/
|
|
106
|
+
ingest?: IngestMode;
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Where the SDK narrates what it did. Defaults to `'console'`, because the things it says are
|
|
110
|
+
* the ones you need before anything is configured - which transport resolved, that a run is
|
|
111
|
+
* gating nothing, that what was reported names no entitlement the budget converts.
|
|
112
|
+
*
|
|
113
|
+
* Pass `'none'` to silence it, or your own logger to route it.
|
|
114
|
+
*/
|
|
115
|
+
logger?: Logger | 'console' | 'none';
|
|
116
|
+
/**
|
|
117
|
+
* Every instrumentation failure, as structure rather than as a sentence.
|
|
118
|
+
*
|
|
119
|
+
* The one observability hook that is environment-scoped, because it is the only one that fires
|
|
120
|
+
* with no run to belong to: engine resolution, a bare `register()`, `close()`. Watching a
|
|
121
|
+
* decision or an ingest is per-run and lives on `run.start(..., { hooks })`.
|
|
122
|
+
*
|
|
123
|
+
* It is also the only place metering failures surface at all - they are swallowed so they
|
|
124
|
+
* cannot break a run - so it defaults to a warning through `logger` rather than to silence.
|
|
125
|
+
* Replace it to route an `EngineError` and its {@link ErrorContext} into your own telemetry.
|
|
126
|
+
*/
|
|
127
|
+
onError?: (error: EngineError, context: ErrorContext) => void;
|
|
128
|
+
};
|
|
129
|
+
export type ResolvedConfig = Omit<Required<z.infer<typeof dataSchema>>, 'baseUrl'> & {
|
|
130
|
+
baseUrl?: string;
|
|
131
|
+
cache?: CacheOptions;
|
|
132
|
+
client?: NodeSdkClient;
|
|
133
|
+
logger: Logger;
|
|
134
|
+
onError: (error: EngineError, context: ErrorContext) => void;
|
|
135
|
+
};
|
|
136
|
+
export declare function resolveConfig(options?: StiggAgentSDKOptions): ResolvedConfig;
|
|
137
|
+
/** The slice of config the engine factory needs. */
|
|
138
|
+
export type EngineConfig = {
|
|
139
|
+
apiKey: string;
|
|
140
|
+
baseUrl?: string;
|
|
141
|
+
timeoutMs: number;
|
|
142
|
+
cache?: CacheOptions;
|
|
143
|
+
client?: NodeSdkClient;
|
|
144
|
+
logger: Logger;
|
|
145
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveConfig = resolveConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Client configuration - everything that is true of one Stigg *environment*, and nothing else.
|
|
6
|
+
*
|
|
7
|
+
* A Stigg API key is environment-bound, so the key identifies the environment: one instance per
|
|
8
|
+
* environment, like the Node server SDK. That draws the line for what belongs here. Anything that
|
|
9
|
+
* varies within an environment - which customer, which agent, what it bills against, who wants to
|
|
10
|
+
* watch it happen - is context for the call that needs it, not configuration for the process.
|
|
11
|
+
*
|
|
12
|
+
* The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
|
|
13
|
+
* a readable message instead of as a 404 three calls later. The collaborator half (logger,
|
|
14
|
+
* onError) is injected as-is - those are substitution seams, not data to validate.
|
|
15
|
+
*/
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const context_1 = require("./core/context");
|
|
18
|
+
const logger_1 = require("./logger");
|
|
19
|
+
/** The key sent when the target is a URL that needs none. Never reaches a real environment. */
|
|
20
|
+
const KEYLESS = 'offline-mock';
|
|
21
|
+
const dataSchema = zod_1.z.object({
|
|
22
|
+
apiKey: zod_1.z.string().min(1),
|
|
23
|
+
baseUrl: zod_1.z.string().url().optional(),
|
|
24
|
+
// Both default through the context's own table rather than restating it: the two used to
|
|
25
|
+
// disagree, and the one written here was the one nothing read.
|
|
26
|
+
checkErrorAction: zod_1.z.enum(['deny', 'allow']).default(context_1.CONTEXT_DEFAULTS.checkErrorAction),
|
|
27
|
+
ingest: zod_1.z.enum(['sync', 'async']).default(context_1.CONTEXT_DEFAULTS.ingestMode),
|
|
28
|
+
timeoutMs: zod_1.z.number().int().positive().default(3_000),
|
|
29
|
+
});
|
|
30
|
+
const resolveLogger = (logger) => {
|
|
31
|
+
if (logger === 'console' || logger === undefined)
|
|
32
|
+
return logger_1.consoleLogger;
|
|
33
|
+
if (logger === 'none')
|
|
34
|
+
return logger_1.noopLogger;
|
|
35
|
+
return logger;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The key to send, which is the environment's identity.
|
|
39
|
+
*
|
|
40
|
+
* The environment variable is tried before the keyless fallback, and that order is the whole of
|
|
41
|
+
* it. A `baseUrl` says *where* to send traffic and nothing about who is sending it, so the two
|
|
42
|
+
* are independent: a self-hosted deployment names its URL in options and keeps its key in
|
|
43
|
+
* `STIGG_SERVER_API_KEY`, and substituting a sentinel there would 401 with nothing in the config
|
|
44
|
+
* to say why.
|
|
45
|
+
*
|
|
46
|
+
* {@link KEYLESS} is the last resort, not the rule: it stands in for a deployment that needs no
|
|
47
|
+
* key at all - a local stand-in, or the built-in mock - and exists only because the schema below
|
|
48
|
+
* demands a non-empty string. Nothing authenticates with it.
|
|
49
|
+
*
|
|
50
|
+
* The failure for a missing key is raised here rather than left to zod, because "set
|
|
51
|
+
* STIGG_SERVER_API_KEY" is the actionable half and the schema only knows the field was too short.
|
|
52
|
+
*/
|
|
53
|
+
function resolveApiKey({ apiKey, baseUrl }) {
|
|
54
|
+
const named = apiKey?.trim();
|
|
55
|
+
if (named)
|
|
56
|
+
return named;
|
|
57
|
+
const fromEnv = process.env.STIGG_SERVER_API_KEY?.trim();
|
|
58
|
+
if (fromEnv)
|
|
59
|
+
return fromEnv;
|
|
60
|
+
// No key anywhere. A URL to send to is the one case where that is a configuration rather than
|
|
61
|
+
// an omission.
|
|
62
|
+
if (baseUrl)
|
|
63
|
+
return KEYLESS;
|
|
64
|
+
throw new Error('No Stigg API key - pass `apiKey`, or set STIGG_SERVER_API_KEY. To run without one, pass ' +
|
|
65
|
+
'`baseUrl` instead: a local stand-in, a self-hosted deployment, or the built-in mock.');
|
|
66
|
+
}
|
|
67
|
+
function resolveConfig(options = {}) {
|
|
68
|
+
const parsed = dataSchema.parse({
|
|
69
|
+
apiKey: resolveApiKey(options),
|
|
70
|
+
baseUrl: options.baseUrl,
|
|
71
|
+
checkErrorAction: options.checkErrorAction,
|
|
72
|
+
ingest: options.ingest,
|
|
73
|
+
timeoutMs: options.timeoutMs,
|
|
74
|
+
});
|
|
75
|
+
// Refused rather than silently preferred, like the two-URL case above: `cache` configures a
|
|
76
|
+
// client, and a client that was handed in is already configured. Whichever the SDK picked, the
|
|
77
|
+
// other would be settings the caller believes are in force and are not.
|
|
78
|
+
if (options.client && options.cache) {
|
|
79
|
+
throw new Error('This SDK was given both a `client` and `cache` options. The cache options configure a ' +
|
|
80
|
+
'client at construction, and the one you passed is already built - so they would be ' +
|
|
81
|
+
'ignored. Pass one: `client` for a client you own, or `cache` to have the SDK build one.');
|
|
82
|
+
}
|
|
83
|
+
const logger = resolveLogger(options.logger);
|
|
84
|
+
// Said rather than ignored in silence: someone who set this was sizing an overshoot window, and
|
|
85
|
+
// the answer to that question is now different rather than smaller.
|
|
86
|
+
if (options.cacheTtlMs !== undefined) {
|
|
87
|
+
logger.warn('[stigg] cacheTtlMs is ignored - this SDK no longer caches decisions. Checks are served by ' +
|
|
88
|
+
"the Node server SDK's entitlement cache, which invalidates on updates and writes reported " +
|
|
89
|
+
'usage back, so there is no TTL to size. Configure it with `cache` instead.');
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...parsed,
|
|
93
|
+
cache: options.cache,
|
|
94
|
+
client: options.client,
|
|
95
|
+
logger,
|
|
96
|
+
// Never silent, never fatal. Replace it to route into your own telemetry.
|
|
97
|
+
onError: options.onError ?? ((error, context) => logger.warn(`[stigg] ${context.operation} failed: ${error.message}`)),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CheckErrorAction, Entitlement, IngestMode, Metrics, MetricsMapper } from '../types';
|
|
2
|
+
import type { EventIngest } from '../engine/engine';
|
|
3
|
+
/**
|
|
4
|
+
* What a call is governed by. Every field is optional here because each one may have been given
|
|
5
|
+
* at `wrap` instead - `customerId` is the only one that has to come from somewhere, and the
|
|
6
|
+
* wrapped client's types say which calls still owe it.
|
|
7
|
+
*/
|
|
8
|
+
export type StiggContext = {
|
|
9
|
+
/** The tenant. Every governance call is scoped to one customer. */
|
|
10
|
+
customerId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The agent this call spends as. Travels as a dimension under
|
|
13
|
+
* {@link StiggContext.attributionKey}, which is the entire mechanism by which an agent is
|
|
14
|
+
* attributed - so naming one without a key is refused rather than guessed at.
|
|
15
|
+
*
|
|
16
|
+
* Omitted, the call resolves no entity and is checked against the customer's own budget.
|
|
17
|
+
*/
|
|
18
|
+
agentId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The key the agent's entity type declares, under which `agentId` is sent on every call.
|
|
21
|
+
* Required whenever `agentId` is given, and not defaulted: a guessed key resolves no entity, so
|
|
22
|
+
* the spend lands on the customer looking exactly like a call that was never governed.
|
|
23
|
+
*
|
|
24
|
+
* `register` takes an `entityTypeId` as well, because that call creates the entity. Attributing
|
|
25
|
+
* a call does not need to know what type it was created as - only what key to send it under.
|
|
26
|
+
*/
|
|
27
|
+
attributionKey?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Extra attribution, sent verbatim on the check and the report. Every key must be an attribution
|
|
30
|
+
* key on one of the entity types involved, or it matches nothing and the usage rolls up to the
|
|
31
|
+
* customer instead.
|
|
32
|
+
*
|
|
33
|
+
* Overriding this **merges** per key over the wrap's own, so a per-call `{ tenantId }` does not
|
|
34
|
+
* silently drop a client-wide `{ region }`.
|
|
35
|
+
*/
|
|
36
|
+
dimensions?: Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* What this call **gates on** - a credit currency or a metered feature, never both. **Omit it
|
|
39
|
+
* and the call gates nothing**: no check leaves the process, nothing can be refused, and
|
|
40
|
+
* metering is unchanged. The presence of this field is the enforcement switch, because a call
|
|
41
|
+
* with nothing to check against cannot be enforced, and a separate flag would only be a second
|
|
42
|
+
* place for the two to disagree.
|
|
43
|
+
*/
|
|
44
|
+
entitlement?: Entitlement;
|
|
45
|
+
/**
|
|
46
|
+
* What a check resolves to when the engine could not answer it. Defaults to `'allow'` - a soft
|
|
47
|
+
* limit, so an outage does not take the application down with it.
|
|
48
|
+
*
|
|
49
|
+
* Worth knowing what that trades: reporting fails in the same outage and is swallowed, so the
|
|
50
|
+
* spend allowed through is neither capped nor recorded afterwards. `'deny'` mirrors governance,
|
|
51
|
+
* which is fail-closed server-side.
|
|
52
|
+
*/
|
|
53
|
+
checkErrorAction?: CheckErrorAction;
|
|
54
|
+
/**
|
|
55
|
+
* What to report for each completed call. Defaults to the vendor's own metric names and values
|
|
56
|
+
* exactly as they came back, so what lands in Stigg reconciles against that vendor's invoice.
|
|
57
|
+
*
|
|
58
|
+
* The name on the left is a Stigg feature or credit id, and metering the one the call checks -
|
|
59
|
+
* {@link StiggContext.entitlement} - is what makes the budget move.
|
|
60
|
+
*/
|
|
61
|
+
usageMapper?: MetricsMapper;
|
|
62
|
+
/** Which of Stigg's ingest APIs this call reports through. Defaults to `'async'`. */
|
|
63
|
+
ingestMode?: IngestMode;
|
|
64
|
+
/**
|
|
65
|
+
* The event name to report under in `'async'` mode. Defaults to the vendor call that produced
|
|
66
|
+
* the usage - `anthropic.messages.create` - which is what a Stigg meter filters on.
|
|
67
|
+
*/
|
|
68
|
+
eventName?: EventIngest;
|
|
69
|
+
/**
|
|
70
|
+
* The key that deduplicates this report. Defaults to the vendor's own request id when it has
|
|
71
|
+
* one - stable across the vendor's retries, which a local counter cannot be - and to a
|
|
72
|
+
* generated key otherwise.
|
|
73
|
+
*/
|
|
74
|
+
idempotencyKey?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Correlates work that continues in another process, as `x-stigg-run-id`. Defaults to the id
|
|
77
|
+
* the wrap generated, so a gateway sees one value per client unless a call says otherwise.
|
|
78
|
+
*/
|
|
79
|
+
correlationId?: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* A context with everything decided and everything derivable already derived.
|
|
83
|
+
*
|
|
84
|
+
* Nothing here is optional that the call path would otherwise have to default, and nothing is
|
|
85
|
+
* computed that a call could be asked to compute twice.
|
|
86
|
+
*/
|
|
87
|
+
export type ResolvedContext = {
|
|
88
|
+
/**
|
|
89
|
+
* Empty only for a client wrapped without one, whose calls each name their own. Every path that
|
|
90
|
+
* reaches the engine resolves it first, so nothing downstream has to re-check it.
|
|
91
|
+
*/
|
|
92
|
+
customerId: string;
|
|
93
|
+
entitlement?: Entitlement;
|
|
94
|
+
/** The agent folded in, so the check and the report send one object and neither re-derives it. */
|
|
95
|
+
dimensions: Record<string, string>;
|
|
96
|
+
checkErrorAction: CheckErrorAction;
|
|
97
|
+
ingestMode: IngestMode;
|
|
98
|
+
eventName?: EventIngest;
|
|
99
|
+
usageMapper: MetricsMapper;
|
|
100
|
+
idempotencyKey?: string;
|
|
101
|
+
correlationId: string;
|
|
102
|
+
/** The entitlement's own id, resolved once - undefined when this context gates on nothing. */
|
|
103
|
+
checkedId?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Kept so a per-call `agentId` can inherit it.
|
|
106
|
+
*
|
|
107
|
+
* The key belongs to the entity type, which is a property of the client, while the agent varies
|
|
108
|
+
* per call - so naming it once at `wrap` and then only the agent per call is the shape a
|
|
109
|
+
* multi-agent server wants. The folded dimension alone would not do: an override needs the key
|
|
110
|
+
* itself to build its own.
|
|
111
|
+
*/
|
|
112
|
+
attributionKey?: string;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* What nobody told the SDK, and it will not guess.
|
|
116
|
+
*
|
|
117
|
+
* Exported because the constructor's own schema defaults to these: two copies of "what happens
|
|
118
|
+
* when nothing says" is how a configured `deny` came to be parsed, stored and then ignored in
|
|
119
|
+
* favour of an `allow` written somewhere else.
|
|
120
|
+
*
|
|
121
|
+
* `allow` is the soft limit - an unreachable Stigg does not take the application down with it.
|
|
122
|
+
*/
|
|
123
|
+
export declare const CONTEXT_DEFAULTS: {
|
|
124
|
+
readonly checkErrorAction: "allow";
|
|
125
|
+
readonly ingestMode: "async";
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Validates and derives, once. Everything expensive about a context happens here rather than on
|
|
129
|
+
* the call path - see the note at the top of this file for why that split exists.
|
|
130
|
+
*/
|
|
131
|
+
/** The one field a call cannot proceed without, said the same way wherever it is missing. */
|
|
132
|
+
export declare const noCustomer: () => Error;
|
|
133
|
+
/**
|
|
134
|
+
* The client-wide policy a context falls back to before the SDK's own defaults.
|
|
135
|
+
*
|
|
136
|
+
* The chain is per-call, then `wrap`, then the constructor, then {@link CONTEXT_DEFAULTS}. The
|
|
137
|
+
* constructor link is this parameter: without it a configured policy is inert, because every
|
|
138
|
+
* context would land on the defaults instead.
|
|
139
|
+
*/
|
|
140
|
+
export type ContextDefaults = {
|
|
141
|
+
checkErrorAction?: CheckErrorAction;
|
|
142
|
+
ingestMode?: IngestMode;
|
|
143
|
+
};
|
|
144
|
+
export declare function resolveContext(context: StiggContext, correlationId: string, { requireCustomer, defaults }?: {
|
|
145
|
+
requireCustomer?: boolean;
|
|
146
|
+
defaults?: ContextDefaults;
|
|
147
|
+
}): ResolvedContext;
|
|
148
|
+
/**
|
|
149
|
+
* One call's override, applied over what `wrap` already resolved.
|
|
150
|
+
*
|
|
151
|
+
* Only reached when a call actually carried a `stiggContext`; a call that did not reuses the
|
|
152
|
+
* resolved object as-is. The ordering below is the one the caller asked for: the base identity
|
|
153
|
+
* first, because a changed customer or agent changes which budget is consulted, and the metering
|
|
154
|
+
* fields last, because nothing before the response depends on them.
|
|
155
|
+
*/
|
|
156
|
+
export declare function overrideContext(base: ResolvedContext, override: StiggContext): ResolvedContext;
|
|
157
|
+
/** Whether a context gates at all. The enforcement switch, read on every call. */
|
|
158
|
+
export declare const isEnforced: (context: ResolvedContext) => boolean;
|
|
159
|
+
/** Reported metrics, accumulated across a client's calls. */
|
|
160
|
+
export declare const addMetrics: (into: Metrics, add: Metrics) => void;
|