Skip to content

Providers

The review pass runs against one LLM provider. Providers are config-driven: there are two first-class kinds (anthropic and openai) and any specific vendor (GLM via z.ai, DeepSeek, Moonshot, a self-hosted gateway, any Anthropic- or OpenAI-compatible endpoint) is just a named profile of one of those kinds. New vendors are added by config alone, no rebuild.

Nothing here is persisted; see Credentials.

Settings resolve in this order, highest wins:

CLI flags > environment > config file > built-in defaults

The optional config file lives at ~/.config/miu/cr/config.toml (same on macOS and Linux), alongside the SQLite state DB at ~/.config/miu/cr/state.db, matching the miu family convention.

It is entirely optional; miu-cr works with zero config. A fully commented starter is in config.example.toml.

--provider takes a profile name: a built-in (anthropic, openai), any name you defined in the config file, or auto (the default).

With auto, miu-cr picks OpenAI when OPENAI_API_KEY is set and no Anthropic credential is present (--api-key, --auth-token, ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN); otherwise it uses default_provider from the config file (Anthropic by default). Anthropic is the default because it backs both the native API and Anthropic-compatible gateways.

default_provider = "anthropic" # profile to use when --provider is omitted
[providers.<name>]
kind = "anthropic" # or "openai"; the first-class family
base_url = "https://…" # optional; gateway/endpoint override
model = "…" # optional; default model for this profile
auth_env = "MY_TOKEN" # RECOMMENDED; NAME of an env var holding the credential
auth_command = ["gopass", "show", "-o", "ai/provider"] # argv only; stdout is the token
auth_token = "…" # discouraged literal credential; plaintext on disk
auth = "bearer" # "bearer" | "api_key" | "oauth" | omit for legacy auto

The two built-in profiles anthropic and openai always exist; you only declare a [providers.<name>] block to add a vendor or override a built-in’s model/base_url. Standard env vars and CLI flags still override profile credentials. Profile credential precedence is auth_token > non-empty auth_env > auth_command. auth_command executes the argv directly, not through a shell; stdout is trimmed as the token and stderr is omitted from errors because it may contain secrets.

kind selects the protocol family. auth selects the credential mechanism:

authValid kindMeaning
beareranthropicProfile credential is sent as Authorization: Bearer ...; use for Anthropic-compatible gateways.
api_keyanthropic, openaiProfile credential is sent as the provider API key (x-api-key for Anthropic, OpenAI API-key slot for OpenAI-compatible).
oauthopenaiUse miucr login / ChatGPT-plan OAuth; profile static credentials are rejected.
omittedbothLegacy auto: Anthropic profile credentials are Bearer; OpenAI uses profile key > OAuth > ambient OPENAI_API_KEY.

Credential source precedence is auth_token > non-empty auth_env > auth_command. auth_command is an argv array executed directly, never through a shell; it must print exactly one credential line to stdout. If a selected auth_command fails, resolution fails instead of silently falling through to OAuth or ambient env keys; stderr is omitted from the error because secret helpers may print credentials there.

Per-provider usage quota: [providers.<name>.quota]

Section titled “Per-provider usage quota: [providers.<name>.quota]”

Optionally cap how much a provider instance may be used over a recurring window. There is no quota by default (a provider with no quota block is uncapped). The cap is per instance and aggregates across every review/repo that uses it, so one block limits, say, the pricier OAuth provider everywhere at once.

[providers.openai.quota]
dimension = "tokens" # tokens (default; input incl. cache + output) | requests (one per review)
limit = 2_000_000 # cap in the chosen dimension; must be > 0
window = "5h" # a Go duration (1h, 5h, 24h, 168h) OR "monthly" (calendar month, UTC)
  • Dimensiontokens meters all tokens processed — uncached input + cache-read + cache-creation + output (so cached input is not undercounted); requests counts reviews. (cost/$ is not yet supported.)
  • Window — a fixed window: a Go duration bucketed off the epoch (so 5h resets every 5 hours on fixed boundaries, 24h daily), or monthly for a calendar month. Hourly and 5-hourly windows are first-class. Changing the window starts a fresh bucket.
  • Enforcementfail-closed and hard: before each review the accumulated usage for the current window is checked; at/over the limit the review is blocked with a typed quota.exceeded error (and a one-shot warning at ≥80%). A counter that can’t be read/opened also blocks, but as a retryable store.unavailable (not quota.exceeded) so a transient DB outage is retried, not mistaken for a hit. On the serve host, a genuine quota-blocked PR is skipped and logged (the poller keeps running); a later push or comment re-triggers a fresh job that re-checks the window.
  • State — usage counters persist in the same store as history (state.db for the CLI, Postgres for the host), surviving one-shot CLI invocations. A bad dimension/window/limit is a typed config.invalid (exit 2).
  • Metering scope — the counter records every LLM call in a review — the main pass, parallel subagents, and the optional --patch-repair second pass — summed. Usage is captured with its cache breakdown: uncached input, cache-read, and cache-creation are all metered (Anthropic/z.ai report cache as separate buckets; OpenAI reports cached tokens as a sub-count of prompt tokens, normalized so cached input is counted exactly once). Review stats expose input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, total_input_tokens, and cache_hit_ratio for usage optimization. (Codex/ChatGPT-plan reviews report no usage and meter as zero.)

