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.
Files changed (49) hide show
  1. {dot_inference-2.0.2 → dot_inference-2.0.3}/CHANGELOG.md +12 -1
  2. {dot_inference-2.0.2 → dot_inference-2.0.3}/PKG-INFO +2 -2
  3. {dot_inference-2.0.2 → dot_inference-2.0.3}/README.md +1 -1
  4. {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/ARCHITECTURE.md +1 -1
  5. {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/VERSIONING.md +3 -3
  6. {dot_inference-2.0.2 → dot_inference-2.0.3}/pyproject.toml +1 -1
  7. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_openai_compat.py +27 -15
  8. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_openai_compat.py +84 -0
  9. {dot_inference-2.0.2 → dot_inference-2.0.3}/uv.lock +1 -1
  10. {dot_inference-2.0.2 → dot_inference-2.0.3}/.env.template +0 -0
  11. {dot_inference-2.0.2 → dot_inference-2.0.3}/.gitignore +0 -0
  12. {dot_inference-2.0.2 → dot_inference-2.0.3}/.gitlab-ci.yml +0 -0
  13. {dot_inference-2.0.2 → dot_inference-2.0.3}/.pre-commit-config.yaml +0 -0
  14. {dot_inference-2.0.2 → dot_inference-2.0.3}/.python-version +0 -0
  15. {dot_inference-2.0.2 → dot_inference-2.0.3}/AUTHORS.md +0 -0
  16. {dot_inference-2.0.2 → dot_inference-2.0.3}/CONTRIBUTING.md +0 -0
  17. {dot_inference-2.0.2 → dot_inference-2.0.3}/DCO +0 -0
  18. {dot_inference-2.0.2 → dot_inference-2.0.3}/LICENSE.md +0 -0
  19. {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/DEVELOPMENT.md +0 -0
  20. {dot_inference-2.0.2 → dot_inference-2.0.3}/docs/PUBLISHING.md +0 -0
  21. {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/demo.py +0 -0
  22. {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/demo_streaming.py +0 -0
  23. {dot_inference-2.0.2 → dot_inference-2.0.3}/examples/doc_stream.md +0 -0
  24. {dot_inference-2.0.2 → dot_inference-2.0.3}/scripts/test_all_models.py +0 -0
  25. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/README.md +0 -0
  26. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/__init__.py +0 -0
  27. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_client.py +0 -0
  28. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_cost_tracker.py +0 -0
  29. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_llm_registry.py +0 -0
  30. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_local_check.py +0 -0
  31. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_mistral_compat.py +0 -0
  32. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_rate_limit.py +0 -0
  33. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_retry.py +0 -0
  34. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_sync.py +0 -0
  35. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/_types.py +0 -0
  36. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/mock.py +0 -0
  37. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/models.yaml +0 -0
  38. {dot_inference-2.0.2 → dot_inference-2.0.3}/src/dot_inference/settings.py +0 -0
  39. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/__init__.py +0 -0
  40. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_cost_tracker.py +0 -0
  41. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_factories.py +0 -0
  42. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_llm_registry.py +0 -0
  43. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_local_check.py +0 -0
  44. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_mistral_compat.py +0 -0
  45. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_mistral_live.py +0 -0
  46. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_rate_limit.py +0 -0
  47. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_retry.py +0 -0
  48. {dot_inference-2.0.2 → dot_inference-2.0.3}/tests/test_settings.py +0 -0
  49. {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.2...main
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.2
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>=1.0,<2"]
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>=1.0,<2"]
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 + compute cost from ResolvedModelConfig
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 `>=1.0,<2` means something concrete.
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>=1.0,<2"]
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 1.x line, and never an unannounced break.
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.2"
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 = None
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
- # Cost
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
@@ -119,7 +119,7 @@ wheels = [
119
119
 
120
120
  [[package]]
121
121
  name = "dot-inference"
122
- version = "2.0.2"
122
+ version = "2.0.3"
123
123
  source = { editable = "." }
124
124
  dependencies = [
125
125
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes