adapter-factory
Skill1 starsChanged 12 months agoMITView on GitHub
Guide for creating new CLI or HTTP adapters to integrate AI models into the AI Counsel deliberation system
- Reads credentials
We split this skill into the rules it gives an agent
32 rules. About 0 are common, 18 are distinctive, and 14 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈18
Distinctive · estimate
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 15
Use this skill when you need to:
- Distinctive: line 16
Add support for a new AI model or service to participate in deliberations
- Distinctive: line 51
Follow these 6 steps to add a new command-line AI tool:
- Very specific: line 135
Add your CLI to config.yaml:
- Very specific: line 205
Update the MCP tool description in server.py:
- Very specific: line 219
Update server.py::RECOMMENDED_MODELS with suggested models for your CLI:
- Very specific: line 233
Create unit tests in tests/unit/test_adapters.py:
- Very specific: line 271
Add integration tests in tests/integration/:
- Distinctive: line 302
Follow these 6 steps to add a new HTTP API integration:
- Very specific: line 407
Add your HTTP adapter to config.yaml:
- Distinctive: line 462
If your adapter uses API keys:
- Very specific: line 474
Create unit tests with VCR for HTTP response recording in tests/unit/test_your_adapter.py:
- Distinctive: line 580
Optional: Add integration tests (requires running service):
- Distinctive: line 609
Create a simple test script to verify integration:
- Distinctive: line 651
Only implement what's unique to your adapter
- Distinctive: line 655
Don't add features until they're required
- Distinctive: line 656
Start simple, extend as needed
- Distinctive: line 659
Write tests first (red)
- Distinctive: line 664
Use type hints throughout
- Very specific: line 739
Test parse_output() with various input formats
- Very specific: line 740
Test build_request() with different parameters
- Very specific: line 741
Test parse_response() with success and error cases
- Distinctive: line 746
Test actual CLI/API invocation
- Distinctive: line 761
Use for final validation only
- Very specific: line 778
Solution: Add --settings '{"disableAllHooks": true}' to args
- Distinctive: line 784
Solution: Verify base_url and service is running
- Very specific: line 788
Solution: Verify API key is set: echo $YOUR_API_KEY
- Distinctive: line 796
Solution: Check if service is healthy
- Very specific: line 802
Solution: Verify registration in adapters/__init__.py
- Very specific: line 806
Solution: Add CLI name to Participant.cli Literal in models/schema.py
- Distinctive: line 835
Update CLAUDE.md: if you added new patterns or gotchas
- Distinctive: line 840
Share findings: if you discover best practices for your model
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Adapter Factory Skill
This skill teaches how to integrate new AI models into the AI Counsel MCP server by creating adapters. There are two types of adapters:
- CLI Adapters - For command-line AI tools (e.g.,
claude,droid,codex) - HTTP Adapters - For HTTP API integrations (e.g., Ollama, LM Studio, OpenRouter)
When to Use This Skill
Use this skill when you need to:
- Add support for a new AI model or service to participate in deliberations
- Integrate a command-line AI tool (CLI adapter)
- Integrate an HTTP-based AI API (HTTP adapter)
- Understand the adapter pattern used in AI Counsel
- Debug or modify existing adapters
Architecture Overview
Base Classes
BaseCLIAdapter (adapters/base.py)
- Handles subprocess execution, timeout management, and error handling
- Provides
invoke()method that manages the full CLI lifecycle - Subclasses only implement
parse_output()for tool-specific parsing - Optional: Override
_adjust_args_for_context()for deliberation vs. non-deliberation behavior - Optional: Implement
validate_prompt_length()for length limits
BaseHTTPAdapter (adapters/base_http.py)
- Handles HTTP requests, retry logic with exponential backoff, and timeout management
- Uses
httpxfor async HTTP andtenacityfor retry logic - Retries on 5xx/429/network errors, fails fast on 4xx client errors
- Subclasses implement
build_request()andparse_response() - Supports environment variable substitution for API keys
Factory Pattern
The create_adapter() function in adapters/__init__.py:
- Maintains registries for CLI and HTTP adapters
- Creates appropriate adapter instances from config
- Handles backward compatibility with legacy config formats
Creating a CLI Adapter
Follow these 6 steps to add a new command-line AI tool:
Step 1: Create Adapter File
Create adapters/your_cli.py:
"""Your CLI tool adapter."""
from adapters.base import BaseCLIAdapter
class YourCLIAdapter(BaseCLIAdapter):
"""Adapter for your-cli tool."""
def parse_output(self, raw_output: str) -> str:
"""
Parse your-cli output to extract model response.
Args:
raw_output: Raw stdout from CLI tool
Returns:
Parsed model response text
"""
# Example: Strip headers and extract main response
lines = raw_output.strip().split("\n")
# Skip header lines (tool-specific logic)
start_idx = 0
for i, line in enumerate(lines):
if line.strip() and not line.startswith("Loading"):
start_idx = i
break
return "\n".join(lines[start_idx:]).strip()Optional: Override _adjust_args_for_context() if your CLI needs different args for deliberation vs. regular use:
def _adjust_args_for_context(self, is_deliberation: bool) -> list[str]:
"""
Adjust CLI arguments based on context.
Args:
is_deliberation: True if part of multi-model deliberation
Returns:
Adjusted argument list
"""
args = self.args.copy()
if is_deliberation:
# Remove flags that interfere with deliberation
if "--interactive" in args:
args.remove("--interactive")
else:
# Add flags for regular Claude Code work
if "--context" not in args:
args.append("--context")
return argsOptional: Implement validate_prompt_length() if your API has length limits:
MAX_PROMPT_CHARS = 100000 # Example: 100k char limit
def validate_prompt_length(self, prompt: str) -> bool:
"""
Validate prompt length against API limits.
Args:
prompt: The full prompt to validate
Returns:
True if valid, False if too long
"""
return len(prompt) <= self.MAX_PROMPT_CHARSStep 2: Update Config
Add your CLI to config.yaml:
adapters:
your_cli:
type: cli
command: "your-cli"
args: ["--model", "{model}", "{prompt}"]
timeout: 60 # Adjust based on your model's speedPlaceholder Variables:
{model}- Replaced with model identifier{prompt}- Replaced with full prompt text (including context)
Timeout Guidelines:
- Fast models (GPT-3.5): 30-60s
- Reasoning models (Claude Sonnet 4.5, GPT-5): 180-300s
- Local models: Varies, test and adjust
Step 3: Register Adapter
Update adapters/__init__.py:
# Add import at top
from adapters.your_cli import YourCLIAdapter
# Add to cli_adapters dict in create_adapter()
cli_adapters: dict[str, Type[BaseCLIAdapter]] = {
"claude": ClaudeAdapter,
"codex": CodexAdapter,
"droid": DroidAdapter,
"gemini": GeminiAdapter,
"llamacpp": LlamaCppAdapter,
"your_cli": YourCLIAdapter, # Add this line
}
# Add to __all__ export list
__all__ = [
"BaseCLIAdapter",
"BaseHTTPAdapter",
"ClaudeAdapter",
"CodexAdapter",
"DroidAdapter",
"GeminiAdapter",
"LlamaCppAdapter",
"YourCLIAdapter", # Add this line
# ... rest of exports
]Step 4: Update Schema
Update models/schema.py:
# Find the Participant class and add your CLI to the Literal type
class Participant(BaseModel):
cli: Literal[
"claude",
"codex",
"droid",
"gemini",
"llamacpp",
"your_cli" # Add this
]
model: strUpdate the MCP tool description in server.py:
# Find RECOMMENDED_MODELS and add your models
RECOMMENDED_MODELS = {
"claude": ["sonnet-4.5", "opus-4"],
"codex": ["gpt-5-codex", "gpt-4o"],
# ... other models
"your_cli": ["your-model-1", "your-model-2"], # Add this
}Step 5: Add Recommended Models
Update server.py::RECOMMENDED_MODELS with suggested models for your CLI:
RECOMMENDED_MODELS = {
# ... existing models
"your_cli": [
"your-fast-model", # For quick responses
"your-reasoning-model", # For complex analysis
],
}Step 6: Write Tests
Create unit tests in tests/unit/test_adapters.py:
import pytest
from adapters.your_cli import YourCLIAdapter
class TestYourCLIAdapter:
def test_parse_output_basic(self):
"""Test basic output parsing."""
adapter = YourCLIAdapter(
command="your-cli",
args=["--model", "{model}", "{prompt}"],
timeout=60
)
raw_output = "Loading...\n\nActual response text here"
result = adapter.parse_output(raw_output)
assert result == "Actual response text here"
assert "Loading" not in result
def test_parse_output_multiline(self):
"""Test multiline response parsing."""
adapter = YourCLIAdapter(
command="your-cli",
args=["--model", "{model}", "{prompt}"],
timeout=60
)
raw_output = "Header\n\nLine 1\nLine 2\nLine 3"
result = adapter.parse_output(raw_output)
assert "Line 1" in result
assert "Line 2" in result
assert "Line 3" in resultAdd integration tests in tests/integration/:
import pytest
from adapters.your_cli import YourCLIAdapter
@pytest.mark.integration
@pytest.mark.asyncio
async def test_your_cli_integration():
"""Test actual CLI invocation (requires your-cli installed)."""
adapter = YourCLIAdapter(
command="your-cli",
args=["--model", "{model}", "{prompt}"],
timeout=60
)
result = await adapter.invoke(
prompt="What is 2+2?",
model="your-default-model"
)
assert result
assert len(result) > 0
# Add assertions specific to your model's response formatCreating an HTTP Adapter
Follow these 6 steps to add a new HTTP API integration:
Step 1: Create Adapter File
Create adapters/your_adapter.py:
"""Your API adapter."""
from typing import Tuple
from adapters.base_http import BaseHTTPAdapter
class YourAdapter(BaseHTTPAdapter):
"""
Adapter for Your AI API.
API reference: https://docs.yourapi.com
Default endpoint: https://api.yourservice.com
Example:
adapter = YourAdapter(
base_url="https://api.yourservice.com",
api_key="your-key",
timeout=120
)
result = await adapter.invoke(prompt="Hello", model="your-model")
"""
def build_request(
self, model: str, prompt: str
) -> Tuple[str, dict[str, str], dict]:
"""
Build API request components.
Args:
model: Model identifier (e.g., "your-model-v1")
prompt: The prompt to send
Returns:
Tuple of (endpoint, headers, body):
- endpoint: URL path (e.g., "/v1/chat/completions")
- headers: Request headers dict
- body: Request body dict (will be JSON-encoded)
"""
endpoint = "/v1/chat/completions"
headers = {
"Content-Type": "application/json",
}
# Add authentication if API key provided
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
# Build request body (adapt to your API format)
body = {
"model": model,
"messages": [
{"role": "user", "content": prompt}
],
"temperature": 0.7,
"stream": False,
}
return (endpoint, headers, body)
def parse_response(self, response_json: dict) -> str:
"""
Parse API response to extract model output.
Your API response format:
{
"id": "resp-123",
"model": "your-model",
"choices": [
{
"message": {
"role": "assistant",
"content": "The model's response text"
}
}
]
}
Args:
response_json: Parsed JSON response from API
Returns:
Extracted model response text
Raises:
KeyError: If response format is unexpected
"""
try:
return response_json["choices"][0]["message"]["content"]
except (KeyError, IndexError) as e:
raise KeyError(
f"Unexpected API response format. "
f"Expected 'choices[0].message.content', "
f"got keys: {list(response_json.keys())}"
) from eStep 2: Update Config
Add your HTTP adapter to config.yaml:
adapters:
your_adapter:
type: http
base_url: "https://api.yourservice.com"
api_key: "${YOUR_API_KEY}" # Environment variable substitution
timeout: 120
max_retries: 3Configuration Options:
base_url: Base URL for API (no trailing slash)api_key: API key (use${ENV_VAR}for environment variables)timeout: Request timeout in secondsmax_retries: Max retry attempts for 5xx/429/network errors (default: 3)headers: Optional default headers dict
Environment Variable Substitution:
- Pattern:
${VAR_NAME}in config - Substituted at runtime from environment
- Secure: Keeps secrets out of config files
Step 3: Register Adapter
Update adapters/__init__.py:
# Add import at top
from adapters.your_adapter import YourAdapter
# Add to http_adapters dict in create_adapter()
http_adapters: dict[str, Type[BaseHTTPAdapter]] = {
"ollama": OllamaAdapter,
"lmstudio": LMStudioAdapter,
"openrouter": OpenRouterAdapter,
"your_adapter": YourAdapter, # Add this line
}
# Add to __all__ export list
__all__ = [
"BaseCLIAdapter",
"BaseHTTPAdapter",
# ... CLI adapters
"OllamaAdapter",
"LMStudioAdapter",
"OpenRouterAdapter",
"YourAdapter", # Add this line
"create_adapter",
]Step 4: Set Environment Variables
If your adapter uses API keys:
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
export YOUR_API_KEY="your-actual-api-key-here"
# Or set temporarily for testing
export YOUR_API_KEY="test-key" && python server.pyStep 5: Write Tests
Create unit tests with VCR for HTTP response recording in tests/unit/test_your_adapter.py:
import pytest
import vcr
from adapters.your_adapter import YourAdapter
# Configure VCR for recording HTTP interactions
vcr_instance = vcr.VCR(
cassette_library_dir="tests/fixtures/vcr_cassettes/your_adapter/",
record_mode="once", # Record once, then replay
match_on=["method", "scheme", "host", "port", "path", "query"],
filter_headers=["authorization"], # Hide API keys in recordings
)
class TestYourAdapter:
def test_build_request_basic(self):
"""Test request building without API key."""
adapter = YourAdapter(
base_url="https://api.example.com",
timeout=60
)
endpoint, headers, body = adapter.build_request(
model="test-model",
prompt="Hello"
)
assert endpoint == "/v1/chat/completions"
assert headers["Content-Type"] == "application/json"
assert body["model"] == "test-model"
assert body["messages"][0]["content"] == "Hello"
def test_build_request_with_auth(self):
"""Test request building with API key."""
adapter = YourAdapter(
base_url="https://api.example.com",
api_key="test-key",
timeout=60
)
endpoint, headers, body = adapter.build_request(
model="test-model",
prompt="Hello"
)
assert "Authorization" in headers
assert headers["Authorization"] == "Bearer test-key"
def test_parse_response_success(self):
"""Test parsing successful response."""
adapter = YourAdapter(
base_url="https://api.example.com",
timeout=60
)
response = {
"id": "resp-123",
"choices": [
{
"message": {
"role": "assistant",
"content": "Hello! How can I help?"
}
}
]
}
result = adapter.parse_response(response)
assert result == "Hello! How can I help?"
def test_parse_response_missing_field(self):
"""Test error handling for malformed response."""
adapter = YourAdapter(
base_url="https://api.example.com",
timeout=60
)
response = {"id": "resp-123"} # Missing choices
with pytest.raises(KeyError) as exc_info:
adapter.parse_response(response)
assert "Unexpected API response format" in str(exc_info.value)
@pytest.mark.asyncio
@vcr_instance.use_cassette("invoke_basic.yaml")
async def test_invoke_with_vcr(self):
"""Test full invoke() with VCR recording."""
adapter = YourAdapter(
base_url="https://api.example.com",
api_key="test-key",
timeout=60
)
result = await adapter.invoke(
prompt="What is 2+2?",
model="test-model"
)
assert result
assert len(result) > 0Optional: Add integration tests (requires running service):
@pytest.mark.integration
@pytest.mark.asyncio
async def test_your_adapter_live():
"""Test with live API (requires service running and API key)."""
import os
api_key = os.getenv("YOUR_API_KEY")
if not api_key:
pytest.skip("YOUR_API_KEY not set")
adapter = YourAdapter(
base_url="https://api.yourservice.com",
api_key=api_key,
timeout=120
)
result = await adapter.invoke(
prompt="What is the capital of France?",
model="your-model"
)
assert "Paris" in result or "paris" in resultStep 6: Test with Deliberation
Create a simple test script to verify integration:
"""Test your adapter in a deliberation context."""
import asyncio
from adapters.your_adapter import YourAdapter
async def test():
adapter = YourAdapter(
base_url="https://api.yourservice.com",
api_key="your-key",
timeout=120
)
# Test basic invocation
result = await adapter.invoke(
prompt="What is 2+2?",
model="your-model"
)
print(f"Response: {result}")
# Test with context (simulating Round 2+ in deliberation)
result_with_context = await adapter.invoke(
prompt="Do you agree with this answer?",
model="your-model",
context="Previous participant said: 2+2 equals 4"
)
print(f"Response with context: {result_with_context}")
if __name__ == "__main__":
asyncio.run(test())Key Design Principles
DRY (Don't Repeat Yourself)
- Common logic in base classes (
BaseCLIAdapter,BaseHTTPAdapter) - Tool-specific logic in concrete adapters
- Only implement what's unique to your adapter
YAGNI (You Aren't Gonna Need It)
- Build only what's needed for basic integration
- Don't add features until they're required
- Start simple, extend as needed
TDD (Test-Driven Development)
- Write tests first (red)
- Implement adapter (green)
- Refactor for clarity (refactor)
Type Safety
- Use type hints throughout
- Pydantic validation where applicable
- Let mypy catch errors early
Error Isolation
- Adapter failures don't halt deliberations
- Other participants continue if one fails
- Graceful degradation with informative errors
Common Patterns
CLI Adapter with Custom Parsing
def parse_output(self, raw_output: str) -> str:
"""Parse CLI output with multiple header formats."""
lines = raw_output.strip().split("\n")
# Skip all header lines until we find content
content_started = False
result_lines = []
for line in lines:
# Detect header patterns
if any(marker in line.lower() for marker in ["loading", "initializing", "version"]):
continue
# Detect content start
if line.strip():
content_started = True
if content_started:
result_lines.append(line)
return "\n".join(result_lines).strip()HTTP Adapter with Streaming Support
def build_request(self, model: str, prompt: str) -> Tuple[str, dict, dict]:
"""Build request with optional streaming."""
# Note: BaseHTTPAdapter doesn't support streaming yet
# This is for future extension
body = {
"model": model,
"prompt": prompt,
"stream": False, # Keep False for now
}
return ("/api/generate", {"Content-Type": "application/json"}, body)Environment Variable Validation
def __init__(self, base_url: str, api_key: str = None, **kwargs):
"""Initialize with API key validation."""
if not api_key:
raise ValueError(
"API key required. Set YOUR_API_KEY environment variable or "
"provide api_key parameter."
)
super().__init__(base_url=base_url, api_key=api_key, **kwargs)Testing Guidelines
Unit Tests (Required)
- Test
parse_output()with various input formats - Test
build_request()with different parameters - Test
parse_response()with success and error cases - Mock external dependencies (no actual API calls)
- Fast execution (< 1s total)
Integration Tests (Optional)
- Test actual CLI/API invocation
- Requires tool installed or service running
- Mark with
@pytest.mark.integration - May be slow, use sparingly
VCR for HTTP Tests
- Record real HTTP interactions once
- Replay from cassettes in CI/CD
- Filter sensitive data (API keys, auth tokens)
- Store in
tests/fixtures/vcr_cassettes/your_adapter/
E2E Tests (Optional)
- Full deliberation with your adapter
- Mark with
@pytest.mark.e2e - Very slow, expensive (real API calls)
- Use for final validation only
Troubleshooting
CLI Adapter Issues
Problem: Timeout errors
- Solution: Increase timeout in config.yaml
- Reasoning models need 180-300s, not 60s
Problem: Output parsing fails
- Solution: Print
raw_outputand examine format - Each CLI has unique output format, adjust parsing logic
Problem: Hook interference (Claude CLI)
- Solution: Add
--settings '{"disableAllHooks": true}'to args - User hooks can interfere with deliberation invocations
HTTP Adapter Issues
Problem: Connection refused
- Solution: Verify base_url and service is running
- Check with
curl $BASE_URL/healthor similar
Problem: 401 Authentication errors
- Solution: Verify API key is set:
echo $YOUR_API_KEY - Check environment variable substitution in config
Problem: 400 Bad Request errors
- Solution: Log request body, check API documentation
- Common issue: Wrong field names or missing required fields
Problem: Retries exhausted
- Solution: Check if service is healthy
- 5xx errors trigger retries, 4xx do not (by design)
General Issues
Problem: Adapter not found
- Solution: Verify registration in
adapters/__init__.py - Check both import and dict addition
Problem: Schema validation errors
- Solution: Add CLI name to
Participant.cliLiteral inmodels/schema.py - MCP won't accept unlisted CLI names
Reference Files
Essential Files
adapters/base.py- CLI adapter base classadapters/base_http.py- HTTP adapter base classadapters/__init__.py- Adapter factory and registrymodels/config.py- Configuration schemamodels/schema.py- Data models and validation
Example Adapters
adapters/claude.py- CLI adapter with context-aware argsadapters/gemini.py- CLI adapter with length validationadapters/ollama.py- HTTP adapter for local APIadapters/lmstudio.py- HTTP adapter with OpenAI-compatible format
Test Examples
tests/unit/test_adapters.py- CLI adapter unit teststests/unit/test_ollama.py- HTTP adapter unit tests with VCRtests/integration/test_cli_adapters.py- CLI integration tests
Next Steps After Creating Adapter
- Update CLAUDE.md if you added new patterns or gotchas
- Add to RECOMMENDED_MODELS in
server.pywith usage guidance - Document API quirks in adapter docstrings for future maintainers
- Test in real deliberation with 2-3 participants
- Monitor transcript for response quality and voting behavior
- Share findings if you discover best practices for your model
Quick Reference
CLI Adapter Checklist
- Create
adapters/your_cli.pywithparse_output() - Add to
config.yamlwith command, args, timeout - Register in
adapters/__init__.py(import + dict + export) - Add to
Participant.cliLiteral inmodels/schema.py - Add to
RECOMMENDED_MODELSinserver.py - Write unit tests for
parse_output() - Optional: Write integration test with real CLI
HTTP Adapter Checklist
- Create
adapters/your_adapter.pywithbuild_request()andparse_response() - Add to
config.yamlwith base_url, api_key, timeout - Register in
adapters/__init__.py(import + dict + export) - Set environment variables for API keys
- Write unit tests with VCR cassettes
- Test with simple script before full deliberation
Common Commands
# Run unit tests for new adapter
pytest tests/unit/test_your_adapter.py -v
# Run with coverage
pytest tests/unit/test_your_adapter.py --cov=adapters.your_adapter
# Format and lint
black adapters/your_adapter.py && ruff check adapters/your_adapter.py
# Test integration (requires tool/service)
pytest tests/integration/ -v -m integration
# Record VCR cassette (first run, then commit)
pytest tests/unit/test_your_adapter.py -vAdditional Resources
- CLAUDE.md - Full project documentation with architecture details
- MCP Protocol - https://modelcontextprotocol.io/introduction
- Pydantic Docs - https://docs.pydantic.dev/latest/
- httpx Docs - https://www.python-httpx.org/
- VCR.py Docs - https://vcrpy.readthedocs.io/
This skill encodes the institutional knowledge for extending AI Counsel with new model integrations. Follow the patterns, write tests, and maintain backward compatibility.
In this repository6 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- config-schema-migratorSkill
- convergence-debuggerSkill
- decision-graph-analyzerSkill
- deliberation-testerSkill
- mcp-server-enhancementSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

