@frugalco/frugal-sdk-node 0.4.0 → 0.6.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 (53) hide show
  1. package/README.md +91 -17
  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 +41 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +232 -53
  9. package/dist/config.js.map +1 -1
  10. package/dist/instrument.d.ts +3 -0
  11. package/dist/instrument.d.ts.map +1 -1
  12. package/dist/instrument.js +70 -22
  13. package/dist/instrument.js.map +1 -1
  14. package/dist/internal-errors.d.ts +16 -0
  15. package/dist/internal-errors.d.ts.map +1 -0
  16. package/dist/internal-errors.js +54 -0
  17. package/dist/internal-errors.js.map +1 -0
  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 +45 -4
  23. package/dist/otel.d.ts.map +1 -1
  24. package/dist/otel.js +233 -24
  25. package/dist/otel.js.map +1 -1
  26. package/dist/providers.d.ts +2 -1
  27. package/dist/providers.d.ts.map +1 -1
  28. package/dist/providers.js +36 -19
  29. package/dist/providers.js.map +1 -1
  30. package/dist/semconv.d.ts +4 -0
  31. package/dist/semconv.d.ts.map +1 -1
  32. package/dist/semconv.js +17 -2
  33. package/dist/semconv.js.map +1 -1
  34. package/dist/streams.d.ts.map +1 -1
  35. package/dist/streams.js +6 -5
  36. package/dist/streams.js.map +1 -1
  37. package/dist/track.d.ts +6 -3
  38. package/dist/track.d.ts.map +1 -1
  39. package/dist/track.js +22 -7
  40. package/dist/track.js.map +1 -1
  41. package/dist/wrappers/anthropic.d.ts.map +1 -1
  42. package/dist/wrappers/anthropic.js +11 -31
  43. package/dist/wrappers/anthropic.js.map +1 -1
  44. package/dist/wrappers/batch.d.ts.map +1 -1
  45. package/dist/wrappers/batch.js +14 -34
  46. package/dist/wrappers/batch.js.map +1 -1
  47. package/dist/wrappers/bedrock.d.ts.map +1 -1
  48. package/dist/wrappers/bedrock.js +11 -30
  49. package/dist/wrappers/bedrock.js.map +1 -1
  50. package/dist/wrappers/openai.d.ts.map +1 -1
  51. package/dist/wrappers/openai.js +10 -31
  52. package/dist/wrappers/openai.js.map +1 -1
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,7 @@ All configuration is via environment variables. No `init()` call, no config obje
38
38
 
39
39
  ### Required
40
40
 
41
- Set both or the wrapper silently returns the client unmodified:
41
+ Set both (or, with `FRUGAL_METRICS_EXPORTER=console`, only `FRUGAL_PROJECT_ID`) or the wrapper silently returns the client unmodified:
42
42
 
43
43
  | Variable | Purpose |
44
44
  | ------------------- | ---------------------------------------------------------- |
@@ -49,24 +49,38 @@ Set both or the wrapper silently returns the client unmodified:
49
49
 
50
50
  | Variable | Purpose |
51
51
  | ---------------------------------------- | --------------------------------------------------------------------------------------- |
52
+ | `FRUGAL_METRICS_PROJECT_ID` / `FRUGAL_METRICS_API_KEY` | Namespaced overrides read before `FRUGAL_PROJECT_ID` / `FRUGAL_API_KEY`; an empty value falls through. Use them when the host app already owns the bare names. |
52
53
  | `FRUGAL_CUSTOMER_ID` | Customer ID issued by Frugal. When unset, the tenant label (auto-injected server-side by VMAuth from the bearer token) identifies the customer and `frugal.customer_id` is omitted. |
53
54
  | `FRUGAL_REPO_ROOT` | Absolute path to the source root; the strip prefix for `caller_file`. Defaults to the process working directory. |
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
- | `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. |
55
+ | `FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector URL for Frugal metrics. Defaults to `https://metrics.frugal.co/opentelemetry/v1/metrics`; an empty value falls through to the default. |
56
+ | `FRUGAL_METRICS_EXPORTER` | `otlp` (default) or `console`. `console` prints metrics to stdout through the console exporter for local verification; console mode needs only `FRUGAL_PROJECT_ID`; the API key, endpoint and header variables are ignored. Value is trimmed and case-insensitive; anything else warns once and uses `otlp`. |
57
+ | `FRUGAL_OTEL_EXPORTER_OTLP_HEADERS` | Export auth headers (e.g. `Authorization=Bearer ...`). Overrides the header built from `FRUGAL_METRICS_API_KEY` / `FRUGAL_API_KEY`; an empty or blank (whitespace-only) value is treated as unset and falls through to it. A non-blank value with no `key=value` pairs (e.g. `Authorization: Bearer x`) counts as missing auth, even when an API key is set — it never falls back to the key. Values are percent-decoded (`Bearer%20tok` → `Bearer tok`); a value that fails to decode is kept as-is. |
58
+ | `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
59
  | `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). |
