bivelio-savings-layer 0.1.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.
@@ -0,0 +1,7 @@
1
+ # Artefactos de Python: nada de esto se versiona.
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
@@ -0,0 +1,163 @@
1
+ BiVelio Inc. — BV-SALA Commercial Software License Agreement
2
+
3
+ Copyright (c) 2026 BiVelio Inc. All Rights Reserved.
4
+
5
+ IMPORTANT — READ BEFORE USE. This Commercial Software License Agreement (the
6
+ "Agreement") governs your use of the "@bivelio/savings-layer" software, also known
7
+ as "BV-SALA" (the "Software"), published by BiVelio Inc. ("BiVelio"). By installing,
8
+ copying, or using the Software, you agree to this Agreement. If you do not agree, do
9
+ not install or use the Software.
10
+
11
+ 1. LICENSE GRANT. Subject to your continuous compliance with this Agreement and to
12
+ an active BiVelio subscription (paid, or granted by BiVelio at no charge, for
13
+ example through a promotional code, a pilot, or the free Measure license) with a
14
+ valid license key ("License Key"), BiVelio grants you a limited, non-exclusive,
15
+ non-transferable, non-sublicensable, revocable license to install and use the
16
+ Software, as published, solely for your own internal business purposes, and only
17
+ for the number of seats covered by your subscription. The Software is licensed,
18
+ not sold. Under a Measure license the Software runs in shadow mode only: it
19
+ forwards your requests to your provider unchanged, applies no optimization, and
20
+ reports estimated savings that are never billed.
21
+
22
+ 2. LICENSE KEY; FAIL-CLOSED OPERATION. The Software requires a valid, active License
23
+ Key and does not operate without one. Expiry, non-payment, revocation, suspension,
24
+ or termination of your subscription immediately ends the license granted in
25
+ Section 1, and the Software will cease to function; when a paid subscription
26
+ ends, BiVelio may instead grant you a Measure license, under which the Software
27
+ continues in shadow mode only. You will not circumvent, disable, remove, or tamper
28
+ with the Software's licensing, metering, or revocation mechanisms, nor use the
29
+ Software after your license has ended.
30
+
31
+ 3. RESTRICTIONS. Except as expressly permitted above, you will not: (a) distribute,
32
+ publish, resell, rent, lease, lend, offer as a hosted or managed service, or
33
+ otherwise make the Software available to any third party; (b) sublicense, assign,
34
+ or transfer your rights; (c) use the Software for more seats than your
35
+ subscription covers, or share a License Key across more machines or service
36
+ identities than it covers; (d) remove, alter, or obscure any proprietary or
37
+ copyright notice; (e) reverse engineer, decompile, or disassemble the Software,
38
+ except to the limited extent applicable law expressly permits notwithstanding
39
+ this restriction; or (f) use the Software to develop, train, or improve a product
40
+ or service that competes with the Software.
41
+
42
+ 4. OWNERSHIP. The Software is the exclusive property of BiVelio and its licensors and
43
+ embodies valuable proprietary information and trade secrets. BiVelio retains all
44
+ right, title, and interest in and to the Software and all intellectual property
45
+ rights therein. No rights are granted to you except as expressly set out in this
46
+ Agreement, whether by implication, estoppel, or otherwise.
47
+
48
+ 5. YOUR DATA; PRIVACY.
49
+ (a) What the Software never sends to BiVelio. The Software never transmits to
50
+ BiVelio the content of your requests or responses (including prompts, system
51
+ instructions, messages, tool definitions, tool arguments and results, retrieved
52
+ documents, and completions) or the credentials you use with your LLM provider
53
+ (API keys, OAuth tokens, or other authorization headers). That content and those
54
+ credentials travel only between your systems and the LLM provider you configure.
55
+ (b) What the Software sends to BiVelio. The SDK contacts BiVelio only when you
56
+ configure a license server or metering URL; the local gateway always does. With a
57
+ BiVelio service-account key, the Software sends BiVelio's control plane usage
58
+ records for the requests it meters, each limited to:
59
+ (i) identifiers: a request identifier generated on your side (at random, or by
60
+ your own generator); for a repeated request valued by reference, the
61
+ identifier and timestamp of the earlier request whose provider counter is
62
+ used; a seat identifier, which is either a value you set or an opaque value
63
+ computed on your machine as a keyed hash (HMAC-SHA-256 wherever the
64
+ runtime provides it), keyed with your service-account key, of the
65
+ platform, CPU architecture, runtime executable path, host name,
66
+ operating-system user name, and home directory (those underlying values
67
+ are never sent); for traffic through the local gateway,
68
+ the session and sub-agent identifiers that your coding agent attaches to
69
+ its own requests (for example, Claude Code's session id), only when they
70
+ are plain identifiers; an organization hint; and a flag marking CI or
71
+ ephemeral runners;
72
+ (ii) the identifier of the model that served the request (the model named in
73
+ the request or, where the Software sent the request to another model under
74
+ a setting you enabled, that model; for a self-hosted or Azure deployment,
75
+ the name you gave it) and the time of the request;
76
+ (iii) counts: LLM calls executed and avoided; input and output tokens before
77
+ and after the Software's optimizations, including tokens removed,
78
+ compressed, or read from the provider's cache; and the cache outcome; and
79
+ (iv) cost figures computed by the Software from its price catalogue (or from
80
+ a cost model you supply): baseline cost, actual cost, the net-saving
81
+ ratio, the basis of the figures (measured, reference, estimated, shadow,
82
+ or unmeasured), the value, at the provider's cache-read price, of
83
+ input tokens your provider reported as cleared from context, and the
84
+ discount your provider's own prompt cache gave the request and the
85
+ cache-write premium it billed, which are shown apart and never used to
86
+ calculate the fees in Section 6.
87
+ Each batch of records also carries the Software's surface and version (for
88
+ example, "gateway/0.4.23"). BiVelio's ingest rejects any record that carries a
89
+ field not listed above. Separately, the Software fetches from BiVelio its current
90
+ license token, the signed license revocation list and, for the gateway, your
91
+ organization's feature entitlements; these requests carry your service-account
92
+ key where authentication is required, and no request data. Like any HTTPS
93
+ request, all of the above also reveals ordinary network metadata, such as your
94
+ IP address. If you enable the SDK's optional verification, which is off by
95
+ default, the SDK also sends BiVelio, through a separate channel and for each
96
+ request it samples: a label marking the record as a verification sample; the
97
+ request identifier and time; the model requested and the model that served it;
98
+ the cache outcome; the category of the request and the outcome of the check;
99
+ the input, cached-input, and output token counts and the cost of the served
100
+ request and of the unoptimized copy the SDK sent to your LLM provider; how
101
+ that copy finished and whether it met the response contract; and the per-token
102
+ prices applied; together with daily counts of requests per category and a
103
+ random batch identifier. That channel likewise rejects any field not listed
104
+ here, and none of it is used to calculate the fees in Section 6.
105
+ (c) BiVelio uses this data to operate licensing and revocation, show your
106
+ dashboard, count billable seats, and calculate the fees in Section 6. Each
107
+ field, and how long BiVelio keeps it, is further described at
108
+ https://savings.bivelio.com/privacy.
109
+
110
+ 6. FEES. Your subscription is subject to BiVelio's pricing as published at
111
+ https://savings.bivelio.com/pricing (the "Published Pricing") for each billing
112
+ period:
113
+ (a) a subscription fee, per seat or for a plan that includes a number of seats,
114
+ payable whether or not the Software produces any saving; and (b) only if your
115
+ subscription is on a plan that the Published Pricing lists as carrying a
116
+ commission, a commission, at tiered marginal rates, charged only on measured
117
+ savings, meaning savings whose basis is labelled "measured" or "reference" in
118
+ your usage records (Section 5(b)(iv)). A "measured" saving is established from
119
+ your LLM provider's own usage counter for that request; a "reference" saving, for
120
+ a repeated request the Software served without calling your provider, from your
121
+ provider's counter for the identical earlier request that did run. Both are
122
+ valued at the per-token prices the Software applied to that request: those of a
123
+ cost model you configure, where you configure one, and otherwise your provider's
124
+ list prices in the price catalogue included in the Software. Savings labelled
125
+ "estimated", "shadow", or "unmeasured" never carry commission. Traffic the
126
+ Software identifies as a flat-rate provider subscription (a Claude or ChatGPT
127
+ subscription used through the local gateway) is metered but credited no measured
128
+ saving, so it carries no commission. A period with no measured savings carries no
129
+ commission. The first calendar month (UTC) in which your organization reports any
130
+ usage is a calibration period, however little of that month remains, and carries
131
+ no commission. Nor is commission charged for a period that BiVelio closes while
132
+ your account is exempt from it. Your account is exempt if BiVelio grants it an
133
+ exemption and, until a payment is made on the account, if its subscription was
134
+ activated with a promotional code that waives the subscription fee in full for
135
+ its entire term. For a calibration or exempt period the statement still shows the
136
+ commission that would have applied. Each period's commission is
137
+ itemized on a statement in your dashboard, together with the exchange rate
138
+ applied. Rates, tier thresholds, and currency are those of the Published Pricing
139
+ and are not restated here.
140
+
141
+ 7. TERM AND TERMINATION. This Agreement remains in effect while your subscription is
142
+ active. BiVelio may terminate this Agreement immediately if you breach it. Upon
143
+ expiry or termination you must cease all use of the Software and destroy your
144
+ copies. Sections 2 through 10 survive termination.
145
+
146
+ 8. DISCLAIMER OF WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
147
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
148
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
149
+
150
+ 9. LIMITATION OF LIABILITY. TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL
151
+ BIVELIO INC. BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR
152
+ EXEMPLARY DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, OR DATA, ARISING FROM OR
153
+ RELATED TO THE SOFTWARE OR THIS AGREEMENT. BIVELIO'S TOTAL LIABILITY SHALL NOT
154
+ EXCEED THE FEES YOU PAID FOR THE SOFTWARE IN THE TWELVE MONTHS PRECEDING THE CLAIM.
155
+
156
+ 10. GENERAL. This Agreement is governed by the laws of the State of Delaware, USA,
157
+ without regard to its conflict-of-laws rules. If any provision is held
158
+ unenforceable, the remaining provisions remain in effect. This Agreement, together
159
+ with the applicable BiVelio subscription terms, constitutes the entire agreement
160
+ between you and BiVelio regarding the Software. Unauthorized use, reproduction, or
161
+ distribution of the Software may result in civil and criminal penalties.
162
+
163
+ For licensing and compliance inquiries, contact: BiVelio Inc. — privacy@bivelio.com
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: bivelio-savings-layer
3
+ Version: 0.1.0
4
+ Summary: Python client for the BiVelio Savings Layer gateway: route the official OpenAI and Anthropic SDKs through your local bvsala gateway and get each request's report, measured with your provider's own usage counter.
5
+ Project-URL: Homepage, https://savings.bivelio.com
6
+ Project-URL: Pricing, https://savings.bivelio.com/pricing
7
+ Project-URL: Privacy, https://savings.bivelio.com/privacy
8
+ Project-URL: Gateway (npm), https://www.npmjs.com/package/@bivelio/savings-layer
9
+ Author-email: "BiVelio Inc." <support@bivelio.com>
10
+ License-Expression: LicenseRef-BiVelio-Commercial
11
+ License-File: LICENSE
12
+ Keywords: anthropic,cost-optimization,llm,llm-costs,openai,token-accounting
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+
28
+ # BiVelio Savings Layer for Python
29
+
30
+ A thin client for the local **bvsala gateway**. It routes the official OpenAI
31
+ and Anthropic Python SDKs through the gateway and hands you, for each request,
32
+ the report the gateway recorded for your BiVelio dashboard: the provider's own
33
+ token counts, the cost at the model's list price, and where the figure comes
34
+ from (`cost.basis`).
35
+
36
+ - **The gateway does the work.** It ships in the npm package
37
+ [`@bivelio/savings-layer`](https://www.npmjs.com/package/@bivelio/savings-layer),
38
+ holds your BiVelio license, forwards each request to your provider and
39
+ measures it with the provider's usage counter. This package prices nothing
40
+ itself, so its numbers are exactly the dashboard's.
41
+ - **Fail-closed.** Without a running gateway and a live license, `wrap()`
42
+ raises. It never falls back to calling your provider directly.
43
+ - **No dependencies.** Standard library only. It does not import the OpenAI or
44
+ Anthropic SDKs; you bring them.
45
+ - **Your prompts never reach BiVelio.** The client only talks to the gateway on
46
+ `127.0.0.1`, ignoring any HTTP proxy settings, and the gateway forwards your
47
+ requests to your provider. What it sends to BiVelio is per-request usage
48
+ (token counts, cost, the model id and opaque ids), never your content: the
49
+ LICENSE, section 5, lists every field.
50
+
51
+ This package measures. The saving levers of the TypeScript SDK do not run in
52
+ Python; the gateway's own levers apply to Anthropic `/v1/messages` traffic with
53
+ a PRO license.
54
+
55
+ ## Requirements
56
+
57
+ - Python 3.10 or newer.
58
+ - Node.js 20 or newer, for the gateway, and a BiVelio license with a service key
59
+ (`BIVELIO_SERVICE_KEY`): the gateway does not start without them.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install bivelio-savings-layer
65
+ ```
66
+
67
+ The package is `bivelio-savings-layer` on PyPI and `bvsala` in your code
68
+ (`import bvsala`). From a checkout of the repository, to try unreleased changes:
69
+
70
+ ```bash
71
+ pip install ./packages/python
72
+ ```
73
+
74
+ ## Start a gateway
75
+
76
+ One gateway per provider:
77
+
78
+ ```bash
79
+ export BIVELIO_SERVICE_KEY=…
80
+ npx -p @bivelio/savings-layer bvsala gateway # Anthropic, on 8402
81
+ npx -p @bivelio/savings-layer bvsala gateway --upstream openai --port 8403 # OpenAI, on 8403
82
+ python -m bvsala --url http://127.0.0.1:8403 status # version, upstream, license
83
+ ```
84
+
85
+ ## Use it
86
+
87
+ ```python
88
+ import bvsala
89
+ from openai import OpenAI
90
+
91
+ gw = bvsala.Gateway("http://127.0.0.1:8403")
92
+ client = gw.wrap(OpenAI()) # a copy that goes through the gateway; OpenAI() is untouched
93
+
94
+ resp = client.chat.completions.create(
95
+ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}],
96
+ )
97
+ report = gw.report(resp)
98
+ print(report.summary()) # gpt-4o-mini · measured · $0.000024 (in 100, cached 40, out 20)
99
+ print(report.cost.actual, report.cost.basis, report.tokens.actual_output)
100
+ ```
101
+
102
+ Streaming works the same way. For OpenAI Chat Completions, ask for usage in the
103
+ request, or the provider sends no counter and the report says so:
104
+
105
+ ```python
106
+ stream = client.chat.completions.create(
107
+ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}],
108
+ stream=True, stream_options={"include_usage": True},
109
+ )
110
+ for chunk in stream:
111
+ if chunk.choices:
112
+ print(chunk.choices[0].delta.content or "", end="")
113
+ print(gw.report(stream).summary())
114
+ ```
115
+
116
+ Anthropic:
117
+
118
+ ```python
119
+ from anthropic import Anthropic
120
+
121
+ gw = bvsala.Gateway() # $BVSALA_GATEWAY_URL or http://127.0.0.1:8402
122
+ claude = gw.wrap(Anthropic())
123
+ msg = claude.messages.create(model="claude-haiku-4-5", max_tokens=256,
124
+ messages=[{"role": "user", "content": "Hi"}])
125
+ print(gw.report(msg).summary())
126
+ ```
127
+
128
+ `gw.report()` accepts a response, a stream you have read, any chunk or event of
129
+ it, a raw response from `with_raw_response`, or an id string. It also reads a
130
+ LiteLLM response sent with `extra_headers=bvsala.REPORT_HEADERS`, from the
131
+ provider headers LiteLLM keeps in `_hidden_params` (checked against LiteLLM's
132
+ source, not yet end to end).
133
+
134
+ `gw.report()` waits up to `wait` seconds (5 by default) for a request still in
135
+ flight; after that the report comes back with `status == "pending"`. In async
136
+ code use `await gw.areport(resp)`; `wrap()` also takes `AsyncOpenAI`,
137
+ `AzureOpenAI` and `AsyncAnthropic`.
138
+
139
+ `gw.totals()` adds up the reports this process fetched. Savings stay split by
140
+ basis (`measured`, `reference`, `estimated`), never added into one figure.
141
+
142
+ ### Any other client
143
+
144
+ The gateway side works with any HTTP client: send `x-bvsala-report: 1`, read
145
+ the `x-bvsala-request-id` response header, then
146
+ `GET /_bvsala/v1/reports/<id>` on the gateway. `bvsala.REPORT_HEADERS` is that
147
+ header as a dict, for `extra_headers=` or `default_headers=`.
148
+
149
+ ## Errors
150
+
151
+ | Exception | When |
152
+ |---|---|
153
+ | `GatewayUnavailable` | nothing answers at the URL, or it is not a bvsala gateway with local reports (0.4.26 or later is needed) |
154
+ | `LicenseInactive` | the gateway's license does not serve new requests; `.state` says why |
155
+ | `GatewayMismatch` | the client points at a different provider than the one the gateway forwards to |
156
+ | `ReportNotFound` | the gateway holds no report for that id (only requests sent with `x-bvsala-report: 1` have one, kept for at most an hour) |
157
+
158
+ ## License
159
+
160
+ Commercial software, © 2026 BiVelio Inc. Free to install, paid to run: see
161
+ [LICENSE](./LICENSE) and [savings.bivelio.com](https://savings.bivelio.com).
@@ -0,0 +1,134 @@
1
+ # BiVelio Savings Layer for Python
2
+
3
+ A thin client for the local **bvsala gateway**. It routes the official OpenAI
4
+ and Anthropic Python SDKs through the gateway and hands you, for each request,
5
+ the report the gateway recorded for your BiVelio dashboard: the provider's own
6
+ token counts, the cost at the model's list price, and where the figure comes
7
+ from (`cost.basis`).
8
+
9
+ - **The gateway does the work.** It ships in the npm package
10
+ [`@bivelio/savings-layer`](https://www.npmjs.com/package/@bivelio/savings-layer),
11
+ holds your BiVelio license, forwards each request to your provider and
12
+ measures it with the provider's usage counter. This package prices nothing
13
+ itself, so its numbers are exactly the dashboard's.
14
+ - **Fail-closed.** Without a running gateway and a live license, `wrap()`
15
+ raises. It never falls back to calling your provider directly.
16
+ - **No dependencies.** Standard library only. It does not import the OpenAI or
17
+ Anthropic SDKs; you bring them.
18
+ - **Your prompts never reach BiVelio.** The client only talks to the gateway on
19
+ `127.0.0.1`, ignoring any HTTP proxy settings, and the gateway forwards your
20
+ requests to your provider. What it sends to BiVelio is per-request usage
21
+ (token counts, cost, the model id and opaque ids), never your content: the
22
+ LICENSE, section 5, lists every field.
23
+
24
+ This package measures. The saving levers of the TypeScript SDK do not run in
25
+ Python; the gateway's own levers apply to Anthropic `/v1/messages` traffic with
26
+ a PRO license.
27
+
28
+ ## Requirements
29
+
30
+ - Python 3.10 or newer.
31
+ - Node.js 20 or newer, for the gateway, and a BiVelio license with a service key
32
+ (`BIVELIO_SERVICE_KEY`): the gateway does not start without them.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install bivelio-savings-layer
38
+ ```
39
+
40
+ The package is `bivelio-savings-layer` on PyPI and `bvsala` in your code
41
+ (`import bvsala`). From a checkout of the repository, to try unreleased changes:
42
+
43
+ ```bash
44
+ pip install ./packages/python
45
+ ```
46
+
47
+ ## Start a gateway
48
+
49
+ One gateway per provider:
50
+
51
+ ```bash
52
+ export BIVELIO_SERVICE_KEY=…
53
+ npx -p @bivelio/savings-layer bvsala gateway # Anthropic, on 8402
54
+ npx -p @bivelio/savings-layer bvsala gateway --upstream openai --port 8403 # OpenAI, on 8403
55
+ python -m bvsala --url http://127.0.0.1:8403 status # version, upstream, license
56
+ ```
57
+
58
+ ## Use it
59
+
60
+ ```python
61
+ import bvsala
62
+ from openai import OpenAI
63
+
64
+ gw = bvsala.Gateway("http://127.0.0.1:8403")
65
+ client = gw.wrap(OpenAI()) # a copy that goes through the gateway; OpenAI() is untouched
66
+
67
+ resp = client.chat.completions.create(
68
+ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}],
69
+ )
70
+ report = gw.report(resp)
71
+ print(report.summary()) # gpt-4o-mini · measured · $0.000024 (in 100, cached 40, out 20)
72
+ print(report.cost.actual, report.cost.basis, report.tokens.actual_output)
73
+ ```
74
+
75
+ Streaming works the same way. For OpenAI Chat Completions, ask for usage in the
76
+ request, or the provider sends no counter and the report says so:
77
+
78
+ ```python
79
+ stream = client.chat.completions.create(
80
+ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}],
81
+ stream=True, stream_options={"include_usage": True},
82
+ )
83
+ for chunk in stream:
84
+ if chunk.choices:
85
+ print(chunk.choices[0].delta.content or "", end="")
86
+ print(gw.report(stream).summary())
87
+ ```
88
+
89
+ Anthropic:
90
+
91
+ ```python
92
+ from anthropic import Anthropic
93
+
94
+ gw = bvsala.Gateway() # $BVSALA_GATEWAY_URL or http://127.0.0.1:8402
95
+ claude = gw.wrap(Anthropic())
96
+ msg = claude.messages.create(model="claude-haiku-4-5", max_tokens=256,
97
+ messages=[{"role": "user", "content": "Hi"}])
98
+ print(gw.report(msg).summary())
99
+ ```
100
+
101
+ `gw.report()` accepts a response, a stream you have read, any chunk or event of
102
+ it, a raw response from `with_raw_response`, or an id string. It also reads a
103
+ LiteLLM response sent with `extra_headers=bvsala.REPORT_HEADERS`, from the
104
+ provider headers LiteLLM keeps in `_hidden_params` (checked against LiteLLM's
105
+ source, not yet end to end).
106
+
107
+ `gw.report()` waits up to `wait` seconds (5 by default) for a request still in
108
+ flight; after that the report comes back with `status == "pending"`. In async
109
+ code use `await gw.areport(resp)`; `wrap()` also takes `AsyncOpenAI`,
110
+ `AzureOpenAI` and `AsyncAnthropic`.
111
+
112
+ `gw.totals()` adds up the reports this process fetched. Savings stay split by
113
+ basis (`measured`, `reference`, `estimated`), never added into one figure.
114
+
115
+ ### Any other client
116
+
117
+ The gateway side works with any HTTP client: send `x-bvsala-report: 1`, read
118
+ the `x-bvsala-request-id` response header, then
119
+ `GET /_bvsala/v1/reports/<id>` on the gateway. `bvsala.REPORT_HEADERS` is that
120
+ header as a dict, for `extra_headers=` or `default_headers=`.
121
+
122
+ ## Errors
123
+
124
+ | Exception | When |
125
+ |---|---|
126
+ | `GatewayUnavailable` | nothing answers at the URL, or it is not a bvsala gateway with local reports (0.4.26 or later is needed) |
127
+ | `LicenseInactive` | the gateway's license does not serve new requests; `.state` says why |
128
+ | `GatewayMismatch` | the client points at a different provider than the one the gateway forwards to |
129
+ | `ReportNotFound` | the gateway holds no report for that id (only requests sent with `x-bvsala-report: 1` have one, kept for at most an hour) |
130
+
131
+ ## License
132
+
133
+ Commercial software, © 2026 BiVelio Inc. Free to install, paid to run: see
134
+ [LICENSE](./LICENSE) and [savings.bivelio.com](https://savings.bivelio.com).
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bivelio-savings-layer"
7
+ # La versión vive en UN sitio, src/bvsala/__init__.py (`__version__`), y hatch la
8
+ # lee de ahí: la que devuelve `bvsala.__version__` es la que sube a PyPI.
9
+ # Política: versión PROPIA, no la del paquete de npm (docs/PUBLICAR-PYPI.md). La
10
+ # etiqueta de la Release es `py-v<versión>` y publish-pypi.yml exige que coincida.
11
+ dynamic = ["version"]
12
+ description = "Python client for the BiVelio Savings Layer gateway: route the official OpenAI and Anthropic SDKs through your local bvsala gateway and get each request's report, measured with your provider's own usage counter."
13
+ readme = "README.md"
14
+ requires-python = ">=3.10"
15
+ # Software comercial: la LICENSE es la misma que la del paquete de npm, byte a
16
+ # byte (test/python-cliente.test.ts lo comprueba).
17
+ license = "LicenseRef-BiVelio-Commercial"
18
+ license-files = ["LICENSE"]
19
+ authors = [{ name = "BiVelio Inc.", email = "support@bivelio.com" }]
20
+ keywords = ["llm", "cost-optimization", "llm-costs", "token-accounting", "openai", "anthropic"]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Typing :: Typed",
34
+ ]
35
+ # Sin clasificador «License :: …»: con `license` como expresión SPDX (PEP 639)
36
+ # están prohibidos, y PyPI rechaza la subida si van los dos.
37
+ # Sin dependencias: solo la biblioteca estándar. Los SDK de OpenAI y Anthropic
38
+ # los trae el usuario; este paquete no los importa.
39
+ dependencies = []
40
+
41
+ # Sin «Repository»: el repositorio es privado y el enlace daría un 404 en PyPI.
42
+ [project.urls]
43
+ Homepage = "https://savings.bivelio.com"
44
+ Pricing = "https://savings.bivelio.com/pricing"
45
+ Privacy = "https://savings.bivelio.com/privacy"
46
+ "Gateway (npm)" = "https://www.npmjs.com/package/@bivelio/savings-layer"
47
+
48
+ [tool.hatch.version]
49
+ path = "src/bvsala/__init__.py"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/bvsala"]
53
+
54
+ [tool.hatch.build.targets.sdist]
55
+ include = ["src/bvsala", "tests", "README.md", "LICENSE", "pyproject.toml"]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
59
+ pythonpath = ["src"]
60
+ addopts = "-p no:cacheprovider"
@@ -0,0 +1,46 @@
1
+ """BiVelio Savings Layer for Python — a thin client for the local ``bvsala`` gateway.
2
+
3
+ The gateway (``bvsala gateway``, in the npm package ``@bivelio/savings-layer``)
4
+ holds the BiVelio license, forwards your requests to the provider and measures
5
+ each one with the provider's own usage counter. This package routes the
6
+ official OpenAI and Anthropic SDKs through it and hands you, per request, the
7
+ report the gateway recorded for your dashboard::
8
+
9
+ import bvsala
10
+ from openai import OpenAI
11
+
12
+ gw = bvsala.Gateway("http://127.0.0.1:8403") # bvsala gateway --upstream openai --port 8403
13
+ client = gw.wrap(OpenAI())
14
+ resp = client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}])
15
+ print(gw.report(resp).summary())
16
+
17
+ Commercial software, © 2026 BiVelio Inc. See LICENSE.
18
+ """
19
+
20
+ from ._errors import BvsalaError, GatewayMismatch, GatewayUnavailable, LicenseInactive, ReportNotFound
21
+ from ._gateway import DEFAULT_URL, ENV_URL, REPORT_HEADER, REPORT_HEADERS, Gateway
22
+ from ._keys import REQUEST_ID_HEADER
23
+ from ._report import Calls, Cost, GatewayStatus, Report, Tokens, Totals
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ __all__ = [
28
+ "BvsalaError",
29
+ "Calls",
30
+ "Cost",
31
+ "DEFAULT_URL",
32
+ "ENV_URL",
33
+ "Gateway",
34
+ "GatewayMismatch",
35
+ "GatewayStatus",
36
+ "GatewayUnavailable",
37
+ "LicenseInactive",
38
+ "REPORT_HEADER",
39
+ "REPORT_HEADERS",
40
+ "REQUEST_ID_HEADER",
41
+ "Report",
42
+ "ReportNotFound",
43
+ "Tokens",
44
+ "Totals",
45
+ "__version__",
46
+ ]
@@ -0,0 +1,66 @@
1
+ """``python -m bvsala``: comprobar el gateway y leer un informe desde la terminal.
2
+
3
+ Códigos de salida: 0 bien, 2 licencia sin servir, 3 gateway ausente o de otra
4
+ versión, 4 informe no encontrado, 1 cualquier otro error.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from typing import List, Optional
13
+
14
+ from ._errors import BvsalaError, GatewayUnavailable, LicenseInactive, ReportNotFound
15
+ from ._gateway import Gateway
16
+
17
+
18
+ def main(argv: Optional[List[str]] = None) -> int:
19
+ p = argparse.ArgumentParser(prog="python -m bvsala", description="Check a local bvsala gateway and read its per-request reports.")
20
+ p.add_argument("--url", help="gateway URL (default: $BVSALA_GATEWAY_URL or http://127.0.0.1:8402)")
21
+ sub = p.add_subparsers(dest="cmd", required=True)
22
+ sub.add_parser("status", help="print the gateway's version, upstream and license")
23
+ r = sub.add_parser("report", help="print the report of one request as JSON")
24
+ r.add_argument("id", help="the x-bvsala-request-id header, or the provider's response id")
25
+ r.add_argument("--wait", type=float, default=0.0, help="seconds to wait for a request still in flight")
26
+ a = p.parse_args(argv)
27
+
28
+ try:
29
+ gw = Gateway(a.url)
30
+ if a.cmd == "status":
31
+ st = gw.status()
32
+ print(f"bvsala gateway {st.version} at {gw.url}")
33
+ print(f" upstream: {st.upstream}")
34
+ vence = f", expires {st.license_expires_at}" if st.license_expires_at else ""
35
+ print(f" license: {st.license_state} ({st.license_plan or 'unknown plan'}{vence})")
36
+ print(f" metering: {'on' if st.metering else 'off (no BIVELIO_SERVICE_KEY)'}")
37
+ return 0 if st.serving else 2
38
+ informe = gw.report(a.id, wait=a.wait)
39
+ print(json.dumps(
40
+ {
41
+ "requestId": informe.request_id,
42
+ "responseId": informe.response_id,
43
+ "path": informe.path,
44
+ "status": informe.status,
45
+ "reason": informe.reason,
46
+ "report": informe.raw_report,
47
+ },
48
+ indent=2,
49
+ ))
50
+ return 0
51
+ except LicenseInactive as e:
52
+ print(f"license {e.state}: {e}", file=sys.stderr)
53
+ return 2
54
+ except GatewayUnavailable as e:
55
+ print(str(e), file=sys.stderr)
56
+ return 3
57
+ except ReportNotFound as e:
58
+ print(str(e), file=sys.stderr)
59
+ return 4
60
+ except (BvsalaError, ValueError) as e:
61
+ print(str(e), file=sys.stderr)
62
+ return 1
63
+
64
+
65
+ if __name__ == "__main__":
66
+ sys.exit(main())