The host config (host.yaml) takes the same block under each providers.<name>.

The optional [review] table sets defaults for miucr review flags. An explicit flag always wins; a [review] value only fills a flag you did not pass. A bad enum or timeout is a typed config.invalid error (exit 2).

[review]
gate = "high" # CLI default --gate: none|info|low|medium|high|critical
filter_mode = "diff_context" # CLI default --filter-mode (--pr): added|diff_context|file|nofilter
min_severity = "low" # optional --min-severity inline floor; unset means no floor
format = "full" # CLI default --format: full|minimal
timeout = "900s" # review timeout (a Go duration; CLI review default is 900s)
stalled_timeout = "5m" # no-progress watchdog; "0s" disables it
expand = 5 # CLI default --expand; --deep-context raises it to 20
token_budget = 0 # CLI default --token-budget; 0 = no cap
deep_context = false # CLI default --deep-context
# context_hops = 3 # optional override; omit to let deep_context choose automatically
conversation = false # CLI default --conversation on --pr
thinking = "auto" # auto|off|low|medium|high. auto = extended thinking/reasoning
# ON when the model supports it (Claude, gpt-5/o-series, codex) —
# deeper analysis. Off (or unsupported model) falls back to
# temperature. Thinking omits temperature (it forces temp 1).
temperature = 0 # LLM sampling temperature (0–2), used when thinking is OFF.
# Default 0 = deterministic: re-reviews of the same diff stay
# stable instead of churning findings. Applies to anthropic +
# openai chat models; reasoning models (which need temp 1) ignore it.
suggest = false # CLI default --suggest (Action input defaults true)
patch_repair = false # CLI default --patch-repair; requires suggest=true
[review.provider_retry] # provider API retry behavior
max_retries = 10 # retries after the first failed provider request; 0 disables
initial_backoff = "5s" # jittered exponential backoff starts here
max_backoff = "2m" # cap each provider retry sleep
max_elapsed = "10m" # cap total retry sleep budget inside the review timeout
[review.tools] # built-in reviewer tool behavior
max_retries = 2 # transient tool execution retries; 0 disables
max_turns = 24 # max model tool turns before forced finalization; 0 uses default, capped at 64
retry_backoff = "250ms" # initial retry delay, doubled per retry and capped
[review.tools.symbol_context] # internal symbol_context tool limits
max_bytes = 16000 # cap returned text before it reaches the model
max_files = 2000 # cap repo-wide files scanned per tool call
max_parallel = 8 # bounded workers for large repo-wide scans
[review.subagents] # optional scoped fanout inside one review
mode = "auto" # off|auto|always
max_parallel = 2 # default 2, capped at 8
min_files = 8 # auto threshold; 0 uses default
min_context_bytes = 60000 # auto threshold; 0 uses default
require_all = true # failed subagent prevents approval/check success
[review.approval] # optional default for --approval on PR --post
mode = "off" # off|clean|threshold
max_priority = "P4" # threshold mode only; P0|P1|P2|P3|P4
note = "on_findings" # none|on_findings|always
[[review.subagents.agents]]
name = "go"
include = ["**/*.go"]
exclude = ["**/*_test.go"]
system_prompt = "Focus on correctness, concurrency, error handling, and API compatibility."
[review.category_urls] # map a finding Category → a docs URL (clickable link + SARIF helpUri)
"security" = "https://example.com/docs/security"

timeout caps total review wall clock. stalled_timeout cancels a run that stops emitting progress, which catches stuck provider/tool turns sooner in host mode; set it to "0s" only when debugging. [review.provider_retry] retries transient provider failures (429, 5xx, 529, and temporary transport errors) with bounded jittered exponential backoff. It does not retry auth, invalid request, content-policy, or context cancellation errors. [review.tools] sets the agent tool-loop budget and retries transient tool execution failures such as temporary filesystem/process errors, but does not retry malformed tool calls, unknown tools, missing files, or context cancellation.

symbol_context is part of the core read-only reviewer toolset, alongside file_read and grep. [review.tools.symbol_context] tunes its bounds. The internal revision-pinned scanner can fetch bounded document_symbols, definition, references, incoming_calls, outgoing_calls, implementations, dependencies, and lightweight Astro/Vue/Svelte component symbols. It also supplies a compact changed-symbol prelude before the model turn, capped separately to a small first-pass slice of changed files. Large repo-wide scans use up to max_parallel bounded readers while preserving deterministic output order. It reads the same reviewed git revision as the other tools, so PR and staged reviews do not inspect the live worktree. The tool is intended for code-intelligence lookups first; use grep for raw text and file_read for exact lines after a target is located.

Only these review attributes can be defaulted from config; there is intentionally no post or force config because write-action and repeat-spend defaults are footguns. Approval remains explicit as a policy object so host configs can choose strict clean approval or a priority threshold per repo.

Viewing and editing config (config show / set / edit)

Section titled “Viewing and editing config (config show / set / edit)”

