Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions e2e-tests/test_dynamic_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""End-to-end tests for dynamic control features with real Claude API calls."""

import pytest

from claude_code_sdk import (
ClaudeCodeOptions,
ClaudeSDKClient,
)


@pytest.mark.e2e
@pytest.mark.asyncio
async def test_set_permission_mode():
"""Test that permission mode can be changed dynamically during a session."""

options = ClaudeCodeOptions(
permission_mode="default",
)

async with ClaudeSDKClient(options=options) as client:
# Change permission mode to acceptEdits
await client.set_permission_mode("acceptEdits")

# Make a query that would normally require permission
await client.query("What is 2+2? Just respond with the number.")

async for message in client.receive_response():
print(f"Got message: {message}")
pass # Just consume messages

# Change back to default
await client.set_permission_mode("default")

# Make another query
await client.query("What is 3+3? Just respond with the number.")

async for message in client.receive_response():
print(f"Got message: {message}")
pass # Just consume messages


@pytest.mark.e2e
@pytest.mark.asyncio
async def test_set_model():
"""Test that model can be changed dynamically during a session."""

options = ClaudeCodeOptions()

async with ClaudeSDKClient(options=options) as client:
# Start with default model
await client.query("What is 1+1? Just the number.")

async for message in client.receive_response():
print(f"Default model response: {message}")
pass

# Switch to Haiku model
await client.set_model("claude-3-5-haiku-20241022")

await client.query("What is 2+2? Just the number.")

async for message in client.receive_response():
print(f"Haiku model response: {message}")
pass

# Switch back to default (None means default)
await client.set_model(None)

await client.query("What is 3+3? Just the number.")

async for message in client.receive_response():
print(f"Back to default model: {message}")
pass


@pytest.mark.e2e
@pytest.mark.asyncio
async def test_interrupt():
"""Test that interrupt can be sent during a session."""

options = ClaudeCodeOptions()

async with ClaudeSDKClient(options=options) as client:
# Start a query
await client.query("Count from 1 to 100 slowly.")

# Send interrupt (may or may not stop the response depending on timing)
try:
await client.interrupt()
print("Interrupt sent successfully")
except Exception as e:
print(f"Interrupt resulted in: {e}")

# Consume any remaining messages
async for message in client.receive_response():
print(f"Got message after interrupt: {message}")
pass
9 changes: 9 additions & 0 deletions src/claude_code_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,15 @@ async def set_permission_mode(self, mode: str) -> None:
}
)

async def set_model(self, model: str | None) -> None:
"""Change the AI model."""
await self._send_control_request(
{
"subtype": "set_model",
"model": model,
}
)

async def stream_input(self, stream: AsyncIterable[dict[str, Any]]) -> None:
"""Stream input messages to transport."""
try:
Expand Down
48 changes: 48 additions & 0 deletions src/claude_code_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,54 @@ async def interrupt(self) -> None:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.interrupt()

async def set_permission_mode(self, mode: str) -> None:
"""Change permission mode during conversation (only works with streaming mode).

Args:
mode: The permission mode to set. Valid options:
- 'default': CLI prompts for dangerous tools
- 'acceptEdits': Auto-accept file edits
- 'bypassPermissions': Allow all tools (use with caution)

Example:
```python
async with ClaudeSDKClient() as client:
# Start with default permissions
await client.query("Help me analyze this codebase")

# Review mode done, switch to auto-accept edits
await client.set_permission_mode('acceptEdits')
await client.query("Now implement the fix we discussed")
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.set_permission_mode(mode)

async def set_model(self, model: str | None = None) -> None:
"""Change the AI model during conversation (only works with streaming mode).

Args:
model: The model to use, or None to use default. Examples:
- 'claude-sonnet-4-20250514'
- 'claude-opus-4-1-20250805'
- 'claude-opus-4-20250514'

Example:
```python
async with ClaudeSDKClient() as client:
# Start with default model
await client.query("Help me understand this problem")

# Switch to a different model for implementation
await client.set_model('claude-3-5-sonnet-20241022')
await client.query("Now implement the solution")
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.set_model(model)

async def get_server_info(self) -> dict[str, Any] | None:
"""Get server initialization info including available commands and output styles.

Expand Down
Loading