Skip to main content

Chat

One command. The model runs on your key (BYOK), and the agent’s tools are webrain itself: the session spawns webrain mcp over stdio, so the model gets the same 18 webrain_* tools an MCP client would, named webrain-webrain_*. Nothing is reimplemented for chat — the child inherits your CDP_URL, vault and engine cache, so it drives whatever browser the CLI would.

Requirements

  • A release binary from this repository (webrain-linux-x86_64, webrain-macos-aarch64, webrain-windows-x86_64.exe, …). The chat feature is built into every published target, the two static musl builds included — those vendor OpenSSL 3 and compile it with musl-gcc, so the result stays a static binary.
  • Building from source needs Rust 1.94+ and --features chat:
Expect a large binary: the feature embeds the Copilot CLI runtime (~135 MB per platform, downloaded and SHA-256 verified at build time).

What each build target carries

CI compiles pdfium,chat on Linux for every PR, and on Linux, macOS, Windows and static musl for every push to main — the four shapes a release tag would otherwise be the first to try. The musl rows are the reason this table exists at all. chat reaches native-tls through the SDK’s network-forwarding, which is openssl-sys on Linux; a static target has no OpenSSL to link, so webrain-cli declares openssl with the vendored feature for target_env = "musl" only and the build compiles OpenSSL 3 from source with musl-gcc. The SDK itself supports the platform: its build script fetches copilot-linuxmusl-x64, not a glibc package. Building it needs musl-tools, perl, make, pkg-config and libssl-dev (the last one for the build-host copy of openssl-sys), and about 8 GB of RAM to compile the SDK crate — build with -j 1 if your machine is tighter than that. The container image is glibc (Debian) and takes features as a build arg:

Quick start

See what would be used before spending a token — this works in every build, with or without the feature, and never prints the key:

Provider resolution

First match wins:
  1. --provider, --model, --base-url, --key-from.
  2. WEBRAIN_CHAT_PROVIDER / _MODEL / _BASE_URL / _API_KEY.
  3. The ambient key chain, in this order:
--provider accepts any alias stem: openrouter, openai, anthropic, claude, gemini, google, fireworks, dashscope, qwen, groq, deepseek. A named provider uses only its own variables — naming groq while OPENAI_API_KEY is set is an error, not a silent mix-up.

A local server needs no key

Any OpenAI-compatible endpoint works. Point the base url at it and a key is optional:

Flags

What it cost

The turn ends with three lines on stderr, so the answer on stdout stays pipeable:
Numbers come from the runtime’s own accounting (session.usage.getMetrics), not from counting characters: input_tokens / output_tokens per model, request count, and the time actually spent in model API calls — which is what the tok/s figure divides by. --json carries the same numbers as usage: {input_tokens, output_tokens, requests, api_ms, tokens_per_sec, nano_aiu}. Two honest limits:
  • Cost is n/a on a BYOK turn. nano_aiu is GitHub’s billing unit and the runtime reports 0 because your key pays the provider, not GitHub. Piping through OpenRouter removes even that (OpenRouter meters its own credits), so the AIU line only means something with GitHub-authenticated models.
  • Tokens are per model call, not per tool call. A tool call consumes no tokens by itself; each agent step that follows one does. That is why the live line reads · model · next to the · tool · lines rather than on them.

--search: live web results, per provider

The vendors do not sell the same thing, so the flag does whatever the provider actually supports instead of pretending they are interchangeable: Why the split: this CLI cannot inject a request field into the model call — the Copilot SDK forwards a fixed provider config, not arbitrary body parameters — so the search it can switch on is one the vendor exposes as a model variant (OpenRouter) or as an endpoint it can call itself (DeepSeek, OpenAI, Anthropic, Gemini, and ours). The vendor row means the vendor’s model plans the queries and writes the grounded answer; the fourth column is whose index found the pages. Two things that follow from that shape:
  • The vendor call is a second request, billed by the vendor. OpenAI, Anthropic and Google charge per search on top of the turn (Gemini 3+ bills per query the model decides to run, with 5,000 free per month). Their fees do not appear in webrain’s closing cost block — that block reports the runtime’s token totals, which only cover the turn.
  • A vendor search that fails falls back to ours, never to a dead turn. A rejected key, a model that cannot use the tool, or an empty answer prints the reason and grounds the turn on webrain’s own results instead.
Every branch prints one line on stderr naming the source and what it found:
--search adds one search per turn. The agent can also call webrain_search / webrain_serp itself, mid-turn, as many times as the question needs — that is what it does without the flag.

In a browser

