Multi-LLM council protocol SDK. Fan out code reviews and design questions to multiple LLM providers, then classify findings by consensus.
pip install star-chamberOr with uv:
uv add star-chamberCreate ~/.config/star-chamber/providers.json:
{
"providers": [
{"provider": "openai", "model": "gpt-4o", "api_key": "${OPENAI_API_KEY}"},
{"provider": "anthropic", "model": "claude-sonnet-4-20250514", "api_key": "${ANTHROPIC_API_KEY}"}
],
"timeout_seconds": 90,
"consensus_threshold": 2
}API keys can be literal values or ${ENV_VAR} references that are resolved at runtime.
When several council members share one provider — for example, multiple models behind an OpenAI-compatible gateway such as OpenRouter — give each entry a unique display_name. It becomes that member's identity in council output (consensus classification, flagged_by, quality ratings, individual issues) and is matched by --provider filters. Without it, same-provider members collapse into a single vote and unanimous consensus becomes unreachable:
{
"providers": [
{"provider": "openrouter", "display_name": "gpt-5.2", "model": "openai/gpt-5.2", "api_key": "${OPENROUTER_API_KEY}"},
{"provider": "openrouter", "display_name": "gemini-3.1-pro", "model": "google/gemini-3.1-pro-preview", "api_key": "${OPENROUTER_API_KEY}"},
{"provider": "openrouter", "display_name": "grok-4.3", "model": "x-ai/grok-4.3", "api_key": "${OPENROUTER_API_KEY}"}
],
"consensus_threshold": 2
}In the JSON output, each provider response carries both the underlying provider (the routing target) and this display_name, and the display name is the key used throughout the aggregated result — providers_used, quality_ratings, individual_issues, flagged_by, and failed_providers. A member with no display_name falls back to the provider name.
Instead of managing API keys per provider, you can route all non-local providers through Otari, Mozilla AI's OpenAI-compatible LLM gateway, by adding a top-level otari object:
{
"providers": [
{"provider": "openai", "model": "openai:gpt-4o"},
{"provider": "anthropic", "model": "anthropic:claude-sonnet-4-20250514"}
],
"otari": {
"api_base": "https://your-gateway.example/v1",
"api_key": "${OTARI_API_KEY}"
},
"timeout_seconds": 90,
"consensus_threshold": 2
}In Otari mode, Otari — not the SDK — picks the upstream provider, so the per-provider provider field becomes a label. Otari expects the model field to use a provider:model prefix such as "openai:gpt-4o"; consult Otari's documentation for its model-naming convention.
api_base and api_key may be omitted from the config; when omitted, the SDK's OtariProvider auto-detects credentials from its own env vars: OTARI_API_KEY for the hosted otari.ai platform (Authorization: Bearer, matching otari.ai's docs), or GATEWAY_API_KEY for a self-hosted gateway (Otari-Key header). OTARI_AI_TOKEN is also accepted as a platform-token alias. Both fields also support ${ENV_VAR} references.
Providers marked "local": true always bypass Otari and continue to use their own api_base.
Override the config path with the STAR_CHAMBER_CONFIG environment variable.
star-chamber review src/auth.py src/db.pystar-chamber ask "Should we use Redis or Memcached for session storage?"--provider, -p Provider to include (repeatable)
--config Path to providers.json
--timeout Per-provider timeout in seconds
--context-file File containing project context to include in the prompt
--council-context File containing prior council round feedback (debate mode)
--format Output format: text or json
--output Write JSON result to file
star-chamber list-providersThe SDK ships the council protocol specification as package data.
# List available schemas.
star-chamber schema list
# Print a specific schema.
star-chamber schema code-review-resultfrom star_chamber import run_council_sync, CouncilConfig, ProviderConfig
config = CouncilConfig(
providers=(
ProviderConfig(provider="openai", model="gpt-4o"),
ProviderConfig(provider="anthropic", model="claude-sonnet-4-20250514"),
),
timeout_seconds=90,
consensus_threshold=2,
)
# Code review.
result = run_council_sync(
files={"auth.py": open("auth.py").read()},
config=config,
mode="code-review",
)
print(result.summary)
for issue in result.consensus_issues:
print(f" [{issue.severity}] {issue.location}: {issue.description}")
# Design question.
result = run_council_sync(
prompt="Should we use a monorepo or polyrepo?",
config=config,
mode="design-question",
)
print(result.consensus_recommendation)import asyncio
from star_chamber import run_council
result = asyncio.run(run_council(
files={"auth.py": open("auth.py").read()},
mode="code-review",
))from star_chamber import get_schema, list_schemas
# List available schema names.
names = list_schemas()
# Get a specific schema as a JSON string.
schema_json = get_schema("code-review-result")Issues from multiple providers are grouped by file, line proximity (within 5 lines), and category, then classified as:
- Consensus -- all providers agree.
- Majority -- two or more providers agree, but not all.
- Individual -- flagged by a single provider.
Results are sorted by severity within each bucket.
Apache-2.0