Skip to content

Commit 79aad69

Browse files
author
Krrish Dholakia
committed
fix: fix greptile reviews
1 parent e1db405 commit 79aad69

File tree

3 files changed

+225
-16
lines changed

3 files changed

+225
-16
lines changed

litellm/policy_templates_backup.json

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,26 +1078,15 @@
10781078
{
10791079
"id": "claude-code-cost-optimization",
10801080
"title": "Claude Code Cost Optimization",
1081-
"description": "Reduces Claude Code API spend by blocking expensive inference modes (fast/turbo, inference_geo, and extended thinking) and automatically injecting prompt caching headers into system messages to maximize cache hit rates.",
1081+
"description": "Reduces Claude Code API spend by blocking expensive inference modes (fast/turbo, inference_geo, and extended thinking) and Anthropic-hosted tools.",
10821082
"icon": "CurrencyDollarIcon",
10831083
"iconColor": "text-green-500",
10841084
"iconBg": "bg-green-50",
10851085
"guardrails": [
1086-
"claude-code-inject-prompt-cache",
10871086
"claude-code-block-expensive-flags"
10881087
],
10891088
"complexity": "Low",
10901089
"guardrailDefinitions": [
1091-
{
1092-
"guardrail_name": "claude-code-inject-prompt-cache",
1093-
"litellm_params": {
1094-
"guardrail": "claude_code_prompt_cache",
1095-
"mode": "pre_call"
1096-
},
1097-
"guardrail_info": {
1098-
"description": "Automatically adds cache_control: {type: ephemeral} to system messages so Anthropic can cache the system prompt prefix. Only applies to Anthropic API models. Reduces cost on repeated calls that share the same system prompt."
1099-
}
1100-
},
11011090
{
11021091
"guardrail_name": "claude-code-block-expensive-flags",
11031092
"litellm_params": {
@@ -1111,9 +1100,8 @@
11111100
],
11121101
"templateData": {
11131102
"policy_name": "claude-code-cost-optimization",
1114-
"description": "Reduces Claude Code API spend by blocking fast mode, inference_geo, extended thinking, and Anthropic-hosted tools, while auto-injecting prompt caching into system messages.",
1103+
"description": "Reduces Claude Code API spend by blocking fast mode, inference_geo, extended thinking, and Anthropic-hosted tools.",
11151104
"guardrails_add": [
1116-
"claude-code-inject-prompt-cache",
11171105
"claude-code-block-expensive-flags"
11181106
],
11191107
"guardrails_remove": []

tests/test_litellm/proxy/guardrails/guardrail_hooks/block_hosted_tools/test_block_hosted_tools.py

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from fastapi import HTTPException
77

88
from litellm.integrations.custom_guardrail import CustomGuardrail
9-
from litellm.proxy.guardrails.guardrail_hooks.block_hosted_tools.guardrail import \
10-
BlockHostedToolsGuardrail
9+
from litellm.proxy.guardrails.guardrail_hooks.block_hosted_tools.guardrail import (
10+
BlockHostedToolsGuardrail,
11+
)
1112

1213

1314
class TestBlockHostedToolsGuardrail:
@@ -140,6 +141,133 @@ async def test_skips_response_input_type(self):
140141
)
141142
assert result == inputs
142143

144+
@pytest.mark.asyncio
145+
async def test_blocks_anthropic_text_editor_tool(self):
146+
"""Test that Anthropic text_editor_* versioned tool is blocked."""
147+
guardrail = BlockHostedToolsGuardrail(
148+
guardrail_name="test-block-hosted-tools"
149+
)
150+
tools = [{"type": "text_editor_20250429", "name": "str_replace_based_edit_tool"}]
151+
inputs = {"tools": tools}
152+
request_data = {}
153+
154+
with pytest.raises(HTTPException) as exc_info:
155+
await guardrail.apply_guardrail(
156+
inputs=inputs,
157+
request_data=request_data,
158+
input_type="request",
159+
)
160+
161+
assert exc_info.value.status_code == 403
162+
assert "disabled" in str(exc_info.value.detail).lower()
163+
164+
@pytest.mark.asyncio
165+
async def test_blocks_openai_file_search(self):
166+
"""Test that OpenAI file_search hosted tool is blocked."""
167+
guardrail = BlockHostedToolsGuardrail(
168+
guardrail_name="test-block-hosted-tools"
169+
)
170+
tools = [{"type": "file_search"}]
171+
inputs = {"tools": tools}
172+
request_data = {}
173+
174+
with pytest.raises(HTTPException) as exc_info:
175+
await guardrail.apply_guardrail(
176+
inputs=inputs,
177+
request_data=request_data,
178+
input_type="request",
179+
)
180+
181+
assert exc_info.value.status_code == 403
182+
assert "disabled" in str(exc_info.value.detail).lower()
183+
184+
@pytest.mark.asyncio
185+
async def test_blocks_gemini_code_execution_top_level_key(self):
186+
"""Test that Gemini native code_execution top-level key is blocked."""
187+
guardrail = BlockHostedToolsGuardrail(
188+
guardrail_name="test-block-hosted-tools"
189+
)
190+
tools = [{"code_execution": {}}]
191+
inputs = {"tools": tools}
192+
request_data = {}
193+
194+
with pytest.raises(HTTPException) as exc_info:
195+
await guardrail.apply_guardrail(
196+
inputs=inputs,
197+
request_data=request_data,
198+
input_type="request",
199+
)
200+
201+
assert exc_info.value.status_code == 403
202+
assert "disabled" in str(exc_info.value.detail).lower()
203+
204+
@pytest.mark.asyncio
205+
async def test_reports_all_blocked_tools(self):
206+
"""Test that all blocked tools are reported when multiple are present."""
207+
guardrail = BlockHostedToolsGuardrail(
208+
guardrail_name="test-block-hosted-tools"
209+
)
210+
tools = [
211+
{"type": "bash_20250124", "name": "run_bash"},
212+
{"type": "code_interpreter"},
213+
]
214+
inputs = {"tools": tools}
215+
request_data = {}
216+
217+
with pytest.raises(HTTPException) as exc_info:
218+
await guardrail.apply_guardrail(
219+
inputs=inputs,
220+
request_data=request_data,
221+
input_type="request",
222+
)
223+
224+
detail = exc_info.value.detail
225+
assert exc_info.value.status_code == 403
226+
assert len(detail["blocked_tools"]) == 2
227+
228+
@pytest.mark.asyncio
229+
async def test_allows_tools_none(self):
230+
"""Test that tools=None passes through without error."""
231+
guardrail = BlockHostedToolsGuardrail(
232+
guardrail_name="test-block-hosted-tools"
233+
)
234+
inputs = {"tools": None}
235+
request_data = {}
236+
237+
result = await guardrail.apply_guardrail(
238+
inputs=inputs,
239+
request_data=request_data,
240+
input_type="request",
241+
)
242+
assert result == inputs
243+
244+
@pytest.mark.asyncio
245+
async def test_mixed_allowed_and_blocked_tools(self):
246+
"""Test that a mix of function tools and one blocked tool raises 403."""
247+
guardrail = BlockHostedToolsGuardrail(
248+
guardrail_name="test-block-hosted-tools"
249+
)
250+
tools = [
251+
{
252+
"type": "function",
253+
"function": {"name": "get_weather", "description": "Get weather"},
254+
},
255+
{"type": "web_search_20250305"},
256+
]
257+
inputs = {"tools": tools}
258+
request_data = {}
259+
260+
with pytest.raises(HTTPException) as exc_info:
261+
await guardrail.apply_guardrail(
262+
inputs=inputs,
263+
request_data=request_data,
264+
input_type="request",
265+
)
266+
267+
assert exc_info.value.status_code == 403
268+
detail = exc_info.value.detail
269+
assert len(detail["blocked_tools"]) == 1
270+
143271
@pytest.mark.asyncio
144272
async def test_http_403_classified_as_guardrail_intervention(self):
145273
"""Test that HTTP 403 from guardrail is classified as guardrail_intervened."""

tests/test_litellm/proxy/guardrails/guardrail_hooks/claude_code/test_block_expensive_flags.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,99 @@ async def test_skips_response_input_type(self):
111111
)
112112
assert result == inputs
113113

114+
@pytest.mark.asyncio
115+
async def test_allows_thinking_type_not_enabled(self):
116+
"""Test that thinking.type != 'enabled' is not blocked."""
117+
guardrail = ClaudeCodeBlockExpensiveFlagsGuardrail(
118+
guardrail_name="test-block-expensive"
119+
)
120+
request_data = {"thinking": {"type": "disabled"}}
121+
inputs = {"tools": None}
122+
123+
result = await guardrail.apply_guardrail(
124+
inputs=inputs,
125+
request_data=request_data,
126+
input_type="request",
127+
)
128+
assert result == inputs
129+
130+
@pytest.mark.asyncio
131+
async def test_allows_speed_not_fast(self):
132+
"""Test that speed != 'fast' is not blocked."""
133+
guardrail = ClaudeCodeBlockExpensiveFlagsGuardrail(
134+
guardrail_name="test-block-expensive"
135+
)
136+
request_data = {"speed": "normal"}
137+
inputs = {"tools": None}
138+
139+
result = await guardrail.apply_guardrail(
140+
inputs=inputs,
141+
request_data=request_data,
142+
input_type="request",
143+
)
144+
assert result == inputs
145+
146+
@pytest.mark.asyncio
147+
async def test_blocks_inference_geo_any_value(self):
148+
"""Test that inference_geo is blocked for any value (wildcard)."""
149+
guardrail = ClaudeCodeBlockExpensiveFlagsGuardrail(
150+
guardrail_name="test-block-expensive"
151+
)
152+
for geo_value in ["eu", "ap", "us-east"]:
153+
request_data = {"inference_geo": geo_value}
154+
inputs = {"tools": None}
155+
156+
with pytest.raises(HTTPException) as exc_info:
157+
await guardrail.apply_guardrail(
158+
inputs=inputs,
159+
request_data=request_data,
160+
input_type="request",
161+
)
162+
163+
assert exc_info.value.status_code == 403
164+
165+
@pytest.mark.asyncio
166+
async def test_blocks_inherited_anthropic_hosted_tool(self):
167+
"""Test that inherited Anthropic hosted tools are blocked."""
168+
guardrail = ClaudeCodeBlockExpensiveFlagsGuardrail(
169+
guardrail_name="test-block-expensive"
170+
)
171+
# bash_* prefix is inherited from block_hosted_tools/anthropic.yaml
172+
tools = [{"type": "bash_20250124", "name": "run_bash"}]
173+
inputs = {"tools": tools}
174+
request_data = {}
175+
176+
with pytest.raises(HTTPException) as exc_info:
177+
await guardrail.apply_guardrail(
178+
inputs=inputs,
179+
request_data=request_data,
180+
input_type="request",
181+
)
182+
183+
assert exc_info.value.status_code == 403
184+
assert "bash" in str(exc_info.value.detail).lower()
185+
186+
@pytest.mark.asyncio
187+
async def test_param_block_takes_priority_over_tools(self):
188+
"""Test that a blocked param raises before tool checks."""
189+
guardrail = ClaudeCodeBlockExpensiveFlagsGuardrail(
190+
guardrail_name="test-block-expensive"
191+
)
192+
tools = [{"type": "bash_20250124", "name": "run_bash"}]
193+
request_data = {"speed": "fast"}
194+
inputs = {"tools": tools}
195+
196+
with pytest.raises(HTTPException) as exc_info:
197+
await guardrail.apply_guardrail(
198+
inputs=inputs,
199+
request_data=request_data,
200+
input_type="request",
201+
)
202+
203+
assert exc_info.value.status_code == 403
204+
# speed=fast check fires first
205+
assert "fast" in str(exc_info.value.detail).lower()
206+
114207
@pytest.mark.asyncio
115208
async def test_http_403_classified_as_guardrail_intervention(self):
116209
"""Test that HTTP 403 from guardrail is classified as guardrail_intervened."""

0 commit comments

Comments
 (0)