canact
Probe an LLM against this host's tools and return a capability card the host can use: how many tools to send, which edit format to pick, whether to enable XML fallback, and whether to wrap JSON in a repair layer.
Catalog flags (supports_function_calling: true, context: 128k)
are priors. canact spends seconds of real prompts on this model, this
template, and this tool schema, then writes host policy.
- Repo: canact/canact
- Crate: crates.io/crates/canact
- API docs: docs.rs/canact
- Releases: GitHub Releases
Default features are empty. A library pin uses
default-features = false plus runtime. The CLI and MCP server
need --features cli.
Read the card defines host, host policy, the fixed probe tool set, the context ladder, and the envelope.
Getting started
Install
cargo install canact --locked --features cli
Prebuilt archives ship on GitHub Releases (macOS, Linux, Windows x64):
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/canact/canact/releases/latest/download/canact-installer.sh | sh
brew trust canact/tap
brew install canact/tap/canact
Windows (Scoop, x64):
scoop bucket add canact https://github.com/canact/scoop-bucket
scoop install canact/canact
Library (runtime only, no CLI):
canact = { version = "0.10", default-features = false, features = ["runtime"] }
MSRV is Rust 1.95.
First probe
canact probe --provider ollama --model llama3.2:3b --cheap --json
The built-in Ollama URL is http://127.0.0.1:11434/v1. That string
is the crate constant OLLAMA_BASE_URL. Nothing reads an environment
variable of that name. Pass --base-url to use a different URL.
--cheap is --suite=policy (host-policy fields, 4k ladder).
--full adds sequencing and the 8k/16k ladder. --suite=all
adds diagnostics.
Cloud hosts need a key before any HTTP call. The first match in
this table wins. --api-key is visible in shell history and the
process list; prefer an env var. canact probe --no-login skips
stored Grok and Claude Code logins. Env vars still apply.
canact probe --dry-run prints the provider, redacted base URL,
suite, and probe names, then exits. It does not read a login, the
cache, or the network.
canact probe --fail-on weak exits 2 when a finished row is Weak.
--fail-on degraded also exits 2 for Medium. A skipped row or a
probe error does not count. An unknown value exits 1.
| Provider label | Default URL when --base-url is omitted | What is tried, first match wins | Stored login |
|---|---|---|---|
| empty | https://api.openai.com/v1, unless a key selects the xAI, Anthropic, or OpenRouter host | --api-key, else OPENAI_API_KEY, else XAI_API_KEY then GROK_API_KEY then Grok login, else ANTHROPIC_AUTH_TOKEN then ANTHROPIC_API_KEY then Claude Code login, else OPENROUTER_API_KEY | Grok login unless --api-key, OPENAI_API_KEY, OPENROUTER_API_KEY, XAI_API_KEY, or GROK_API_KEY is set. An Anthropic env key does not skip it. Claude Code login runs only when nothing earlier matched. --no-login skips only those two helpers. |
xai, grok, api.x.ai, x.ai | https://api.x.ai/v1 | --api-key, else XAI_API_KEY, else GROK_API_KEY, else Grok login. Ignores OPENAI_API_KEY. | ~/.grok/auth.json. A named xAI provider still loads it when OPENAI_API_KEY is set. |
grok-build, xai-grok-build, cli-chat-proxy.grok.com, and the -messages pair | https://cli-chat-proxy.grok.com/v1 | Same xAI pack | Same Grok login |
claude, anthropic, api.anthropic.com | https://api.anthropic.com/v1 | --api-key, else ANTHROPIC_AUTH_TOKEN, else ANTHROPIC_API_KEY, else Claude Code. Ignores OpenAI and xAI. | Claude Code login |
openrouter, openrouter.ai | https://openrouter.ai/api/v1 | --api-key, else OPENAI_API_KEY, else OPENROUTER_API_KEY. No stored login. | none |
groq, api.groq.com | https://api.groq.com/openai/v1 | --api-key, else GROQ_API_KEY. No stored login. | none |
amazon-bedrock, bedrock | https://bedrock-runtime.us-east-1.amazonaws.com | --api-key, else AWS_BEARER_TOKEN_BEDROCK. No stored login. | none |
openai-codex, codex | https://api.openai.com/v1 | --api-key, else OPENAI_API_KEY. No stored login. | none |
ollama, localhost, 127.0.0.1, ::1, 0.0.0.0 | http://127.0.0.1:11434/v1 (this constant, not an environment variable) | none | none |
lmstudio | http://127.0.0.1:1234/v1 | none | none |
vllm | http://127.0.0.1:8000/v1 | none | none |
Auth, a missing model, and connect failures abort the suite.
--json prints the host-policy envelope. Field definitions and
the rest of that object are in Read the card.
Dry runs that do not call a model live in the repo
examples/
directory.
Read the card
The first command is:
canact probe --provider ollama --model llama3.2:3b --cheap --json
The built-in Ollama URL is http://127.0.0.1:11434/v1. That string
is the crate constant OLLAMA_BASE_URL. Nothing reads an environment
variable of that name. Pass --base-url to use a different URL.
Definitions
A host is the program that will call the model. That is the CLI user,
Aider, Cline, or a ProbeClient. The card is for that host.
Host policy is the set of envelope fields a host branches on: how many tools to send, which edit format to pick, whether to fall back to XML, and whether to repair JSON. It is not a rank of the model.
The fixed probe tool set is what canact sends. tool_selection sends
eight names: read_file, edit_file, doc_set, search,
run_command, list_dir, md_replace_section, and write_file.
parallel_tool_scale sends read_file only. That set is not the
caller's schema. This page does not describe a host-supplied tool
list.
The context ladder climbs 4096, then 8192, then 16384. --cheap and
--suite=policy stop after 4096. --full and --suite=all continue
through 8192 and 16384.
canact probe --json prints the host-policy envelope. The cache file
is probes.json. Entries last 30 days. The cache key includes the
suite version (currently 97). fromCache is true only when this
print came from that file. cacheable means the result may be
stored. A context-window rejection keeps the last passing ladder
rung, and the card stays cacheable. A provider safety stop is not
a score, so the card is not stored. A malformed tool call is a
completed Weak. Do not paste the envelope over probes.json.
Envelope fields
The Getting started page and the README host-policy table list the fields a host branches on. The envelope also always includes these:
| Field | Meaning |
|---|---|
model | Model id that was probed |
provider | Provider label |
overall | Lowest measured level among tool calling, JSON output, and instruction following (weak, medium, or strong). weak when none of those three were measured. |
canUseTools | Native or XML tool calling completed Medium or stronger |
supportsVision | The vision probe completed Medium or stronger |
toolSelectionStatus | completed, skipped, unprobed, or error for tool_selection. Read this before treating maxTools 10 as measured Weak. |
effectiveContextTokens | Measured usable context, in tokens, or JSON null until a suite writes it |
probedContextFloor | Highest passing ladder rung when the climb is incomplete, or JSON null |
skipExpensive | True on the policy suite |
suite | policy, full, or all |
advertisedContextTokens | Catalog or flag prior, or JSON null |
probedAt | Unix seconds when the card was stored |
scoreScale | min 0.0, max 1.0, strongMin 0.8, mediumMin 0.4 |
probes | Policy dimensions. multiTurnTaskSequencing is present on policy with status skipped. |
diagnostics | Empty on policy and full. Populated on --suite=all. |
maxOutputTokens is inserted only when it was measured. It is
omitted otherwise. constraintPlacement is inserted only on
--suite=all, and only when a placement was measured. Both fields
are already in the README host-policy table.
On a policy run, agentLoop is JSON null and
probes.multiTurnTaskSequencing.status is skipped. A full suite
that completes sequencing sets agentLoop to full, assisted,
or single.
Exit code 2
When the model cannot use tools, canact prints the envelope on stdout first, then this line on stderr, then exits 2:
error: cannot use tools (native and XML both failed to complete)
JSON on stdout is still valid. A shell with set -e stops on that
exit code.
Library
ProbeRunner stays on feature runtime. ProbeRunner::new is the
full suite and paid concurrency (64). ProbeRunner::new_throttled
is the policy suite and free concurrency (3). The README example
uses new_throttled. .suite() changes the suite only.
.throttled() changes concurrency only. .cheap() and .full()
set both.
A snippet that constructs OpenAiCompatClient needs features
runtime and openai. The key below is None because this URL is
local Ollama. Do not put a raw key in the source.
canact = { version = "0.10", default-features = false, features = ["runtime", "openai"] }
#![allow(unused)] fn main() { use canact::{CatalogPriors, OpenAiCompatClient}; let _client = OpenAiCompatClient::new( "http://127.0.0.1:11434/v1", None, "llama3.2:3b", "ollama", CatalogPriors::default(), ) .expect("client"); }
Matrix
canact matrix prints pretty JSON. Each cell is pass, degraded,
fail, or skipped. There is no --json flag and no human table.
--provider is optional. Omit it to include every cached provider.
The command does not call a model.
MCP
canact mcp --api-key-env XAI_API_KEY --base-url http://127.0.0.1:11434/v1
--api-key-env is the name of an environment variable. The tool
cannot name a different variable, and it must not receive a raw key.
--base-url is the probe URL. The tool cannot replace it.
--allow-base-url lets the tool pass a base URL that is not a
loopback host. --allow-cache lets the tool pass a cache path
outside the default cache directory.
Other commands
The credential table is in Getting started.
canact probe --no-login skips stored Grok and Claude Code logins.
Environment variables and --api-key still apply. See
#288.
canact cache path prints the cache file. canact cache list
prints the stored rows. This page does not copy that output. See
#287.
canact export --all writes the Aider pair and cline.modelinfo.json
and leaves stdout empty. The file list stays in the
README. See
#289.
canact probe --dry-run prints the provider, redacted base URL,
suite, and probe names, then exits. It does not read a login, the
cache, or the network. See
#290.
canact probe --fail-on weak exits 2 when a finished row is Weak.
--fail-on degraded also exits 2 for Medium. A skipped row or a
probe error does not count. An unknown value exits 1.
Architecture
canact is one crate: a library plus an optional canact binary.
src/lib.rs public types and re-exports
cache probes.json (30-day TTL, not the CLI envelope)
endpoint provider URL and host-family hints
types CapabilityProfile, levels, host-policy fields
error Auth / NotFound abort; Transient stays session-local
a window rejection keeps the last ladder rung
a safety stop is Transient and is not cached
a malformed tool call is a completed Weak
runtime ProbeClient, ProbeRunner, graders
adapters/openai OpenAI-compat, Anthropic, Ollama, xAI
src/bin/canact.rs CLI, export, and `canact mcp` (feature `cli`)
export Aider / Cline overlays
Default features are empty. A library pin uses default-features = false plus runtime. The binary needs --features cli.
The host-policy card is the product: max_tools, edit-format
ladder, XML fallback, and JSON repair. Graders live next to the
probes they score. Types and CLI goldens are the source of truth.
Tests: cargo test --locked --features runtime --lib. Local gate:
make check.
Contributing
See CONTRIBUTING.md in the repository.
Local gate: make check. Commits need a DCO sign-off
(git commit -s). PR titles use conventional types
(feat, fix, perf, docs, chore, ci).
Security
See SECURITY.md for how to report a vulnerability.
Private reports go through
GitHub Security Advisories.
Release archives include Cosign .sigstore.json and SLSA
.intoto.jsonl assets. The Release workflow also uploads a CycloneDX SBOM,
canact-sbom.cdx.json. Git release tags are GPG-signed
annotated tags (git verify-tag vX.Y.Z).