DSPy LM integration for Claude Code CLI - route DSPy requests through local Claude Code agent
Project description
dspy-coding-agent-lms
DSPy Language Model integration for Claude Code CLI - route DSPy requests through your local Claude Code agent instead of using Anthropic APIs directly.
Overview
dspy-coding-agent-lms provides a DSPy-compatible language model that invokes the Claude Code CLI for each request, enabling:
- Agentic capabilities: Access Claude Code's file editing, bash execution, and tool use within DSPy programs
- Structured output: Use Claude Code's built-in JSON schema validation instead of string parsing
- Permission control: Fine-grained control over which tools Claude can use
- Cost tracking: Automatic transcript capture with usage and cost metrics
- Local execution: Run Claude Code locally with your existing authentication
Installation
Prerequisites
-
Claude Code CLI - Install and authenticate:
npm install -g @anthropic-ai/claude-code claude login # or use ANTHROPIC_API_KEY
-
Python 3.11+
Install the package
# Using uv (recommended)
uv add dspy-coding-agent-lms
# Using pip
pip install dspy-coding-agent-lms
Quick Start
import dspy
from dspy_coding_agent_lms import ClaudeCodeLM
# Initialize the LM
lm = ClaudeCodeLM(model="sonnet", permission_mode="plan")
# Configure DSPy with JSONAdapter (RECOMMENDED)
# JSONAdapter uses Claude Code's native --json-schema for reliable structured output
dspy.configure(lm=lm, adapter=dspy.JSONAdapter())
# Use DSPy as normal
predict = dspy.Predict("question -> answer")
result = predict(question="What is 2+2?")
print(result.answer)
# Check cost and usage
print(f"Cost: ${lm.transcript.total_cost_usd():.4f}")
Recommended: Always use
dspy.JSONAdapter()with ClaudeCodeLM. This enables native JSON schema output via Claude Code's--json-schemaflag, providing more reliable structured output than string parsing.
Features
DSPy Adapters: JSONAdapter vs ChatAdapter
When using DSPy Signatures, you can choose between two adapters:
JSONAdapter (Recommended)
Uses Claude Code's native --json-schema flag for structured output. The response is pure JSON in the structured_output field.
import dspy
from dspy_coding_agent_lms import ClaudeCodeLM
lm = ClaudeCodeLM(model="sonnet")
dspy.configure(lm=lm, adapter=dspy.JSONAdapter()) # Recommended
class SentimentSignature(dspy.Signature):
"""Analyze sentiment of text."""
text: str = dspy.InputField()
sentiment: str = dspy.OutputField()
confidence: float = dspy.OutputField()
result = dspy.Predict(SentimentSignature)(text="I love this!")
# Output format: {"sentiment": "positive", "confidence": 0.95}
ChatAdapter (Default)
Uses string parsing with [[ ## field ## ]] markers. This is DSPy's default but less reliable.
dspy.configure(lm=lm, adapter=dspy.ChatAdapter()) # Default, not recommended
# Output format:
# [[ ## sentiment ## ]]
# positive
# [[ ## confidence ## ]]
# 0.95
Why JSONAdapter is recommended:
- More reliable parsing (no regex-based extraction)
- Native JSON schema validation by Claude Code
- Cleaner API responses with
structured_outputfield - Better type safety for complex nested structures
Direct JSON Schema
Use Claude Code's built-in JSON schema validation for reliable structured outputs:
from dspy_coding_agent_lms import ClaudeCodeLM
import json
lm = ClaudeCodeLM(model="sonnet")
schema = {
"type": "object",
"properties": {
"answer": {"type": "integer"},
"explanation": {"type": "string"}
},
"required": ["answer"]
}
result = lm.forward(
prompt="What is 15% of 200?",
json_schema=schema
)
data = json.loads(result[0]["text"])
print(f"Answer: {data['answer']}") # Answer: 30
Tool Restrictions
Control which tools Claude can access:
# Read-only code analysis
lm = ClaudeCodeLM(
model="sonnet",
permission_mode="plan",
allowed_tools=["Read", "Glob", "Grep"],
disallowed_tools=["Bash", "Edit", "Write"],
)
# Git operations only
lm = ClaudeCodeLM(
allowed_tools=[
"Bash(git status)",
"Bash(git log*)",
"Bash(git diff*)",
],
)
Async Support
Execute concurrent requests for better throughput:
import asyncio
from dspy_coding_agent_lms import ClaudeCodeLM
async def main():
lm = ClaudeCodeLM(model="sonnet")
prompts = ["Question 1", "Question 2", "Question 3"]
tasks = [lm.aforward(prompt=p) for p in prompts]
results = await asyncio.gather(*tasks)
asyncio.run(main())
Transcript and Cost Tracking
Access complete interaction history with usage metrics:
lm = ClaudeCodeLM(model="sonnet")
# Make some requests...
lm.forward(prompt="Hello")
lm.forward(prompt="World")
# Access transcript
for entry in lm.transcript:
print(f"Prompt: {entry.prompt}")
print(f"Duration: {entry.duration_ms}ms")
print(f"Cost: ${entry.cost_usd:.4f}")
# Aggregated stats
print(f"Total cost: ${lm.transcript.total_cost_usd():.4f}")
print(f"Total tokens: {lm.transcript.total_tokens()}")
# Export transcript
lm.transcript.save_to_file("transcript.json")
Configuration Options
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
str | "sonnet" | Model to use (sonnet, opus, haiku) |
permission_mode |
str | "plan" | Permission mode (plan, dontAsk, acceptEdits, etc.) |
system_prompt |
str | None | Override default system prompt |
append_system_prompt |
str | None | Append to default system prompt |
dangerously_skip_permissions |
bool | False | Bypass all permission checks |
allowed_tools |
list | None | Tools that can execute without prompts |
disallowed_tools |
list | None | Tools that are blocked |
working_directory |
str | None | Working directory for execution |
timeout_seconds |
float | 300 | Command timeout |
max_budget_usd |
float | None | Maximum budget limit |
cache |
bool | True | Enable response caching |
capture_transcript |
bool | True | Capture interaction transcript |
anthropic_api_key |
str | None | Explicit API key |
oauth_token |
str | None | OAuth token for Pro/Max subscribers |
Permission Modes
| Mode | Description |
|---|---|
plan |
Allow planning but require approval for execution |
dontAsk |
Don't ask for permissions (use with caution) |
acceptEdits |
Auto-accept file edits |
default |
Default Claude Code behavior |
delegate |
Delegate permission decisions |
bypassPermissions |
Bypass all permissions (dangerous) |
Authentication
The library supports two authentication methods:
- API Key: Set
ANTHROPIC_API_KEYenvironment variable or passanthropic_api_keyparameter - OAuth Token: Set
CLAUDE_CODE_OAUTH_TOKENor passoauth_tokenparameter (for Pro/Max subscribers)
# Using environment variables (recommended)
lm = ClaudeCodeLM(model="sonnet") # Uses ANTHROPIC_API_KEY
# Explicit credentials
lm = ClaudeCodeLM(
model="sonnet",
anthropic_api_key="sk-...",
)
Examples
See the examples/ directory for complete examples:
basic_usage.py- Simple DSPy integrationstructured_output.py- JSON schema structured outputswith_tools.py- Tool restriction configurationsasync_usage.py- Concurrent async requests
Testing
# Run unit tests
uv run pytest tests/unit/ -v
# Run with coverage
uv run pytest tests/unit/ -v --cov=dspy_coding_agent_lms
# Run integration tests (requires Claude Code CLI)
RUN_INTEGRATION_TESTS=1 uv run pytest tests/integration/ -v
API Reference
ClaudeCodeLM
Main class for DSPy integration.
class ClaudeCodeLM:
def forward(
self,
prompt: str | None = None,
messages: list[dict] | None = None,
json_schema: dict | None = None,
**kwargs
) -> list[dict]:
"""Execute synchronous request."""
async def aforward(
self,
prompt: str | None = None,
messages: list[dict] | None = None,
json_schema: dict | None = None,
**kwargs
) -> list[dict]:
"""Execute asynchronous request."""
@property
def transcript(self) -> Transcript:
"""Access interaction transcript."""
def copy(self, **kwargs) -> ClaudeCodeLM:
"""Create copy with modified parameters."""
Transcript
Interaction history with aggregation methods.
class Transcript:
def total_cost_usd(self) -> float: ...
def total_tokens(self) -> dict[str, int]: ...
def total_duration_ms(self) -> int: ...
def to_json(self) -> str: ...
def save_to_file(self, path: str) -> None: ...
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
Acknowledgments
- DSPy - The declarative programming framework for LMs
- Claude Code - Anthropic's agentic coding assistant
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dspy_coding_agent_lms-0.1.3.tar.gz.
File metadata
- Download URL: dspy_coding_agent_lms-0.1.3.tar.gz
- Upload date:
- Size: 220.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2713204251092b929fc49f02b1b79e40e068b3f164ff1e99d15624b1fa8e617
|
|
| MD5 |
ec12e97fd8136473abee01137f8d2933
|
|
| BLAKE2b-256 |
b3b76661997db88194bcfba26757ece8d83533cdb57f5e14480a1a8ad30f6970
|
File details
Details for the file dspy_coding_agent_lms-0.1.3-py3-none-any.whl.
File metadata
- Download URL: dspy_coding_agent_lms-0.1.3-py3-none-any.whl
- Upload date:
- Size: 26.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6dab30522461f950113a576e77153fe0a5e6f408b3168304045f1361a991acb8
|
|
| MD5 |
9032d4b58f6c84311ac40e160662ebb9
|
|
| BLAKE2b-256 |
298b659090b0475a958a114e29df8e110694427c9e299c821fb64209010e6a82
|