dehydrator 0.2.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.
@@ -0,0 +1,23 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:www.anthropic.com)",
5
+ "WebSearch",
6
+ "WebFetch(domain:github.com)",
7
+ "WebFetch(domain:medium.com)",
8
+ "WebFetch(domain:raw.githubusercontent.com)",
9
+ "WebFetch(domain:docs.anthropic.com)",
10
+ "Bash(uv init:*)",
11
+ "Bash(uv sync:*)",
12
+ "Bash(uv run pytest:*)",
13
+ "Bash(uv run ruff check:*)",
14
+ "Bash(uv run mypy:*)",
15
+ "Bash(uv run ruff:*)",
16
+ "WebFetch(domain:gofastmcp.com)",
17
+ "WebFetch(domain:platform.openai.com)",
18
+ "WebFetch(domain:modelcontextprotocol.github.io)",
19
+ "WebFetch(domain:gist.github.com)",
20
+ "Bash(wc:*)"
21
+ ]
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Lock file (library — let consumers resolve deps)
13
+ uv.lock
14
+ .python-version
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.4
2
+ Name: dehydrator
3
+ Version: 0.2.0
4
+ Summary: Client-side BM25 tool search for LLM APIs — Anthropic, OpenAI-compatible, and MCP
5
+ Project-URL: Homepage, https://github.com/arrmlet/dehydrator
6
+ Project-URL: Repository, https://github.com/arrmlet/dehydrator
7
+ Project-URL: Issues, https://github.com/arrmlet/dehydrator/issues
8
+ Author-email: arrmlet <trubavolodymyr@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: anthropic,bm25,context-window,lazy-loading,llm,mcp,openai,tools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: anthropic>=0.40.0
22
+ Requires-Dist: mcp>=1.26.0
23
+ Requires-Dist: rank-bm25>=0.2.2
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Dehydrator
27
+
28
+ Client-side BM25 tool search for LLM APIs. Use thousands of tools without bloating the context window.
29
+
30
+ Works with **Anthropic**, **OpenAI**, and any **OpenAI-compatible** provider (Groq, OpenRouter, Chutes, etc.). Accepts tools from **MCP servers** natively.
31
+
32
+ ## The problem
33
+
34
+ LLM APIs require you to send all tool definitions in every request. With 100+ tools, this wastes tokens and degrades tool selection. Anthropic offers a server-side `tool_search_tool_bm25`, but it's not available on all platforms (e.g. Bedrock) and doesn't work with ZDR. Dehydrator gives you the same capability client-side, so it works everywhere — with any provider.
35
+
36
+ ## How it works
37
+
38
+ Dehydrator wraps your LLM client and replaces the full tool list with a single `tool_search` tool. When the model needs a tool, it searches by description. Dehydrator intercepts the call, runs BM25 locally, and re-calls the API with only the matched tools injected.
39
+
40
+ ```
41
+ User request
42
+
43
+
44
+ ┌─────────────────────────────┐
45
+ │ API call #1 │
46
+ │ tools = [tool_search] │
47
+ │ │
48
+ │ Model responds: │
49
+ │ tool_search("send email") │
50
+ └─────────────┬───────────────┘
51
+ │ intercepted by Dehydrator
52
+
53
+ ┌─────────────────────────────┐
54
+ │ BM25 search (local) │
55
+ │ → matches: send_email, │
56
+ │ send_slack_message │
57
+ └─────────────┬───────────────┘
58
+
59
+
60
+ ┌─────────────────────────────┐
61
+ │ API call #2 │
62
+ │ tools = [tool_search, │
63
+ │ send_email, │
64
+ │ send_slack_message]│
65
+ │ │
66
+ │ Model responds: │
67
+ │ send_email({...}) │
68
+ └─────────────────────────────┘
69
+
70
+
71
+ Returned to you
72
+ ```
73
+
74
+ Only the tools the model actually needs are ever sent. Discovered tools persist across turns within a conversation.
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install dehydrator
80
+ ```
81
+
82
+ ## Quick start
83
+
84
+ ### Anthropic
85
+
86
+ ```python
87
+ import anthropic
88
+ from dehydrator import DehydratedClient
89
+
90
+ client = DehydratedClient(
91
+ anthropic.Anthropic(),
92
+ tools=tools,
93
+ top_k=5,
94
+ )
95
+
96
+ response = client.messages.create(
97
+ model="claude-sonnet-4-6",
98
+ max_tokens=1024,
99
+ messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
100
+ )
101
+ ```
102
+
103
+ The response is a standard `anthropic.types.Message`.
104
+
105
+ ### OpenAI-compatible (OpenAI, Groq, OpenRouter, Chutes, etc.)
106
+
107
+ ```python
108
+ from openai import OpenAI
109
+ from dehydrator import OpenAIDehydratedClient
110
+
111
+ client = OpenAIDehydratedClient(
112
+ OpenAI(),
113
+ tools=tools,
114
+ top_k=5,
115
+ )
116
+
117
+ response = client.chat.completions.create(
118
+ model="gpt-4o",
119
+ messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
120
+ )
121
+ ```
122
+
123
+ Works with any client that implements `client.chat.completions.create()`. No `openai` import required — fully duck-typed.
124
+
125
+ ### MCP tools
126
+
127
+ Tools from MCP servers use `inputSchema` (camelCase) instead of `input_schema`. Dehydrator accepts both formats automatically:
128
+
129
+ ```python
130
+ # MCP format tools work directly
131
+ tools = [
132
+ {"name": "get_weather", "description": "...", "inputSchema": {...}},
133
+ ]
134
+ client = DehydratedClient(anthropic.Anthropic(), tools=tools)
135
+
136
+ # Or use mcp.types.Tool objects with ToolIndex.from_mcp()
137
+ from dehydrator import ToolIndex
138
+
139
+ tools = await session.list_tools() # returns list[mcp.types.Tool]
140
+ index = ToolIndex.from_mcp(tools, top_k=5)
141
+ ```
142
+
143
+ ## API
144
+
145
+ ### `DehydratedClient(client, tools, *, top_k=5, always_available=None, max_search_rounds=3)`
146
+
147
+ Wraps an `anthropic.Anthropic` client.
148
+
149
+ | Parameter | Type | Description |
150
+ |---|---|---|
151
+ | `client` | `anthropic.Anthropic` | An Anthropic SDK client instance |
152
+ | `tools` | `list[dict]` | Tool definitions (Anthropic or MCP format) |
153
+ | `top_k` | `int` | Max tools returned per search (default: 5) |
154
+ | `always_available` | `list[str]` | Tool names to include in every request, bypassing search |
155
+ | `max_search_rounds` | `int` | Max search iterations per `create()` call (default: 3) |
156
+
157
+ #### Methods
158
+
159
+ - **`client.messages.create(**kwargs)`** — Same signature as the Anthropic SDK. The `tools` kwarg is ignored (Dehydrator manages tools). Returns `anthropic.types.Message`.
160
+ - **`client.reset_discoveries()`** — Clears discovered tools. Call this when starting a new conversation.
161
+ - **`client.inner`** — Access the underlying `anthropic.Anthropic` client.
162
+
163
+ ### `AsyncDehydratedClient`
164
+
165
+ Same API as `DehydratedClient`, but wraps `anthropic.AsyncAnthropic` and `create()` is async.
166
+
167
+ ### `OpenAIDehydratedClient(client, tools, *, top_k=5, always_available=None, max_search_rounds=3)`
168
+
169
+ Wraps any OpenAI-compatible client.
170
+
171
+ | Parameter | Type | Description |
172
+ |---|---|---|
173
+ | `client` | any | Any client with `client.chat.completions.create()` |
174
+ | `tools` | `list[dict]` | Tool definitions (Anthropic or MCP format — converted to OpenAI format automatically) |
175
+ | `top_k` | `int` | Max tools returned per search (default: 5) |
176
+ | `always_available` | `list[str]` | Tool names to include in every request, bypassing search |
177
+ | `max_search_rounds` | `int` | Max search iterations per `create()` call (default: 3) |
178
+
179
+ #### Methods
180
+
181
+ - **`client.chat.completions.create(**kwargs)`** — Same signature as the OpenAI SDK. The `tools` kwarg is ignored. Returns the provider's response object.
182
+ - **`client.reset_discoveries()`** — Clears discovered tools.
183
+ - **`client.inner`** — Access the underlying client.
184
+
185
+ ### `AsyncOpenAIDehydratedClient`
186
+
187
+ Same API as `OpenAIDehydratedClient`, but `create()` is async.
188
+
189
+ ### `ToolIndex`
190
+
191
+ The BM25 index is also available standalone if you want to use it directly.
192
+
193
+ ```python
194
+ from dehydrator import ToolIndex
195
+
196
+ index = ToolIndex(tools, top_k=5)
197
+ matched_names = index.search("weather forecast")
198
+ matched_tools = index.get_tools(matched_names)
199
+
200
+ # From MCP Tool objects
201
+ index = ToolIndex.from_mcp(mcp_tools, top_k=5)
202
+ ```
203
+
204
+ ## Always-available tools
205
+
206
+ Some tools should always be in context (e.g. a `help` tool). Pass their names to `always_available`:
207
+
208
+ ```python
209
+ client = DehydratedClient(
210
+ anthropic.Anthropic(),
211
+ tools=tools,
212
+ always_available=["help", "get_current_user"],
213
+ )
214
+ ```
215
+
216
+ These tools are sent in every request without requiring a search.
217
+
218
+ ## Multi-turn conversations
219
+
220
+ Discovered tools persist across calls to `create()`. If the model found `send_email` in turn 1, it's still available in turn 2 without re-searching.
221
+
222
+ Call `client.reset_discoveries()` when starting a new conversation:
223
+
224
+ ```python
225
+ # Turn 1: model discovers send_email
226
+ response = client.messages.create(...)
227
+
228
+ # Turn 2: send_email is still available
229
+ response = client.messages.create(...)
230
+
231
+ # New conversation
232
+ client.reset_discoveries()
233
+ ```
234
+
235
+ ## Benchmarks
236
+
237
+ Benchmarked against **139 real tool definitions** from 6 popular MCP servers (Chrome DevTools, GitHub, Playwright, Filesystem, Git, Notion).
238
+
239
+ ### Token savings
240
+
241
+ Sending all tools in every request is expensive. Dehydrator replaces them with a single `tool_search` tool and only injects the tools the model actually needs:
242
+
243
+ | Tools | top_k=3 | top_k=5 | top_k=10 | Baseline |
244
+ |------:|--------:|--------:|---------:|---------:|
245
+ | 50 | 274 tokens (94%) | 349 tokens (93%) | 678 tokens (86%) | 4,864 |
246
+ | 100 | 274 tokens (97%) | 349 tokens (96%) | 678 tokens (92%) | 8,954 |
247
+ | 200 | 274 tokens (98%) | 349 tokens (98%) | 678 tokens (96%) | 18,159 |
248
+
249
+ With 200 tools and `top_k=5`, you go from **18,159 → 349 tokens** per request — a **98% reduction**.
250
+
251
+ ### Search quality
252
+
253
+ BM25 finds the right tools reliably across all 6 MCP servers:
254
+
255
+ | Metric | k=3 | k=5 | k=10 |
256
+ |--------|----:|----:|-----:|
257
+ | Precision@k | 51.1% | 32.7% | 17.3% |
258
+ | Recall@k | 88.6% | 95.3% | 98.3% |
259
+ | **MRR** | | **95.8%** | |
260
+
261
+ 30/30 test queries found at least one correct tool in the top 10. The right tool is ranked #1 or #2 in almost every case.
262
+
263
+ ### Run the benchmarks
264
+
265
+ ```bash
266
+ uv run python benchmarks/search_quality.py # local, no API key
267
+ uv run python benchmarks/token_savings_openai.py # local, uses tiktoken
268
+ ```
269
+
270
+ ## Limitations
271
+
272
+ - **No streaming** — `stream=True` raises `NotImplementedError`. Planned for a future release.
273
+ - **Reserved tool name** — You cannot have a tool named `tool_search`. Dehydrator will raise `ValueError` if you do.
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ git clone https://github.com/Arrmlet/dehydrator.git
279
+ cd dehydrator
280
+ uv sync
281
+
282
+ uv run pytest # tests
283
+ uv run ruff check src/ # lint
284
+ uv run mypy src/ # type check
285
+ ```
286
+
287
+ ## License
288
+
289
+ MIT
@@ -0,0 +1,264 @@
1
+ # Dehydrator
2
+
3
+ Client-side BM25 tool search for LLM APIs. Use thousands of tools without bloating the context window.
4
+
5
+ Works with **Anthropic**, **OpenAI**, and any **OpenAI-compatible** provider (Groq, OpenRouter, Chutes, etc.). Accepts tools from **MCP servers** natively.
6
+
7
+ ## The problem
8
+
9
+ LLM APIs require you to send all tool definitions in every request. With 100+ tools, this wastes tokens and degrades tool selection. Anthropic offers a server-side `tool_search_tool_bm25`, but it's not available on all platforms (e.g. Bedrock) and doesn't work with ZDR. Dehydrator gives you the same capability client-side, so it works everywhere — with any provider.
10
+
11
+ ## How it works
12
+
13
+ Dehydrator wraps your LLM client and replaces the full tool list with a single `tool_search` tool. When the model needs a tool, it searches by description. Dehydrator intercepts the call, runs BM25 locally, and re-calls the API with only the matched tools injected.
14
+
15
+ ```
16
+ User request
17
+
18
+
19
+ ┌─────────────────────────────┐
20
+ │ API call #1 │
21
+ │ tools = [tool_search] │
22
+ │ │
23
+ │ Model responds: │
24
+ │ tool_search("send email") │
25
+ └─────────────┬───────────────┘
26
+ │ intercepted by Dehydrator
27
+
28
+ ┌─────────────────────────────┐
29
+ │ BM25 search (local) │
30
+ │ → matches: send_email, │
31
+ │ send_slack_message │
32
+ └─────────────┬───────────────┘
33
+
34
+
35
+ ┌─────────────────────────────┐
36
+ │ API call #2 │
37
+ │ tools = [tool_search, │
38
+ │ send_email, │
39
+ │ send_slack_message]│
40
+ │ │
41
+ │ Model responds: │
42
+ │ send_email({...}) │
43
+ └─────────────────────────────┘
44
+
45
+
46
+ Returned to you
47
+ ```
48
+
49
+ Only the tools the model actually needs are ever sent. Discovered tools persist across turns within a conversation.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install dehydrator
55
+ ```
56
+
57
+ ## Quick start
58
+
59
+ ### Anthropic
60
+
61
+ ```python
62
+ import anthropic
63
+ from dehydrator import DehydratedClient
64
+
65
+ client = DehydratedClient(
66
+ anthropic.Anthropic(),
67
+ tools=tools,
68
+ top_k=5,
69
+ )
70
+
71
+ response = client.messages.create(
72
+ model="claude-sonnet-4-6",
73
+ max_tokens=1024,
74
+ messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
75
+ )
76
+ ```
77
+
78
+ The response is a standard `anthropic.types.Message`.
79
+
80
+ ### OpenAI-compatible (OpenAI, Groq, OpenRouter, Chutes, etc.)
81
+
82
+ ```python
83
+ from openai import OpenAI
84
+ from dehydrator import OpenAIDehydratedClient
85
+
86
+ client = OpenAIDehydratedClient(
87
+ OpenAI(),
88
+ tools=tools,
89
+ top_k=5,
90
+ )
91
+
92
+ response = client.chat.completions.create(
93
+ model="gpt-4o",
94
+ messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
95
+ )
96
+ ```
97
+
98
+ Works with any client that implements `client.chat.completions.create()`. No `openai` import required — fully duck-typed.
99
+
100
+ ### MCP tools
101
+
102
+ Tools from MCP servers use `inputSchema` (camelCase) instead of `input_schema`. Dehydrator accepts both formats automatically:
103
+
104
+ ```python
105
+ # MCP format tools work directly
106
+ tools = [
107
+ {"name": "get_weather", "description": "...", "inputSchema": {...}},
108
+ ]
109
+ client = DehydratedClient(anthropic.Anthropic(), tools=tools)
110
+
111
+ # Or use mcp.types.Tool objects with ToolIndex.from_mcp()
112
+ from dehydrator import ToolIndex
113
+
114
+ tools = await session.list_tools() # returns list[mcp.types.Tool]
115
+ index = ToolIndex.from_mcp(tools, top_k=5)
116
+ ```
117
+
118
+ ## API
119
+
120
+ ### `DehydratedClient(client, tools, *, top_k=5, always_available=None, max_search_rounds=3)`
121
+
122
+ Wraps an `anthropic.Anthropic` client.
123
+
124
+ | Parameter | Type | Description |
125
+ |---|---|---|
126
+ | `client` | `anthropic.Anthropic` | An Anthropic SDK client instance |
127
+ | `tools` | `list[dict]` | Tool definitions (Anthropic or MCP format) |
128
+ | `top_k` | `int` | Max tools returned per search (default: 5) |
129
+ | `always_available` | `list[str]` | Tool names to include in every request, bypassing search |
130
+ | `max_search_rounds` | `int` | Max search iterations per `create()` call (default: 3) |
131
+
132
+ #### Methods
133
+
134
+ - **`client.messages.create(**kwargs)`** — Same signature as the Anthropic SDK. The `tools` kwarg is ignored (Dehydrator manages tools). Returns `anthropic.types.Message`.
135
+ - **`client.reset_discoveries()`** — Clears discovered tools. Call this when starting a new conversation.
136
+ - **`client.inner`** — Access the underlying `anthropic.Anthropic` client.
137
+
138
+ ### `AsyncDehydratedClient`
139
+
140
+ Same API as `DehydratedClient`, but wraps `anthropic.AsyncAnthropic` and `create()` is async.
141
+
142
+ ### `OpenAIDehydratedClient(client, tools, *, top_k=5, always_available=None, max_search_rounds=3)`
143
+
144
+ Wraps any OpenAI-compatible client.
145
+
146
+ | Parameter | Type | Description |
147
+ |---|---|---|
148
+ | `client` | any | Any client with `client.chat.completions.create()` |
149
+ | `tools` | `list[dict]` | Tool definitions (Anthropic or MCP format — converted to OpenAI format automatically) |
150
+ | `top_k` | `int` | Max tools returned per search (default: 5) |
151
+ | `always_available` | `list[str]` | Tool names to include in every request, bypassing search |
152
+ | `max_search_rounds` | `int` | Max search iterations per `create()` call (default: 3) |
153
+
154
+ #### Methods
155
+
156
+ - **`client.chat.completions.create(**kwargs)`** — Same signature as the OpenAI SDK. The `tools` kwarg is ignored. Returns the provider's response object.
157
+ - **`client.reset_discoveries()`** — Clears discovered tools.
158
+ - **`client.inner`** — Access the underlying client.
159
+
160
+ ### `AsyncOpenAIDehydratedClient`
161
+
162
+ Same API as `OpenAIDehydratedClient`, but `create()` is async.
163
+
164
+ ### `ToolIndex`
165
+
166
+ The BM25 index is also available standalone if you want to use it directly.
167
+
168
+ ```python
169
+ from dehydrator import ToolIndex
170
+
171
+ index = ToolIndex(tools, top_k=5)
172
+ matched_names = index.search("weather forecast")
173
+ matched_tools = index.get_tools(matched_names)
174
+
175
+ # From MCP Tool objects
176
+ index = ToolIndex.from_mcp(mcp_tools, top_k=5)
177
+ ```
178
+
179
+ ## Always-available tools
180
+
181
+ Some tools should always be in context (e.g. a `help` tool). Pass their names to `always_available`:
182
+
183
+ ```python
184
+ client = DehydratedClient(
185
+ anthropic.Anthropic(),
186
+ tools=tools,
187
+ always_available=["help", "get_current_user"],
188
+ )
189
+ ```
190
+
191
+ These tools are sent in every request without requiring a search.
192
+
193
+ ## Multi-turn conversations
194
+
195
+ Discovered tools persist across calls to `create()`. If the model found `send_email` in turn 1, it's still available in turn 2 without re-searching.
196
+
197
+ Call `client.reset_discoveries()` when starting a new conversation:
198
+
199
+ ```python
200
+ # Turn 1: model discovers send_email
201
+ response = client.messages.create(...)
202
+
203
+ # Turn 2: send_email is still available
204
+ response = client.messages.create(...)
205
+
206
+ # New conversation
207
+ client.reset_discoveries()
208
+ ```
209
+
210
+ ## Benchmarks
211
+
212
+ Benchmarked against **139 real tool definitions** from 6 popular MCP servers (Chrome DevTools, GitHub, Playwright, Filesystem, Git, Notion).
213
+
214
+ ### Token savings
215
+
216
+ Sending all tools in every request is expensive. Dehydrator replaces them with a single `tool_search` tool and only injects the tools the model actually needs:
217
+
218
+ | Tools | top_k=3 | top_k=5 | top_k=10 | Baseline |
219
+ |------:|--------:|--------:|---------:|---------:|
220
+ | 50 | 274 tokens (94%) | 349 tokens (93%) | 678 tokens (86%) | 4,864 |
221
+ | 100 | 274 tokens (97%) | 349 tokens (96%) | 678 tokens (92%) | 8,954 |
222
+ | 200 | 274 tokens (98%) | 349 tokens (98%) | 678 tokens (96%) | 18,159 |
223
+
224
+ With 200 tools and `top_k=5`, you go from **18,159 → 349 tokens** per request — a **98% reduction**.
225
+
226
+ ### Search quality
227
+
228
+ BM25 finds the right tools reliably across all 6 MCP servers:
229
+
230
+ | Metric | k=3 | k=5 | k=10 |
231
+ |--------|----:|----:|-----:|
232
+ | Precision@k | 51.1% | 32.7% | 17.3% |
233
+ | Recall@k | 88.6% | 95.3% | 98.3% |
234
+ | **MRR** | | **95.8%** | |
235
+
236
+ 30/30 test queries found at least one correct tool in the top 10. The right tool is ranked #1 or #2 in almost every case.
237
+
238
+ ### Run the benchmarks
239
+
240
+ ```bash
241
+ uv run python benchmarks/search_quality.py # local, no API key
242
+ uv run python benchmarks/token_savings_openai.py # local, uses tiktoken
243
+ ```
244
+
245
+ ## Limitations
246
+
247
+ - **No streaming** — `stream=True` raises `NotImplementedError`. Planned for a future release.
248
+ - **Reserved tool name** — You cannot have a tool named `tool_search`. Dehydrator will raise `ValueError` if you do.
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ git clone https://github.com/Arrmlet/dehydrator.git
254
+ cd dehydrator
255
+ uv sync
256
+
257
+ uv run pytest # tests
258
+ uv run ruff check src/ # lint
259
+ uv run mypy src/ # type check
260
+ ```
261
+
262
+ ## License
263
+
264
+ MIT
File without changes