feat: add dynamic permission mode and model switching to ClaudeSDKClient (#171)

## Summary
- Adds `set_permission_mode()` method to dynamically change permission
modes during streaming sessions
- Adds `set_model()` method to switch AI models mid-conversation
- Implements control protocol support matching the TypeScript SDK's
capabilities

## Motivation
The TypeScript SDK supports dynamic control through
`setPermissionMode()` and `setModel()` methods on the Query interface.
This PR brings the same functionality to the Python SDK, allowing users
to:

1. Start with restrictive permissions for code review, then switch to
`acceptEdits` for implementation
2. Use different models for different parts of a task (e.g., Sonnet for
complex analysis, Haiku for simple tasks)
3. Adjust permissions based on workflow needs without restarting
sessions

## Changes
- **ClaudeSDKClient**: Added `set_permission_mode(mode)` and
`set_model(model)` methods
- **Internal Query class**: Added `set_model(model)` method to send
control requests
- **E2E tests**: Added comprehensive tests verifying the functionality
works with real API calls

## Test Plan
- [x] All existing unit tests pass (102 tests)
- [x] New E2E tests added and passing:
- `test_set_permission_mode`: Verifies permission mode changes take
effect
  - `test_set_model`: Confirms model switching works mid-conversation
  - `test_interrupt`: Validates interrupt capability
- [x] Type checking passes (`mypy`)
- [x] Linting passes (`ruff`)

## Usage Example
```python
async with ClaudeSDKClient() as client:
    # Start with default permissions for review
    await client.query("Analyze this code for issues")
    
    # Switch to auto-accept edits for implementation
    await client.set_permission_mode('acceptEdits')
    await client.query("Now fix the issues we found")
    
    # Use a different model for simpler tasks
    await client.set_model('claude-3-5-haiku-20241022')
    await client.query("Add a simple docstring")
```

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
This commit is contained in:
Dickson Tsai 2025-09-28 14:09:29 -07:00 committed by GitHub
parent 0d2404e5d9
commit 62289d2dce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 154 additions and 0 deletions

View file

@ -469,6 +469,15 @@ class Query:
}
)
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:

View file

@ -193,6 +193,54 @@ class ClaudeSDKClient:
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.