Configuration

One layered, pydantic-validated config. Unknown keys are an error, not a silent typo.

This page is the reference — every key and its default. For what the keys mean — phases, attribution, denoising, matchers, budget, redaction — start with Concepts. The shipped matcher packs are listed on Matchers.

How layers combine

schema defaults  <  defaults.yml  <  repo .ci-doctor.yml  <  CI_DOCTOR_* env  <  CLI flags

Mappings deep-merge; lists and scalars replace. The one exception is a list whose entries all carry an id — today only extraction.matchers — which merges per id, so your packs add to the shipped ones instead of wiping them. Point at specific files with -f path/to/.ci-doctor.yml, repeated as often as you like — each one overrides the last.

Inspecting the result

Config bugs are almost always a layer you forgot was applied. Ask:

ci-doctor config           # the effective config, every layer merged
ci-doctor config --diff    # only what your file/env/flags change vs the defaults
ci-doctor config --schema  # the JSON Schema, for editors and CI validation
ci-doctor config --validate            # merge every layer and report what fails
ci-doctor config -f base.yml -f ci.yml # repeatable, the rightmost file wins
# Output pages through $PAGER on a terminal; --plain prints raw (as does piping).

--diff reads like git diff: green is what your config adds, red is the default it replaced.

Editor completion

Every release ships ci-doctor.schema.json, generated from the same pydantic model that validates your file, so it can never drift. Point your editor at it:

# yaml-language-server: $schema=https://github.com/ci-doctor/ci-doctor/releases/latest/download/ci-doctor.schema.json
llm:
  model: qwen2.5-coder:32b

Environment variables

# Nested keys use a double underscore:
export CI_DOCTOR_LLM__MODEL="qwen2.5-coder:32b"
export CI_DOCTOR_LLM__API_BASE="http://vllm.internal:8000/v1"
export CI_DOCTOR_CI="github"

# Secret-looking single-underscore vars (e.g. CI_DOCTOR_GITLAB_TOKEN) are
# referenced by NAME (token_env) and ignored by the config loader.

LLM backends

The LLM step is bring-your-own, via Pydantic AI. Pick one with llm.backend:

backendWhat it callsNeeds
openai (default)Any OpenAI-compatible endpoint (Ollama, vLLM, llama.cpp, internal gateway) or OpenAI itselfmodel + api_base; optionally openai.api: responses for the Responses API (default chat); pip install ci-doctorr[openai]
anthropicThe Anthropic Messages API directlymodel + api_key_env; pip install ci-doctorr[anthropic]
azureAzure OpenAImodel, azure.endpoint, azure.api_version, api_key_env; pip install ci-doctorr[openai]
bedrockAmazon Bedrock, AWS IAM auth (env vars, profile, or instance role) — not an API keymodel; aws.region or AWS_DEFAULT_REGION; pip install ci-doctorr[bedrock]
bedrock-mantleBedrock's OpenAI-shaped models, AWS IAM authmodel; aws (region optional); pip install ci-doctorr[bedrock-mantle]
googleThe Gemini APImodel; api_key_env or GOOGLE_API_KEY; pip install ci-doctorr[google]
google-cloudVertex AI, Google Cloud authmodel; gcp.project/gcp.location with Application Default Credentials, or an API key; pip install ci-doctorr[google]
groqGroq's own APImodel; api_key_env or GROQ_API_KEY; pip install ci-doctorr[groq]
mistralMistral's own APImodel; api_key_env or MISTRAL_API_KEY; pip install ci-doctorr[mistral]
cohereCohere's own APImodel; api_key_env or CO_API_KEY; pip install ci-doctorr[cohere]
xaixAI's Grok APImodel; api_key_env or XAI_API_KEY; pip install ci-doctorr[xai]
huggingfaceHugging Face Inferencemodel; api_key_env or HF_TOKEN; pip install ci-doctorr[huggingface]
litellmWhatever else litellm reaches — Vertex, Cohere, watsonx, custom proxies, ~100 providers totalmodel in litellm's own provider/model form; pip install ci-doctorr[litellm]
Reach for openai first. The OpenAI Chat Completions shape is what Ollama, vLLM, llama.cpp, Together, Groq, DeepSeek, Mistral and most others already speak; a provider with its own backend above (Groq, Mistral, Google, ...) talks to its native API instead. Keep openai.api on chat for self-hosted servers; only OpenAI itself and some gateways offer the newer Responses API. There is no default SDK in the base install any more — every backend needs its own extra, picked for exactly the provider you're using.
openai/azure/bedrock-mantle and litellm are mutually exclusive installs, not a menu to combine. litellm hard-pins openai<3.0, while Pydantic AI's own OpenAI integration needs openai>=3.8 — the two cannot be installed in one environment. Pick whichever track covers your provider; Every other native backend is unaffected either way. litellm may also pull tiktoken, which fetches vocab at runtime, so it is unfit for a strict air gap. When the LLM is disabled or unavailable, ci-doctor emits the deterministic report instead of failing.
bedrock with no resolvable AWS credentials can probe the network at startup. Unlike the other backends, building the client eagerly resolves AWS credentials (env vars, profile, or instance metadata) — with none available anywhere, that resolution itself makes a request rather than failing instantly. Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (or run with an instance role) so it never reaches that path.

