{"slug": "zero-budget-release-notes-a-case-study-in-free-tier-llm-automation", "title": "Zero-Budget Release Notes: A Case Study in Free-Tier LLM Automation", "summary": "A developer built a release-notes bot that automatically converts raw git history into user-facing notes using only free-tier LLM tokens and a free server. The project, which runs unattended on a schedule, uses MonkeyCode's free model access and a single Python file to collect commits, summarize them via an OpenAI-compatible endpoint, and render Markdown notes. The design prioritizes resilience, ensuring output continues even when the free model tier fails.", "body_md": "The current wave of AI tooling pushes teams toward bigger agents, bigger context windows, and bigger monthly bills. The opposite constraint produces a more honest design. This case study walks through one small project built on free model tokens and a free server: a release-notes bot that turns raw git history into user-facing notes, end to end, from background to lessons learned.\n\nThe project started with a familiar annoyance. A library shipped weekly, and every release required a human to read forty or fifty commit messages and translate them into something a user could understand. The messages were technical, inconsistent, and occasionally embarrassing. Automating the translation with an LLM was the obvious fix, but the budget was exactly zero: no paid API credits, no paid server, no tolerance for a recurring bill.\n\nThe goal had three parts. Generate concise, user-facing release notes from the git history between two tags. Run unattended on a schedule. Cost nothing. A fourth constraint appeared during design: the job had to keep producing output even when the free model tier failed, because a release process that depends on a rate limit is a release process that breaks on a Tuesday.\n\nThe project used MonkeyCode's free model access and its free server option for hosting. Disclosure: This article was prepared as part of MonkeyCode's product outreach. At the time of writing, the free tier includes a 10-million-token allowance, but quotas and terms change, so the current numbers should be verified before any team relies on them. The architecture below does not depend on those specific numbers; it works with any OpenAI-compatible endpoint and any free server that can run cron.\n\nThe implementation is a single Python file with three responsibilities: collect commits, summarize chunks, and write the notes file. The collect step runs git log between the two refs and keeps only the subject line of each commit. The summarize step sends chunks of twenty-five commits to an OpenAI-compatible chat endpoint, asks for a JSON array of categorized bullets, and validates the response. The write step renders the result into a Markdown file. The only dependencies are the openai and tiktoken packages.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Generate release notes from git history using a free-tier LLM.\n\nUsage:\n    python release_notes.py <from_ref> <to_ref> [--out RELEASE_NOTES.md]\n\nEnvironment:\n    LLM_BASE_URL   provider endpoint (OpenAI-compatible)\n    LLM_API_KEY    provider key\n    LLM_MODEL      model name from the provider dashboard\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\nCHUNK_SIZE = int(os.getenv(\"COMMIT_CHUNK_SIZE\", \"25\"))\nMAX_TOKENS = int(os.getenv(\"LLM_MAX_TOKENS\", \"400\"))\nMAX_RETRIES = 3\n\nSYSTEM_PROMPT = (\n    \"You turn raw git commit subjects into concise user-facing release notes. \"\n    \"Return a JSON object with one key, 'items', an array of strings. \"\n    \"Each string must start with a category: Feature, Fix, Docs, or Chore. \"\n    \"Do not invent details that are not in the commits.\"\n)\n\ndef git_log(from_ref: str, to_ref: str) -> list[str]:\n    result = subprocess.run(\n        [\"git\", \"log\", \"--oneline\", f\"{from_ref}..{to_ref}\"],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    return [line.split(\" \", 1)[1] for line in result.stdout.splitlines() if line.strip()]\n\ndef chunks(items: list[str], size: int):\n    for i in range(0, len(items), size):\n        yield items[i : i + size]\n\ndef summarize_chunk(client: OpenAI, commit_chunk: list[str]) -> list[str]:\n    response = client.chat.completions.create(\n        model=os.environ[\"LLM_MODEL\"],\n        messages=[\n            {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n            {\"role\": \"user\", \"content\": \"\\n\".join(f\"- {c}\" for c in commit_chunk)},\n        ],\n        temperature=0.2,\n        max_tokens=MAX_TOKENS,\n        response_format={\"type\": \"json_object\"},\n    )\n    payload = json.loads(response.choices[0].message.content)\n    return payload[\"items\"]\n\ndef fallback_summarize(commit_chunk: list[str]) -> list[str]:\n    categories = {\"feat\": \"Feature\", \"fix\": \"Fix\", \"docs\": \"Docs\", \"refactor\": \"Chore\"}\n    grouped: dict[str, list[str]] = {}\n    for commit in commit_chunk:\n        prefix = commit.split(\":\", 1)[0].lower()\n        category = categories.get(prefix, \"Chore\")\n        grouped.setdefault(category, []).append(commit)\n    return [f\"{category}: {commit}\" for category, commits in grouped.items() for commit in commits]\n\ndef with_retry(client: OpenAI, commit_chunk: list[str]) -> list[str]:\n    size = len(commit_chunk)\n    for attempt in range(MAX_RETRIES):\n        try:\n            return summarize_chunk(client, commit_chunk[:size])\n        except Exception:\n            if attempt == MAX_RETRIES - 1:\n                return fallback_summarize(commit_chunk)\n            size = max(1, size // 2)\n            time.sleep(2**attempt)\n\ndef main() -> int:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"from_ref\")\n    parser.add_argument(\"to_ref\")\n    parser.add_argument(\"--out\", default=\"RELEASE_NOTES.md\")\n    args = parser.parse_args()\n\n    commits = git_log(args.from_ref, args.to_ref)\n    if not commits:\n        Path(args.out).write_text(\"No user-facing changes in this release.\\n\", encoding=\"utf-8\")\n        return 0\n\n    client = OpenAI(base_url=os.environ[\"LLM_BASE_URL\"], api_key=os.environ[\"LLM_API_KEY\"])\n    notes: list[str] = []\n    for chunk in chunks(commits, CHUNK_SIZE):\n        notes.extend(with_retry(client, chunk))\n\n    body = \"\\n\".join(f\"- {note}\" for note in notes)\n    Path(args.out).write_text(f\"## Release notes\\n\\n{body}\\n\", encoding=\"utf-8\")\n    return 0\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\nThree design decisions matter more than the code itself. First, the chunk size keeps every request far below the model's context limit, so a large release window is processed as many small requests instead of one fragile one. Second, the retry loop halves the chunk on every failure, which handles both rate limits and context overflows with the same mechanism. Third, the fallback summarizer is deterministic: it groups commits by conventional-commit prefix, so the job always produces a file, even if the model never answers. If the model does not support structured JSON output, the validation step rejects the response and the fallback takes over, which keeps the pipeline honest about its dependency.\n\nDeployment is deliberately boring. The free server runs a weekly cron job that executes the script between the two latest tags and commits the resulting file back to the repository.\n\n```\n0 9 * * 1 cd /opt/release-notes && /usr/bin/python3 release_notes.py v1.2.0 v1.3.0 --out RELEASE_NOTES.md\n```\n\nA missed run is harmless, because the script is idempotent and the git range can be replayed. The file is written atomically, so a partial write never ships.\n\nThe interesting results are the failure modes, because that is where free tiers reveal their personality. The table below maps each scenario to the behavior the script implements.\n\n| Scenario | Observed behavior | Mitigation |\n|---|---|---|\n| Endpoint returns 429 (rate limited) | Request fails | Exponential backoff, then fallback summarizer |\n| Chunk exceeds the context limit | API error or truncated JSON | Chunk size halves on retry |\n| Model returns malformed JSON | json.JSONDecodeError | Re-request once, then fallback |\n| Empty diff between tags | No commits | Writes \"No user-facing changes\" and exits cleanly |\n| Free allowance exhausted | 402 or 403 errors | Fallback path keeps the release notes flowing |\n\nA sample of the output, formatted by the script, looks like this (illustrative):\n\n```\n## Release notes\n\n- Feature: Added exponential backoff for rate-limited LLM calls\n- Fix: Corrected chunk-size overflow on large release windows\n- Docs: Documented the LLM_BASE_URL environment variable\n```\n\nToken accounting is the part most teams skip. The prompt for a chunk of twenty-five commits costs roughly 1,200 input tokens, and the response roughly 300, but those numbers vary with commit length. The honest way to measure is tiktoken, not a guess:\n\n``` php\nfrom tiktoken import encoding_for_model\n\ndef estimate_tokens(text: str) -> int:\n    return len(encoding_for_model(\"gpt-4o\").encode(text))\n```\n\nWith a 10-million-token allowance, even a pessimistic 2,000 tokens per chunk supports thousands of chunks. The arithmetic matters less than the habit: measure the prompt, multiply by expected runs, and keep the projected consumption below ten percent of the allowance so retries never exhaust the budget mid-cycle.\n\nThree lessons carried over to other projects. First, the fallback path is the real product; the LLM is an enhancement on top of it. Second, free tiers are burst budgets, not baselines, so the schedule must assume occasional 429s and empty replies. Third, a free server is a fine home for a weekly batch job and a poor home for a latency-sensitive endpoint, which is why the design kept all heavy work off the request path.\n\nThe approach is wrong for several teams. Anyone processing customer data should not point it at a free tier with unclear retention. Anyone with a user-facing, latency-sensitive endpoint should not host it on a free server. Anyone who needs a guaranteed SLA should buy one. The bot is a tool for internal, low-frequency, non-critical automation, and it is honest about being that.\n\nThe full script is above; forking it into a weekly cron job takes an afternoon. The project is small, the failure modes are contained, and the next release notes will write themselves. MonkeyCode's free tier is one way to get the tokens and the server, but the script works with any OpenAI-compatible endpoint, which is the point: the design, not the provider, is what makes the cost zero.", "url": "https://wpnews.pro/news/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation", "canonical_source": "https://dev.to/aiio_6471/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation-1a88", "published_at": "2026-08-25 05:20:38+00:00", "updated_at": "2026-08-25 05:43:35.681098+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools", "ai-tools"], "entities": ["MonkeyCode", "OpenAI"], "alternates": {"html": "https://wpnews.pro/news/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation", "markdown": "https://wpnews.pro/news/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation.md", "text": "https://wpnews.pro/news/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation.txt", "jsonld": "https://wpnews.pro/news/zero-budget-release-notes-a-case-study-in-free-tier-llm-automation.jsonld"}}