OpenPullRequest
CLI tool that generates PR titles, descriptions, and post-review change summaries using a local or cloud LLM. Supports Ollama, OpenAI, Anthropic, Google Gemini, OpenRouter, DeepSeek, llama.cpp, and MLX.
Built primarily for local-first use — pair it with Ollama and a Gemma model (Google's Gemma family is currently the best fit for this kind of task at a size you can run locally) for a free, fully offline workflow. On Apple Silicon Macs, prefer the -mlx tagged variant of your chosen model (e.g. gemma4:26b-mlx) — it runs on Apple's MLX framework instead of plain GGUF and is noticeably faster on that hardware. Don't want to run anything locally? Google's Gemini API has a generous free tier and is a great low-friction cloud option.
Installation
Recommended
uv tool install openpurr
Or
pipx install openpurr
On first run, the setup wizard creates ~/.openpurr automatically. You can also run it any time:
opo setup
Requirements
- Python 3.11+
giton PATH- One of: Ollama running locally, or an API key for a cloud provider
Usage
# Generate a PR title + description from the diff against main
opo
# Diff against a different base branch
opo --base develop
# Summarise changes made since the last N commits (for reviewers)
opo review
opo review --commits 3
Example output
Running opo against a diff produces a title and description like this — generated
with the default system prompt, no custom overrides. This is PR #11 from this very repo — in fact, every PR in openpurr's history was generated by openpurr itself:
feat(config): add support for custom system prompt overrides
📝 Summary
Introduced a mechanism to override default system prompts via the configuration file. Users can now define custom
INIT PROMPT:andREVIEW PROMPT:sections to tailor LLM behavior for PR generation and reviews.🛠 Type of Change
- New feature
🔍 Key Changes
- Implemented a new configuration parsing logic that uses a
---delimiter to separate key-value pairs from free-text prompt sections.- Added support for
INIT PROMPT:andREVIEW PROMPT:headers to override default system prompts.- Updated
Configclass to load and expose custom prompt overrides.- Updated
run_initandrun_reviewto prioritize custom prompts over built-in defaults.- Enhanced
config_describeto inform users of active prompt overrides.- Updated
README.mdwith new installation instructions and documentation for custom prompts.🧪 How Has This Been Tested?
- Extensive unit tests in
tests/test_config.pycovering delimiter splitting, prompt parsing, and configuration preservation.- Functional tests in
tests/test_pr.pyverifying that custom prompts are correctly passed to the LLM provider.
Want a different tone or format? See Custom system prompts below.
Configuration
Settings are stored in ~/.openpurr as flat OPO_KEY=value lines — no sections, no quoting:
OPO_PROVIDER=ollama
OPO_MODEL=gemma4:26b
OPO_API_KEY=
OPO_HOST=http://localhost:11434
OPO_TEMPERATURE=0.0
OPO_KEEP_ALIVE=5m
OPO_BASE=main
OPO_LANGUAGE=en
Use the config subcommand to inspect and update values without editing the file directly.
# Show all keys with descriptions and current values
opo config describe
# Read a single value
opo config get model
# Update a value
opo config set provider ollama
opo config set model gemma4:26b
opo config set api_key sk-...
opo config set keep_alive 0s
Custom system prompts
The prompts opo sends to the LLM aren't fixed — everything after a lone --- line in
~/.openpurr is free text, not OPO_KEY=value config, so it can hold Markdown, headers,
= signs, anything. Add an INIT PROMPT: section to override the prompt behind opo
(title + description) and/or a REVIEW PROMPT: section to override the one behind
opo review. Either section is optional; whichever is missing keeps the built-in default.
OPO_PROVIDER=ollama
OPO_MODEL=gemma4:26b
OPO_API_KEY=
OPO_HOST=http://localhost:11434
OPO_TEMPERATURE=0.0
OPO_KEEP_ALIVE=5m
OPO_BASE=main
OPO_LANGUAGE=en
---
INIT PROMPT:
You are a Senior Principal Engineer. Write a terse PR title and description in
Conventional Commits style. No emoji, no checklists.
REVIEW PROMPT:
Summarize what changed since the last review in one short paragraph.
opo config set/opo config describe only ever touch the OPO_KEY=value part above the
--- line — the prompt section is left exactly as you wrote it, and opo config describe
tells you which of init/review are currently overridden. Note that OPO_LANGUAGE (see
below) only affects the built-in prompts — once you write your own INIT PROMPT:/REVIEW PROMPT:,
you're in full control, so specify the language directly in the prompt text if you need one.
Configuration keys
| Key | Env var | Default | Description |
|---|---|---|---|
provider |
OPO_PROVIDER |
ollama |
ollama · openai · anthropic · gemini · openrouter · deepseek · llamacpp · mlx |
model |
OPO_MODEL |
(empty) | Model name — set via opo setup or opo config set model <name> |
api_key |
OPO_API_KEY |
(empty) | API key for cloud providers |
host |
OPO_HOST |
http://localhost:11434 |
Base URL — Ollama default; set to a custom endpoint when needed |
temperature |
OPO_TEMPERATURE |
0.0 |
Sampling temperature (0.0 = deterministic) |
keep_alive |
OPO_KEEP_ALIVE |
5m |
Ollama VRAM keep-alive (0s = unload immediately, 5m = keep warm) |
base |
OPO_BASE |
main |
Default base branch to diff against |
language |
OPO_LANGUAGE |
en |
Output language for generated PR text (ISO 639-1 code, e.g. es, fr, de, ja, zh) |
There's no hardcoded default model: opo setup always has you pick one (from a live-fetched list, or typed manually), and opo/opo review will error out with a pointer back to opo setup if model is ever left blank (e.g. after hand-editing the file).
Example: switch to Google Gemini (free tier)
opo config set provider gemini
opo config set model <your-chosen-model>
opo config set api_key <your-gemini-api-key>
Model names aren't hardcoded anywhere in this README on purpose — providers retire and rename models constantly. Run opo setup or opo models --provider gemini to pick from what's actually available right now.
Example: unload Ollama from VRAM after each request
opo config set keep_alive 0s
Example: generate PR text in another language
opo config set language es
opo setup also asks for this up front, with arrow-key selection from common languages
plus a custom-code fallback. This only affects the built-in prompts — see
Custom system prompts above.
Models
List available models for the current or a specified provider:
opo models
opo models --provider anthropic
Model lists are always fetched live from the provider — Ollama's /api/tags, OpenAI/Anthropic/Gemini/OpenRouter/DeepSeek/llama.cpp/MLX's models.list() (or public models endpoint) — never a hardcoded/curated list baked into the tool. If the fetch fails (offline, bad key, local server not running) the command prints an empty result instead of erroring; check connectivity/credentials, or pull a model directly with ollama pull <model>.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openpurr-0.8.7.tar.gz.
File metadata
- Download URL: openpurr-0.8.7.tar.gz
- Upload date:
- Size: 540.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
18c429b9284db3277d4c570ca657c3f9be65846768f42ff28c90f75a4be90e55
|
|
| MD5 |
9046f62a34ce40534ee987aad53807d6
|
|
| BLAKE2b-256 |
2075ff6fb10fce03e94a816d5825a1ac659400a39976bacb9774d8c9100ab05d
|
Provenance
The following attestation bundles were made for openpurr-0.8.7.tar.gz:
Publisher:
publish.yml on ilypopv/openpurr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openpurr-0.8.7.tar.gz -
Subject digest:
18c429b9284db3277d4c570ca657c3f9be65846768f42ff28c90f75a4be90e55 - Sigstore transparency entry: 2617147523
- Sigstore integration time:
-
Permalink:
ilypopv/openpurr@8cf20a141a28c284ece2561fa25147cceebd26d2 -
Branch / Tag:
refs/tags/v0.8.7 - Owner: https://github.com/ilypopv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8cf20a141a28c284ece2561fa25147cceebd26d2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openpurr-0.8.7-py3-none-any.whl.
File metadata
- Download URL: openpurr-0.8.7-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2550845cc6cf7d420d5c9c5532d550df952a8ec91b42c2ead060221e85d7e97
|
|
| MD5 |
792242993614113eb2752a4d98df8b59
|
|
| BLAKE2b-256 |
adbc074327ef9dd532fa39e70ebb21e5f56dbc1988e33a0142bff0a6c3f29137
|
Provenance
The following attestation bundles were made for openpurr-0.8.7-py3-none-any.whl:
Publisher:
publish.yml on ilypopv/openpurr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openpurr-0.8.7-py3-none-any.whl -
Subject digest:
c2550845cc6cf7d420d5c9c5532d550df952a8ec91b42c2ead060221e85d7e97 - Sigstore transparency entry: 2617147564
- Sigstore integration time:
-
Permalink:
ilypopv/openpurr@8cf20a141a28c284ece2561fa25147cceebd26d2 -
Branch / Tag:
refs/tags/v0.8.7 - Owner: https://github.com/ilypopv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8cf20a141a28c284ece2561fa25147cceebd26d2 -
Trigger Event:
push
-
Statement type: