Skip to content

Commit 096f246

Browse files
committed
Security: disable agent tools and pass system prompt properly
The SDK defaults to full agent mode with file/bash/web access and multi-turn looping. Lock it down for use as a plain model wrapper: - allowed_tools=[] — empty whitelist, no tool access - max_turns=1 — single response, no agentic looping - system_prompt extracted from caller's OpenAI-format system message instead of being flattened into the conversation string (falls back to "You are a helpful assistant." when no system message is provided)
1 parent 2d8ce75 commit 096f246

1 file changed

Lines changed: 37 additions & 16 deletions

File tree

providers/claude_agent_provider.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,39 @@
1111
from claude_agent_sdk import query, ClaudeAgentOptions
1212
from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock
1313

14-
ROLE_PREFIXES = {
15-
"system": "System",
16-
"user": "Human",
17-
"assistant": "Assistant",
18-
}
14+
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant."
15+
16+
ROLE_PREFIXES = {"user": "Human", "assistant": "Assistant"}
1917

2018

2119
class ClaudeAgentSDKProvider(CustomLLM):
22-
"""LiteLLM provider that routes requests through the Claude Agent SDK."""
20+
"""LiteLLM provider that wraps the Claude Agent SDK as a plain model call."""
2321

2422
def __init__(self):
2523
super().__init__()
2624
print("ClaudeAgentSDKProvider initialized")
2725

2826
@staticmethod
29-
def _format_prompt(messages: List[Dict]) -> str:
30-
parts = []
27+
def _split_messages(messages: List[Dict]) -> Tuple[str, str]:
28+
"""Separate system prompt from conversation turns.
29+
30+
Returns (system_prompt, conversation_prompt).
31+
"""
32+
system_parts = []
33+
turn_parts = []
3134
for msg in messages:
32-
prefix = ROLE_PREFIXES.get(msg.get("role", "user"))
33-
if prefix:
34-
parts.append(f"{prefix}: {msg.get('content', '')}")
35-
return "\n\n".join(parts)
35+
role = msg.get("role", "user")
36+
content = msg.get("content", "")
37+
if role == "system":
38+
system_parts.append(content)
39+
else:
40+
prefix = ROLE_PREFIXES.get(role)
41+
if prefix:
42+
turn_parts.append(f"{prefix}: {content}")
43+
44+
system_prompt = "\n\n".join(system_parts) if system_parts else DEFAULT_SYSTEM_PROMPT
45+
conversation = "\n\n".join(turn_parts)
46+
return system_prompt, conversation
3647

3748
@staticmethod
3849
def _extract_model(model: str) -> str:
@@ -90,8 +101,13 @@ def completion(self, model: str, messages: List[Dict], **kwargs) -> ModelRespons
90101

91102
async def acompletion(self, model: str, messages: List[Dict], **kwargs) -> ModelResponse:
92103
"""Async completion using Claude Agent SDK."""
93-
prompt = self._format_prompt(messages)
94-
options = ClaudeAgentOptions(model=self._extract_model(model))
104+
system_prompt, prompt = self._split_messages(messages)
105+
options = ClaudeAgentOptions(
106+
model=self._extract_model(model),
107+
system_prompt=system_prompt,
108+
allowed_tools=[],
109+
max_turns=1,
110+
)
95111

96112
content = ""
97113
prompt_tokens = 0
@@ -120,8 +136,13 @@ def streaming(self, model: str, messages: List[Dict], **kwargs) -> Iterator[Gene
120136

121137
async def astreaming(self, model: str, messages: List[Dict], **kwargs) -> AsyncIterator[GenericStreamingChunk]:
122138
"""Async streaming using Claude Agent SDK."""
123-
prompt = self._format_prompt(messages)
124-
options = ClaudeAgentOptions(model=self._extract_model(model))
139+
system_prompt, prompt = self._split_messages(messages)
140+
options = ClaudeAgentOptions(
141+
model=self._extract_model(model),
142+
system_prompt=system_prompt,
143+
allowed_tools=[],
144+
max_turns=1,
145+
)
125146

126147
total_chars = 0
127148
prompt_tokens = 0

0 commit comments

Comments
 (0)