60
+ | `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. A warning fires only when non-empty entries past the 20th are dropped (a trailing comma does not warn). `other` is reserved and ignored as an entry. See [Custom call sites](#custom-call-sites). |
59
61
  | `FRUGAL_PATH_BLOCK_LIST` | Comma-separated repo-relative paths to skip for caller attribution (see below). |
60
- | `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence in milliseconds. Default `60000`. |
62
+ | `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence in milliseconds, `1`-`2147483647`. Default `60000`; a larger value warns once and uses the default. |
61
63
  | `FRUGAL_MAX_CALL_SITES` | Cardinality cap — max distinct `(caller_file, caller_function)` pairs to attribute. Default `500`. Set to `0` to disable. |
62
- | `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. |
64
+ | `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. |
65
+ | `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). |
66
+ | `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. A decimal in [0, 1], plain or scientific notation (e.g. `0.25`, `1e-1`); anything else warns once and uses `0.1`. |
63
67
  | `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. |
64
68
  | `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)). |
65
69
  | `FRUGAL_DUPLICATE_CAPACITY` | Expected distinct requests per window (Bloom filter sizing). Default `10000`, max `1000000`. Raise if the saturation warning fires. |
66
- | `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate for the duplicate filter. Default `0.01`, max `0.5`. |
70
+ | `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate for the duplicate filter. Default `0.01`; clamped to `[1e-6, 0.5]` with a warning. |
67
71
  | `FRUGAL_DUPLICATE_GENERATIONS` | Rotating-Bloom generation count. Default `2`, max `16`. |
68
72
  | `FRUGAL_DISABLED` | Set to `1` to disable all instrumentation without uninstalling. |
69
73
 
74
+ The export endpoint and headers come only from `FRUGAL_`-prefixed variables.
75
+ The SDK never reads `OTEL_EXPORTER_OTLP_ENDPOINT` or
76
+ `OTEL_EXPORTER_OTLP_HEADERS`, and it hides `OTEL_EXPORTER_OTLP_HEADERS` and
77
+ `OTEL_EXPORTER_OTLP_METRICS_HEADERS` from the OTel exporter library while
78
+ constructing its exporter (the library would otherwise merge them into every
79
+ request), so the host's own OpenTelemetry headers never reach Frugal and the
80
+ Frugal bearer never reaches the host's collector. The exporter library still
81
+ reads its other standard variables (TLS, compression, timeout, temporality) on
82
+ its own.
83
+
70
84
  ## What gets captured
71
85
 
72
86
  Every wrapped call emits OTel metrics tagged with the caller's source file and function, the provider, and the model:
@@ -79,8 +93,9 @@ Every wrapped call emits OTel metrics tagged with the caller's source file and f
79
93
  | `frugal.gen_ai.cache_read_tokens` | histogram | Prompt cache hits | — |
80
94
  | `frugal.gen_ai.cache_write_tokens` | histogram | Prompt cache writes | — |
81
95
  | `frugal.gen_ai.reasoning_tokens` | histogram | OpenAI reasoning tokens (o-series), Anthropic thinking tokens| — |
96
+ | `frugal.gen_ai.agent.turns` | histogram | Model-response turns in a successful agent invocation | — |
82
97
  | `frugal.gen_ai.errors` | counter | Errors by exception type | `frugal.error.type` |
83
- | `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality cap, bugs) | `frugal.internal_error.stage` |
98
+ | `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality caps, bugs) | `frugal.internal_error.stage`, `frugal.internal_error.type`, `frugal.instrument` (on `otel_cardinality_overflow` only) |
84
99
  | `frugal.gen_ai.output_cap_utilization` | histogram | `output_tokens / max_tokens` ratio when `max_tokens` is set | `frugal.structured_output_mode` |
85
100
  | `frugal.gen_ai.history_depth` | histogram | `messages.length` — every chat call, success or error (100%, not sampled) | — |
86
101
  | `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`) |
@@ -101,9 +116,9 @@ Every metric carries this base attribute set:
101
116
  - `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
