litellm-haystack 1.1.2__tar.gz → 2.0.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.
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/litellm-v1.2.0] - 2026-07-30
4
+
5
+ ### 🚀 Features
6
+
7
+ - *(litellm)* Include provider cache tokens in usage metadata (#3678)
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - Cap litellm dependency below 1.92.0 (#3601)
12
+
13
+ ### 🌀 Miscellaneous
14
+
15
+
3
16
  ## [integrations/litellm-v1.1.1] - 2026-07-15
4
17
 
5
18
  ### 🧹 Chores
@@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work.
58
58
 
59
59
  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
60
 
61
- Copyright [yyyy] [name of copyright owner]
61
+ Copyright 2023-present deepset GmbH
62
62
 
63
63
  Licensed under the Apache License, Version 2.0 (the "License");
64
64
  you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: litellm-haystack
3
- Version: 1.1.2
3
+ Version: 2.0.0
4
4
  Summary: An integration of LiteLLM AI gateway into the Haystack framework, routing to 100+ LLM providers.
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/litellm#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -12,13 +12,12 @@ Keywords: ai-gateway,haystack,litellm,llm
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Programming Language :: Python :: 3.14
20
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
22
21
  Requires-Dist: haystack-ai>=2.30.0
23
22
  Requires-Dist: litellm<1.92.0,>=1.84.0
24
23
  Description-Content-Type: text/markdown
@@ -7,7 +7,7 @@ name = "litellm-haystack"
7
7
  dynamic = ["version"]
8
8
  description = "An integration of LiteLLM AI gateway into the Haystack framework, routing to 100+ LLM providers."
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
12
12
  keywords = ["haystack", "litellm", "llm", "ai-gateway"]
13
13
  authors = [{ name = "deepset GmbH", email = "info@deepset.ai" }]
@@ -15,7 +15,6 @@ classifiers = [
15
15
  "License :: OSI Approved :: Apache Software License",
16
16
  "Development Status :: 4 - Beta",
17
17
  "Programming Language :: Python",
18
- "Programming Language :: Python :: 3.10",
19
18
  "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
@@ -25,7 +25,7 @@ from haystack.tools import (
25
25
  flatten_tools_or_toolsets,
26
26
  serialize_tools_or_toolset,
27
27
  )
28
- from haystack.utils.auth import Secret, deserialize_secrets_inplace
28
+ from haystack.utils.auth import Secret
29
29
  from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
30
30
 
31
31
  logger = logging.getLogger(__name__)
@@ -149,7 +149,9 @@ class LiteLLMChatGenerator:
149
149
  :param messages: Input messages as ChatMessage instances.
150
150
  If a string is provided, it is converted to a list containing a ChatMessage with user role.
151
151
  :param streaming_callback: Override the streaming callback for this call.
152
- :param generation_kwargs: Override generation parameters for this call.
152
+ :param generation_kwargs: Additional parameters for this call. These are merged per key with the
153
+ `generation_kwargs` passed at initialization: keys provided here take precedence, keys set only
154
+ at initialization are kept.
153
155
  :param tools: Override tools for this call.
154
156
  :returns: A dict with key ``replies`` containing ChatMessage instances.
155
157
  """
@@ -186,7 +188,9 @@ class LiteLLMChatGenerator:
186
188
  :param messages: Input messages as ChatMessage instances.
187
189
  If a string is provided, it is converted to a list containing a ChatMessage with user role.
188
190
  :param streaming_callback: Override the streaming callback for this call.
189
- :param generation_kwargs: Override generation parameters for this call.
191
+ :param generation_kwargs: Additional parameters for this call. These are merged per key with the
192
+ `generation_kwargs` passed at initialization: keys provided here take precedence, keys set only
193
+ at initialization are kept.
190
194
  :param tools: Override tools for this call.
191
195
  :returns: A dict with key ``replies`` containing ChatMessage instances.
192
196
  """
@@ -246,7 +250,6 @@ class LiteLLMChatGenerator:
246
250
  @classmethod
247
251
  def from_dict(cls, data: dict[str, Any]) -> "LiteLLMChatGenerator":
248
252
  """Deserialize a component from a dictionary."""
249
- deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
250
253
  deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
251
254
  if data["init_parameters"].get("streaming_callback"):
252
255
  data["init_parameters"]["streaming_callback"] = deserialize_callable(
@@ -266,16 +269,12 @@ _FINISH_REASON_MAPPING: dict[str, FinishReason] = {
266
269
  }
267
270
 
268
271
 
269
- def _extract_usage(obj: Any) -> dict[str, int]:
272
+ def _extract_usage(obj: Any) -> dict[str, Any]:
270
273
  """Pull token usage off a litellm response or chunk, tolerating its absence."""
271
274
  usage = getattr(obj, "usage", None)
272
- if not usage:
273
- return {}
274
- return {
275
- "prompt_tokens": getattr(usage, "prompt_tokens", 0),
276
- "completion_tokens": getattr(usage, "completion_tokens", 0),
277
- "total_tokens": getattr(usage, "total_tokens", 0),
278
- }
275
+ if usage is not None and hasattr(usage, "model_dump"):
276
+ return usage.model_dump(exclude_none=True)
277
+ return {}
279
278
 
280
279
 
281
280
  def _build_chat_message(response: Any, choice: Any) -> ChatMessage:
@@ -13,6 +13,7 @@ import pytest
13
13
  from haystack.dataclasses import ChatMessage, StreamingChunk
14
14
  from haystack.tools import Tool
15
15
  from haystack.utils.auth import Secret
16
+ from litellm.types.utils import Usage
16
17
 
17
18
  from haystack_integrations.components.generators.litellm import LiteLLMChatGenerator
18
19
 
@@ -29,14 +30,9 @@ def _make_mock_response(content="Hello!", model="openai/gpt-4o", tool_calls=None
29
30
  choice.index = 0
30
31
  choice.finish_reason = "stop"
31
32
 
32
- usage = MagicMock()
33
- usage.prompt_tokens = 10
34
- usage.completion_tokens = 5
35
- usage.total_tokens = 15
36
-
37
33
  resp = MagicMock()
38
34
  resp.choices = [choice]
39
- resp.usage = usage
35
+ resp.usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
40
36
  resp.model = model
41
37
  return resp
42
38
 
@@ -205,18 +201,20 @@ class TestRun:
205
201
  call_kwargs = fake_litellm.completion.call_args
206
202
  assert call_kwargs.kwargs["api_base"] == "https://proxy.local"
207
203
 
208
- def test_generation_kwargs_merged(self):
209
- gen = LiteLLMChatGenerator(model="openai/gpt-4o", generation_kwargs={"temperature": 0.5})
204
+ def test_run_with_generation_kwargs(self):
205
+ gen = LiteLLMChatGenerator(
206
+ model="openai/gpt-4o", generation_kwargs={"temperature": 0.5, "max_completion_tokens": 100}
207
+ )
210
208
  mock_resp = _make_mock_response()
211
209
 
212
210
  fake_litellm = types.ModuleType("litellm")
213
211
  fake_litellm.completion = MagicMock(return_value=mock_resp)
214
212
 
215
213
  with mock.patch.dict(sys.modules, {"litellm": fake_litellm}):
216
- gen.run(messages=[ChatMessage.from_user("hi")], generation_kwargs={"max_tokens": 100})
214
+ gen.run(messages=[ChatMessage.from_user("hi")], generation_kwargs={"temperature": 0.9})
217
215
  call_kwargs = fake_litellm.completion.call_args
218
- assert call_kwargs.kwargs["temperature"] == 0.5
219
- assert call_kwargs.kwargs["max_tokens"] == 100
216
+ assert call_kwargs.kwargs["temperature"] == 0.9
217
+ assert call_kwargs.kwargs["max_completion_tokens"] == 100
220
218
 
221
219
  def test_empty_messages_returns_empty(self):
222
220
  gen = LiteLLMChatGenerator()
@@ -235,6 +233,30 @@ class TestRun:
235
233
  meta = result["replies"][0].meta
236
234
  assert meta["usage"]["prompt_tokens"] == 10
237
235
  assert meta["usage"]["completion_tokens"] == 5
236
+ assert set(meta["usage"]) == {"prompt_tokens", "completion_tokens", "total_tokens"}
237
+
238
+ def test_cache_tokens_in_meta(self):
239
+ gen = LiteLLMChatGenerator(model="anthropic/claude-sonnet-4-20250514")
240
+ mock_resp = _make_mock_response(model="anthropic/claude-sonnet-4-20250514")
241
+ mock_resp.usage = Usage(
242
+ prompt_tokens=2010,
243
+ completion_tokens=5,
244
+ total_tokens=2015,
245
+ cache_creation_input_tokens=1200,
246
+ cache_read_input_tokens=800,
247
+ )
248
+
249
+ fake_litellm = types.ModuleType("litellm")
250
+ fake_litellm.completion = MagicMock(return_value=mock_resp)
251
+
252
+ with mock.patch.dict(sys.modules, {"litellm": fake_litellm}):
253
+ usage = gen.run(messages=[ChatMessage.from_user("hi")])["replies"][0].meta["usage"]
254
+ assert usage["cache_creation_input_tokens"] == 1200
255
+ assert usage["cache_read_input_tokens"] == 800
256
+ assert usage["prompt_tokens"] == 2010
257
+ # litellm also normalizes cache counts into the OpenAI-style nested breakdown
258
+ assert usage["prompt_tokens_details"]["cached_tokens"] == 800
259
+ assert usage["prompt_tokens_details"]["cache_creation_tokens"] == 1200
238
260
 
239
261
  def test_tool_calls_parsed(self):
240
262
  tc = MagicMock()
@@ -258,7 +280,7 @@ class TestRun:
258
280
 
259
281
  def test_streaming_content_accumulated(self):
260
282
  gen = LiteLLMChatGenerator(model="openai/gpt-4o")
261
- usage = SimpleNamespace(prompt_tokens=10, completion_tokens=5, total_tokens=15)
283
+ usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
262
284
  chunks = _make_mock_stream_chunks("Hi!", usage=usage)
263
285
 
264
286
  fake_litellm = types.ModuleType("litellm")
@@ -443,6 +465,27 @@ class TestAsync:
443
465
  assert len(result["replies"]) == 1
444
466
  assert isinstance(result["replies"][0], ChatMessage)
445
467
 
468
+ @pytest.mark.asyncio
469
+ async def test_run_async_with_generation_kwargs(self):
470
+ gen = LiteLLMChatGenerator(
471
+ model="openai/gpt-4o", generation_kwargs={"temperature": 0.5, "max_completion_tokens": 100}
472
+ )
473
+ mock_resp = _make_mock_response()
474
+
475
+ fake_litellm = types.ModuleType("litellm")
476
+ captured_kwargs = {}
477
+
478
+ async def _acompletion(**kwargs):
479
+ captured_kwargs.update(kwargs)
480
+ return mock_resp
481
+
482
+ fake_litellm.acompletion = _acompletion
483
+
484
+ with mock.patch.dict(sys.modules, {"litellm": fake_litellm}):
485
+ await gen.run_async(messages=[ChatMessage.from_user("hi")], generation_kwargs={"temperature": 0.9})
486
+ assert captured_kwargs["temperature"] == 0.9
487
+ assert captured_kwargs["max_completion_tokens"] == 100
488
+
446
489
  @pytest.mark.asyncio
447
490
  async def test_run_async_empty_messages(self):
448
491
  gen = LiteLLMChatGenerator(model="openai/gpt-4o")
@@ -452,7 +495,7 @@ class TestAsync:
452
495
  @pytest.mark.asyncio
453
496
  async def test_run_async_streaming(self):
454
497
  gen = LiteLLMChatGenerator(model="openai/gpt-4o")
455
- usage = SimpleNamespace(prompt_tokens=10, completion_tokens=5, total_tokens=15)
498
+ usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
456
499
  chunks = _make_mock_stream_chunks("Hi!", usage=usage)
457
500
 
458
501
  async def _aiter():