@frugalco/frugal-sdk-node 0.3.0 → 0.5.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.
Files changed (59) hide show
  1. package/README.md +73 -12
  2. package/dist/block-hash-lru.d.ts +8 -1
  3. package/dist/block-hash-lru.d.ts.map +1 -1
  4. package/dist/block-hash-lru.js +13 -4
  5. package/dist/block-hash-lru.js.map +1 -1
  6. package/dist/config.d.ts +33 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +218 -27
  9. package/dist/config.js.map +1 -1
  10. package/dist/duplicate-filter.d.ts +46 -0
  11. package/dist/duplicate-filter.d.ts.map +1 -0
  12. package/dist/duplicate-filter.js +168 -0
  13. package/dist/duplicate-filter.js.map +1 -0
  14. package/dist/instrument.d.ts +33 -0
  15. package/dist/instrument.d.ts.map +1 -1
  16. package/dist/instrument.js +277 -21
  17. package/dist/instrument.js.map +1 -1
  18. package/dist/model-cap.d.ts +24 -0
  19. package/dist/model-cap.d.ts.map +1 -0
  20. package/dist/model-cap.js +72 -0
  21. package/dist/model-cap.js.map +1 -0
  22. package/dist/otel.d.ts +32 -3
  23. package/dist/otel.d.ts.map +1 -1
  24. package/dist/otel.js +203 -7
  25. package/dist/otel.js.map +1 -1
  26. package/dist/prompt-analyzer.d.ts +12 -1
  27. package/dist/prompt-analyzer.d.ts.map +1 -1
  28. package/dist/prompt-analyzer.js +185 -14
  29. package/dist/prompt-analyzer.js.map +1 -1
  30. package/dist/providers.d.ts +1 -0
  31. package/dist/providers.d.ts.map +1 -1
  32. package/dist/providers.js +4 -1
  33. package/dist/providers.js.map +1 -1
  34. package/dist/semconv.d.ts +14 -0
  35. package/dist/semconv.d.ts.map +1 -1
  36. package/dist/semconv.js +38 -2
  37. package/dist/semconv.js.map +1 -1
  38. package/dist/streams.d.ts.map +1 -1
  39. package/dist/streams.js +15 -2
  40. package/dist/streams.js.map +1 -1
  41. package/dist/track.d.ts +18 -0
  42. package/dist/track.d.ts.map +1 -1
  43. package/dist/track.js +4 -1
  44. package/dist/track.js.map +1 -1
  45. package/dist/wrappers/anthropic.d.ts.map +1 -1
  46. package/dist/wrappers/anthropic.js +9 -1
  47. package/dist/wrappers/anthropic.js.map +1 -1
  48. package/dist/wrappers/batch.d.ts +11 -0
  49. package/dist/wrappers/batch.d.ts.map +1 -0
  50. package/dist/wrappers/batch.js +338 -0
  51. package/dist/wrappers/batch.js.map +1 -0
  52. package/dist/wrappers/openai.d.ts.map +1 -1
  53. package/dist/wrappers/openai.js +37 -30
  54. package/dist/wrappers/openai.js.map +1 -1
  55. package/dist/wrappers/patching.d.ts +10 -0
  56. package/dist/wrappers/patching.d.ts.map +1 -0
  57. package/dist/wrappers/patching.js +38 -0
  58. package/dist/wrappers/patching.js.map +1 -0
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -53,12 +53,20 @@ Set both or the wrapper silently returns the client unmodified:
53
53
  | `FRUGAL_REPO_ROOT` | Absolute path to the source root; the strip prefix for `caller_file`. Defaults to the process working directory. |
54
54
  | `FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector URL for Frugal metrics. Defaults to `https://metrics.frugal.co/opentelemetry/v1/metrics`. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`. |
55
55
  | `FRUGAL_OTEL_EXPORTER_OTLP_HEADERS` | Export auth headers (e.g. `Authorization=Bearer ...`). Overrides the header built from `FRUGAL_API_KEY`. Falls back to `OTEL_EXPORTER_OTLP_HEADERS`. |