102
117
  - `frugal.caller_file`, `frugal.caller_function` — auto-detected from the call stack
103
118
  - `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).
104
- - `gen_ai.request.model`, `gen_ai.response.model` — the model used
119
+ - `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
105
120
  - `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)
106
- - `frugal.batched` — `true` for batch-API paths (OpenAI Batch API submissions; Anthropic Message Batches submissions and results), `false` otherwise
121
+ - `frugal.batched` — `true` for batch-API paths (OpenAI Batch API submissions; Anthropic Message Batches submissions and results), `false` otherwise; on every metric of the call except the `frugal.gen_ai.internal_errors` points
107
122
 
108
123
  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.
109
124
 
@@ -171,9 +186,57 @@ try {
171
186
 
172
187
  `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.
173
188
 
174
- **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`.
189
+ For an agent invocation, set `operation: "invoke_agent"` and report the number
190
+ of model-response steps completed by the whole run:
191
+
192
+ ```ts
193
+ const result = await trackAICall(
194
+ { operation: "invoke_agent", model: "agent-model" },
195
+ async (report) => {
196
+ const run = await agent.invoke(task);
197
+ report({ agentTurns: run.turnCount });
198
+ return run.output;
199
+ },
200
+ );
201
+ ```
202
+
203
+ The manual-handle form uses the same contract:
204
+
205
+ ```ts
206
+ const handle = startAICall({ operation: "invoke_agent", model: "agent-model" });
207
+ try {
208
+ const run = await agent.invoke(task);
209
+ handle.end({ agentTurns: run.turnCount });
210
+ } catch (err) {
211
+ handle.fail(err);
212
+ throw err;
213
+ }
214
+ ```
175
215
 
176
- **Usage** — `report(...)` / `end(...)` fields (all optional): `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `responseModel`.
216
+ Provider wrappers continue to measure individual model calls. Agent frameworks
217
+ use `trackAICall` or `startAICall` to measure the enclosing invocation; dedicated
218
+ framework adapters are not available yet. A turn sample is emitted only for a
219
+ successful `invoke_agent` call with a positive-integer `agentTurns` value.
220
+
221
+ **Avoid double-counting tokens.** The examples above report `agentTurns` only,
222
+ which is correct when the agent's inner model-client calls are already wrapped
223
+ by a supported provider wrapper: those wrappers emit token usage per inner
224
+ call, so also reporting aggregate tokens on the `invoke_agent` call would
225
+ double-count them. Only when the inner calls are **not** wrapped, add the
226
+ aggregate `inputTokens` / `outputTokens` (and the other usage fields):
227
+
228
+ ```ts
229
+ // Inner model calls are NOT wrapped by a provider wrapper.
230
+ report({
231
+ agentTurns: run.turnCount,
232
+ inputTokens: run.usage.inputTokens,
233
+ outputTokens: run.usage.outputTokens,
234
+ });
235
+ ```
236
+
237
+ **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 except the `frugal.gen_ai.internal_errors` `setup_failed` / `emission` points; 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`.
238
+
239
+ **Usage** — `report(...)` / `end(...)` fields (all optional): `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `responseModel`, `agentTurns`.
177
240
 
178
241
  The same env-var configuration, safety guarantees, and exported metrics apply.
179
242
 
@@ -269,17 +332,28 @@ It is **opt-in** because installing signal handlers is intrusive. If you run you
269
332
 
270
333
  ## Verifying the setup
271
334
 
272
- Set the OTLP endpoint to empty to print metrics to the console instead of exporting them:
335
+ Set `FRUGAL_METRICS_EXPORTER=console` to print the metrics to stdout instead
336
+ of exporting them (console mode needs only `FRUGAL_PROJECT_ID`; the API key, endpoint and header variables are ignored):
337
+
338
+ ```bash
339
+ export FRUGAL_PROJECT_ID=my-project
340
+ export FRUGAL_METRICS_EXPORTER=console
341
+ export FRUGAL_METRIC_EXPORT_INTERVAL_MS=5000
342
+ node my_script.js
343
+ ```
344
+
345
+ Or point the library at a local OTLP/HTTP collector. An empty endpoint is not a
346
+ console switch: it falls through to the default Frugal endpoint.
273
347
 
