convene
Run Claude Code headlessly as a local inference layer — named experts, multi-turn sessions, and bounded concurrency, on your own machine under your own login.
claude -p is already a capable inference endpoint once you strip the coding
agent off it. What it lacks is everything around the call: a way to name and
reuse a configuration, conversations that survive a process, a concurrency
policy grounded in measurement rather than folklore, and a check that tells you
which account is about to be billed. That is what this adds.
pip install git+https://github.com/YashShelar007/convene@main
convene doctor # what's set up, and whose account pays
from convene import ask, ask_json
ask("Name the capital of France in one word.")
# 'Paris'
ask_json("Extract: Ada Lovelace, born 1815.", {
"type": "object",
"properties": {"name": {"type": "string"}, "born": {"type": "integer"}},
"required": ["name", "born"],
"additionalProperties": False,
})
# {'name': 'Ada Lovelace', 'born': 1815}
Zero dependencies. Python 3.11+. The only requirement is a working Claude Code install you have already logged into.
Scope, and a word about terms
This runs the first-party claude binary on your machine, under your own
interactive login. It is not a proxy, it exposes no network endpoint, and it
never extracts or replays your credentials. Those three properties are
deliberate and this project intends to keep them.
They matter because Anthropic's Consumer Terms §3 restricts
…access[ing] the Services through automated or non-human means, whether through a bot, script, or otherwise
with the carve-out "except when you are accessing our Services via an
Anthropic API Key or where we otherwise explicitly permit it." Claude Code's
headless mode is squarely inside that carve-out: Anthropic
ships and documents it, and states
plainly that non-bare -p uses your subscription login.
What is not inside it is turning a subscription into an API for other software. Between January and April 2026 Anthropic blocked exactly that — OpenClaw, OpenCode, Cline, Aider and the OpenAI-compatible proxies. The common factor in every tool that got cut off was exposing the subscription as an endpoint to other programs.
So convene will not grow an HTTP server, an OpenAI-compatible shim, or a credential-forwarding mode, and pull requests adding them will be declined. This is a scope decision, not legal advice; read the terms yourself and decide what you are comfortable with. Locality is not the test — the carve-out is.
What this measured that the folklore gets wrong
Every number is reproducible with convene bench; the full tables, with
sample sizes and build versions, are in FINDINGS.md.
1. claude -p uses your subscription. --bare is the one that doesn't.
The widely repeated claim that -p "bypasses OAuth and requires an API key" is
false, and Anthropic's own docs say so: it is --bare that
"doesn't use your subscription login." convene therefore never passes
--bare, despite it being the faster and officially recommended flag for
scripts.
2. Prompt caching works, and is worth ~9.5x. The same 4.4k-token system prompt across three separate processes:
cache_creation |
cache_read |
cost | |
|---|---|---|---|
| cold | 4434 | 0 | $0.02670 |
| warm | 243 | 4191 | $0.00281 |
The cache is server-side and survives process death. This inverts the usual advice: a long, stable system prompt is the cheap option. It is the whole reason experts exist here.
3. Concurrency goes much further than "about 5." One burst per row, no retries:
| n | wall | errors |
|---|---|---|
| 1 | 6.3s | 0 |
| 5 | 6.8s | 0 |
| 10 | 9.9s | 0 |
| 20 | 12.1s | 0 |
~10x the throughput of serial calls, zero rate-limit errors. 20 is the largest burst tested, not a discovered ceiling. The default here is 12.
4. Multi-turn works, two ways. --session-id/--resume gives durable
conversations that outlive the process; --input-format stream-json gives a
hot process that holds one open. On a three-turn conversation the hot process
was 4.3x cheaper, because resume re-establishes context every turn.
Experts
An expert bundles everything that makes a call a kind of call, behind a name.
# experts.toml
[triage]
description = "Sorts inbound support mail into a queue, with a confidence score"
model = "claude-sonnet-5"
effort = "low"
system_prompt = '''
You are a support triage specialist. You read one inbound customer message
and assign it to exactly one queue.
...the full rubric, worked examples, and edge cases...
'''
[triage.json_schema]
type = "object"
required = ["queue", "confidence", "rationale"]
additionalProperties = false
# ...
from convene import load_experts, ask_expert
load_experts("experts.toml")
r = ask_expert("triage", ticket_text)
r.structured_output # {'queue': 'security', 'confidence': 0.98, ...}
Because of finding 2, the registry pushes you toward long prompts. Put the whole rubric in the system prompt and only the per-item data in the user prompt: you get better output and a smaller bill from the same change.
Two ways to get that wrong, both checked:
convene experts lint
- A prompt too short to cache — every call pays full price.
- A prompt interpolated per call (
{},%s) — changing the prefix misses the cache every single time. Per-call data belongs in the user prompt.
Running one over a batch
convene run --expert triage --in tickets.jsonl --out results.jsonl
Bounded concurrency, resumable (a rerun skips ids already in --out), and it
warms the cache with one serial call before releasing the rest — so the batch
pays to create the cache entry once instead of once per call.
5/5 ok, 0 failed | 14.9s wall | $0.0322 | 4/5 warm cache reads
Asking several at once
from convene import consult
await consult(["security", "performance", "style"], diff_text)
# {'security': Result(...), 'performance': Result(...), 'style': Result(...)}
A failing expert returns its exception rather than sinking the panel — the useful thing about asking five specialists is usually the four who answered.
Sessions
Two kinds, because they fail differently.
from convene import Session, LiveSession
# Durable: on disk, resumable from another process, another day, another cwd.
s = Session(system_prompt=RUBRIC)
s.ask("Here is the first document.")
print(s.id) # save this
later = Session.resume(saved_id, system_prompt=RUBRIC)
# Hot: one long-lived process. No per-turn startup, 4.3x cheaper, dies with
# the process.
with LiveSession(system_prompt=RUBRIC) as live:
live.ask("First document.")
live.ask("Now compare it to the second.")
For many concurrent conversations, SessionPool keeps n hot processes and
hands them out:
async with SessionPool(size=4, system_prompt=RUBRIC) as pool:
async with pool.acquire() as session:
turn = await session.ask("...")
A pooled session keeps its history between checkouts — that is the point when
you want shared accumulated context, and a bug when you don't. Pass
reset_between=True for a fresh process per checkout.
Cost gotcha. The two paths report
total_cost_usddifferently — per call for durable sessions, cumulative for live ones. Summing the raw field across a live session triple-counts.Turn.cost_usdis always the incremental figure; the raw one stays onturn.result.cost_usd.
Which account am I billing?
The expensive failure mode is silent: an ANTHROPIC_API_KEY in the environment
outranks your subscription login, and the call succeeds identically while
billing API credits. claude-code#37686
reports $1,800 in two days from exactly this.
Two defences. convene strips ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN
from the subprocess environment on every subscription call, and:
convene doctor
[ok ] claude binary 2.1.237 (Claude Code)
[ok ] api key no ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN in the environment
[ok ] account you@example.com (max, via claude.ai)
[ok ] state dir ~/.convene
[ok ] live probe claude-sonnet-5 answered in 3.4s (in=282 out=4 cost=$0.00091)
In code, at the top of any batch job:
from convene import assert_account
assert_account("you@example.com") # raises on wrong account, or an API key takeover
The live probe is not decoration. Anthropic's docs say --bare "will become
the default for -p in a future release", and bare mode never reads OAuth. If
that lands, this whole approach stops working — the probe finds out with a real
call rather than guessing from a version string.
Auth modes
| Mode | Uses | Setup | Billed to |
|---|---|---|---|
LOGIN (default) |
the claude /login you already did |
none | your subscription |
SANDBOX_TOKEN |
its own long-lived token, isolated HOME |
convene setup-token |
your subscription |
API_KEY |
ANTHROPIC_API_KEY |
set the variable | API credits |
API_KEY works but is never selected implicitly — you have to name it.
What you don't get
convene is not a drop-in for the Messages API. Check here before promising a caller a feature.
| model choice | yes | aliases and full ids both resolve |
| system prompt | yes | replaces the agent prompt entirely |
| reasoning depth | yes | effort="low".."max" — the lever, not a token budget |
| JSON schema output | yes | enforced server-side, auto-retries bad JSON |
| per-call spend cap | yes | max_budget_usd |
| usage + cost reporting | yes | a list-price estimate, not a charge |
| multi-turn | yes | Session and LiveSession |
| prompt caching | automatic | no breakpoint control, but see finding 2 |
| model fallback | yes | fallback_model |
| images | conditional | file on disk, and allow tools="Read" for that call |
| streaming | not wired | the CLI supports it; this does not expose it yet |
max_tokens |
no | no such flag. Constrain via the prompt |
temperature / top_p |
no | also rejected by the Opus 5 / Sonnet 5 API |
stop_sequences |
no | |
| token counting endpoint | no | |
| Batch API (50% off) | no |
Latency is ~3–9s per call. If you need sub-second responses, max_tokens
control, or the Batch API, use the anthropic SDK with an API key — and say so
plainly rather than working around it here.
Prior art
This is a well-trodden idea and this repo is not the first crack at it. If a different shape suits you better:
| Project | Shape | Notes |
|---|---|---|
| Claude Agent SDK | first-party | Python and TypeScript. The supported path for agentic work. Start here unless you specifically want inference-shaped calls |
| RichardAtCT/claude-code-openai-wrapper | OpenAI-compatible server | What convene deliberately is not — see Scope above |
| dtzp555-max/ocp | OpenAI-compatible server | LAN auth, per-key quotas, response cache |
The difference convene is going for is not features, it is verified claims
— every number in FINDINGS.md carries its sample size and the
build it came from, and convene bench re-runs them on your machine.
Gotchas
- Auth failure returns
subtype: "success"and exit code 0.is_erroris the only reliable signal. Handled; don't "simplify" that check. - Never drop the lockdown flags. A plain
claude -pcosts ~88x more — verify on your own machine withconvene doctor --lockdown. - Never add
--bare. It forces API-key auth and never reads OAuth. - stdout is pure JSON; warnings go to stderr. Don't merge the streams.
result_textvsstructured_output. With a schema, readstructured_output— it is already a parsed dict.- A long session is a growing prompt. Input tokens climb every turn.
Migrating from claude_cli.py
This project began as a single file called claude_cli.py. That module still
exists as a shim, so from claude_cli import ask, ask_json, run_claude_cli_sync
keeps working, with a DeprecationWarning. It will be removed in 1.0.
| old | new |
|---|---|
run_claude_cli_sync(system_prompt=…, user_prompt=…, …) |
run_sync(Call(…)) |
run_claude_cli(...) |
await run(Call(…)) |
ClaudeCLIError |
ConveneError (still exported under the old name) |
ClaudeResult |
Result |
r.result_text, r.total_cost_usd |
r.text, r.cost_usd (old names still work) |
sandbox_ready() |
ready() |
./setup.sh |
convene setup-token |
State moved from ./data and ./logs to ~/.convene, overridable with
CONVENE_HOME.
Contributing
The most valuable contribution is usually a measurement, not a patch — see CONTRIBUTING.md for the standard every empirical claim has to meet, and ROADMAP.md for what is planned and what is deliberately out of scope.
MIT.
Metadata
Release files for convene 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| convene-0.1.0.tar.gz | 54.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| convene-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.7 kB
Release files / convene-0.1.0.tar.gz
| Download URL | convene-0.1.0.tar.gz |
|---|---|
| Size | 54.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
462df9b1fe1bdf4307e81cc36f1b160db6e5e2fce4e9153c8fa59b0b4eebc7af
|
|
BLAKE2b-256 checksum How to use checksums |
270992e9afaf781d6081a0dea3253cf41f699e6fdcb113f2a265501f4dca39c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency logRelease files / convene-0.1.0-py3-none-any.whl
| Download URL | convene-0.1.0-py3-none-any.whl |
|---|---|
| Size | 43.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d9d1f5c509bd72ff8ff5a52b08be530011ed430a1cef52d99a3ac916772c64f
|
|
BLAKE2b-256 checksum How to use checksums |
be193b4286a1534fa18d43554e4765c1f2e31d5426e3fd279c24edc534e173bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency log