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.
- bivelio_savings_layer-0.1.0/.gitignore +7 -0
- bivelio_savings_layer-0.1.0/LICENSE +163 -0
- bivelio_savings_layer-0.1.0/PKG-INFO +161 -0
- bivelio_savings_layer-0.1.0/README.md +134 -0
- bivelio_savings_layer-0.1.0/pyproject.toml +60 -0
- bivelio_savings_layer-0.1.0/src/bvsala/__init__.py +46 -0
- bivelio_savings_layer-0.1.0/src/bvsala/__main__.py +66 -0
- bivelio_savings_layer-0.1.0/src/bvsala/_errors.py +32 -0
- bivelio_savings_layer-0.1.0/src/bvsala/_gateway.py +282 -0
- bivelio_savings_layer-0.1.0/src/bvsala/_keys.py +85 -0
- bivelio_savings_layer-0.1.0/src/bvsala/_report.py +208 -0
- bivelio_savings_layer-0.1.0/src/bvsala/py.typed +0 -0
- bivelio_savings_layer-0.1.0/tests/conftest.py +212 -0
- bivelio_savings_layer-0.1.0/tests/e2e/escenario.py +100 -0
- bivelio_savings_layer-0.1.0/tests/test_cli.py +45 -0
- bivelio_savings_layer-0.1.0/tests/test_report.py +154 -0
- bivelio_savings_layer-0.1.0/tests/test_status.py +81 -0
- bivelio_savings_layer-0.1.0/tests/test_wrap.py +185 -0
|
@@ -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())
|