dot-inference 2.0.2__tar.gz → 2.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.
- {dot_inference-2.0.2 → dot_inference-2.0.3}/CHANGELOG.md +12 -1
- {dot_inference-2.0.2 → dot_inference-2.0.3}/PKG-INFO +2 -2
- {dot_inference-2.0.2 → dot_inference-2.0.3}/README.md +1 -1
- {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/ARCHITECTURE.md +1 -1
- {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/VERSIONING.md +3 -3
- {dot_inference-2.0.2 → dot_inference-2.0.3}/pyproject.toml +1 -1
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_openai_compat.py +27 -15
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_openai_compat.py +84 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/uv.lock +1 -1
- {dot_inference-2.0.2 → dot_inference-2.0.3}/.env.template +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/.gitignore +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/.gitlab-ci.yml +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/.pre-commit-config.yaml +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/.python-version +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/AUTHORS.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/CONTRIBUTING.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/DCO +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/LICENSE.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/DEVELOPMENT.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/PUBLISHING.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/demo.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/demo_streaming.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/doc_stream.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/scripts/test_all_models.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/README.md +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/__init__.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_client.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_cost_tracker.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_llm_registry.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_local_check.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_mistral_compat.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_rate_limit.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_retry.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_sync.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_types.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/mock.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/models.yaml +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/settings.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/__init__.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_cost_tracker.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_factories.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_llm_registry.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_local_check.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_mistral_compat.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_mistral_live.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_rate_limit.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_retry.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_settings.py +0 -0
- {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_types.py +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.3] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `LLMResponse.cost` and `StreamUsage.cost` no longer overstate calls that hit
|
|
15
|
+
the prompt cache: they are now the cost the provider bills when it reports
|
|
16
|
+
one (OpenRouter's `usage.cost`), instead of an estimate from the registry's
|
|
17
|
+
per-token prices that charged cached tokens at the full rate. The estimate
|
|
18
|
+
remains the fallback for providers that do not report a cost.
|
|
19
|
+
|
|
10
20
|
## [2.0.2] - 2026-08-27
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -62,6 +72,7 @@ this release.
|
|
|
62
72
|
- `LLMCallable` and `EmbeddingCallable` return a `Coroutine` rather than the
|
|
63
73
|
looser `Awaitable`, matching what `asyncio.run_coroutine_threadsafe` accepts.
|
|
64
74
|
|
|
65
|
-
[Unreleased]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference/-/compare/v2.0.
|
|
75
|
+
[Unreleased]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference/-/compare/v2.0.3...main
|
|
76
|
+
[2.0.3]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference/-/tags/v2.0.3
|
|
66
77
|
[2.0.2]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference/-/tags/v2.0.2
|
|
67
78
|
[2.0.0]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference/-/tags/v2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dot-inference
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Unified async Python library for LLM inference and embeddings across multiple providers
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-inference
|
|
@@ -313,7 +313,7 @@ package is covered, anything underscore-prefixed is internal and may change in
|
|
|
313
313
|
any release. Public names are never removed without a deprecation period.
|
|
314
314
|
|
|
315
315
|
```toml
|
|
316
|
-
dependencies = ["dot-inference>=
|
|
316
|
+
dependencies = ["dot-inference>=2.0,<3"]
|
|
317
317
|
```
|
|
318
318
|
|
|
319
319
|
See [docs/VERSIONING.md](docs/VERSIONING.md) for the full policy.
|
|
@@ -286,7 +286,7 @@ package is covered, anything underscore-prefixed is internal and may change in
|
|
|
286
286
|
any release. Public names are never removed without a deprecation period.
|
|
287
287
|
|
|
288
288
|
```toml
|
|
289
|
-
dependencies = ["dot-inference>=
|
|
289
|
+
dependencies = ["dot-inference>=2.0,<3"]
|
|
290
290
|
```
|
|
291
291
|
|
|
292
292
|
See [docs/VERSIONING.md](docs/VERSIONING.md) for the full policy.
|
|
@@ -164,7 +164,7 @@ sequenceDiagram
|
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
Compat->>Compat: Parse response +
|
|
167
|
+
Compat->>Compat: Parse response + cost (provider-reported, else from ResolvedModelConfig)
|
|
168
168
|
Compat-->>App: LLMResponse (content, cost, tokens)
|
|
169
169
|
```
|
|
170
170
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Versioning and stability
|
|
2
2
|
|
|
3
3
|
`dot-inference` follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
-
This page says what that actually covers, so `>=
|
|
4
|
+
This page says what that actually covers, so `>=2.0,<3` means something concrete.
|
|
5
5
|
|
|
6
6
|
## What is public
|
|
7
7
|
|
|
@@ -94,11 +94,11 @@ manual. Use them to validate a release without committing to it.
|
|
|
94
94
|
### From PyPI — the supported way
|
|
95
95
|
|
|
96
96
|
```toml
|
|
97
|
-
dependencies = ["dot-inference>=
|
|
97
|
+
dependencies = ["dot-inference>=2.0,<3"]
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
The upper bound is what makes the guarantees above useful: you get every fix and
|
|
101
|
-
feature of the
|
|
101
|
+
feature of the 2.x line, and never an unannounced break.
|
|
102
102
|
|
|
103
103
|
### From git — development only
|
|
104
104
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dot-inference"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.3"
|
|
4
4
|
description = "Unified async Python library for LLM inference and embeddings across multiple providers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Kannon For Deep Tech", email = "louis.letarnec@deepika.ai" }]
|
|
@@ -99,6 +99,19 @@ def _extract_cached_tokens(usage: Any) -> int | None:
|
|
|
99
99
|
return getattr(details, "cached_tokens", None)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def _reported_cost(usage: Any) -> float | None:
|
|
103
|
+
"""The request's cost as the provider billed it, if it reports one.
|
|
104
|
+
|
|
105
|
+
OpenRouter returns it as ``usage.cost`` (USD, cached tokens included) on
|
|
106
|
+
every response, streamed or not. Returns None when the provider does not
|
|
107
|
+
report it.
|
|
108
|
+
"""
|
|
109
|
+
cost = getattr(usage, "cost", None) if usage is not None else None
|
|
110
|
+
if isinstance(cost, bool) or not isinstance(cost, (int, float)):
|
|
111
|
+
return None
|
|
112
|
+
return float(cost)
|
|
113
|
+
|
|
114
|
+
|
|
102
115
|
class OpenAICompatClient(LLMClient):
|
|
103
116
|
"""LLM client for any OpenAI-compatible endpoint.
|
|
104
117
|
|
|
@@ -218,6 +231,18 @@ class OpenAICompatClient(LLMClient):
|
|
|
218
231
|
|
|
219
232
|
return create_kwargs, response_model
|
|
220
233
|
|
|
234
|
+
def _cost(self, usage: Any) -> float | None:
|
|
235
|
+
"""What the call cost: as the provider billed it when it says, else
|
|
236
|
+
estimated from the registry's per-token prices (None if unknown)."""
|
|
237
|
+
reported = _reported_cost(usage)
|
|
238
|
+
if reported is not None or self._model_config is None:
|
|
239
|
+
return reported
|
|
240
|
+
return compute_cost(
|
|
241
|
+
usage,
|
|
242
|
+
self._model_config.input_token_cost,
|
|
243
|
+
self._model_config.output_token_cost,
|
|
244
|
+
)
|
|
245
|
+
|
|
221
246
|
async def _create_with_retry(self, create_kwargs: dict[str, Any]) -> Any:
|
|
222
247
|
"""Call ``chat.completions.create`` with transient-error retry."""
|
|
223
248
|
for attempt in range(1, _MAX_ATTEMPTS + 1):
|
|
@@ -267,13 +292,7 @@ class OpenAICompatClient(LLMClient):
|
|
|
267
292
|
completion_tokens = response.usage.completion_tokens if response.usage else None
|
|
268
293
|
cached_tokens = _extract_cached_tokens(response.usage)
|
|
269
294
|
|
|
270
|
-
cost =
|
|
271
|
-
if self._model_config:
|
|
272
|
-
cost = compute_cost(
|
|
273
|
-
response.usage,
|
|
274
|
-
self._model_config.input_token_cost,
|
|
275
|
-
self._model_config.output_token_cost,
|
|
276
|
-
)
|
|
295
|
+
cost = self._cost(response.usage)
|
|
277
296
|
if response.usage:
|
|
278
297
|
LOGGER.debug(
|
|
279
298
|
"tokens: %s in (%s cached) / %s out, cost=$%s",
|
|
@@ -473,14 +492,7 @@ class OpenAICompatClient(LLMClient):
|
|
|
473
492
|
if inspect.isawaitable(result):
|
|
474
493
|
await result
|
|
475
494
|
|
|
476
|
-
|
|
477
|
-
cost = None
|
|
478
|
-
if self._model_config:
|
|
479
|
-
cost = compute_cost(
|
|
480
|
-
last_usage,
|
|
481
|
-
self._model_config.input_token_cost,
|
|
482
|
-
self._model_config.output_token_cost,
|
|
483
|
-
)
|
|
495
|
+
cost = self._cost(last_usage)
|
|
484
496
|
if prompt_tokens is not None:
|
|
485
497
|
LOGGER.debug(
|
|
486
498
|
"tokens: %s in (%s cached) / %s out, cost=$%s",
|
|
@@ -26,6 +26,7 @@ from dot_inference._types import (
|
|
|
26
26
|
ReasoningDelta,
|
|
27
27
|
Role,
|
|
28
28
|
StreamDone,
|
|
29
|
+
StreamUsage,
|
|
29
30
|
ToolDef,
|
|
30
31
|
)
|
|
31
32
|
|
|
@@ -751,3 +752,86 @@ async def test_cached_tokens_none_when_provider_omits_details():
|
|
|
751
752
|
client = OpenAICompatClient(model="test", base_url="http://test", client=mock_client)
|
|
752
753
|
response = await client.call([Message(role=Role.USER, content="Hi")])
|
|
753
754
|
assert response.cached_tokens is None
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
# --- cost: as the provider billed it, else estimated from the registry ---
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _priced_config():
|
|
761
|
+
return ResolvedModelConfig(
|
|
762
|
+
model_id="test",
|
|
763
|
+
provider="test",
|
|
764
|
+
api_model_name="test",
|
|
765
|
+
input_token_cost=0.001,
|
|
766
|
+
output_token_cost=0.002,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _usage(**cost):
|
|
771
|
+
return SimpleNamespace(
|
|
772
|
+
prompt_tokens=100, completion_tokens=10, prompt_tokens_details=None, **cost
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _mock_client_with_usage(usage):
|
|
777
|
+
mock_msg = MagicMock(content="ok", tool_calls=None)
|
|
778
|
+
mock_response = MagicMock(choices=[MagicMock(message=mock_msg)], usage=usage)
|
|
779
|
+
mock_client = MagicMock()
|
|
780
|
+
mock_client.chat.completions.create = AsyncMock(return_value=mock_response)
|
|
781
|
+
return mock_client
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@pytest.mark.asyncio
|
|
785
|
+
async def test_call_cost_is_the_reported_one():
|
|
786
|
+
"""OpenRouter bills `usage.cost` (cache included): it wins over the estimate."""
|
|
787
|
+
client = OpenAICompatClient(
|
|
788
|
+
model="test",
|
|
789
|
+
base_url="http://test",
|
|
790
|
+
client=_mock_client_with_usage(_usage(cost=0.0042)),
|
|
791
|
+
model_config=_priced_config(),
|
|
792
|
+
)
|
|
793
|
+
response = await client.call([Message(role=Role.USER, content="Hi")])
|
|
794
|
+
assert response.cost == 0.0042
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
@pytest.mark.asyncio
|
|
798
|
+
async def test_call_cost_falls_back_to_the_registry_prices():
|
|
799
|
+
client = OpenAICompatClient(
|
|
800
|
+
model="test",
|
|
801
|
+
base_url="http://test",
|
|
802
|
+
client=_mock_client_with_usage(_usage()),
|
|
803
|
+
model_config=_priced_config(),
|
|
804
|
+
)
|
|
805
|
+
response = await client.call([Message(role=Role.USER, content="Hi")])
|
|
806
|
+
assert response.cost == 100 * 0.001 + 10 * 0.002
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@pytest.mark.asyncio
|
|
810
|
+
async def test_call_cost_is_reported_without_registry_prices():
|
|
811
|
+
client = OpenAICompatClient(
|
|
812
|
+
model="test",
|
|
813
|
+
base_url="http://test",
|
|
814
|
+
client=_mock_client_with_usage(_usage(cost=0.0042)),
|
|
815
|
+
)
|
|
816
|
+
response = await client.call([Message(role=Role.USER, content="Hi")])
|
|
817
|
+
assert response.cost == 0.0042
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
@pytest.mark.asyncio
|
|
821
|
+
async def test_stream_cost_is_the_reported_one():
|
|
822
|
+
"""The reported cost rides the last chunk, with the usage."""
|
|
823
|
+
chunks = [
|
|
824
|
+
_stream_chunk(content="hi", finish_reason="stop"),
|
|
825
|
+
SimpleNamespace(usage=_usage(cost=0.0042), choices=[]),
|
|
826
|
+
]
|
|
827
|
+
client = OpenAICompatClient(
|
|
828
|
+
model="test",
|
|
829
|
+
base_url="http://test",
|
|
830
|
+
client=_make_mock_stream_client(chunks),
|
|
831
|
+
model_config=_priced_config(),
|
|
832
|
+
)
|
|
833
|
+
events = await _collect(client)
|
|
834
|
+
|
|
835
|
+
[usage] = [e for e in events if isinstance(e, StreamUsage)]
|
|
836
|
+
assert usage.cost == 0.0042
|
|
837
|
+
assert events[-1].response.cost == 0.0042
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|