Regex syntax

Every regex you can configure — matcher pattern, start, end and exclude, denoise.noise_patterns, redaction.extra_patterns — is RE2, compiled when the config loads. RE2 matches in linear time, so no pattern can hang a job. What that rules out:

Full reference

Drop a .ci-doctor.yml at your repo root. Everything below is optional — every key has a default.

# .ci-doctor.yml — every knob, with defaults shown.
ci: github                       # which CI to read the run from (github | gitlab)
scm: null                        # which git host gets the note (defaults to "ci")

gitlab:
  base_url: https://gitlab.com   # default; override for a self-hosted instance
  api_version: v4
  token_env: CI_DOCTOR_GITLAB_TOKEN
  token_file: null               # e.g. /run/secrets/gitlab_token (wins over token_env)
  ca_bundle: null                # path to a custom/internal CA bundle
  verify_ssl: true
  timeout_seconds: 30

github:                          # used when github is the ci or the scm
  base_url: https://api.github.com     # default; override with a GHE base, e.g. https://ghe/api/v3
  token_env: CI_DOCTOR_GITHUB_TOKEN
  token_file: null
  ca_bundle: null
  verify_ssl: true
  timeout_seconds: 30

llm:
  enabled: true                  # false => deterministic-only report
  backend: openai                # openai | anthropic | azure | bedrock | bedrock-mantle | google |
                                 # google-cloud | groq | mistral | cohere | xai | huggingface | litellm
  model: null                    # e.g. "qwen2.5-coder:32b" or "claude-opus-4-8"
  api_base: null                 # OpenAI-compatible / litellm base URL
  api_key_env: null              # optional; local servers often need none
  ca_bundle: null                # LLM endpoint's own CA, independent of the provider's
  openai:
    api: chat                    # backend: openai; chat (Chat Completions) | responses (Responses API)
  azure: null                    # backend: azure; required there
  #   endpoint: https://<resource>.openai.azure.com
  #   api_version: "2024-10-21"
  aws: null                      # bedrock, bedrock-mantle; required there
  #   region: us-east-1          # optional; falls back to AWS_DEFAULT_REGION/AWS_REGION
  gcp: null                      # backend: google-cloud; required there
  #   project: my-project        # optional; falls back to the environment's
  #   location: us-central1      # optional; e.g. us-central1 or global
  max_input_tokens: 12000
  temperature: 0.1
  timeout_seconds: 120           # wall-clock budget for one call, retries included
  max_retries: 1                 # SDK retries (not bedrock/litellm); Pydantic AI's own Agent retries once more on an invalid reply

analysis:
  include_allowed_failures: false   # allow_failure jobs are noise by default
  max_jobs_analyzed: 10
  max_parallel_jobs: 4              # jobs analyzed at once; 1 for a strictly sequential run
  skip_llm_for: [no_runner, missing_dependency, cancelled]  # fully-determined -> templated

phases:                          # section/step name -> phase (merged onto the shipped map)
  my_custom_deploy_section: script

extraction:
  tail_lines: 120
  matchers:                      # evidence windows, ranked by (role, position) under budget
                                 # Merged by id onto the shipped language packs: a new
                                 # id adds a pack, a shipped id retunes one (and warns).
    - id: my_service_errors      # new id -> added alongside the shipped packs
      role: tool                 # tool | wrapper | fallback
      pattern: '^(ERROR|FATAL)\b'
      before: 5
      after: 20
    - id: pytest                 # shipped id -> only these fields change, the rest is inherited
      role: wrapper

denoise:
  collapse_carriage_returns: true   # collapse \r progress bars to their final state
  dedupe_repeats: true              # repeated lines -> "<line>  (×47)"
  noise_patterns:
    - '^\s*$'

output:
  terminal: true
  markdown_path: report.md
  json_path: report.json
  mr_note: false                 # post/update a note on the MR/PR (needs a live pipeline)

redaction:
  enabled: true
  extra_patterns: []             # additional regexes to scrub
base_url defaults to the public host (gitlab.com / api.github.com). Override it for a self-hosted GitLab or GitHub Enterprise instance. Custom CA bundles and proxies are honoured either way.