frugal-sdk-python 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/PKG-INFO +23 -11
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/README.md +21 -9
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/docs/metrics-reference.md +101 -5
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/docs/quickstart.md +27 -7
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/__init__.py +4 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/config.py +176 -0
- frugal_sdk_python-0.4.0/src/frugal_metrics/duplicate_filter.py +256 -0
- frugal_sdk_python-0.4.0/src/frugal_metrics/instrument.py +520 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/otel.py +20 -0
- frugal_sdk_python-0.4.0/src/frugal_metrics/prompt_analyzer.py +554 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/semconv.py +38 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/track.py +21 -1
- frugal_sdk_python-0.4.0/src/frugal_metrics/wrappers/_batch.py +445 -0
- frugal_sdk_python-0.4.0/src/frugal_metrics/wrappers/_patching.py +51 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/wrappers/anthropic.py +10 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/wrappers/openai.py +44 -24
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/conftest.py +20 -0
- frugal_sdk_python-0.4.0/tests/test_config.py +357 -0
- frugal_sdk_python-0.4.0/tests/test_duplicate_filter.py +234 -0
- frugal_sdk_python-0.4.0/tests/test_prompt_analyzer.py +655 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_track_ai_call.py +167 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_wrap_anthropic.py +24 -0
- frugal_sdk_python-0.4.0/tests/test_wrap_anthropic_batches.py +244 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_wrap_openai.py +16 -0
- frugal_sdk_python-0.4.0/tests/test_wrap_openai_analyzer.py +499 -0
- frugal_sdk_python-0.4.0/tests/test_wrap_openai_batches.py +199 -0
- frugal_sdk_python-0.3.0/src/frugal_metrics/instrument.py +0 -282
- frugal_sdk_python-0.3.0/src/frugal_metrics/prompt_analyzer.py +0 -273
- frugal_sdk_python-0.3.0/tests/test_config.py +0 -181
- frugal_sdk_python-0.3.0/tests/test_prompt_analyzer.py +0 -203
- frugal_sdk_python-0.3.0/tests/test_wrap_openai_analyzer.py +0 -188
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/.github/workflows/cd.yml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/.gitignore +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/LICENSES/Apache-2.0.txt +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/REUSE.toml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/README.md +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/collector/otel-collector-config.yaml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/docker-compose.yml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/grafana/dashboards/frugal-metrics.json +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/grafana/provisioning/dashboards/frugal.yml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/grafana/provisioning/datasources/prometheus.yml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/mockllm/Dockerfile +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/mockllm/package.json +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/mockllm/src/server.js +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/prometheus/consoles/index.html.example +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/prometheus/consoles/models.html +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/prometheus/consoles/overview.html +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/prometheus/consoles/sites.html +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/prometheus/prometheus.yml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/Dockerfile +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/clients.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/anon.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/classify.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/deferred.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/fail.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/pipeline.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/summarize.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/translate.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/via_blocked_lib.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/handlers/via_unblocked_lib.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/internal_lib/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/internal_lib/ai_helper.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/main.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/sibling_lib/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/app/sibling_lib/ai_helper.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/pyproject.toml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/sample-app/requirements.txt +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/examples/test-env/scripts/verify.sh +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/pyproject.toml +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/block_hash_lru.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/caller.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/debug.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/providers.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/safe.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/shutdown.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/streams.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/wrappers/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/wrappers/bedrock.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/src/frugal_metrics/wrappers/bedrock_models.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/__init__.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_bedrock_models.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_block_hash_lru.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_caller.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_caller_edge_cases.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_debug.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_providers.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_shutdown.py +0 -0
- {frugal_sdk_python-0.3.0 → frugal_sdk_python-0.4.0}/tests/test_wrap_bedrock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: frugal-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Instrumentation for Cost-to-Code attribution.
|
|
5
5
|
Project-URL: Homepage, https://frugal.co
|
|
6
6
|
Author: Frugal AI Inc.
|
|
@@ -79,10 +79,16 @@ Set both or the wrapper silently returns the client unmodified:
|
|
|
79
79
|
| `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`. |
|
|
80
80
|
| `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. |
|
|
81
81
|
| `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. |
|
|
82
|
+
| `FRUGAL_CUSTOM_LABEL` | Comma-separated allowlist of values for the `frugal.custom_label` label (e.g. `billing,search`). Applies only to `track_ai_call` / `start_ai_call` calls that pass `custom_label=...`; a value outside the first 20 entries (hard cap, not configurable) is coerced to `other`. |
|
|
82
83
|
| `FRUGAL_PATH_BLOCK_LIST` | Comma-separated repo-relative paths to skip for caller attribution (see below). |
|
|
83
84
|
| `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence in milliseconds. Default `60000`. |
|
|
84
85
|
| `FRUGAL_MAX_CALL_SITES` | Cardinality cap — max distinct `(caller_file, caller_function)` pairs to attribute. Default `500`. Set to `0` to disable. |
|
|
85
86
|
| `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. |
|
|
87
|
+
| `FRUGAL_DUPLICATE_REQUEST_DETECTION` | Duplicate-request detection (the `frugal.gen_ai.duplicate_requests` metric) is **on by default** and hashes each chat request in-process (the hash never leaves the SDK). Set to `0`/`false`/`no`/`off` to disable — no hashing, no filter memory, no metric. |
|
|
88
|
+
| `FRUGAL_DUPLICATE_WINDOW_SECONDS` | How long a request signature is remembered for duplicate detection. Default `3600`, max `604800` (7 days). |
|
|
89
|
+
| `FRUGAL_DUPLICATE_CAPACITY` | Expected distinct request signatures per window (sizes the in-memory filter). Default `10000`, max `1000000`. Raise it if the SDK warns the filter is saturated. |
|
|
90
|
+
| `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate of the duplicate filter. Default `0.01`, clamped to `[1e-6, 0.5]`. |
|
|
91
|
+
| `FRUGAL_DUPLICATE_GENERATIONS` | Rotation granularity of the duplicate filter's time window. Default `2`, max `16`. |
|
|
86
92
|
| `FRUGAL_DISABLED` | Set to `1` to disable all instrumentation without uninstalling. |
|
|
87
93
|
|
|
88
94
|
## What gets captured
|
|
@@ -92,32 +98,38 @@ Every wrapped call emits OTel metrics tagged with the caller's source file and f
|
|
|
92
98
|
| Metric | Kind | Description | Additional attributes |
|
|
93
99
|
| --------------------------------------- | --------- | ------------------------------------------------------------ | -------------------------------------- |
|
|
94
100
|
| `gen_ai.client.operation.duration` | histogram | Call latency in seconds | — |
|
|
95
|
-
| `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type`
|
|
96
|
-
| `frugal.gen_ai.calls` | counter | Number of calls per site |
|
|
101
|
+
| `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type`, `frugal.history_bucket` |
|
|
102
|
+
| `frugal.gen_ai.calls` | counter | Number of calls per site | `frugal.history_bucket`, `frugal.structured_output_mode` |
|
|
97
103
|
| `frugal.gen_ai.cache_read_tokens` | histogram | Prompt cache hits | — |
|
|
98
104
|
| `frugal.gen_ai.cache_write_tokens` | histogram | Prompt cache writes | — |
|
|
99
105
|
| `frugal.gen_ai.reasoning_tokens` | histogram | OpenAI reasoning tokens (o-series), Anthropic thinking tokens| — |
|
|
100
106
|
| `frugal.gen_ai.errors` | counter | Errors by exception type | `frugal.error.type` |
|
|
101
107
|
| `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality cap, bugs) | `frugal.internal_error.stage` |
|
|
102
108
|
| `frugal.gen_ai.output_cap_utilization` | histogram | `output_tokens / max_tokens` ratio when `max_tokens` is set | `frugal.structured_output_mode` |
|
|
103
|
-
| `frugal.gen_ai.history_depth` | histogram | `messages.length` (
|
|
109
|
+
| `frugal.gen_ai.history_depth` | histogram | `messages.length` — every chat call (100%, not sampled) | — |
|
|
110
|
+
| `frugal.gen_ai.duplicate_requests` | counter | Repeat-request detection — every chat call (100%, not sampled; disable via `FRUGAL_DUPLICATE_REQUEST_DETECTION`) | `frugal.dup_state` |
|
|
104
111
|
| `frugal.gen_ai.few_shot_count` | histogram | Few-shot exemplar pairs before final user (analyzer-sampled) | — |
|
|
112
|
+
| `frugal.gen_ai.few_shot_tokens` | histogram | Approx token size of the few-shot example block (analyzer-sampled) | — |
|
|
105
113
|
| `frugal.gen_ai.noise_ratio` | histogram | HTML/base64/whitespace fraction of input (analyzer-sampled) | — |
|
|
106
114
|
| `frugal.gen_ai.cache_prefix_stable` | counter | Per-block prompt-cache stability (analyzer-sampled) | `frugal.block`, `frugal.stable` |
|
|
107
115
|
| `frugal.gen_ai.structured_output_hint` | counter | "Return JSON" prompts missing `response_format`/`tools` | — |
|
|
108
116
|
|
|
109
|
-
The
|
|
110
|
-
(`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%); the token-side metrics
|
|
111
|
-
100% — never sampled.
|
|
117
|
+
The analyzer-sampled metrics fire on a fraction of calls
|
|
118
|
+
(`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%); the token-side metrics,
|
|
119
|
+
`history_depth`, and `duplicate_requests` are 100% — never sampled.
|
|
112
120
|
|
|
113
|
-
Every metric carries this base attribute set
|
|
121
|
+
Every metric carries this base attribute set (except
|
|
122
|
+
`frugal.gen_ai.internal_errors`, which is emitted from failure paths where
|
|
123
|
+
these attributes may not exist yet and carries only `frugal.project_id`, the
|
|
124
|
+
stage/type, and `frugal.customer_id` / `frugal.custom_label` when set):
|
|
114
125
|
|
|
115
126
|
- `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
|
|
116
127
|
- `frugal.caller_file`, `frugal.caller_function` — auto-detected from the call stack
|
|
117
128
|
- `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`). Set via the `system` kwarg; an unrecognized value is coerced to `custom` with a one-time warning. See [`docs/metrics-reference.md`](docs/metrics-reference.md#attributes).
|
|
118
129
|
- `gen_ai.request.model`, `gen_ai.response.model` — the model used
|
|
119
130
|
- `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)
|
|
120
|
-
- `frugal.batched` — `true` for batch-API submissions, `false` otherwise
|
|
131
|
+
- `frugal.batched` — `true` for batch-API paths (OpenAI Batch API submissions; Anthropic Message Batches submissions and results), `false` otherwise
|
|
132
|
+
- `frugal.custom_label` — only on `track_ai_call` / `start_ai_call` calls that pass `custom_label=...`; the value must be in the `FRUGAL_CUSTOM_LABEL` allowlist or it is coerced to `other`
|
|
121
133
|
|
|
122
134
|
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.
|
|
123
135
|
|
|
@@ -179,7 +191,7 @@ except BaseException as exc:
|
|
|
179
191
|
|
|
180
192
|
`success()` and `error()` finalize the call exactly once and are idempotent — extra calls are ignored.
|
|
181
193
|
|
|
182
|
-
**Metadata** — `track_ai_call` / `start_ai_call` kwargs (all optional): `system` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `request_model` (default `"<unknown>"`). To also run the sampled prompt analyzer, pass any of `messages` / `system_prompt` / `tools` / `response_format` / `max_tokens`.
|
|
194
|
+
**Metadata** — `track_ai_call` / `start_ai_call` kwargs (all optional): `system` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `request_model` (default `"<unknown>"`), `custom_label` (→ the `frugal.custom_label` label on every metric of the call; trimmed, and coerced to `other` unless in the `FRUGAL_CUSTOM_LABEL` allowlist). To also run the sampled prompt analyzer, pass any of `messages` / `system_prompt` / `tools` / `response_format` / `max_tokens`.
|
|
183
195
|
|
|
184
196
|
**Usage** — `set_usage(...)` / `success(...)` kwargs (all optional): `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `response_model`.
|
|
185
197
|
|
|
@@ -50,10 +50,16 @@ Set both or the wrapper silently returns the client unmodified:
|
|
|
50
50
|
| `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`. |
|
|
51
51
|
| `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. |
|
|
52
52
|
| `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. |
|
|
53
|
+
| `FRUGAL_CUSTOM_LABEL` | Comma-separated allowlist of values for the `frugal.custom_label` label (e.g. `billing,search`). Applies only to `track_ai_call` / `start_ai_call` calls that pass `custom_label=...`; a value outside the first 20 entries (hard cap, not configurable) is coerced to `other`. |
|
|
53
54
|
| `FRUGAL_PATH_BLOCK_LIST` | Comma-separated repo-relative paths to skip for caller attribution (see below). |
|
|
54
55
|
| `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence in milliseconds. Default `60000`. |
|
|
55
56
|
| `FRUGAL_MAX_CALL_SITES` | Cardinality cap — max distinct `(caller_file, caller_function)` pairs to attribute. Default `500`. Set to `0` to disable. |
|
|
56
57
|
| `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. |
|
|
58
|
+
| `FRUGAL_DUPLICATE_REQUEST_DETECTION` | Duplicate-request detection (the `frugal.gen_ai.duplicate_requests` metric) is **on by default** and hashes each chat request in-process (the hash never leaves the SDK). Set to `0`/`false`/`no`/`off` to disable — no hashing, no filter memory, no metric. |
|
|
59
|
+
| `FRUGAL_DUPLICATE_WINDOW_SECONDS` | How long a request signature is remembered for duplicate detection. Default `3600`, max `604800` (7 days). |
|
|
60
|
+
| `FRUGAL_DUPLICATE_CAPACITY` | Expected distinct request signatures per window (sizes the in-memory filter). Default `10000`, max `1000000`. Raise it if the SDK warns the filter is saturated. |
|
|
61
|
+
| `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate of the duplicate filter. Default `0.01`, clamped to `[1e-6, 0.5]`. |
|
|
62
|
+
| `FRUGAL_DUPLICATE_GENERATIONS` | Rotation granularity of the duplicate filter's time window. Default `2`, max `16`. |
|
|
57
63
|
| `FRUGAL_DISABLED` | Set to `1` to disable all instrumentation without uninstalling. |
|
|
58
64
|
|
|
59
65
|
## What gets captured
|
|
@@ -63,32 +69,38 @@ Every wrapped call emits OTel metrics tagged with the caller's source file and f
|
|
|
63
69
|
| Metric | Kind | Description | Additional attributes |
|
|
64
70
|
| --------------------------------------- | --------- | ------------------------------------------------------------ | -------------------------------------- |
|
|
65
71
|
| `gen_ai.client.operation.duration` | histogram | Call latency in seconds | — |
|
|
66
|
-
| `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type`
|
|
67
|
-
| `frugal.gen_ai.calls` | counter | Number of calls per site |
|
|
72
|
+
| `gen_ai.client.token.usage` | histogram | Input and output token counts | `gen_ai.token.type`, `frugal.history_bucket` |
|
|
73
|
+
| `frugal.gen_ai.calls` | counter | Number of calls per site | `frugal.history_bucket`, `frugal.structured_output_mode` |
|
|
68
74
|
| `frugal.gen_ai.cache_read_tokens` | histogram | Prompt cache hits | — |
|
|
69
75
|
| `frugal.gen_ai.cache_write_tokens` | histogram | Prompt cache writes | — |
|
|
70
76
|
| `frugal.gen_ai.reasoning_tokens` | histogram | OpenAI reasoning tokens (o-series), Anthropic thinking tokens| — |
|
|
71
77
|
| `frugal.gen_ai.errors` | counter | Errors by exception type | `frugal.error.type` |
|
|
72
78
|
| `frugal.gen_ai.internal_errors` | counter | frugal-metrics internal failures (cardinality cap, bugs) | `frugal.internal_error.stage` |
|
|
73
79
|
| `frugal.gen_ai.output_cap_utilization` | histogram | `output_tokens / max_tokens` ratio when `max_tokens` is set | `frugal.structured_output_mode` |
|
|
74
|
-
| `frugal.gen_ai.history_depth` | histogram | `messages.length` (
|
|
80
|
+
| `frugal.gen_ai.history_depth` | histogram | `messages.length` — every chat call (100%, not sampled) | — |
|
|
81
|
+
| `frugal.gen_ai.duplicate_requests` | counter | Repeat-request detection — every chat call (100%, not sampled; disable via `FRUGAL_DUPLICATE_REQUEST_DETECTION`) | `frugal.dup_state` |
|
|
75
82
|
| `frugal.gen_ai.few_shot_count` | histogram | Few-shot exemplar pairs before final user (analyzer-sampled) | — |
|
|
83
|
+
| `frugal.gen_ai.few_shot_tokens` | histogram | Approx token size of the few-shot example block (analyzer-sampled) | — |
|
|
76
84
|
| `frugal.gen_ai.noise_ratio` | histogram | HTML/base64/whitespace fraction of input (analyzer-sampled) | — |
|
|
77
85
|
| `frugal.gen_ai.cache_prefix_stable` | counter | Per-block prompt-cache stability (analyzer-sampled) | `frugal.block`, `frugal.stable` |
|
|
78
86
|
| `frugal.gen_ai.structured_output_hint` | counter | "Return JSON" prompts missing `response_format`/`tools` | — |
|
|
79
87
|
|
|
80
|
-
The
|
|
81
|
-
(`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%); the token-side metrics
|
|
82
|
-
100% — never sampled.
|
|
88
|
+
The analyzer-sampled metrics fire on a fraction of calls
|
|
89
|
+
(`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`, default 10%); the token-side metrics,
|
|
90
|
+
`history_depth`, and `duplicate_requests` are 100% — never sampled.
|
|
83
91
|
|
|
84
|
-
Every metric carries this base attribute set
|
|
92
|
+
Every metric carries this base attribute set (except
|
|
93
|
+
`frugal.gen_ai.internal_errors`, which is emitted from failure paths where
|
|
94
|
+
these attributes may not exist yet and carries only `frugal.project_id`, the
|
|
95
|
+
stage/type, and `frugal.customer_id` / `frugal.custom_label` when set):
|
|
85
96
|
|
|
86
97
|
- `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
|
|
87
98
|
- `frugal.caller_file`, `frugal.caller_function` — auto-detected from the call stack
|
|
88
99
|
- `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`). Set via the `system` kwarg; an unrecognized value is coerced to `custom` with a one-time warning. See [`docs/metrics-reference.md`](docs/metrics-reference.md#attributes).
|
|
89
100
|
- `gen_ai.request.model`, `gen_ai.response.model` — the model used
|
|
90
101
|
- `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)
|
|
91
|
-
- `frugal.batched` — `true` for batch-API submissions, `false` otherwise
|
|
102
|
+
- `frugal.batched` — `true` for batch-API paths (OpenAI Batch API submissions; Anthropic Message Batches submissions and results), `false` otherwise
|
|
103
|
+
- `frugal.custom_label` — only on `track_ai_call` / `start_ai_call` calls that pass `custom_label=...`; the value must be in the `FRUGAL_CUSTOM_LABEL` allowlist or it is coerced to `other`
|
|
92
104
|
|
|
93
105
|
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.
|
|
94
106
|
|
|
@@ -150,7 +162,7 @@ except BaseException as exc:
|
|
|
150
162
|
|
|
151
163
|
`success()` and `error()` finalize the call exactly once and are idempotent — extra calls are ignored.
|
|
152
164
|
|
|
153
|
-
**Metadata** — `track_ai_call` / `start_ai_call` kwargs (all optional): `system` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `request_model` (default `"<unknown>"`). To also run the sampled prompt analyzer, pass any of `messages` / `system_prompt` / `tools` / `response_format` / `max_tokens`.
|
|
165
|
+
**Metadata** — `track_ai_call` / `start_ai_call` kwargs (all optional): `system` (→ `gen_ai.system`, default `"custom"`), `operation` (default `"chat"`), `request_model` (default `"<unknown>"`), `custom_label` (→ the `frugal.custom_label` label on every metric of the call; trimmed, and coerced to `other` unless in the `FRUGAL_CUSTOM_LABEL` allowlist). To also run the sampled prompt analyzer, pass any of `messages` / `system_prompt` / `tools` / `response_format` / `max_tokens`.
|
|
154
166
|
|
|
155
167
|
**Usage** — `set_usage(...)` / `success(...)` kwargs (all optional): `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `response_model`.
|
|
156
168
|
|
|
@@ -16,18 +16,20 @@ metric may carry the extra attributes shown in the rightmost column.
|
|
|
16
16
|
| ------------------------------------- | --------- | ----------- | ----------------------------------------------------- | ---------------------------------------------- |
|
|
17
17
|
| `gen_ai.client.operation.duration` | histogram | `s` | every call, success or error | — |
|
|
18
18
|
| `gen_ai.client.token.usage` | histogram | `{token}` | every call with a usage payload | `gen_ai.token.type` |
|
|
19
|
-
| `frugal.gen_ai.calls` | counter | `{call}` | every call, success or error |
|
|
19
|
+
| `frugal.gen_ai.calls` | counter | `{call}` | every call, success or error | `frugal.history_bucket` (chat calls), `frugal.structured_output_mode` (with payload) |
|
|
20
20
|
| `frugal.gen_ai.cache_read_tokens` | histogram | `{token}` | when provider reports cache reads | — |
|
|
21
21
|
| `frugal.gen_ai.cache_write_tokens` | histogram | `{token}` | when provider reports cache writes | — |
|
|
22
22
|
| `frugal.gen_ai.reasoning_tokens` | histogram | `{token}` | OpenAI o-series reasoning models | — |
|
|
23
23
|
| `frugal.gen_ai.errors` | counter | `{error}` | when the wrapped call raises | `frugal.error.type` |
|
|
24
24
|
| `frugal.gen_ai.internal_errors` | counter | `{error}` | frugal-metrics internal failures (cardinality cap, instrumentation bugs) | `frugal.internal_error.stage` |
|
|
25
25
|
| `frugal.gen_ai.output_cap_utilization`| histogram | `1` | when request had `max_tokens` (ratio 0..1) | `frugal.structured_output_mode` |
|
|
26
|
-
| `frugal.gen_ai.history_depth` | histogram | `{message}` |
|
|
26
|
+
| `frugal.gen_ai.history_depth` | histogram | `{message}` | every chat call (`messages.length`) — 100%, not sampled | — |
|
|
27
27
|
| `frugal.gen_ai.few_shot_count` | histogram | `{example}` | analyzer-sampled single-turn chat (count of `(user, assistant)` pairs before final user) | — |
|
|
28
|
+
| `frugal.gen_ai.few_shot_tokens` | histogram | `{token}` | analyzer-sampled single-turn chat — approx token size of the `(user, assistant)` example block, beside `few_shot_count` | — |
|
|
28
29
|
| `frugal.gen_ai.noise_ratio` | histogram | `1` | analyzer-sampled chat call (HTML / base64 / whitespace fraction of input) | — |
|
|
29
30
|
| `frugal.gen_ai.cache_prefix_stable` | counter | `{call}` | analyzer-sampled chat call, when previous call to the same call site existed (one point per block, see Attributes) | `frugal.block`, `frugal.stable` |
|
|
30
31
|
| `frugal.gen_ai.structured_output_hint`| counter | `{call}` | analyzer-sampled chat call where the prompt asks for JSON without `response_format` or `tools` | — |
|
|
32
|
+
| `frugal.gen_ai.duplicate_requests` | counter | `{call}` | every chat call with a canonicalizable request, success or error — 100%, not sampled; off when `FRUGAL_DUPLICATE_REQUEST_DETECTION=false` | `frugal.dup_state` |
|
|
31
33
|
|
|
32
34
|
`gen_ai.client.token.usage` is split across input and output via the
|
|
33
35
|
`gen_ai.token.type` attribute (`input` | `output`).
|
|
@@ -37,7 +39,90 @@ The analyzer-sampled metrics are gated by `FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`
|
|
|
37
39
|
minute)` — within a minute a given call site is either fully sampled or fully
|
|
38
40
|
skipped, so the LRU comparisons that drive `cache_prefix_stable` see contiguous
|
|
39
41
|
data when active. The token-side metrics (cap utilization, batched label,
|
|
40
|
-
calls, token usage) are 100% — never sampled.
|
|
42
|
+
calls, token usage) are 100% — never sampled. `frugal.gen_ai.history_depth` and
|
|
43
|
+
the `frugal.history_bucket` label are likewise 100% — `messages.length` is a
|
|
44
|
+
trivially-cheap integer needing no prompt analysis, so it is emitted on every chat
|
|
45
|
+
call (`few_shot_count`, `few_shot_tokens`, `noise_ratio`, `cache_prefix_stable`, and
|
|
46
|
+
`structured_output_hint` remain analyzer-sampled). `few_shot_tokens` is emitted from
|
|
47
|
+
the same analyzer pass as `few_shot_count` (identifying which turns are examples
|
|
48
|
+
needs prompt inspection, so there is no cheap 100% path); its token count is an
|
|
49
|
+
approximate chars/4 estimate over the example block.
|
|
50
|
+
|
|
51
|
+
Label placement and cardinality: `frugal.history_bucket` rides BOTH the
|
|
52
|
+
`frugal.gen_ai.calls` counter and the `gen_ai.client.token.usage` histogram —
|
|
53
|
+
on token.usage deliberately, so spend can be cut by conversation depth.
|
|
54
|
+
Worst-case that multiplies the token.usage series by 7 buckets × 2 token types
|
|
55
|
+
per call site (in practice a site occupies 1–3 buckets, so expect 2–6× — but
|
|
56
|
+
provision for 14×). `frugal.structured_output_mode` rides ONLY `calls` (and
|
|
57
|
+
`output_cap_utilization`), NOT token.usage — the full 7×4 bucket×mode
|
|
58
|
+
cross-tab lives on `calls` alone, where worst-case it is 28 series per call
|
|
59
|
+
site.
|
|
60
|
+
|
|
61
|
+
`frugal.gen_ai.duplicate_requests` is also 100% (independent of
|
|
62
|
+
`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`). Each chat call's request is canonicalized
|
|
63
|
+
(`model + system + messages + tools + response_format + max_tokens` — plus
|
|
64
|
+
`previous_response_id` for stateful OpenAI Responses-API calls — with the call
|
|
65
|
+
site folded in) and hashed; the hash is checked against a shared, time-and-size-
|
|
66
|
+
limited rotating Bloom filter and classified `first_seen` / `repeat_seen`. Errored
|
|
67
|
+
calls are classified too — a failed-then-retried identical request reads
|
|
68
|
+
`repeat_seen`, keeping retry storms visible. The raw signature never leaves the
|
|
69
|
+
SDK — only the 2-value state is exported. Hashing is incremental and hard-capped
|
|
70
|
+
at 256 KiB of canonicalized request per call: the small discriminators (call
|
|
71
|
+
site, model, system, tools, response_format, max_tokens, previous_response_id)
|
|
72
|
+
are hashed FIRST, then `messages` under the remaining budget — so an over-budget
|
|
73
|
+
history truncates the tail of the conversation (plus a truncation marker), never
|
|
74
|
+
the fields that keep distinct call sites or models from colliding. Binary/image
|
|
75
|
+
content contributes only its type and length — never its bytes — covering raw
|
|
76
|
+
`bytes` blocks AND the base64-string forms providers actually send (OpenAI
|
|
77
|
+
`data:` URLs, Anthropic base64 source blocks). If the filter saturates (actual
|
|
78
|
+
traffic far above `FRUGAL_DUPLICATE_CAPACITY`, generation union over 50% full),
|
|
79
|
+
a one-time `warn` diagnostic is emitted through the debug callback. If the
|
|
80
|
+
filter's lazy bit-array allocation fails (`MemoryError`), duplicate detection is
|
|
81
|
+
**disabled for the rest of the process**: a one-time `error` diagnostic is
|
|
82
|
+
emitted, the first failing call records a
|
|
83
|
+
`frugal.gen_ai.internal_errors{stage=duplicate_detection}` point, and every
|
|
84
|
+
subsequent call skips detection silently (no `duplicate_requests` metric).
|
|
85
|
+
Controls:
|
|
86
|
+
|
|
87
|
+
- `FRUGAL_DUPLICATE_REQUEST_DETECTION` (default `true`) — kill switch. When
|
|
88
|
+
`false`/`0`/`no`/`off`, no filter is allocated, no signature is hashed, and the
|
|
89
|
+
metric is not emitted. Any other value keeps detection enabled and emits a
|
|
90
|
+
one-time `warn` diagnostic naming the unrecognized value.
|
|
91
|
+
- `FRUGAL_DUPLICATE_WINDOW_SECONDS` (default `3600`, clamped to `604800` — 7
|
|
92
|
+
days) — retention window. Because the filter rotates in `generations` slots,
|
|
93
|
+
a signature is remembered for **at least**
|
|
94
|
+
`window × (generations−1)/generations` and **at most** `window` seconds
|
|
95
|
+
(30–60 min at defaults) — treat the counterfactual cache TTL as that range,
|
|
96
|
+
not a flat window.
|
|
97
|
+
- `FRUGAL_DUPLICATE_CAPACITY` (default `10000`, clamped to `1000000`) — expected
|
|
98
|
+
distinct signatures per window (Bloom sizing target).
|
|
99
|
+
- `FRUGAL_DUPLICATE_FP_RATE` (default `0.01`, clamped to `[1e-6, 0.5]`) —
|
|
100
|
+
target false-positive rate.
|
|
101
|
+
- `FRUGAL_DUPLICATE_GENERATIONS` (default `2`, clamped to `16`) — rotating-Bloom
|
|
102
|
+
generation count.
|
|
103
|
+
|
|
104
|
+
Out-of-range or non-finite values for any of these fall back to the default (or
|
|
105
|
+
clamp to the bound) with a one-time named `warn` through the debug callback.
|
|
106
|
+
|
|
107
|
+
Reading `duplicate_requests` correctly:
|
|
108
|
+
|
|
109
|
+
- **Denominator.** Coverage is a per-endpoint subset of `calls` (embeddings,
|
|
110
|
+
legacy text completions, bare `track_ai_call` without messages, and other
|
|
111
|
+
non-chat surfaces emit no duplicate signal), so compute repeat rates as
|
|
112
|
+
`repeat_seen / (first_seen + repeat_seen)` — never against `calls`.
|
|
113
|
+
- **Retries count as repeats.** Errored calls insert into the filter, so a
|
|
114
|
+
retried-after-failure request reads `repeat_seen`. That keeps retry storms
|
|
115
|
+
visible, but it also means `repeat_seen` includes required retry traffic that
|
|
116
|
+
a response cache would not eliminate — net it against
|
|
117
|
+
`frugal.gen_ai.errors` when estimating cacheable waste.
|
|
118
|
+
- **Sampling params are ignored.** The signature deliberately excludes
|
|
119
|
+
`temperature`, `top_p`, `seed`, `n`, and `tool_choice`, so best-of-N /
|
|
120
|
+
self-consistency workloads read as 100% duplicates. Fine for spotting
|
|
121
|
+
caching opportunities; wrong for assuming every repeat is waste.
|
|
122
|
+
- **Why a separate counter, not a label on `calls`:** the metric has its own
|
|
123
|
+
kill switch, covers only a subset of calls, and its 2-value state would
|
|
124
|
+
otherwise double every `calls` series; keeping it separate keeps `calls`
|
|
125
|
+
stable when detection is toggled.
|
|
41
126
|
|
|
42
127
|
## Attributes
|
|
43
128
|
|
|
@@ -80,10 +165,13 @@ calls, token usage) are 100% — never sampled.
|
|
|
80
165
|
| `frugal.caller_file` | Source file repo-relative to `FRUGAL_REPO_ROOT` |
|
|
81
166
|
| `frugal.caller_function` | Function/method name at the call site |
|
|
82
167
|
| `frugal.error.type` | Exception class name (error-path metrics only) |
|
|
83
|
-
| `frugal.batched` | `true` when
|
|
84
|
-
| `frugal.structured_output_mode` | `none` \| `json_object` \| `json_schema` \| `tools`.
|
|
168
|
+
| `frugal.batched` | `true` when the metric came from a provider batch-API path, `false` otherwise. Present on every metric. Batch submissions (OpenAI `batches.create`, Anthropic `messages.batches.create`) emit `frugal.gen_ai.calls` with `batched="true"` — one point per request for Anthropic (the requests array is in hand, grouped by model), one per submission for OpenAI (per-request counts are not reported at submission time, so OpenAI batched call volume is a floor, never an overcount). Iterating Anthropic `messages.batches.results()` additionally emits per-entry `gen_ai.client.token.usage` (+ cache metrics) with `batched="true"`, attributed to the call site that submitted the batch when the submission happened in the same process (bounded in-process memory; falls back to the results call site). OpenAI batch token usage lives in the output file read through the generic files API and is not intercepted. The manual track API accepts a `batched` flag for hand-instrumented batch paths. An app on an SDK predating batch support reads `false` on genuinely batched traffic — the label reflects what the deployed SDK can see. |
|
|
169
|
+
| `frugal.structured_output_mode` | `none` \| `json_object` \| `json_schema` \| `tools`. Derived from request params by precedence: non-empty `tools` → `tools`; else `response_format.type` `json_object` / `json_schema` → the matching value; else `none`. On `frugal.gen_ai.output_cap_utilization` (when `max_tokens` set), and on `frugal.gen_ai.calls` for every call with a request payload (omitted when there is no payload). |
|
|
85
170
|
| `frugal.block` | `system` \| `user` \| `tools` \| `schema`. Only on `frugal.gen_ai.cache_prefix_stable`. |
|
|
86
171
|
| `frugal.stable` | `true` when the block hash matches the previous call at this site. Only on `frugal.gen_ai.cache_prefix_stable`. |
|
|
172
|
+
| `frugal.history_bucket` | Bounded bucket of `messages.length`: `0` \| `1-2` \| `3-5` \| `6-10` \| `11-20` \| `21-50` \| `51+`. On `frugal.gen_ai.calls` and `gen_ai.client.token.usage`, and only for chat calls (payload has a `messages` array); omitted otherwise. 100% (not analyzer-sampled). |
|
|
173
|
+
| `frugal.custom_label` | Custom label (e.g. a business unit) passed as `custom_label=` to `track_ai_call` / `start_ai_call` — never set by the provider wrappers. When passed, it rides EVERY metric of that call (success and error paths alike). The value is trimmed of surrounding whitespace (matching the Node SDK), then must match one of the first 20 entries of `FRUGAL_CUSTOM_LABEL` (hard cap, not configurable) or it is coerced to `other`. Omitted when not passed. |
|
|
174
|
+
| `frugal.dup_state` | `first_seen` \| `repeat_seen` — whether this request's canonical signature was already seen within the duplicate-detection window. Only on `frugal.gen_ai.duplicate_requests`. |
|
|
87
175
|
|
|
88
176
|
### Sentinel values for `frugal.caller_file` / `frugal.caller_function`
|
|
89
177
|
|
|
@@ -118,8 +206,16 @@ attribute describing where our own code failed. Non-exhaustive list:
|
|
|
118
206
|
| `setup_failed` | Our wrapper setup (OTel init, caller resolution) threw; original call continued normally |
|
|
119
207
|
| `stream_proxy_failed` | Constructing the stream wrapper threw; original response returned unwrapped |
|
|
120
208
|
| `emission` | Emitting a success/error metric threw |
|
|
209
|
+
| `duplicate_detection` | The duplicate-request path (signature, filter, or emission) threw — including the one-time filter allocation failure; carries `frugal.internal_error.type` with the exception class |
|
|
121
210
|
| `cardinality_cap_hit` | `FRUGAL_MAX_CALL_SITES` was just reached (once per process) |
|
|
122
211
|
|
|
212
|
+
Unlike every other metric, `frugal.gen_ai.internal_errors` does **not** carry
|
|
213
|
+
the full base attribute set — it is emitted from failure paths where the
|
|
214
|
+
caller/provider/model attributes may not exist yet. Its points carry only
|
|
215
|
+
`frugal.project_id`, `frugal.internal_error.stage` (plus
|
|
216
|
+
`frugal.internal_error.type` where noted), and — when set —
|
|
217
|
+
`frugal.customer_id` and `frugal.custom_label`.
|
|
218
|
+
|
|
123
219
|
## Provider coverage
|
|
124
220
|
|
|
125
221
|
| Provider | Wrapper | `gen_ai.system` |
|
|
@@ -26,10 +26,16 @@ Optional:
|
|
|
26
26
|
| `FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT` | Frugal-specific OTLP collector URL. Defaults to `https://metrics.frugal.co/opentelemetry/v1/metrics`. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`. |
|
|
27
27
|
| `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`. |
|
|
28
28
|
| `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. |
|
|
29
|
+
| `FRUGAL_CUSTOM_LABEL` | Comma-separated allowlist of values for the `frugal.custom_label` label (e.g. `billing,search`). Applies only to `track_ai_call` / `start_ai_call` calls that pass `custom_label=...`; a value outside the first 20 entries (hard cap, not configurable) is coerced to `other`. |
|
|
29
30
|
| `FRUGAL_PATH_BLOCK_LIST` | Comma-separated repo-relative paths to skip when attributing the caller (see below). |
|
|
30
31
|
| `FRUGAL_METRIC_EXPORT_INTERVAL_MS` | Export cadence. Default `60000`. |
|
|
31
32
|
| `FRUGAL_MAX_CALL_SITES` | Cardinality cap — max distinct `(caller_file, caller_function)` pairs to attribute before overflow. Default `500`, set to `0` to disable. See [Cardinality cap](#cardinality-cap). |
|
|
32
|
-
| `FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE` | Fraction of calls (0..1) where the lightweight prompt analyzer runs and emits derived metrics (`
|
|
33
|
+
| `FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE` | Fraction of calls (0..1) where the lightweight prompt analyzer runs and emits derived metrics (`noise_ratio`, `cache_prefix_stable`, `few_shot_*`, …). Default `0.1`. Set to `0` to disable, `1` for every call. See [Prompt analysis](#prompt-analysis). |
|
|
34
|
+
| `FRUGAL_DUPLICATE_REQUEST_DETECTION` | Duplicate-request detection (`frugal.gen_ai.duplicate_requests`) is **on by default**: every chat request is canonicalized and SHA-256 hashed in-process to classify it `first_seen`/`repeat_seen`. Set to `0`/`false`/`no`/`off` to turn it off entirely (no hashing, no filter memory, no metric). |
|
|
35
|
+
| `FRUGAL_DUPLICATE_WINDOW_SECONDS` | How long a request signature is remembered for duplicate detection. Default `3600`, max `604800` (7 days). |
|
|
36
|
+
| `FRUGAL_DUPLICATE_CAPACITY` | Expected distinct request signatures per window — sizes the in-memory duplicate filter. Default `10000`, max `1000000`. Raise it if the debug callback warns the filter is saturated. If the filter's memory can't be allocated at all, detection disables itself for the rest of the process (one-time `error` diagnostic + one `internal_errors` point). |
|
|
37
|
+
| `FRUGAL_DUPLICATE_FP_RATE` | Target false-positive rate of the duplicate filter. Default `0.01`, clamped to `[1e-6, 0.5]`. |
|
|
38
|
+
| `FRUGAL_DUPLICATE_GENERATIONS` | Rotation granularity of the duplicate filter's time window. Default `2`, max `16`. |
|
|
33
39
|
| `FRUGAL_DISABLED` | Kill switch. Set to `1` to disable without uninstalling. |
|
|
34
40
|
|
|
35
41
|
Standard `OTEL_EXPORTER_OTLP_*` env vars are honored as fallbacks if the
|
|
@@ -254,7 +260,9 @@ extra calls are ignored.
|
|
|
254
260
|
|
|
255
261
|
**Metadata** — `track_ai_call` / `start_ai_call` kwargs (all optional):
|
|
256
262
|
`system` (→ `gen_ai.system`, default `"custom"`), `operation` (default
|
|
257
|
-
`"chat"`), `request_model` (default `"<unknown>"`)
|
|
263
|
+
`"chat"`), `request_model` (default `"<unknown>"`), `custom_label` (→ the
|
|
264
|
+
`frugal.custom_label` label on every metric of the call; values outside the
|
|
265
|
+
`FRUGAL_CUSTOM_LABEL` allowlist become `other`). To also run the sampled
|
|
258
266
|
prompt analyzer, pass any of `messages` / `system_prompt` / `tools` /
|
|
259
267
|
`response_format` / `max_tokens`.
|
|
260
268
|
|
|
@@ -316,17 +324,28 @@ detect common AI cost traps:
|
|
|
316
324
|
|
|
317
325
|
| Signal | Metric | Cost trap |
|
|
318
326
|
| --- | --- | --- |
|
|
319
|
-
| Conversation depth | `frugal.gen_ai.history_depth` | unbounded history re-sent on every turn |
|
|
320
327
|
| Few-shot example count | `frugal.gen_ai.few_shot_count` | over-provisioned exemplars where accuracy plateaus |
|
|
328
|
+
| Few-shot example size | `frugal.gen_ai.few_shot_tokens` | oversized exemplar blocks re-sent on every call |
|
|
321
329
|
| Input noise (HTML tags, base64 runs, large whitespace) | `frugal.gen_ai.noise_ratio` | filterable boilerplate paying token cost |
|
|
322
330
|
| Per-block cache stability | `frugal.gen_ai.cache_prefix_stable` | system / tools / schema churn breaking provider prompt cache |
|
|
323
331
|
| Structured-output keyword in prose | `frugal.gen_ai.structured_output_hint` | "return JSON" prompts that should use `response_format` / `tools` |
|
|
324
332
|
|
|
333
|
+
Two related signals run on **every** chat call (100%, independent of the
|
|
334
|
+
analyzer sample rate): `frugal.gen_ai.history_depth` (the `messages.length`
|
|
335
|
+
count — unbounded history re-sent on every turn) and
|
|
336
|
+
`frugal.gen_ai.duplicate_requests` (repeat-request detection; disable via
|
|
337
|
+
`FRUGAL_DUPLICATE_REQUEST_DETECTION`).
|
|
338
|
+
|
|
325
339
|
> **What we read vs what we send.** The analyzer inspects the request's
|
|
326
340
|
> `messages`, `tools`, `response_format`, `system`, and `max_tokens` kwargs to
|
|
327
|
-
> compute the signals above.
|
|
328
|
-
>
|
|
329
|
-
>
|
|
341
|
+
> compute the signals above. Duplicate detection additionally canonicalizes and
|
|
342
|
+
> SHA-256 hashes those same fields on every chat call (capped at 256 KiB per
|
|
343
|
+
> request; binary/image content — raw bytes and the base64 strings providers
|
|
344
|
+
> actually send — contributes only its type and length). We never send raw
|
|
345
|
+
> request content over the wire — only stats (counts, ratios); the hashes are
|
|
346
|
+
> used in-process for the stability comparisons and the duplicate filter, and
|
|
347
|
+
> never leave the process. For `duplicate_requests`, only the two-value
|
|
348
|
+
> `first_seen`/`repeat_seen` state is exported.
|
|
330
349
|
|
|
331
350
|
Sampling defaults to **10% per call site** and is deterministic by call site +
|
|
332
351
|
minute, so a given call site is either fully sampled or fully skipped within a
|
|
@@ -335,7 +354,8 @@ paying analyzer CPU cost on every call. Tune via
|
|
|
335
354
|
`FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE`.
|
|
336
355
|
|
|
337
356
|
The token-side metrics (`gen_ai.client.token.usage`, `frugal.gen_ai.calls`,
|
|
338
|
-
`frugal.gen_ai.cache_*`, `frugal.gen_ai.output_cap_utilization`)
|
|
357
|
+
`frugal.gen_ai.cache_*`, `frugal.gen_ai.output_cap_utilization`) plus
|
|
358
|
+
`frugal.gen_ai.history_depth` and `frugal.gen_ai.duplicate_requests` are 100% —
|
|
339
359
|
never sampled.
|
|
340
360
|
|
|
341
361
|
## See why nothing is exporting (debug callback)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from importlib.metadata import PackageNotFoundError, version
|
|
4
4
|
|
|
5
5
|
from frugal_metrics.debug import set_debug_callback
|
|
6
|
+
from frugal_metrics.semconv import DupState, HistoryBucket, StructuredOutputMode
|
|
6
7
|
from frugal_metrics.providers import (
|
|
7
8
|
PROVIDERS,
|
|
8
9
|
GenAiOperation,
|
|
@@ -32,6 +33,9 @@ __all__ = [
|
|
|
32
33
|
"ProviderEntry",
|
|
33
34
|
"GenAiSystem",
|
|
34
35
|
"GenAiOperation",
|
|
36
|
+
"HistoryBucket",
|
|
37
|
+
"StructuredOutputMode",
|
|
38
|
+
"DupState",
|
|
35
39
|
"normalize_provider",
|
|
36
40
|
"normalize_operation",
|
|
37
41
|
]
|