56
- | `FRUGAL_COMPONENTS` | Comma-separated logical components (e.g. `ai-handler,reco`). When set, each metric is ALSO emitted once per component (tagged `frugal.component`) on top of the base project series, so usage can be viewed for the whole project or split by component. |
56
+ | `FRUGAL_COMPONENTS` | Comma-separated logical components (e.g. `ai-handler,reco`). When set, each metric is ALSO emitted once per component (tagged `frugal.component`) on top of the base project series, so usage can be viewed for the whole project or split by component. Only the **first 10** distinct entries are honored (a fixed cap, not configurable — each component multiplies every instrument's series count); the rest are dropped with one warning. |
57
57
  | `FRUGAL_CUSTOM_PROVIDERS` | Comma-separated allowlist of provider names beyond the built-in registry (e.g. `together_ai,fireworks`) that should pass through verbatim on `gen_ai.system` / `gen_ai.provider.name` instead of being coerced to `custom`. Capped at 10 (extras are coerced) to keep label cardinality bounded. |
58
+ | `FRUGAL_CUSTOM_LABEL` | Comma-separated allowlist for the `frugal.custom_label` label on manual track calls (e.g. `billing,search,recs`). A `customLabel` passed to `trackAICall`/`startAICall` outside the first 20 entries (a hard, non-configurable cap) is coerced to `other` with a warn-once diagnostic. `other` is reserved and ignored as an entry. See [Custom call sites](#custom-call-sites). |
58
59
  | `FRUGAL_PATH_BLOCK_LIST` | Comma-separated repo-relative paths to skip for caller attribution (see below). |
59
60
  | `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence in milliseconds. Default `60000`. |
60
61
  | `FRUGAL_MAX_CALL_SITES` | Cardinality cap — max distinct `(caller_file, caller_function)` pairs to attribute. Default `500`. Set to `0` to disable. |
62
+ | `FRUGAL_MAX_MODELS` | Cardinality cap — max distinct model strings (`gen_ai.request.model` and `gen_ai.response.model` share one registry) before further models collapse to `<overflow>`. Default `100`, which is ~50 logical models: a request alias (`gpt-4o`) and the dated snapshot the response reports (`gpt-4o-2024-08-06`) are two strings in the one shared registry. Set to `0` to disable. |
63
+ | `FRUGAL_MAX_SERIES_PER_INSTRUMENT` | OpenTelemetry's per-instrument limit on distinct label **combinations**. Default `50000` (OTel's own default is 2000, far too low for this SDK's label set); `0` means effectively unlimited (a 1,000,000 ceiling). |
61
64
  | `FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE` | Fraction of calls (0..1) that run the lightweight prompt analyzer. Default `0.1`. Set to `0` to disable, `1` for every call. |
65
+ | `FRUGAL_DUPLICATE_REQUEST_DETECTION` | Duplicate-request detection (default **on**): every chat request is canonicalized and hashed in-process to classify `first_seen`/`repeat_seen` — only the 2-value state is exported, never content. Set to `0`/`false`/`no`/`off` to disable the hashing and the `frugal.gen_ai.duplicate_requests` metric entirely. |
66
+ | `FRUGAL_DUPLICATE_WINDOW_SECONDS` | How long a request signature is remembered. Default `3600`, max `86400` (retention is 30–60 min at defaults — see [docs/metrics-reference.md](docs/metrics-reference.md)). |
67
+ | `FRUGAL_DUPLICATE_CAPACITY` | Expected distinct requests per window (Bloom filter sizing). Default `10000`, max `1000000`. Raise if the saturation warning fires. |
68
+ | `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate for the duplicate filter. Default `0.01`, max `0.5`. |
69
+ | `FRUGAL_DUPLICATE_GENERATIONS` | Rotating-Bloom generation count. Default `2`, max `16`. |
62
70
  | `FRUGAL_DISABLED` | Set to `1` to disable all instrumentation without uninstalling. |
63
71
 
64
72
  ## What gets captured
@@ -68,32 +76,37 @@ Every wrapped call emits OTel metrics tagged with the caller's source file and f
68
76
  | Metric | Kind | Description | Additional attributes |
69
77
  | --------------------------------------- | --------- | ------------------------------------------------------------ | -------------------------------------- |
70
78
  | `gen_ai.client.operation.duration` | histogram | Call latency in seconds | — |
71
- | `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type` |
72
- | `frugal.gen_ai.calls` | counter | Number of calls per site | — |
79
+ | `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type`, `frugal.history_bucket` (chat calls) |
80
+ | `frugal.gen_ai.calls` | counter | Number of calls per site | `frugal.history_bucket` (chat calls), `frugal.structured_output_mode` (calls with a request payload) |
73
81
  | `frugal.gen_ai.cache_read_tokens` | histogram | Prompt cache hits | — |
74
82
  | `frugal.gen_ai.cache_write_tokens` | histogram | Prompt cache writes | — |
75
83
  | `frugal.gen_ai.reasoning_tokens` | histogram | OpenAI reasoning tokens (o-series), Anthropic thinking tokens| — |
84
+ | `frugal.gen_ai.agent.turns` | histogram | Model-response turns in a successful agent invocation | — |
76
85
  | `frugal.gen_ai.errors` | counter | Errors by exception type | `frugal.error.type` |
77
- | `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality cap, bugs) | `frugal.internal_error.stage` |
86
+ | `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality caps, bugs) | `frugal.internal_error.stage`, `frugal.instrument` (on `otel_cardinality_overflow` only) |
78
87
  | `frugal.gen_ai.output_cap_utilization` | histogram | `output_tokens / max_tokens` ratio when `max_tokens` is set | `frugal.structured_output_mode` |
79
- | `frugal.gen_ai.history_depth` | histogram | `messages.length` (analyzer-sampled) | — |
88
+ | `frugal.gen_ai.history_depth` | histogram | `messages.length` — every chat call, success or error (100%, not sampled) | — |
89
+ | `frugal.gen_ai.duplicate_requests` | counter | Request-duplication state from a windowed signature filter — every chat call (100%, not sampled; see `FRUGAL_DUPLICATE_*` vars) | `frugal.dup_state` (`first_seen` \| `repeat_seen`) |
80
90
  | `frugal.gen_ai.few_shot_count` | histogram | Few-shot exemplar pairs before final user (analyzer-sampled) | — |
91
+ | `frugal.gen_ai.few_shot_tokens` | histogram | Approx. token size of the few-shot example block (analyzer-sampled) | — |
81
92
  | `frugal.gen_ai.noise_ratio` | histogram | HTML/base64/whitespace fraction of input (analyzer-sampled) | — |
82
93
  | `frugal.gen_ai.cache_prefix_stable` | counter | Per-block prompt-cache stability (analyzer-sampled) | `frugal.block`, `frugal.stable` |
83
94
  | `frugal.gen_ai.structured_output_hint` | counter | "Return JSON" prompts missing `response_format`/`tools` | — |
84
95
 
85
- The bottom block of analyzer-sampled metrics fires on a fraction of calls
86
- (`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%); the token-side metrics are
87
- 100% — never sampled.
96
+ The analyzer-sampled metrics fire on a fraction of calls
97
+ (`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%). Everything else —
98
+ including `history_depth`, the `frugal.history_bucket` label on `calls` and
99
+ `token.usage`, the `frugal.structured_output_mode` label on `calls`, and
100
+ `duplicate_requests` — is 100%, never sampled.
88
101
 
89
102
  Every metric carries this base attribute set:
90
103
 
91
104
  - `frugal.project_id` — from your env vars (`frugal.customer_id` too when set); `frugal.component` is added on the per-component series when `FRUGAL_COMPONENTS` is set
92
105
  - `frugal.caller_file`, `frugal.caller_function` — auto-detected from the call stack
93
106
  - `gen_ai.system` + `gen_ai.provider.name` — the provider, dual-emitted under both the legacy and current OTel attribute (`openai`, `anthropic`, `aws.bedrock`, `vertex_ai`/`gcp.vertex_ai`, …, or `custom`). Override via the `provider` field; an unrecognized value is coerced to `custom` with a one-time warning. See [`docs/metrics-reference.md`](docs/metrics-reference.md#attributes).
94
- - `gen_ai.request.model`, `gen_ai.response.model` — the model used
107
+ - `gen_ai.request.model`, `gen_ai.response.model` — the model used. Both draw on one shared registry capped by `FRUGAL_MAX_MODELS`; past the cap the value reads `<overflow>` (the same sentinel the caller labels use). `<unknown>` when the wrapper could not read a model off the request
95
108
  - `gen_ai.operation.name` — `chat`, `text_completion`, `embeddings`, `generate_content`, `execute_tool`, `create_agent`, `invoke_agent`, or `other` (the sentinel an unrecognized operation coerces to)
96
- - `frugal.batched` — `true` for batch-API submissions, `false` otherwise
109
+ - `frugal.batched` — `true` for batch-API paths (OpenAI Batch API submissions; Anthropic Message Batches submissions and results), `false` otherwise
97
110
 
98
111
  The "Additional attributes" column above lists what each metric carries on top of the base set. See [`docs/metrics-reference.md`](docs/metrics-reference.md) for the values each attribute takes, sentinel values for caller fields, and the full internal-error-stage list.
99
112
 
@@ -161,9 +174,57 @@ try {
161
174
 
162
175
  `end()` and `fail()` finalize the call exactly once and are idempotent — extra calls are ignored, so a `try/finally` that always calls `end()` won't double-count.
163
176
 
164
- **Metadata** — `info` (all optional): `provider` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `model` (→ `gen_ai.request.model`, default `"<unknown>"`). To also run the sampled prompt analyzer, pass any of `messages` / `system` / `tools` / `responseFormat` / `maxTokens`.
177
+ For an agent invocation, set `operation: "invoke_agent"` and report the number
178
+ of model-response steps completed by the whole run:
165
179
 
166
- **Usage** — `report(...)` / `end(...)` fields (all optional): `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `responseModel`.
180
+ ```ts
181
+ const result = await trackAICall(
182
+ { operation: "invoke_agent", model: "agent-model" },
183
+ async (report) => {
184
+ const run = await agent.invoke(task);
185
+ report({ agentTurns: run.turnCount });
186
+ return run.output;
187
+ },
188
+ );
189
+ ```
190
+
191
+ The manual-handle form uses the same contract:
192
+
193
+ ```ts
194
+ const handle = startAICall({ operation: "invoke_agent", model: "agent-model" });
195
+ try {
196
+ const run = await agent.invoke(task);
197
+ handle.end({ agentTurns: run.turnCount });
198
+ } catch (err) {
199
+ handle.fail(err);
200
+ throw err;
201
+ }
202
+ ```
203
+
204
+ Provider wrappers continue to measure individual model calls. Agent frameworks
205
+ use `trackAICall` or `startAICall` to measure the enclosing invocation; dedicated
206
+ framework adapters are not available yet. A turn sample is emitted only for a
207
+ successful `invoke_agent` call with a positive-integer `agentTurns` value.
208
+
209
+ **Avoid double-counting tokens.** The examples above report `agentTurns` only,
210
+ which is correct when the agent's inner model-client calls are already wrapped
211
+ by a supported provider wrapper: those wrappers emit token usage per inner
212
+ call, so also reporting aggregate tokens on the `invoke_agent` call would
213
+ double-count them. Only when the inner calls are **not** wrapped, add the
214
+ aggregate `inputTokens` / `outputTokens` (and the other usage fields):
215
+
216
+ ```ts
217
+ // Inner model calls are NOT wrapped by a provider wrapper.
218
+ report({
219
+ agentTurns: run.turnCount,
220
+ inputTokens: run.usage.inputTokens,
221
+ outputTokens: run.usage.outputTokens,
222
+ });
223
+ ```
224
+
225
+ **Metadata** — `info` (all optional): `provider` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `model` (→ `gen_ai.request.model`, default `"<unknown>"`), `customLabel` (→ `frugal.custom_label` on **every** metric of the call; the value must be one of the first 20 comma-separated entries of `FRUGAL_CUSTOM_LABEL` or it is coerced to `"other"` — the label is exclusive to this manual API, wrapper calls never carry it). To also run the sampled prompt analyzer, pass any of `messages` / `system` / `tools` / `responseFormat` / `maxTokens`.
226
+
227
+ **Usage** — `report(...)` / `end(...)` fields (all optional): `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `responseModel`, `agentTurns`.
167
228
 
168
229
  The same env-var configuration, safety guarantees, and exported metrics apply.
169
230
 
@@ -5,8 +5,15 @@ export interface BlockHashes {
5
5
  schemaHash: string | null;
6
6
  }
7
7
  export declare class BlockHashLRU {
8
- private readonly capacity;
9
8
  private readonly map;
9
+ private readonly capacity;
10
+ /**
11
+ * A capacity of 0 or less means "use the default", never "never evict". The
12
+ * capacity is threaded from FRUGAL_MAX_CALL_SITES, where 0 legitimately means
13
+ * "don't cap the call-site LABEL" — but this map is process memory, not label
14
+ * cardinality, and an unbounded one grows with every distinct call site for
15
+ * the life of the process.
16
+ */
10
17
  constructor(capacity?: number);
11
18
  get(callSiteId: string): BlockHashes | undefined;
12
19
  set(callSiteId: string, hashes: BlockHashes): void;
@@ -1 +1 @@
1
- {"version":3,"file":"block-hash-lru.d.ts","sourceRoot":"","sources":["../src/block-hash-lru.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAFrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkC;gBAEzB,QAAQ,GAAE,MAAY;IAEnD,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAQhD,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAUlD,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;CAGd;AAID,wBAAgB,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAGrE;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
1
+ {"version":3,"file":"block-hash-lru.d.ts","sourceRoot":"","sources":["../src/block-hash-lru.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAKD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkC;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC;;;;;;OAMG;gBACS,QAAQ,GAAE,MAAyB;IAI/C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAQhD,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAUlD,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;CAGd;AAID,wBAAgB,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAGrE;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
@@ -4,11 +4,20 @@
4
4
  //
5
5
  // Default capacity matches the existing call-site cardinality cap so a busy
6
6
  // process can't blow memory by tracking thousands of distinct sites.
7
+ /** Capacity used whenever the caller supplies nothing usable. */
8
+ const DEFAULT_CAPACITY = 500;
7
9
  export class BlockHashLRU {
8
- capacity;
9
10
  map = new Map();
10
- constructor(capacity = 500) {
11
- this.capacity = capacity;
11
+ capacity;
12
+ /**
13
+ * A capacity of 0 or less means "use the default", never "never evict". The
14
+ * capacity is threaded from FRUGAL_MAX_CALL_SITES, where 0 legitimately means
15
+ * "don't cap the call-site LABEL" — but this map is process memory, not label
16
+ * cardinality, and an unbounded one grows with every distinct call site for
17
+ * the life of the process.
18
+ */
19
+ constructor(capacity = DEFAULT_CAPACITY) {
20
+ this.capacity = capacity > 0 ? capacity : DEFAULT_CAPACITY;
12
21
  }
13
22
  get(callSiteId) {
14
23
  const entry = this.map.get(callSiteId);
@@ -22,7 +31,7 @@ export class BlockHashLRU {
22
31
  if (this.map.has(callSiteId)) {
23
32
  this.map.delete(callSiteId);
24
33
  }
25
- else if (this.capacity > 0 && this.map.size >= this.capacity) {
34
+ else if (this.map.size >= this.capacity) {
26
35
  const oldest = this.map.keys().next().value;
27
36
  if (oldest !== undefined)
28
37
  this.map.delete(oldest);
@@ -1 +1 @@
1
- {"version":3,"file":"block-hash-lru.js","sourceRoot":"","sources":["../src/block-hash-lru.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,4EAA4E;AAC5E,uCAAuC;AACvC,EAAE;AACF,4EAA4E;AAC5E,qEAAqE;AASrE,MAAM,OAAO,YAAY;IAGM;IAFZ,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEtD,YAA6B,WAAmB,GAAG;QAAtB,aAAQ,GAAR,QAAQ,CAAc;IAAG,CAAC;IAEvD,GAAG,CAAC,UAAkB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,GAAG,CAAC,UAAkB,EAAE,MAAmB;QACzC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC9B,CAAC;aAAM,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;CACF;AAED,IAAI,MAAM,GAAwB,IAAI,CAAC;AAEvC,MAAM,UAAU,qBAAqB,CAAC,QAAiB;IACrD,IAAI,MAAM,KAAK,IAAI;QAAE,MAAM,GAAG,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC;IACzD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,+BAA+B;IAC7C,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"block-hash-lru.js","sourceRoot":"","sources":["../src/block-hash-lru.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,4EAA4E;AAC5E,uCAAuC;AACvC,EAAE;AACF,4EAA4E;AAC5E,qEAAqE;AASrE,iEAAiE;AACjE,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,MAAM,OAAO,YAAY;IACN,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;IACrC,QAAQ,CAAS;IAElC;;;;;;OAMG;IACH,YAAY,WAAmB,gBAAgB;QAC7C,IAAI,CAAC,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAC7D,CAAC;IAED,GAAG,CAAC,UAAkB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,GAAG,CAAC,UAAkB,EAAE,MAAmB;QACzC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC9B,CAAC;aAAM,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;CACF;AAED,IAAI,MAAM,GAAwB,IAAI,CAAC;AAEvC,MAAM,UAAU,qBAAqB,CAAC,QAAiB;IACrD,IAAI,MAAM,KAAK,IAAI;QAAE,MAAM,GAAG,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC;IACzD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,+BAA+B;IAC7C,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC"}
package/dist/config.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface Config {
11
11
  * keep `gen_ai.system` / `gen_ai.provider.name` cardinality bounded.
12
12
  */
13
13
  customProviders: readonly string[];
14
+ /**
15
+ * Allowlist for the `frugal.custom_label` label on manual track calls. A
16
+ * `customLabel` value outside this list is coerced to `other` at emit time,
17
+ * so the label's cardinality is bounded by MAX_CUSTOM_LABELS + 1.
18
+ */
19
+ customLabels: readonly string[];
14
20
  repoRoot: string;
15
21
  pathBlockList: readonly string[];
16
22
  otlpEndpoint: string;
@@ -18,9 +24,36 @@ export interface Config {
18
24
  exportIntervalMs: number;
19
25
  /** Max distinct (file, function) combinations to attribute before overflow. 0 = unlimited. */
20
26
  maxCallSites: number;
27
+ /**
28
+ * OTel's per-instrument limit on distinct attribute SETS. Beyond it the SDK
29
+ * merges everything into one `{otel.metric.overflow: true}` series with every
30
+ * other attribute dropped. 0 = disabled (effectively unlimited).
31
+ */
32
+ maxSeriesPerInstrument: number;
33
+ /**
34
+ * Max distinct model strings (request + response share one registry) to
35
+ * attribute before they collapse to the `<overflow>` sentinel. 0 = unlimited.
36
+ */
37
+ maxModels: number;
21
38
  /** Fraction of analyzed calls to emit prompt-analysis metrics for, 0..1. */
22
39
  promptAnalysisSampleRate: number;
40
+ duplicateDetection: boolean;
41
+ duplicateWindowSeconds: number;
42
+ duplicateCapacity: number;
43
+ duplicateFpRate: number;
44
+ duplicateGenerations: number;
23
45
  }
46
+ /**
47
+ * Load the configuration, or null when the SDK should stay a no-op.
48
+ *
49
+ * Every wrapX() calls this at the customer's module scope, so a throw here
50
+ * crashes the host app before it has run a line of its own code. Nothing in
51
+ * the body is supposed to throw — but it reads the environment and touches the
52
+ * filesystem (process.cwd() raises ENOENT when the working directory has been
53
+ * removed, realpathSync can raise on a broken mount), so the whole thing is
54
+ * contained: any failure disables the SDK with one diagnostic instead of
55
+ * taking the process with it.
56
+ */
24
57
  export declare function loadConfig(): Config | null;
25
58
  /** Test-only hook to re-arm the one-shot warning. */
26
59
  export declare function resetWarningStateForTests(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,MAAM;IACrB,mGAAmG;IACnG,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,wBAAwB,EAAE,MAAM,CAAC;CAClC;AA6CD,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CA4I1C;AAED,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,MAAM;IACrB,mGAAmG;IACnG,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,wBAAwB,EAAE,MAAM,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAoMD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAW1C;AAsQD,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
package/dist/config.js CHANGED
@@ -29,6 +29,7 @@
29
29
  import { realpathSync } from "node:fs";
30
30
  import { resolve } from "node:path";
31
31
  import { emitLog, emitLogOnce } from "./debug.js";
32
+ import { CUSTOM_LABEL_OTHER } from "./semconv.js";
32
33
  const ENV = {
33
34
  CUSTOMER_ID: "FRUGAL_CUSTOMER_ID",
34
35
  PROJECT_ID: "FRUGAL_PROJECT_ID",
@@ -39,6 +40,7 @@ const ENV = {
39
40
  METRICS_API_KEY: "FRUGAL_METRICS_API_KEY",
40
41
  COMPONENTS: "FRUGAL_COMPONENTS",
41
42
  CUSTOM_PROVIDERS: "FRUGAL_CUSTOM_PROVIDERS",
43
+ CUSTOM_LABEL: "FRUGAL_CUSTOM_LABEL",
42
44
  REPO_ROOT: "FRUGAL_REPO_ROOT",
43
45
  PATH_BLOCK_LIST: "FRUGAL_PATH_BLOCK_LIST",
44
46
  DISABLED: "FRUGAL_DISABLED",
@@ -49,13 +51,62 @@ const ENV = {
49
51
  OTLP_HEADERS: "OTEL_EXPORTER_OTLP_HEADERS",
50
52
  EXPORT_INTERVAL_MS: "FRUGAL_METRIC_EXPORT_INTERVAL_MS",
51
53
  MAX_CALL_SITES: "FRUGAL_MAX_CALL_SITES",
54
+ MAX_SERIES_PER_INSTRUMENT: "FRUGAL_MAX_SERIES_PER_INSTRUMENT",
55
+ MAX_MODELS: "FRUGAL_MAX_MODELS",
52
56
  PROMPT_ANALYSIS_SAMPLE_RATE: "FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE",
57
+ DUPLICATE_DETECTION: "FRUGAL_DUPLICATE_REQUEST_DETECTION",
58
+ DUPLICATE_WINDOW_SECONDS: "FRUGAL_DUPLICATE_WINDOW_SECONDS",
59
+ DUPLICATE_CAPACITY: "FRUGAL_DUPLICATE_CAPACITY",
60
+ DUPLICATE_FP_RATE: "FRUGAL_DUPLICATE_FP_RATE",
61
+ DUPLICATE_GENERATIONS: "FRUGAL_DUPLICATE_GENERATIONS",
53
62
  };
54
63
  const DEFAULT_OTLP_ENDPOINT = "https://metrics.frugal.co/opentelemetry/v1/metrics";
55
64
  const DEFAULT_MAX_CALL_SITES = 500;
65
+ // OTel's own default per-instrument limit is 2000 attribute SETS, which 500
66
+ // call sites × models × token types blows through immediately. See
67
+ // docs/quickstart.md §Cardinality caps for the memory reasoning behind 50000.
68
+ const DEFAULT_MAX_SERIES_PER_INSTRUMENT = 50_000;
69
+ // 100, not 50: one logical model routinely burns TWO registry slots, because
70
+ // the request carries an alias ("gpt-4o", "claude-sonnet-4-6") while the
71
+ // response reports the dated snapshot it resolved to ("gpt-4o-2024-08-06",
72
+ // "claude-sonnet-4-6-20260115"), and both labels share one registry. So a
73
+ // deployment using 50 logical models needs ~100 strings.
74
+ const DEFAULT_MAX_MODELS = 100;
75
+ const DEFAULT_EXPORT_INTERVAL_MS = 60_000;
56
76
  const DEFAULT_PROMPT_ANALYSIS_SAMPLE_RATE = 0.1;
77
+ const DEFAULT_DUPLICATE_WINDOW_SECONDS = 3600;
78
+ const DEFAULT_DUPLICATE_CAPACITY = 10_000;
79
+ const DEFAULT_DUPLICATE_FP_RATE = 0.01;
80
+ const DEFAULT_DUPLICATE_GENERATIONS = 2;
81
+ // Bloom memory is ~(1.44·log2(1/p))/8 bytes × capacity × generations — the fp
82
+ // rate matters: ~1.2 B/entry at the 0.01 default (≈24 KB total at defaults)
83
+ // but ~3.6 B/entry at the 1e-6 floor the filter clamps to. The caps below
84
+ // bound the worst case to ≈58 MB (capacity 1e6 × 16 generations × 3.6 B),
85
+ // allocated inside the customer's first call — large, but a knowable ceiling
86
+ // rather than a fat-fingered env var demanding gigabytes. Saturation checks
87
+ // popcount the full allocation every 1024th insert, so the ceiling also bounds
88
+ // that hot-path cost.
89
+ const MAX_DUPLICATE_CAPACITY = 1_000_000;
90
+ const MAX_DUPLICATE_GENERATIONS = 16;
91
+ // Above the documented window there is no additional duplicate signal — only a
92
+ // filter that effectively never rotates and saturates for the process
93
+ // lifetime. One day is already far beyond any retry/cache horizon.
94
+ const MAX_DUPLICATE_WINDOW_SECONDS = 86_400;
95
+ // bloomParams clamps fp to at most 0.5 (the mathematical ceiling where the
96
+ // filter degenerates); values in [0.5, 1) must warn HERE, not silently differ
97
+ // from what the filter actually uses.
98
+ const MAX_DUPLICATE_FP_RATE = 0.5;
57
99
  /** Upper bound on customer-registered provider names — keeps label cardinality bounded. */
58
100
  const MAX_CUSTOM_PROVIDERS = 10;
101
+ // Hard bound on the FRUGAL_COMPONENTS fan-out list. Every component multiplies
102
+ // each instrument's series count by one, so like MAX_CUSTOM_LABELS this is a
103
+ // module-private literal with no env override: the cap is the cardinality
104
+ // contract for the frugal.component label, not a tuning knob.
105
+ const MAX_COMPONENTS = 10;
106
+ // Hard bound on the frugal.custom_label allowlist. Deliberately a
107
+ // module-private literal with no env override: the cap is the cardinality
108
+ // contract for the label, so it must not be user-tunable.
109
+ const MAX_CUSTOM_LABELS = 20;
59
110
  let warnedOnce = false;
60
111
  function truthy(value) {
61
112
  if (!value)
@@ -63,13 +114,101 @@ function truthy(value) {
63
114
  const v = value.trim().toLowerCase();
64
115
  return v === "1" || v === "true" || v === "yes" || v === "on";
65
116
  }
117
+ function explicitlyOff(value) {
118
+ if (value === undefined)
119
+ return false;
120
+ const v = value.trim().toLowerCase();
121
+ return v === "0" || v === "false" || v === "no" || v === "off";
122
+ }
123
+ // Number(raw) validates the FULL string (unlike parseInt/parseFloat, whose
124
+ // prefix parsing turns "1e6" into 1 and "10,000" into 10 — a capacity of 1
125
+ // means a 10-bit filter that saturates instantly). Warnings are de-duped per
126
+ // var: loadConfig() runs on every call, so a bad value must not flood the
127
+ // bounded debug ring buffer.
128
+ function parsePositive(raw, name, fallback, { integer = false, max } = {}) {
129
+ if (!raw || !raw.trim())
130
+ return fallback;
131
+ const parsed = Number(raw.trim());
132
+ const valid = Number.isFinite(parsed) && parsed > 0 && (!integer || Number.isInteger(parsed));
133
+ if (!valid) {
134
+ emitLogOnce(`bad_env_${name}`, "warn", `could not parse ${name}=${raw}; using default ${fallback}`);
135
+ return fallback;
136
+ }
137
+ if (max !== undefined && parsed > max) {
138
+ emitLogOnce(`clamped_env_${name}`, "warn", `${name}=${raw} exceeds the maximum ${max}; clamping to ${max}`);
139
+ return max;
140
+ }
141
+ return parsed;
142
+ }
143
+ // Shared shape for the integer knobs: the cardinality caps (non-negative, 0 =
144
+ // "disabled") and the export interval (strictly positive).
145
+ //
146
+ // Digits only, deliberately: Number.parseInt PREFIX-parses, so "1e6" becomes 1,
147
+ // "50_000" becomes 50 and "12abc" becomes 12 — each silently installing a cap
148
+ // orders of magnitude tighter than the operator asked for, which then collapses
149
+ // real labels into <overflow> with no diagnostic. A value that is not entirely
150
+ // digits (after trim) is treated as a typo: warn once, use the default. There is
151
+ // deliberately NO upper bound — a gateway in front of the metrics backend does
152
+ // the aggregating, so a large per-process cap is the operator's call.
153
+ const DIGITS_ONLY = /^\d+$/;
154
+ /**
155
+ * Parse a digits-only integer of at least `minimum`, warning once and falling
156
+ * back to `fallback` on anything else.
157
+ *
158
+ * The safe-integer check is a VALIDITY test, not a clamp: past
159
+ * Number.MAX_SAFE_INTEGER integer arithmetic silently loses precision and a
160
+ * long enough digit string parses to Infinity, so such a value is not a cap
161
+ * anyone can reason about. It is rejected exactly like "12abc" — default plus
162
+ * one warning — and every representable value, however large, is honored
163
+ * verbatim.
164
+ */
165
+ function parseDigitsInt(raw, name, fallback, minimum) {
166
+ if (!raw || !raw.trim())
167
+ return fallback;
168
+ const trimmed = raw.trim();
169
+ if (DIGITS_ONLY.test(trimmed)) {
170
+ const parsed = Number(trimmed);
171
+ if (Number.isSafeInteger(parsed) && parsed >= minimum)
172
+ return parsed;
173
+ }
174
+ emitLogOnce(`bad_env_${name}`, "warn", `could not parse ${name}=${raw}; using default ${fallback}`);
175
+ return fallback;
176
+ }
177
+ /** Cardinality caps: 0 means "disabled". */
178
+ function parseNonNegativeInt(raw, name, fallback) {
179
+ return parseDigitsInt(raw, name, fallback, 0);
180
+ }
181
+ /** Knobs where 0 is meaningless (or fatal downstream) rather than "off". */
182
+ function parsePositiveInt(raw, name, fallback) {
183
+ return parseDigitsInt(raw, name, fallback, 1);
184
+ }
66
185
  function warnOnce(message) {
67
186
  if (warnedOnce)
68
187
  return;
69
188
  warnedOnce = true;
70
189
  emitLog("warn", `disabled: ${message}`);
71
190
  }
191
+ /**
192
+ * Load the configuration, or null when the SDK should stay a no-op.
193
+ *
194
+ * Every wrapX() calls this at the customer's module scope, so a throw here
195
+ * crashes the host app before it has run a line of its own code. Nothing in
196
+ * the body is supposed to throw — but it reads the environment and touches the
197
+ * filesystem (process.cwd() raises ENOENT when the working directory has been
198
+ * removed, realpathSync can raise on a broken mount), so the whole thing is
199
+ * contained: any failure disables the SDK with one diagnostic instead of
200
+ * taking the process with it.
201
+ */
72
202
  export function loadConfig() {
203
+ try {
204
+ return loadConfigUnguarded();
205
+ }
206
+ catch (err) {
207
+ emitLogOnce("config_load_failed", "error", `configuration could not be loaded (${err instanceof Error ? err.message : String(err)}); frugal-metrics is disabled for this process`);
208
+ return null;
209
+ }
210
+ }
211
+ function loadConfigUnguarded() {
73
212
  if (truthy(process.env[ENV.DISABLED]))
74
213
  return null;
75
214
  const customerId = process.env[ENV.CUSTOMER_ID] || null;
@@ -118,11 +257,25 @@ export function loadConfig() {
118
257
  .filter((e) => e.length > 0);
119
258
  // Optional comma-separated logical components (e.g. "ai-handler,reco"). Each
120
259
  // becomes an extra per-component series on top of the base project series.
260
+ // Each component multiplies every instrument's series count by one, so the
261
+ // list is truncated to MAX_COMPONENTS at parse time. No sentinel: a dropped
262
+ // component simply produces no per-component series — the base project-level
263
+ // series still counts every call.
264
+ //
265
+ // Deduped (order-preservingly, via Set insertion order) BEFORE the slice: a
266
+ // repeated entry costs no extra series, so letting duplicates consume budget
267
+ // would drop real components — and warn about it — for no cardinality gain.
121
268
  const componentsRaw = process.env[ENV.COMPONENTS] ?? "";
122
- const components = componentsRaw
123
- .split(",")
124
- .map((e) => e.trim())
125
- .filter((e) => e.length > 0);
269
+ const componentsAll = [
270
+ ...new Set(componentsRaw
271
+ .split(",")
272
+ .map((e) => e.trim())
273
+ .filter((e) => e.length > 0)),
274
+ ];
275
+ const components = componentsAll.slice(0, MAX_COMPONENTS);
276
+ if (components.length < componentsAll.length) {
277
+ emitLogOnce("components_truncated", "warn", `${ENV.COMPONENTS} lists ${componentsAll.length} entries; only the first ${MAX_COMPONENTS} are honored and ${componentsAll.length - components.length} are dropped (no per-component series for them).`);
278
+ }
126
279
  // Optional comma-separated allowlist of custom provider names that pass through
127
280
  // verbatim (e.g. "together_ai,fireworks"). Deduped and capped at
128
281
  // MAX_CUSTOM_PROVIDERS so a misconfigured value can't blow up label cardinality.
@@ -137,31 +290,41 @@ export function loadConfig() {
137
290
  if (customProvidersAll.length > MAX_CUSTOM_PROVIDERS) {
138
291
  emitLogOnce("custom_providers_truncated", "warn", `${ENV.CUSTOM_PROVIDERS} lists ${customProvidersAll.length} entries; only the first ${MAX_CUSTOM_PROVIDERS} are honored, the rest are coerced to "custom".`);
139
292
  }
293
+ // Optional comma-separated allowlist for the frugal.custom_label attribute on
294
+ // manual track calls. Only the first MAX_CUSTOM_LABELS comma-separated
295
+ // entries are honored (sliced BEFORE trim/dedupe — the position in the raw
296
+ // list is the contract); customLabel values outside the allowlist are
297
+ // coerced to "other" at emit time.
298
+ const customLabelsRaw = process.env[ENV.CUSTOM_LABEL] ?? "";
299
+ const customLabelEntries = customLabelsRaw.split(",");
300
+ const customLabelsParsed = [
301
+ ...new Set(customLabelEntries
302
+ .slice(0, MAX_CUSTOM_LABELS)
303
+ .map((e) => e.trim())
304
+ .filter((e) => e.length > 0)),
305
+ ];
306
+ // "other" is the coercion sentinel, never a legitimate allowlist entry: a
307
+ // real unit named "other" would inseparably absorb every non-allowlisted
308
+ // value from unrelated call sites.
309
+ const customLabels = customLabelsParsed.filter((e) => e !== CUSTOM_LABEL_OTHER);
310
+ if (customLabels.length < customLabelsParsed.length) {
311
+ emitLogOnce("custom_labels_reserved", "warn", `${ENV.CUSTOM_LABEL} contains the reserved value "${CUSTOM_LABEL_OTHER}" (the coercion sentinel); it is ignored — rename that label to keep it distinguishable from coerced values.`);
312
+ }
313
+ if (customLabelEntries.length > MAX_CUSTOM_LABELS && customLabelsRaw.trim()) {
314
+ emitLogOnce("custom_labels_truncated", "warn", `${ENV.CUSTOM_LABEL} lists ${customLabelEntries.length} entries; only the first ${MAX_CUSTOM_LABELS} are honored, values outside them are coerced to "${CUSTOM_LABEL_OTHER}".`);
315
+ }
140
316
  const otlpEndpoint = process.env[ENV.FRUGAL_OTLP_ENDPOINT] ??
141
317
  process.env[ENV.OTLP_ENDPOINT] ??
142
318
  DEFAULT_OTLP_ENDPOINT;
143
- const intervalRaw = process.env[ENV.EXPORT_INTERVAL_MS];
144
- let exportIntervalMs = 60_000;
145
- if (intervalRaw && intervalRaw.trim()) {
146
- const parsed = Number.parseInt(intervalRaw, 10);
147
- if (Number.isFinite(parsed)) {
148
- exportIntervalMs = parsed;
149
- }
150
- else {
151
- emitLog("warn", `could not parse ${ENV.EXPORT_INTERVAL_MS}=${intervalRaw}; using default ${exportIntervalMs}`);
152
- }
153
- }
154
- const maxCallSitesRaw = process.env[ENV.MAX_CALL_SITES];
155
- let maxCallSites = DEFAULT_MAX_CALL_SITES;
156
- if (maxCallSitesRaw && maxCallSitesRaw.trim()) {
157
- const parsed = Number.parseInt(maxCallSitesRaw, 10);
158
- if (Number.isFinite(parsed) && parsed >= 0) {
159
- maxCallSites = parsed;
160
- }
161
- else {
162
- emitLog("warn", `could not parse ${ENV.MAX_CALL_SITES}=${maxCallSitesRaw}; using default ${maxCallSites}`);
163
- }
164
- }
319
+ // Strictly positive: PeriodicExportingMetricReader THROWS on an interval of
320
+ // 0 or less, and that throw propagates out of getInstruments on the
321
+ // customer's first call. Every wrapper catches it, so the SDK degrades to a
322
+ // no-op rather than breaking the host — but a typo silently costing all
323
+ // metrics is worse than a typo that warns and uses the default.
324
+ const exportIntervalMs = parsePositiveInt(process.env[ENV.EXPORT_INTERVAL_MS], ENV.EXPORT_INTERVAL_MS, DEFAULT_EXPORT_INTERVAL_MS);
325
+ const maxCallSites = parseNonNegativeInt(process.env[ENV.MAX_CALL_SITES], ENV.MAX_CALL_SITES, DEFAULT_MAX_CALL_SITES);
326
+ const maxSeriesPerInstrument = parseNonNegativeInt(process.env[ENV.MAX_SERIES_PER_INSTRUMENT], ENV.MAX_SERIES_PER_INSTRUMENT, DEFAULT_MAX_SERIES_PER_INSTRUMENT);
327
+ const maxModels = parseNonNegativeInt(process.env[ENV.MAX_MODELS], ENV.MAX_MODELS, DEFAULT_MAX_MODELS);
165
328
  const sampleRateRaw = process.env[ENV.PROMPT_ANALYSIS_SAMPLE_RATE];
166
329
  let promptAnalysisSampleRate = DEFAULT_PROMPT_ANALYSIS_SAMPLE_RATE;
167
330
  if (sampleRateRaw && sampleRateRaw.trim()) {
@@ -170,21 +333,49 @@ export function loadConfig() {
170
333
  promptAnalysisSampleRate = parsed;
171
334
  }
172
335
  else {
173
- emitLog("warn", `could not parse ${ENV.PROMPT_ANALYSIS_SAMPLE_RATE}=${sampleRateRaw}; using default ${promptAnalysisSampleRate}`);
336
+ emitLogOnce(`bad_env_${ENV.PROMPT_ANALYSIS_SAMPLE_RATE}`, "warn", `could not parse ${ENV.PROMPT_ANALYSIS_SAMPLE_RATE}=${sampleRateRaw}; using default ${promptAnalysisSampleRate}`);
174
337
  }
175
338
  }
339
+ const fpRate = parsePositive(process.env[ENV.DUPLICATE_FP_RATE], ENV.DUPLICATE_FP_RATE, DEFAULT_DUPLICATE_FP_RATE);
340
+ let duplicateFpRate = fpRate;
341
+ if (fpRate >= 1) {
342
+ emitLogOnce(`bad_range_${ENV.DUPLICATE_FP_RATE}`, "warn", `${ENV.DUPLICATE_FP_RATE}=${fpRate} must be in (0, 1); using default ${DEFAULT_DUPLICATE_FP_RATE}`);
343
+ duplicateFpRate = DEFAULT_DUPLICATE_FP_RATE;
344
+ }
345
+ else if (fpRate > MAX_DUPLICATE_FP_RATE) {
346
+ emitLogOnce(`clamped_env_${ENV.DUPLICATE_FP_RATE}`, "warn", `${ENV.DUPLICATE_FP_RATE}=${fpRate} exceeds the maximum ${MAX_DUPLICATE_FP_RATE}; clamping to ${MAX_DUPLICATE_FP_RATE}`);
347
+ duplicateFpRate = MAX_DUPLICATE_FP_RATE;
348
+ }
349
+ // The kill switch disables only on an explicit off value. Operators reach
350
+ // for this knob on privacy/overhead grounds, so a value we don't recognize
351
+ // ("disabled", "n", …) must not silently leave full-payload hashing on.
352
+ const dupDetectionRaw = process.env[ENV.DUPLICATE_DETECTION];
353
+ if (dupDetectionRaw !== undefined &&
354
+ dupDetectionRaw.trim() !== "" &&
355
+ !explicitlyOff(dupDetectionRaw) &&
356
+ !truthy(dupDetectionRaw)) {
357
+ emitLogOnce(`bad_env_${ENV.DUPLICATE_DETECTION}`, "warn", `unrecognized ${ENV.DUPLICATE_DETECTION}=${dupDetectionRaw}; duplicate detection remains ENABLED. Use 0/false/no/off to disable.`);
358
+ }
176
359
  return {
177
360
  customerId,
178
361
  projectId: projectId,
179
362
  components,
180
363
  customProviders,
364
+ customLabels,
181
365
  repoRoot: repoRootAbs,
182
366
  pathBlockList,
183
367
  otlpEndpoint,
184
368
  otlpHeaders: otlpHeaders,
185
369
  exportIntervalMs,
186
370
  maxCallSites,
371
+ maxSeriesPerInstrument,
372
+ maxModels,
187
373
  promptAnalysisSampleRate,
374
+ duplicateDetection: !explicitlyOff(process.env[ENV.DUPLICATE_DETECTION]),
375
+ duplicateWindowSeconds: parsePositive(process.env[ENV.DUPLICATE_WINDOW_SECONDS], ENV.DUPLICATE_WINDOW_SECONDS, DEFAULT_DUPLICATE_WINDOW_SECONDS, { max: MAX_DUPLICATE_WINDOW_SECONDS }),
376
+ duplicateCapacity: parsePositive(process.env[ENV.DUPLICATE_CAPACITY], ENV.DUPLICATE_CAPACITY, DEFAULT_DUPLICATE_CAPACITY, { integer: true, max: MAX_DUPLICATE_CAPACITY }),
377
+ duplicateFpRate,
378
+ duplicateGenerations: parsePositive(process.env[ENV.DUPLICATE_GENERATIONS], ENV.DUPLICATE_GENERATIONS, DEFAULT_DUPLICATE_GENERATIONS, { integer: true, max: MAX_DUPLICATE_GENERATIONS }),
188
379
  };
189
380
  }
190
381
  /** Test-only hook to re-arm the one-shot warning. */
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}