Configuration
One layered, pydantic-validated config. Unknown keys are an error, not a silent typo.
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:
| backend | What it calls | Needs |
|---|---|---|
openai (default) | Any OpenAI-compatible endpoint (Ollama, vLLM, llama.cpp, internal gateway) or OpenAI itself | model + api_base; optionally
openai.api: responses for the Responses API
(default chat); pip install ci-doctorr[openai] |
anthropic | The Anthropic Messages API directly | model + api_key_env; pip install ci-doctorr[anthropic] |
azure | Azure OpenAI | model, azure.endpoint, azure.api_version, api_key_env; pip install ci-doctorr[openai] |
bedrock | Amazon Bedrock, AWS IAM auth (env vars, profile, or instance role) — not an API key | model; aws.region or
AWS_DEFAULT_REGION; pip install ci-doctorr[bedrock] |
bedrock-mantle | Bedrock's OpenAI-shaped models, AWS IAM auth | model; aws (region optional); pip install ci-doctorr[bedrock-mantle] |
google | The Gemini API | model; api_key_env or GOOGLE_API_KEY; pip install ci-doctorr[google] |
google-cloud | Vertex AI, Google Cloud auth | model; gcp.project/gcp.location with Application Default Credentials, or an API key; pip install ci-doctorr[google] |
groq | Groq's own API | model; api_key_env or GROQ_API_KEY; pip install ci-doctorr[groq] |
mistral | Mistral's own API | model; api_key_env or MISTRAL_API_KEY; pip install ci-doctorr[mistral] |
cohere | Cohere's own API | model; api_key_env or CO_API_KEY; pip install ci-doctorr[cohere] |
xai | xAI's Grok API | model; api_key_env or XAI_API_KEY; pip install ci-doctorr[xai] |
huggingface | Hugging Face Inference | model; api_key_env or HF_TOKEN; pip install ci-doctorr[huggingface] |
litellm | Whatever else litellm reaches — Vertex, Cohere, watsonx, custom proxies, ~100 providers total | model in litellm's own
provider/model form; pip install ci-doctorr[litellm] |
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:
- No lookaround (
(?=,(?!,(?<=,(?<!) — on a matcher, list the lines to reject underexclude. - No backreferences (
\1), and\zrather than\Z. - Repeats of at most 1000 (
a{1001}is rejected). \d \s \w \bare ASCII-only. Use\p{Nd}or\p{L}for Unicode.
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.