The same session, driven from a page instead of a pipe: transcript on the left, a tool card per webrain_* call, a token meter in the header, and a modal for every permission request. The page is compiled into the binary (include_str!), so there is no build step and no second process, and it is the only UI that opens — nothing is fetched from a CDN. Four routes, all on loopback: The event names are the runtime’s own (assistant.message_delta, tool.execution_start, session.usage_info, model.call_failure, …) rather than a translated dialect: the page maps each one to an element, and a name it does not know is dropped instead of breaking the stream. One event is ours: webrain.permission, carrying the request id the modal answers. Two things that matter more than the layout:
  • Nothing is invented. If the runtime did not emit it, the pane stays empty. The composer re-enables on assistant.idle — the only signal a turn is over.
  • The modal is the security boundary. Approving in the page is the same authority as --yes, so it shows the exact request (the type of action, the command or url) and waits. No answer within 120 s is a deny, and the turn says so rather than hanging. --serve does not imply --yes.
--serve is loopback-only on purpose: a listener that can approve a shell command must not be reachable from the LAN. Put a reverse proxy in front of it and add authentication if you need it elsewhere — the binary will not do that for you.

Permissions

Without --yes the session approves reads, navigation and MCP tool calls, and denies shell commands, file writes, and any permission category the SDK does not model (an unrecognised category is not an invitation). --yes installs an approve-all handler — say it out loud when you mean it, because the agent can then drive a logged-in browser. The key is used in-process to configure the runtime and is never written to stdout, stderr, or a log. --print-config, --json and every error path print <redacted> plus the name of the variable that supplied it.

Exit codes

When a turn fails with no answer

The SDK reports No response was returned. Send your message again to retry. whenever the provider produced nothing usable, which can also mean the model does not support tool calling. See what the runtime actually emitted:
Every raw event is mirrored to stderr — including the provider’s own error payload, which is not in the SDK’s error string. If the model answers but never calls a tool, it is usually a model that does not support tool calling; pick another row (--provider openai) rather than retrying.

”The prompt was rejected as too long”

That message is the provider refusing the request, not a webrain limit, and both real causes are quotas — neither is the model’s context window:
  • Tokens per minute. A tool-enabled turn is ~8.4k tokens before your question: 18 tool schemas (~8.0k) plus a short system prompt (~0.3k). Groq’s free tier allows 7000 input tokens/minute, so it rejects that outright (413, Requested 18102 ... Limit 7000 once retries are counted). Use a paid tier or a provider with room.
  • Output budget against balance. The runtime asks for a 64k-token completion by default, and some providers reserve the requested maximum against your balance: OpenRouter answered “you requested up to 64000 tokens, but can only afford 171”. webrain therefore asks for 4096 by default and --max-output-tokens only lowers it further.
Both are visible only in the raw stream (see the WEBRAIN_CHAT_DEBUG example above).

Isolation from your own Copilot install

A BYOK chat owns its configuration. The client starts in the SDK’s ClientMode::Empty — the mode Microsoft documents for “CLI-like ambient behaviour is unsafe” — which requires an explicit state directory instead of the implicit ~/.copilot fallback, disables optional features unless asked for, and keeps the OS keychain out of the runtime’s reach. So none of your own Copilot setup applies: no ambient MCP servers, no user skills, no instructions file, no cross-session store, no memory writes, no session telemetry. This is not politeness — measured on a real turn, inheriting the machine’s config loaded ~20 unrelated MCP servers (~480 tools, toolDefinitionsTokens: 137019 against a tokenLimit: 128000) and the turn died before it reached the provider. Point the state directory elsewhere with WEBRAIN_CHAT_HOME (default: webrain-copilot-home under the system temp directory). What is not removable: the runtime’s built-in github-mcp-server (5 tools) still loads, and the tool list is filtered separately with the SDK’s ToolSet so the model can only call webrain-*. Measured, toolDefinitionsTokens stays 8080 either way — the 18 webrain_* schemas are the floor, not the stragglers.

What chat does not do

No interactive REPL and no --session resume in this version: one prompt, one turn, one exit code. Everything here is additive to webrain mcp, which keeps its stdout contract — chat’s own logs go to stderr for that reason. No GUI either. The runtime already emits everything a conversation window needs (streamed deltas, tool calls, permission requests, context and cost meters) and the CLI flattens all of it into stderr lines; ADR-004 maps each event to the element it should become and specifies webrain chat --serve as the smallest interface that keeps the binary self-contained — including which SDK handler slots a UI has to fill, because today they are policy, not a person.