274
348
  ```bash
275
- export FRUGAL_API_KEY=local
349
+ export FRUGAL_API_KEY=my-key
276
350
  export FRUGAL_PROJECT_ID=my-project
277
- export FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT= # empty → ConsoleMetricExporter
351
+ export FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/metrics
278
352
  export FRUGAL_METRIC_EXPORT_INTERVAL_MS=5000
279
353
  node my_script.js
280
354
  ```
281
355
 
282
- You should see JSON metric records with `gen_ai.*` and `frugal.*` attributes every 5 seconds.
356
+ The collector should receive metric records with `gen_ai.*` and `frugal.*` attributes every 5 seconds.
283
357
 
284
358
  ## License
285
359
 
@@ -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
@@ -19,11 +19,32 @@ export interface Config {
19
19
  customLabels: readonly string[];
20
20
  repoRoot: string;
21
21
  pathBlockList: readonly string[];
22
+ /** Metric exporter: `otlp` (default) or `console` (prints to stdout; endpoint/headers ignored). */
23
+ exporter: "otlp" | "console";
22
24
  otlpEndpoint: string;
25
+ /** Resolved header string (display/compat). Only an explicit FRUGAL_OTEL_EXPORTER_OTLP_HEADERS value is parsed and percent-decoded for export. */
23
26
  otlpHeaders: string;
27
+ /**
28
+ * The raw API key when the export header comes from it (no explicit
29
+ * FRUGAL_OTEL_EXPORTER_OTLP_HEADERS), else null. The exporter builds
30
+ * `Authorization: Bearer <key>` from it directly, so a key containing `%` or
31
+ * `,` is never decoded or split.
32
+ */
33
+ otlpApiKey: string | null;
24
34
  exportIntervalMs: number;
25
35
  /** Max distinct (file, function) combinations to attribute before overflow. 0 = unlimited. */
26
36
  maxCallSites: number;
37
+ /**
38
+ * OTel's per-instrument limit on distinct attribute SETS. Beyond it the SDK
39
+ * merges everything into one `{otel.metric.overflow: true}` series with every
40
+ * other attribute dropped. 0 = disabled (effectively unlimited).
41
+ */
42
+ maxSeriesPerInstrument: number;
43
+ /**
44
+ * Max distinct model strings (request + response share one registry) to
45
+ * attribute before they collapse to the `<overflow>` sentinel. 0 = unlimited.
46
+ */
47
+ maxModels: number;
27
48
  /** Fraction of analyzed calls to emit prompt-analysis metrics for, 0..1. */
28
49
  promptAnalysisSampleRate: number;
29
50
  duplicateDetection: boolean;
@@ -32,7 +53,27 @@ export interface Config {
32
53
  duplicateFpRate: number;
33
54
  duplicateGenerations: number;
34
55
  }
56
+ /**
57
+ * Load the configuration, or null when the SDK should stay a no-op.
58
+ *
59
+ * Every wrapX() calls this at the customer's module scope, so a throw here
60
+ * crashes the host app before it has run a line of its own code. Nothing in
61
+ * the body is supposed to throw — but it reads the environment and touches the
62
+ * filesystem (process.cwd() raises ENOENT when the working directory has been
63
+ * removed, realpathSync can raise on a broken mount), so the whole thing is
64
+ * contained: any failure disables the SDK with one diagnostic instead of
65
+ * taking the process with it.
66
+ */
35
67
  export declare function loadConfig(): Config | null;
68
+ /**
69
+ * Parse an OTLP `key=value,key=value` header string. Values are
70
+ * percent-decoded like OTel's own env parser (`Bearer%20tok` → `Bearer tok`);
71
+ * keys are kept literal, and a value that fails to decode is kept raw. Parts
72
+ * without `=` or with an empty key are dropped; a string with no pairs at all
73
+ * returns undefined.
74
+ * Same rule in the Python and Go SDKs.
75
+ */
76
+ export declare function parseHeaders(raw: string | null): Record<string, string> | undefined;
36
77
  /** Test-only hook to re-arm the one-shot warning. */
37
78
  export declare function resetWarningStateForTests(): void;
38
79
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
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,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;AAqHD,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CA+O1C;AAED,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAwCA,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,mGAAmG;IACnG,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,kJAAkJ;IAClJ,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,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;AAmMD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAW1C;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAoBnF;AAwUD,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}