vantage-litellm-callback 0.0.3__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.
- vantage_litellm_callback-0.0.3/PKG-INFO +100 -0
- vantage_litellm_callback-0.0.3/README.md +91 -0
- vantage_litellm_callback-0.0.3/pyproject.toml +25 -0
- vantage_litellm_callback-0.0.3/setup.cfg +4 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/metadata_tags.json +52 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/anthropic_calculate_usage.json +25 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/bedrock_converse.json +25 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/ocr_non_token.json +20 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/openai_chat_cache.json +19 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/openai_multimodal.json +18 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/openai_multimodal_cache.json +18 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/openai_zero_modality_cache.json +19 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/responses_api_cache.json +19 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/responses_api_cache_no_text_detail.json +18 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/streaming/anthropic_messages.json +16 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/streaming/bedrock_converse.json +32 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/streaming/openai_chat.json +32 -0
- vantage_litellm_callback-0.0.3/tests/fixtures/streaming/responses_api.json +24 -0
- vantage_litellm_callback-0.0.3/tests/test_callback.py +1135 -0
- vantage_litellm_callback-0.0.3/tests/test_provider_matrix.py +247 -0
- vantage_litellm_callback-0.0.3/tests/test_provider_metadata.py +157 -0
- vantage_litellm_callback-0.0.3/vantage_callback.py +3 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/__init__.py +11 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/callback.py +361 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/client.py +185 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/delivery.py +357 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/projection.py +598 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/provider_metadata.py +173 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback/schema.py +104 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/PKG-INFO +100 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/SOURCES.txt +34 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/dependency_links.txt +1 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/requires.txt +2 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/scm_file_list.json +31 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/scm_version.json +8 -0
- vantage_litellm_callback-0.0.3/vantage_litellm_callback.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vantage-litellm-callback
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Durable Vantage lifecycle callback for LiteLLM
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: litellm
|
|
8
|
+
Requires-Dist: pydantic>=2
|
|
9
|
+
|
|
10
|
+
# Vantage LiteLLM Callback
|
|
11
|
+
|
|
12
|
+
Install a released version with `pip install vantage-litellm-callback`. To work
|
|
13
|
+
from a source checkout, install this directory with `pip install .`. Configure
|
|
14
|
+
LiteLLM with:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
litellm_settings:
|
|
18
|
+
callbacks:
|
|
19
|
+
- vantage_callback.callback_instance
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
See the repository README for collector setup and lifecycle billing semantics.
|
|
23
|
+
|
|
24
|
+
The callback is fail-open: collector outages, acknowledgement failures, malformed
|
|
25
|
+
responses, queue pressure, and projection errors never block or fail a provider
|
|
26
|
+
request. Usage events are placed into a bounded in-process queue and delivered by
|
|
27
|
+
a background worker. When delivery is unhealthy, the worker opens a circuit,
|
|
28
|
+
aggregates bounded loss information, and periodically probes the collector with a
|
|
29
|
+
versioned `delivery_gap` control frame. Normal delivery resumes only after that
|
|
30
|
+
frame receives a durable acknowledgement.
|
|
31
|
+
|
|
32
|
+
Configuration:
|
|
33
|
+
|
|
34
|
+
- `VANTAGE_COLLECTOR_DELIVERY_QUEUE_SIZE` (default `1024`)
|
|
35
|
+
- `VANTAGE_COLLECTOR_RECOVERY_PROBE_SECONDS` (default `5`)
|
|
36
|
+
- `VANTAGE_COLLECTOR_GAP_EVENT_ID_SAMPLE_SIZE` (default `32`)
|
|
37
|
+
- `VANTAGE_COLLECTOR_GAP_SUMMARY_INTERVAL_SECONDS` (default `60`)
|
|
38
|
+
- `VANTAGE_COLLECTOR_SOCKET_PATH` (default
|
|
39
|
+
`/var/run/vantage-collector/collector.sock`)
|
|
40
|
+
- `VANTAGE_COLLECTOR_ACK_TIMEOUT_SECONDS` (default `5`)
|
|
41
|
+
- `VANTAGE_COLLECTOR_CONNECTION_POOL_SIZE` (default `16`)
|
|
42
|
+
|
|
43
|
+
The queue and gap aggregate are intentionally process-local and bounded. A
|
|
44
|
+
process exit may therefore lose queued events; the gap frame reports losses
|
|
45
|
+
observed while the process remains alive.
|
|
46
|
+
|
|
47
|
+
## Running the provider matrix locally
|
|
48
|
+
|
|
49
|
+
`tests/test_provider_matrix.py` is a regression matrix over the provider and
|
|
50
|
+
mode combinations whose usage shapes disagree with one another. Every cell is a
|
|
51
|
+
recorded fixture, so the matrix needs no API keys and runs on every pull
|
|
52
|
+
request:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install --editable . pytest pytest-asyncio
|
|
56
|
+
PYTHONPATH=. pytest tests/test_provider_matrix.py -v
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run a single cell while iterating on a provider:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
PYTHONPATH=. pytest tests/test_provider_matrix.py -k anthropic
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Fixtures live in `tests/fixtures/`, one JSON file per cell:
|
|
66
|
+
|
|
67
|
+
| Directory | Cells |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `nonstreaming/` | OpenAI chat + cache, OpenAI multimodal, Anthropic post-`calculate_usage()`, Bedrock converse, Responses API + cache (with and without a `text_tokens` detail), OCR / non-token |
|
|
70
|
+
| `streaming/` | OpenAI SSE, Anthropic `/v1/messages` SSE, Bedrock converse, Responses API `response.completed` |
|
|
71
|
+
|
|
72
|
+
Each file carries its `usage` payload, the `expected_usage` projection, the
|
|
73
|
+
`expected_billable_total`, and a `note` citing the LiteLLM transform the shape
|
|
74
|
+
was taken from. Adding a provider means adding a JSON file — the tests
|
|
75
|
+
parametrize over the directory, so a new file becomes a new cell with no test
|
|
76
|
+
changes.
|
|
77
|
+
|
|
78
|
+
The matrix asserts three things per cell: the four projected `Usage` fields,
|
|
79
|
+
that those fields are mutually exclusive (they must sum to the billable total,
|
|
80
|
+
so no token is counted twice), and that the callback emits exactly one `started`
|
|
81
|
+
and one terminal event. Two further tests assert that nothing the callback
|
|
82
|
+
attaches to a request reaches the outbound provider body.
|
|
83
|
+
|
|
84
|
+
Two token-semantics rules are what the matrix exists to protect, both of which
|
|
85
|
+
have regressed before:
|
|
86
|
+
|
|
87
|
+
- **The input count is gross wherever the cache is reported nested.** LiteLLM's
|
|
88
|
+
transforms fold cache reads and writes into `prompt_tokens`, and the Responses
|
|
89
|
+
API counts `input_tokens_details.cached_tokens` inside `input_tokens`. Those
|
|
90
|
+
buckets have to be subtracted or the cached tokens bill twice. Raw Anthropic
|
|
91
|
+
usage is the exception: it puts `cache_*_input_tokens` beside a *net*
|
|
92
|
+
`input_tokens`, so nothing is subtracted there.
|
|
93
|
+
- **`text_tokens` is not "uncached input".** It means the text modality for
|
|
94
|
+
OpenAI but the net raw input for Anthropic. Reading it verbatim silently drops
|
|
95
|
+
image, video, and audio input tokens, so uncached input is derived from the
|
|
96
|
+
gross count instead.
|
|
97
|
+
|
|
98
|
+
For an end-to-end check against a live proxy on `:4000`, `demo/send_requests.sh`
|
|
99
|
+
sends tagged requests through LiteLLM; see `demo/README.md`. That path needs a
|
|
100
|
+
running proxy and collector and is a manual smoke test, not part of CI.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Vantage LiteLLM Callback
|
|
2
|
+
|
|
3
|
+
Install a released version with `pip install vantage-litellm-callback`. To work
|
|
4
|
+
from a source checkout, install this directory with `pip install .`. Configure
|
|
5
|
+
LiteLLM with:
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
litellm_settings:
|
|
9
|
+
callbacks:
|
|
10
|
+
- vantage_callback.callback_instance
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See the repository README for collector setup and lifecycle billing semantics.
|
|
14
|
+
|
|
15
|
+
The callback is fail-open: collector outages, acknowledgement failures, malformed
|
|
16
|
+
responses, queue pressure, and projection errors never block or fail a provider
|
|
17
|
+
request. Usage events are placed into a bounded in-process queue and delivered by
|
|
18
|
+
a background worker. When delivery is unhealthy, the worker opens a circuit,
|
|
19
|
+
aggregates bounded loss information, and periodically probes the collector with a
|
|
20
|
+
versioned `delivery_gap` control frame. Normal delivery resumes only after that
|
|
21
|
+
frame receives a durable acknowledgement.
|
|
22
|
+
|
|
23
|
+
Configuration:
|
|
24
|
+
|
|
25
|
+
- `VANTAGE_COLLECTOR_DELIVERY_QUEUE_SIZE` (default `1024`)
|
|
26
|
+
- `VANTAGE_COLLECTOR_RECOVERY_PROBE_SECONDS` (default `5`)
|
|
27
|
+
- `VANTAGE_COLLECTOR_GAP_EVENT_ID_SAMPLE_SIZE` (default `32`)
|
|
28
|
+
- `VANTAGE_COLLECTOR_GAP_SUMMARY_INTERVAL_SECONDS` (default `60`)
|
|
29
|
+
- `VANTAGE_COLLECTOR_SOCKET_PATH` (default
|
|
30
|
+
`/var/run/vantage-collector/collector.sock`)
|
|
31
|
+
- `VANTAGE_COLLECTOR_ACK_TIMEOUT_SECONDS` (default `5`)
|
|
32
|
+
- `VANTAGE_COLLECTOR_CONNECTION_POOL_SIZE` (default `16`)
|
|
33
|
+
|
|
34
|
+
The queue and gap aggregate are intentionally process-local and bounded. A
|
|
35
|
+
process exit may therefore lose queued events; the gap frame reports losses
|
|
36
|
+
observed while the process remains alive.
|
|
37
|
+
|
|
38
|
+
## Running the provider matrix locally
|
|
39
|
+
|
|
40
|
+
`tests/test_provider_matrix.py` is a regression matrix over the provider and
|
|
41
|
+
mode combinations whose usage shapes disagree with one another. Every cell is a
|
|
42
|
+
recorded fixture, so the matrix needs no API keys and runs on every pull
|
|
43
|
+
request:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pip install --editable . pytest pytest-asyncio
|
|
47
|
+
PYTHONPATH=. pytest tests/test_provider_matrix.py -v
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run a single cell while iterating on a provider:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
PYTHONPATH=. pytest tests/test_provider_matrix.py -k anthropic
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Fixtures live in `tests/fixtures/`, one JSON file per cell:
|
|
57
|
+
|
|
58
|
+
| Directory | Cells |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `nonstreaming/` | OpenAI chat + cache, OpenAI multimodal, Anthropic post-`calculate_usage()`, Bedrock converse, Responses API + cache (with and without a `text_tokens` detail), OCR / non-token |
|
|
61
|
+
| `streaming/` | OpenAI SSE, Anthropic `/v1/messages` SSE, Bedrock converse, Responses API `response.completed` |
|
|
62
|
+
|
|
63
|
+
Each file carries its `usage` payload, the `expected_usage` projection, the
|
|
64
|
+
`expected_billable_total`, and a `note` citing the LiteLLM transform the shape
|
|
65
|
+
was taken from. Adding a provider means adding a JSON file — the tests
|
|
66
|
+
parametrize over the directory, so a new file becomes a new cell with no test
|
|
67
|
+
changes.
|
|
68
|
+
|
|
69
|
+
The matrix asserts three things per cell: the four projected `Usage` fields,
|
|
70
|
+
that those fields are mutually exclusive (they must sum to the billable total,
|
|
71
|
+
so no token is counted twice), and that the callback emits exactly one `started`
|
|
72
|
+
and one terminal event. Two further tests assert that nothing the callback
|
|
73
|
+
attaches to a request reaches the outbound provider body.
|
|
74
|
+
|
|
75
|
+
Two token-semantics rules are what the matrix exists to protect, both of which
|
|
76
|
+
have regressed before:
|
|
77
|
+
|
|
78
|
+
- **The input count is gross wherever the cache is reported nested.** LiteLLM's
|
|
79
|
+
transforms fold cache reads and writes into `prompt_tokens`, and the Responses
|
|
80
|
+
API counts `input_tokens_details.cached_tokens` inside `input_tokens`. Those
|
|
81
|
+
buckets have to be subtracted or the cached tokens bill twice. Raw Anthropic
|
|
82
|
+
usage is the exception: it puts `cache_*_input_tokens` beside a *net*
|
|
83
|
+
`input_tokens`, so nothing is subtracted there.
|
|
84
|
+
- **`text_tokens` is not "uncached input".** It means the text modality for
|
|
85
|
+
OpenAI but the net raw input for Anthropic. Reading it verbatim silently drops
|
|
86
|
+
image, video, and audio input tokens, so uncached input is derived from the
|
|
87
|
+
gross count instead.
|
|
88
|
+
|
|
89
|
+
For an end-to-end check against a live proxy on `:4000`, `demo/send_requests.sh`
|
|
90
|
+
sends tagged requests through LiteLLM; see `demo/README.md`. That path needs a
|
|
91
|
+
running proxy and collector and is a manual smoke test, not part of CI.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "setuptools-scm[simple]>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vantage-litellm-callback"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Durable Vantage lifecycle callback for LiteLLM"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"litellm",
|
|
13
|
+
"pydantic>=2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
packages = ["vantage_litellm_callback"]
|
|
18
|
+
py-modules = ["vantage_callback"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools_scm]
|
|
21
|
+
root = ".."
|
|
22
|
+
local_scheme = "no-local-version"
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"request_data": {
|
|
3
|
+
"model": "logical-model",
|
|
4
|
+
"litellm_call_id": "request-metadata-tags",
|
|
5
|
+
"litellm_params": {
|
|
6
|
+
"model": "openai/gpt-4.1",
|
|
7
|
+
"custom_llm_provider": "openai",
|
|
8
|
+
"metadata": {
|
|
9
|
+
"tags": {
|
|
10
|
+
"team": "search",
|
|
11
|
+
"collision": "explicit-tag",
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"weight": 1.5,
|
|
14
|
+
"nested_tag": {
|
|
15
|
+
"drop": "this"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"spend_logs_metadata": {
|
|
19
|
+
"environment": "production",
|
|
20
|
+
"collision": "spend-log",
|
|
21
|
+
"retries": 2,
|
|
22
|
+
"nested_spend_metadata": [
|
|
23
|
+
"drop",
|
|
24
|
+
"this"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"collector_event": {
|
|
31
|
+
"event_id": "event-metadata-tags",
|
|
32
|
+
"request_id": "request-metadata-tags",
|
|
33
|
+
"timestamp": "2026-09-01T17:30:00Z",
|
|
34
|
+
"provider": "openai",
|
|
35
|
+
"model": "gpt-4.1",
|
|
36
|
+
"status": "success",
|
|
37
|
+
"usage": {
|
|
38
|
+
"non_cache_input_tokens": 0,
|
|
39
|
+
"output_tokens": 0,
|
|
40
|
+
"cache_read_input_tokens": 0,
|
|
41
|
+
"cache_write_input_tokens": 0
|
|
42
|
+
},
|
|
43
|
+
"tags": {
|
|
44
|
+
"team": "search",
|
|
45
|
+
"collision": "explicit-tag",
|
|
46
|
+
"enabled": true,
|
|
47
|
+
"weight": 1.5,
|
|
48
|
+
"environment": "production",
|
|
49
|
+
"retries": 2
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "anthropic",
|
|
3
|
+
"mode": "non-stream + cache",
|
|
4
|
+
"note": "Post-calculate_usage() Usage. LiteLLM inflates prompt_tokens to gross (raw input + cache read + cache creation); prompt_tokens_details.text_tokens is the net raw input. See litellm/llms/anthropic/chat/transformation.py:2189-2223.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 2000,
|
|
7
|
+
"completion_tokens": 150,
|
|
8
|
+
"total_tokens": 2150,
|
|
9
|
+
"prompt_tokens_details": {
|
|
10
|
+
"cached_tokens": 1500,
|
|
11
|
+
"text_tokens": 200,
|
|
12
|
+
"cache_write_tokens": 300,
|
|
13
|
+
"cache_creation_tokens": 300
|
|
14
|
+
},
|
|
15
|
+
"cache_creation_input_tokens": 300,
|
|
16
|
+
"cache_read_input_tokens": 1500
|
|
17
|
+
},
|
|
18
|
+
"expected_usage": {
|
|
19
|
+
"non_cache_input_tokens": 200,
|
|
20
|
+
"output_tokens": 150,
|
|
21
|
+
"cache_read_input_tokens": 1500,
|
|
22
|
+
"cache_write_input_tokens": 300
|
|
23
|
+
},
|
|
24
|
+
"expected_billable_total": 2150
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "bedrock",
|
|
3
|
+
"mode": "converse",
|
|
4
|
+
"note": "Post-transform Bedrock converse usage. Bedrock passes totalTokens through unchanged while prompt_tokens is inflated, so total_tokens (350) intentionally disagrees with prompt+completion. See litellm/llms/bedrock/chat/converse_transformation.py:1780-1790.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 2000,
|
|
7
|
+
"completion_tokens": 150,
|
|
8
|
+
"total_tokens": 350,
|
|
9
|
+
"prompt_tokens_details": {
|
|
10
|
+
"cached_tokens": 1500,
|
|
11
|
+
"text_tokens": 200,
|
|
12
|
+
"cache_write_tokens": 300,
|
|
13
|
+
"cache_creation_tokens": 300
|
|
14
|
+
},
|
|
15
|
+
"cache_creation_input_tokens": 300,
|
|
16
|
+
"cache_read_input_tokens": 1500
|
|
17
|
+
},
|
|
18
|
+
"expected_usage": {
|
|
19
|
+
"non_cache_input_tokens": 200,
|
|
20
|
+
"output_tokens": 150,
|
|
21
|
+
"cache_read_input_tokens": 1500,
|
|
22
|
+
"cache_write_input_tokens": 300
|
|
23
|
+
},
|
|
24
|
+
"expected_billable_total": 2150
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "mistral",
|
|
3
|
+
"mode": "ocr / non-token",
|
|
4
|
+
"note": "OCR spend-tracking usage carries page meters with zeroed token counts. The collector bills tokens only, so this must project to zeros rather than inventing usage. See litellm/proxy/spend_tracking/spend_tracking_utils.py:164.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 0,
|
|
7
|
+
"completion_tokens": 0,
|
|
8
|
+
"total_tokens": 0,
|
|
9
|
+
"pages_processed": 7,
|
|
10
|
+
"credits": null,
|
|
11
|
+
"doc_size_bytes": 284729
|
|
12
|
+
},
|
|
13
|
+
"expected_usage": {
|
|
14
|
+
"non_cache_input_tokens": 0,
|
|
15
|
+
"output_tokens": 0,
|
|
16
|
+
"cache_read_input_tokens": 0,
|
|
17
|
+
"cache_write_input_tokens": 0
|
|
18
|
+
},
|
|
19
|
+
"expected_billable_total": 0
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "non-stream + cache",
|
|
4
|
+
"note": "OpenAI chat completions. prompt_tokens is gross; cached_tokens is a subset of it.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 100,
|
|
7
|
+
"completion_tokens": 20,
|
|
8
|
+
"total_tokens": 120,
|
|
9
|
+
"prompt_tokens_details": {"cached_tokens": 30},
|
|
10
|
+
"cache_creation_input_tokens": 10
|
|
11
|
+
},
|
|
12
|
+
"expected_usage": {
|
|
13
|
+
"non_cache_input_tokens": 60,
|
|
14
|
+
"output_tokens": 20,
|
|
15
|
+
"cache_read_input_tokens": 30,
|
|
16
|
+
"cache_write_input_tokens": 10
|
|
17
|
+
},
|
|
18
|
+
"expected_billable_total": 120
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "non-stream multimodal",
|
|
4
|
+
"note": "Image input. prompt_tokens_details.text_tokens covers only the text modality, so billable input must include image_tokens rather than trusting text_tokens alone.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 100,
|
|
7
|
+
"completion_tokens": 20,
|
|
8
|
+
"total_tokens": 120,
|
|
9
|
+
"prompt_tokens_details": {"text_tokens": 40, "image_tokens": 60}
|
|
10
|
+
},
|
|
11
|
+
"expected_usage": {
|
|
12
|
+
"non_cache_input_tokens": 100,
|
|
13
|
+
"output_tokens": 20,
|
|
14
|
+
"cache_read_input_tokens": 0,
|
|
15
|
+
"cache_write_input_tokens": 0
|
|
16
|
+
},
|
|
17
|
+
"expected_billable_total": 120
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "non-stream multimodal + cache",
|
|
4
|
+
"note": "Image input on a cached request. LiteLLM bills the prompt detail buckets additively (text + cache_hit + audio + image + video, see litellm/litellm_core_utils/llm_cost_calc/utils.py:652-676), so text_tokens is already net of cached_tokens and the modality counts must not have the cache subtracted from them again.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 100,
|
|
7
|
+
"completion_tokens": 20,
|
|
8
|
+
"total_tokens": 120,
|
|
9
|
+
"prompt_tokens_details": {"text_tokens": 40, "image_tokens": 30, "cached_tokens": 30}
|
|
10
|
+
},
|
|
11
|
+
"expected_usage": {
|
|
12
|
+
"non_cache_input_tokens": 70,
|
|
13
|
+
"output_tokens": 20,
|
|
14
|
+
"cache_read_input_tokens": 30,
|
|
15
|
+
"cache_write_input_tokens": 0
|
|
16
|
+
},
|
|
17
|
+
"expected_billable_total": 120
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "non-stream text-only, zero modality detail",
|
|
4
|
+
"note": "A text-only cached request whose detail block still carries image_tokens: 0. Treating any present modality key as a multimodal breakdown projects the input as zero and under-bills by the whole uncached remainder, so only a positive modality count may select that path.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"prompt_tokens": 100,
|
|
7
|
+
"completion_tokens": 20,
|
|
8
|
+
"total_tokens": 120,
|
|
9
|
+
"prompt_tokens_details": {"image_tokens": 0, "cached_tokens": 30},
|
|
10
|
+
"cache_creation_input_tokens": 10
|
|
11
|
+
},
|
|
12
|
+
"expected_usage": {
|
|
13
|
+
"non_cache_input_tokens": 60,
|
|
14
|
+
"output_tokens": 20,
|
|
15
|
+
"cache_read_input_tokens": 30,
|
|
16
|
+
"cache_write_input_tokens": 10
|
|
17
|
+
},
|
|
18
|
+
"expected_billable_total": 120
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "responses api + cache",
|
|
4
|
+
"note": "Responses API usage. input_tokens is gross and cached_tokens is a subset of it, so cache must be subtracted or the same tokens bill twice. See litellm/types/llms/openai.py:1173-1186.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"input_tokens": 2000,
|
|
7
|
+
"input_tokens_details": {"cached_tokens": 1792, "text_tokens": 208},
|
|
8
|
+
"output_tokens": 350,
|
|
9
|
+
"output_tokens_details": {"reasoning_tokens": 256, "text_tokens": 94},
|
|
10
|
+
"total_tokens": 2350
|
|
11
|
+
},
|
|
12
|
+
"expected_usage": {
|
|
13
|
+
"non_cache_input_tokens": 208,
|
|
14
|
+
"output_tokens": 350,
|
|
15
|
+
"cache_read_input_tokens": 1792,
|
|
16
|
+
"cache_write_input_tokens": 0
|
|
17
|
+
},
|
|
18
|
+
"expected_billable_total": 2350
|
|
19
|
+
}
|
vantage_litellm_callback-0.0.3/tests/fixtures/nonstreaming/responses_api_cache_no_text_detail.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "openai",
|
|
3
|
+
"mode": "responses api + cache, no text detail",
|
|
4
|
+
"note": "InputTokensDetails.text_tokens is optional while cached_tokens defaults to 0, so a cached Responses API call can report only the cache subset. input_tokens stays gross, so the cache must still be subtracted; counting it on both sides billed 4142 tokens for this 2350-token request. See litellm/types/llms/openai.py:1173.",
|
|
5
|
+
"usage": {
|
|
6
|
+
"input_tokens": 2000,
|
|
7
|
+
"input_tokens_details": {"cached_tokens": 1792},
|
|
8
|
+
"output_tokens": 350,
|
|
9
|
+
"total_tokens": 2350
|
|
10
|
+
},
|
|
11
|
+
"expected_usage": {
|
|
12
|
+
"non_cache_input_tokens": 208,
|
|
13
|
+
"output_tokens": 350,
|
|
14
|
+
"cache_read_input_tokens": 1792,
|
|
15
|
+
"cache_write_input_tokens": 0
|
|
16
|
+
},
|
|
17
|
+
"expected_billable_total": 2350
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chunks": [
|
|
3
|
+
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_01XYZ\",\"model\":\"claude-sonnet-4-20250514\",\"usage\":{\"input_tokens\":60,\"cache_read_input_tokens\":30,\"cache_creation_input_tokens\":10,\"output_tokens\":2}}}\n\n",
|
|
4
|
+
"event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n",
|
|
5
|
+
"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\n",
|
|
6
|
+
"event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
|
7
|
+
"event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":20}}\n\n",
|
|
8
|
+
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
|
9
|
+
],
|
|
10
|
+
"expected_usage": {
|
|
11
|
+
"non_cache_input_tokens": 60,
|
|
12
|
+
"output_tokens": 20,
|
|
13
|
+
"cache_read_input_tokens": 30,
|
|
14
|
+
"cache_write_input_tokens": 10
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chunks": [
|
|
3
|
+
{
|
|
4
|
+
"id": "chatcmpl-bedrock-1",
|
|
5
|
+
"model": "anthropic.claude-sonnet-4-20250514-v1:0",
|
|
6
|
+
"choices": [{"delta": {"content": "Hello"}, "finish_reason": null}]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "chatcmpl-bedrock-1",
|
|
10
|
+
"model": "anthropic.claude-sonnet-4-20250514-v1:0",
|
|
11
|
+
"choices": [{"delta": {}, "finish_reason": "stop"}],
|
|
12
|
+
"usage": {
|
|
13
|
+
"prompt_tokens": 100,
|
|
14
|
+
"completion_tokens": 20,
|
|
15
|
+
"total_tokens": 120,
|
|
16
|
+
"prompt_tokens_details": {
|
|
17
|
+
"text_tokens": 60,
|
|
18
|
+
"cached_tokens": 30,
|
|
19
|
+
"cache_creation_tokens": 10
|
|
20
|
+
},
|
|
21
|
+
"cache_read_input_tokens": 30,
|
|
22
|
+
"cache_creation_input_tokens": 10
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"expected_usage": {
|
|
27
|
+
"non_cache_input_tokens": 60,
|
|
28
|
+
"output_tokens": 20,
|
|
29
|
+
"cache_read_input_tokens": 30,
|
|
30
|
+
"cache_write_input_tokens": 10
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chunks": [
|
|
3
|
+
{
|
|
4
|
+
"id": "chatcmpl-oai-1",
|
|
5
|
+
"model": "gpt-4.1",
|
|
6
|
+
"choices": [{"delta": {"content": "Hello"}, "finish_reason": null}]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "chatcmpl-oai-1",
|
|
10
|
+
"model": "gpt-4.1",
|
|
11
|
+
"choices": [{"delta": {"content": " world"}, "finish_reason": null}]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "chatcmpl-oai-1",
|
|
15
|
+
"model": "gpt-4.1",
|
|
16
|
+
"choices": [{"delta": {}, "finish_reason": "stop"}],
|
|
17
|
+
"usage": {
|
|
18
|
+
"prompt_tokens": 100,
|
|
19
|
+
"completion_tokens": 20,
|
|
20
|
+
"total_tokens": 120,
|
|
21
|
+
"prompt_tokens_details": {"cached_tokens": 30},
|
|
22
|
+
"cache_creation_input_tokens": 10
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"expected_usage": {
|
|
27
|
+
"non_cache_input_tokens": 60,
|
|
28
|
+
"output_tokens": 20,
|
|
29
|
+
"cache_read_input_tokens": 30,
|
|
30
|
+
"cache_write_input_tokens": 10
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chunks": [
|
|
3
|
+
{
|
|
4
|
+
"type": "response.created",
|
|
5
|
+
"response": {"id": "resp_1", "model": "gpt-4.1", "status": "in_progress"}
|
|
6
|
+
},
|
|
7
|
+
{"type": "response.output_text.delta", "delta": "Hello"},
|
|
8
|
+
{
|
|
9
|
+
"type": "response.completed",
|
|
10
|
+
"response": {
|
|
11
|
+
"id": "resp_1",
|
|
12
|
+
"model": "gpt-4.1",
|
|
13
|
+
"status": "completed",
|
|
14
|
+
"usage": {"input_tokens": 9, "output_tokens": 4, "total_tokens": 13}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"expected_usage": {
|
|
19
|
+
"non_cache_input_tokens": 9,
|
|
20
|
+
"output_tokens": 4,
|
|
21
|
+
"cache_read_input_tokens": 0,
|
|
22
|
+
"cache_write_input_tokens": 0
|
|
23
|
+
}
|
|
24
|
+
}
|