miucr config show prints the effective configuration with every credential masked (auth_token, the store dsn) by structural redaction, so a token can never reach stdout. By default it shows only your user-set values; --all includes the built-in defaults.

Terminal window
miucr config show # user-set values only (secrets redacted)
miucr config show --all # full effective config incl. built-in defaults
miucr config show -o pretty # TOML view for humans

config set <key> <value> merges one dotted, non-secret scalar key (e.g. default_provider, review.gate, providers.zai.model, providers.zai.auth) into the existing config without re-running init; secret keys (auth_token, store.dsn) are rejected to avoid a plaintext-secret footgun. Use config edit or edit the file directly for array fields like auth_command. config edit opens ~/.config/miu/cr/config.toml in $VISUAL/$EDITOR (it needs an interactive terminal; in CI use config set).

Terminal window
export ANTHROPIC_API_KEY=...
miucr review --staged

Resolution order (first non-empty wins):

SettingFlagEnvProfileDefault
API key (x-api-key)--api-keyANTHROPIC_API_KEY-required unless an auth token is set
Auth token (Bearer)--auth-tokenANTHROPIC_AUTH_TOKENauth_token / auth_env / auth_command with auth = "bearer"-
Base URL--base-urlANTHROPIC_BASE_URLbase_urlSDK default
Model--modelANTHROPIC_MODELmodelclaude-sonnet-4-5-20250929

The API key is sent as x-api-key. An auth token is sent as a Bearer Authorization header instead, what Anthropic-compatible gateways expect.

GLM via z.ai (Anthropic-compatible): example profile

Section titled “GLM via z.ai (Anthropic-compatible): example profile”

z.ai exposes an Anthropic-compatible gateway, so it’s an anthropic-kind profile with a base URL + bearer token. There is no z.ai-specific code; it is purely configuration.

~/.config/miu/cr/config.toml
[providers.zai]
kind = "anthropic"
base_url = "https://api.z.ai/api/anthropic"
model = "glm-5.2"
auth = "bearer"
auth_env = "ZAI_API_KEY"
# auth_command = ["gopass", "show", "-o", "ai/zai"]
Terminal window
export ZAI_API_KEY=...
miucr review --staged --provider zai

Equivalent without a config file, using the generic Anthropic env vars:

Terminal window
export ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
export ANTHROPIC_AUTH_TOKEN=$ZAI_API_KEY
miucr review --staged --model glm-5.2

…or entirely via flags:

Terminal window
miucr review --staged \
--base-url https://api.z.ai/api/anthropic \
--auth-token "$ZAI_API_KEY" \
--model glm-5.2
Terminal window
export OPENAI_API_KEY=...
miucr review --staged --provider openai

Resolution order:

SettingFlagEnvProfileDefault
API key--api-keyOPENAI_API_KEYauth_token / auth_env / auth_command with auth = "api_key"required
Base URL--base-urlOPENAI_BASE_URLbase_urlhttps://api.openai.com/v1
Model--modelOPENAI_MODELmodelgpt-4o

Requests send max_tokens (not max_completion_tokens) for the broadest compatibility with OpenAI-compatible gateways. --auth-token is Anthropic-only; passing it with an OpenAI provider is a typed error.

The OpenAI provider can also authenticate without an API key by reviewing on your ChatGPT plan. miucr login caches an OAuth token and subsequent OpenAI reviews talk to the codex backend (the ChatGPT-plan Responses protocol). On that path the model defaults to gpt-5.5; precedence is --model > MIUCR_CODEX_MODEL > an explicit model in your [providers.openai] profile > gpt-5.5. miucr init writes model = "gpt-5.5" for you on the OAuth path so the codex model is visible and editable. The pinned gpt-4o/OPENAI_MODEL default never applies here: the codex backend rejects api.openai.com models, so a config model = "gpt-4o" is ignored and falls through to gpt-5.5.

When auth is unset, the OpenAI credential resolves intent-ordered so an ambient OPENAI_API_KEY (often set for other tools) never silently overrides a deliberate choice:

  1. a profile-configured key (auth_env / auth_command / auth_token) or --api-key;
  2. a cached miucr login (OAuth → codex / ChatGPT-plan backend);
  3. an ambient OPENAI_API_KEY env var.

Pin the method with auth = "oauth" or auth = "api_key" to skip the auto order. With auth = "oauth", remove auth_env, auth_command, and auth_token from the profile. See Credentials → Using your ChatGPT plan.

Generic OpenAI-compatible gateway: example profile

Section titled “Generic OpenAI-compatible gateway: example profile”
[providers.my-gateway]
kind = "openai"
base_url = "https://gateway.example.com/v1"
model = "your-model-id"
auth = "api_key"
auth_env = "MY_GATEWAY_TOKEN"
# auth_command = ["op", "read", "op://vault/item/token"]
Terminal window
export MY_GATEWAY_TOKEN=...
miucr review --staged --provider my-gateway

If no credential is found for the resolved provider, the review fails with a typed error (exit 1) and a hint, e.g.:

no Anthropic credentials: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN, configure a provider in <config path>, or pass --api-key / --auth-token