-
Notifications
You must be signed in to change notification settings - Fork 9
implement token reduction with headroom
#12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,36 @@ inputs: | |
| description: 'Maximum number of tool-calling rounds before forcing the model to produce a final review. Each iteration is a separate LLM call.' | ||
| required: false | ||
| default: '8' | ||
| headroom_compress: | ||
| description: 'Set to "true" to compress context with headroom-ai before each LLM call (cuts tokens on tool outputs and older turns). Requires the headroom-ai package; install reviewbot with the [headroom] extra. No-op if the package is missing.' | ||
| required: false | ||
| default: 'false' | ||
| headroom_target_ratio: | ||
| description: 'Optional headroom keep-ratio for text compression (e.g. 0.5 keeps ~50%). Leave empty to let headroom decide.' | ||
| required: false | ||
| headroom_compress_user_messages: | ||
| description: 'Set to "true" to also compress user messages (the annotated diff). Off by default so cited line numbers stay intact.' | ||
| required: false | ||
| default: 'false' | ||
| headroom_compress_system_messages: | ||
| description: 'Set to "false" to leave system messages uncompressed. On by default.' | ||
| required: false | ||
| default: 'true' | ||
| headroom_protect_recent: | ||
| description: 'Number of most-recent messages never compressed.' | ||
| required: false | ||
| default: '4' | ||
| headroom_min_tokens: | ||
| description: 'Skip compressing messages shorter than this many tokens.' | ||
| required: false | ||
| default: '250' | ||
| headroom_kompress_model: | ||
| description: 'Kompress model id for ML compression, or "disabled" to skip it. Leave empty for the headroom default.' | ||
| required: false | ||
| headroom_model_limit: | ||
| description: 'Model context window (tokens) used for sizing.' | ||
| required: false | ||
| default: '200000' | ||
| github_token: | ||
| description: 'Token with pull-requests:write. Defaults to the job token.' | ||
| required: false | ||
|
|
@@ -86,7 +116,15 @@ runs: | |
|
|
||
| - name: Install reviewbot | ||
| shell: bash | ||
| run: pip install --disable-pip-version-check "${{ github.action_path }}" | ||
| env: | ||
| HEADROOM_COMPRESS: ${{ inputs.headroom_compress }} | ||
| run: | | ||
| # Pull in the (heavy) headroom-ai extra only when compression is on. | ||
| if [ "${HEADROOM_COMPRESS,,}" = "true" ] || [ "$HEADROOM_COMPRESS" = "1" ]; then | ||
| pip install --disable-pip-version-check "${{ github.action_path }}[headroom]" | ||
| else | ||
| pip install --disable-pip-version-check "${{ github.action_path }}" | ||
| fi | ||
|
|
||
| - name: Run AI reviewer | ||
| shell: bash | ||
|
|
@@ -108,4 +146,12 @@ runs: | |
| CONTEXT_SCRIPT_TIMEOUT: ${{ inputs.context_script_timeout }} | ||
| REPO_CHECKOUT_PATH: ${{ inputs.repo_checkout_path }} | ||
| TOOL_MAX_ITERATIONS: ${{ inputs.tool_max_iterations }} | ||
| HEADROOM_COMPRESS: ${{ inputs.headroom_compress }} | ||
| HEADROOM_TARGET_RATIO: ${{ inputs.headroom_target_ratio }} | ||
| HEADROOM_COMPRESS_USER_MESSAGES: ${{ inputs.headroom_compress_user_messages }} | ||
| HEADROOM_COMPRESS_SYSTEM_MESSAGES: ${{ inputs.headroom_compress_system_messages }} | ||
| HEADROOM_PROTECT_RECENT: ${{ inputs.headroom_protect_recent }} | ||
| HEADROOM_MIN_TOKENS: ${{ inputs.headroom_min_tokens }} | ||
| HEADROOM_KOMPRESS_MODEL: ${{ inputs.headroom_kompress_model }} | ||
| HEADROOM_MODEL_LIMIT: ${{ inputs.headroom_model_limit }} | ||
| run: reviewbot-action | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing forwarding for |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| """Opt-in context compression for LLM calls via the ``headroom-ai`` package. | ||
|
|
||
| Runs only when ``HEADROOM_COMPRESS`` is set and ``headroom`` is importable; | ||
| otherwise messages pass through unchanged. Any failure falls back to the | ||
| original messages so compression can never block a review. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import os | ||
| from dataclasses import dataclass | ||
| from typing import Any, Callable, Optional | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _bool_env(name: str, default: bool = False) -> bool: | ||
| raw = (os.environ.get(name) or "").strip().lower() | ||
| if not raw: | ||
| return default | ||
| return raw in ("1", "true", "yes", "on") | ||
|
|
||
|
|
||
| def _int_env(name: str, default: int) -> int: | ||
| raw = (os.environ.get(name) or "").strip() | ||
| try: | ||
| return int(raw) if raw else default | ||
| except ValueError: | ||
| return default | ||
|
|
||
|
|
||
| def _float_env(name: str, default: float | None = None) -> float | None: | ||
| raw = (os.environ.get(name) or "").strip() | ||
| if not raw: | ||
| return default | ||
| try: | ||
| return float(raw) | ||
| except ValueError: | ||
| return default | ||
|
|
||
|
|
||
| @dataclass | ||
| class CompressionConfig: | ||
| # Defaults are tuned for a review agent: leave user/system messages alone | ||
| # and protect recent turns, so only tool outputs and older assistant turns | ||
| # get compressed. Fields mirror headroom.CompressConfig. | ||
| enabled: bool = False | ||
| compress_user_messages: bool = False | ||
| compress_system_messages: bool = True | ||
| protect_recent: int = 4 | ||
| target_ratio: Optional[float] = None | ||
| min_tokens_to_compress: int = 250 | ||
| # None = headroom default Kompress model; "disabled" = no ML compression. | ||
| kompress_model: Optional[str] = None | ||
| # Model context window, forwarded as compress(model_limit=...). | ||
| model_limit: int = 200_000 | ||
|
|
||
| @classmethod | ||
| def from_env(cls) -> "CompressionConfig": | ||
| return cls( | ||
| enabled=_bool_env("HEADROOM_COMPRESS", False), | ||
| compress_user_messages=_bool_env("HEADROOM_COMPRESS_USER_MESSAGES", False), | ||
| compress_system_messages=_bool_env( | ||
| "HEADROOM_COMPRESS_SYSTEM_MESSAGES", True | ||
| ), | ||
| protect_recent=_int_env("HEADROOM_PROTECT_RECENT", 4), | ||
| target_ratio=_float_env("HEADROOM_TARGET_RATIO", None), | ||
| min_tokens_to_compress=_int_env("HEADROOM_MIN_TOKENS", 250), | ||
| kompress_model=(os.environ.get("HEADROOM_KOMPRESS_MODEL") or "").strip() | ||
| or None, | ||
| model_limit=_int_env("HEADROOM_MODEL_LIMIT", 200_000), | ||
| ) | ||
|
|
||
|
|
||
| class MessageCompressor: | ||
| """Lazily-loaded wrapper around ``headroom.compress``.""" | ||
|
|
||
| def __init__(self, config: Optional[CompressionConfig] = None): | ||
| self.config = config or CompressionConfig() | ||
| self._headroom: Optional[tuple[Callable[..., Any], Any]] = None | ||
| self._unavailable = False | ||
| self._warned_unavailable = False | ||
|
|
||
| @classmethod | ||
| def from_env(cls) -> "MessageCompressor": | ||
| return cls(CompressionConfig.from_env()) | ||
|
|
||
| @property | ||
| def active(self) -> bool: | ||
| return self.config.enabled | ||
|
|
||
| def _load(self) -> Optional[tuple[Callable[..., Any], Any]]: | ||
| # Import once; cache success or failure so we don't retry every call. | ||
| if self._unavailable: | ||
| return None | ||
| if self._headroom is not None: | ||
| return self._headroom | ||
| try: | ||
| from headroom import CompressConfig, compress # type: ignore | ||
| except Exception as exc: # noqa: BLE001 — ImportError or broken install | ||
| self._unavailable = True | ||
| if not self._warned_unavailable: | ||
| log.warning( | ||
| "HEADROOM_COMPRESS is set but 'headroom-ai' is not importable " | ||
| "(%s); sending messages uncompressed. Install with: " | ||
| "pip install 'reviewbot[headroom]'", | ||
| exc, | ||
| ) | ||
| self._warned_unavailable = True | ||
| return None | ||
| self._headroom = (compress, CompressConfig) | ||
| return self._headroom | ||
|
|
||
| def compress( | ||
| self, messages: list[dict[str, Any]], *, model: Optional[str] = None | ||
| ) -> list[dict[str, Any]]: | ||
| if not self.config.enabled or not messages: | ||
| return messages | ||
| loaded = self._load() | ||
| if loaded is None: | ||
| return messages | ||
| compress_fn, compress_config_cls = loaded | ||
| cfg = compress_config_cls( | ||
| compress_user_messages=self.config.compress_user_messages, | ||
| compress_system_messages=self.config.compress_system_messages, | ||
| protect_recent=self.config.protect_recent, | ||
| target_ratio=self.config.target_ratio, | ||
| min_tokens_to_compress=self.config.min_tokens_to_compress, | ||
| kompress_model=self.config.kompress_model, | ||
| ) | ||
| try: | ||
| # `model` is only for token counting / context limit — the request | ||
| # still goes to the bot's own OpenAI-compatible endpoint. | ||
| result = compress_fn( | ||
| messages, | ||
| model=model or "gpt-4o", | ||
| model_limit=self.config.model_limit, | ||
| config=cfg, | ||
| ) | ||
| except Exception: # noqa: BLE001 — never break a review on compression | ||
| log.warning( | ||
| "headroom compression failed; using original messages", exc_info=True | ||
| ) | ||
| return messages | ||
|
|
||
| saved = getattr(result, "tokens_saved", 0) or 0 | ||
| if saved > 0: | ||
| log.info( | ||
| "headroom compressed context: %d -> %d tokens (saved %d, %.0f%%)", | ||
| getattr(result, "tokens_before", 0), | ||
| getattr(result, "tokens_after", 0), | ||
| saved, | ||
| 100.0 * (getattr(result, "compression_ratio", 0.0) or 0.0), | ||
| ) | ||
| return getattr(result, "messages", messages) or messages |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
run:block only forwards three headroom env vars, but the README documents seven. Action users cannot configureHEADROOM_COMPRESS_SYSTEM_MESSAGES,HEADROOM_PROTECT_RECENT,HEADROOM_MIN_TOKENS,HEADROOM_KOMPRESS_MODEL, orHEADROOM_MODEL_LIMITviawith:inputs because there are no corresponding action inputs.Please add the missing inputs and forward them in the
run:step so the Action interface matches the README table.