{"id":50276673,"url":"https://github.com/loglux/grammar-llm-bridge","last_synced_at":"2026-05-27T21:01:34.514Z","repository":{"id":357016462,"uuid":"1234708808","full_name":"loglux/Grammar-LLM-Bridge","owner":"loglux","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-10T23:51:50.000Z","size":348,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-11T00:37:18.259Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loglux.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-10T14:40:56.000Z","updated_at":"2026-05-10T23:51:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/loglux/Grammar-LLM-Bridge","commit_stats":null,"previous_names":["loglux/grammar-llm-bridge"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/loglux/Grammar-LLM-Bridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loglux%2FGrammar-LLM-Bridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loglux%2FGrammar-LLM-Bridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loglux%2FGrammar-LLM-Bridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loglux%2FGrammar-LLM-Bridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loglux","download_url":"https://codeload.github.com/loglux/Grammar-LLM-Bridge/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loglux%2FGrammar-LLM-Bridge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33583399,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-27T02:00:06.184Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-27T21:01:32.910Z","updated_at":"2026-05-27T21:01:34.508Z","avatar_url":"https://github.com/loglux.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Grammar-LLM-Bridge\n\n[![CI](https://github.com/loglux/Grammar-LLM-Bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/loglux/Grammar-LLM-Bridge/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)\n\nA FastAPI server that speaks the `/v2/check` LanguageTool API on one side and OpenAI-compatible chat-completions on the other (OpenAI, DeepSeek, OpenRouter, Anthropic via OpenRouter, Ollama, ...). Drop it in front of any LT client — Obsidian plugin, browser extension, custom integration — and get LLM-quality grammar checks against the model of your choice.\n\nTested with the two main Obsidian LT plugins:\n\n- [`Clemens-E/obsidian-languagetool-plugin`](https://github.com/Clemens-E/obsidian-languagetool-plugin) — the original (maintenance mode).\n- [`wrenger/obsidian-languagetool`](https://github.com/wrenger/obsidian-languagetool) — a more active fork.\n\nIf you want a build that includes a pending fix for the long-standing auto-check\n\"underlines may not move correctly when typing\" race condition, see\n[`loglux/obsidian-languagetool`](https://github.com/loglux/obsidian-languagetool) —\na fork of `wrenger` with the fix applied (also submitted upstream as\n[wrenger #56](https://github.com/wrenger/obsidian-languagetool/pull/56) and\n[Clemens-E #144](https://github.com/Clemens-E/obsidian-languagetool-plugin/pull/144)).\n\nAll three send `apiKey` and `username` in the request body (form-urlencoded); the Bridge's auth middleware understands that flow.\n\n## Quick start\n\n### 1. Clone and configure\n\n```bash\ngit clone https://github.com/loglux/Grammar-LLM-Bridge.git\ncd Grammar-LLM-Bridge\ncp .env.example deploy/load-balancer/.env.bridge\n$EDITOR deploy/load-balancer/.env.bridge       # set OPENAI_API_KEY etc.\n```\n\n`.env.example` documents every variable, including aggressive-chunking options that reduce context-induced false positives.\n\n### 2. Build and run\n\nProduction stack (nginx load-balancer + two app replicas, port `8081`):\n\n```bash\nmake build         # docker build -t grammar-llm-bridge:latest .\nmake up            # docker compose up -d\nmake logs-01       # tail one replica's logs\n```\n\nSingle dev instance on port `9019` (handy for the browser test UI below):\n\n```bash\nmake up-dev\nmake logs-dev\n```\n\n### 3. Point your client at it\n\nFor an Obsidian LT plugin, set **Server URL** to:\n\n- `http://localhost:8081/v2/check` (LB) or\n- `http://localhost:9019/v2/check` (dev).\n\n`grammar-checker.html` in the repo root is a self-contained browser test UI — open it directly in a browser, no build step required.\n\n## Configuration knobs\n\nAll via env vars (see `.env.example` for the canonical list):\n\n| Variable | Purpose |\n|---|---|\n| `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `LLM_MODEL` | Backend selection (any OpenAI-compatible API). |\n| `GRAMMAR_ONLY` | If `true`, suppresses style/wordiness suggestions. |\n| `TYPOGRAPHY_CHECK` | Include typographic issues. |\n| `LLM_CHUNKING` / `LLM_CHUNK_SIZE` / `LLM_CHUNK_THRESHOLD` | Split long inputs into chunks (each its own LLM call). |\n| `LLM_CHUNK_STRATEGY` | `paragraph` (legacy) or `recursive` (LaTeX-safe, splits inside paragraphs by sentence to reduce false positives). |\n| `LLM_TIMEOUT` | Per-request timeout in seconds. |\n\n## Production topology\n\nThe `deploy/load-balancer/` stack runs three containers behind one host port:\n\n```\n                ┌──────────────────────────────┐\nhost :8081 ─────▶ grammar-llm-balancer (nginx) │   round-robin\n                └──────┬──────────────┬────────┘\n                       │              │\n                       ▼              ▼\n              grammar-llm-01    grammar-llm-02\n               (FastAPI/        (FastAPI/\n                uvicorn 4w)      uvicorn 4w)\n                       │              │\n                       └─────┬────────┘\n                             ▼\n                  shared SQLite (bind mount `./data`,\n                  DATABASE_URL=sqlite+aiosqlite:///…)\n```\n\n- `nginx` listens on **`:8081`** and round-robins between the two replicas (no host ports on the replicas themselves — they are reachable only inside the docker network).\n- Each replica is `grammar-llm-bridge:latest` running `uvicorn app.main:app --workers 4`.\n- A separate `grammar-llm-dev` profile exposes a single replica on **`:9019`** for local testing (`make up-dev`).\n- LLM API calls are stateless; auth state and rate-limiter counters live in the shared SQLite file so both replicas see the same `apiKey` records.\n\nFor details (network config, fixed IPs, capacity planning) see:\n\n- [`deploy/load-balancer/LOAD_BALANCER_SETUP.md`](./deploy/load-balancer/LOAD_BALANCER_SETUP.md) — concrete setup steps.\n- [`deploy/load-balancer/NETWORK_NOTES.md`](./deploy/load-balancer/NETWORK_NOTES.md) — `extra_hosts` workaround, DNS notes.\n- [`deploy/load-balancer/CAPACITY.en-GB.md`](./deploy/load-balancer/CAPACITY.en-GB.md) — throughput math and recommended limits.\n\n## Layout\n\n| Path | Purpose |\n|---|---|\n| `app/` | Modular FastAPI package; entrypoint `app.main:app`. |\n| `deploy/load-balancer/` | Production Docker Compose stack (nginx + 2 replicas + shared SQLite). |\n| `docs/` | Architecture, auth, prompt rules, level modes. |\n| `docs/research/` | Background notes and design discussions. |\n| `docs/qa/` | QA-observed model-behaviour notes. |\n| `qa-results/quality/` | Curated gold suites and quality runners. |\n| `qa-results/ad-hoc/` | Ad-hoc tools (`run_samples.py`, `analyze_last_run.py`) and sample texts. |\n| `tests/unit/` | Unit tests for the bridge (chunking, masking, mapping). |\n| `Makefile` | Build / run / logs / smoke / quality. |\n| `grammar-checker.html` | Self-contained browser test UI. |\n\n## Development\n\n```bash\npython -m venv venv\nsource venv/bin/activate\npip install -r requirements.txt\npip install ruff pytest\n\nruff check app/ tests/       # lint\npytest tests/ -v             # unit tests (currently 21)\n```\n\nCI runs the same `ruff` + `pytest` on every push to `master` against Python 3.11 and 3.12 — see [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).\n\n## How it works\n\nThe LT plugin sends an annotated text (text + markup parts) to `/v2/check`. The Bridge extracts a *logical* text without markup, builds a position map, optionally splits the logical text into chunks, and asks the configured LLM to find grammar errors. The LLM's suggestions come back with offsets in logical-text space; the Bridge maps them back to the original markup-aware offsets the plugin expects, deduplicates overlapping spans, and returns an LT-compatible response. LaTeX math blocks are detected and protected so they aren't underlined and aren't cut in the middle by the chunker.\n\nThe system prompt is composed from small forbid/allow blocks — we call it a *semantic firewall*: default is \"be restrained\" (no style rewrites, skip math, no quote-style changes, etc.), then specific blocks explicitly re-open high-value error classes (e.g. missing articles must be reported). Adding a new rule means following the same shape. Details: [`docs/prompt_rules.md`](./docs/prompt_rules.md).\n\n## Languages\n\nThis is an **experimental** project tuned around English (en-GB / en-US is what the test suite covers). Mechanically the Bridge is language-agnostic — it forwards whatever `language` code the client sends (`ru-RU`, `de-DE`, `fr-FR`, ...) to the LLM, and modern multilingual models (DeepSeek, GPT-4.x, Claude, ...) will return grammar suggestions for that language.\n\nTwo caveats — what is and isn't there:\n\n- **Language parameter is plumbed through**: `GET /v2/languages` advertises en-US/GB/AU, ru-RU, de-DE, fr-FR, es-ES, and `auto`. The `language` field from the client is passed into the LLM prompt verbatim.\n- **System prompt is English-centric**: the rule blocks in `app/prompts.py` (subject–verb agreement, article rules, ESL hints) are written for English. On other languages they don't apply or produce a different false-positive profile.\n- **No real auto-detection yet**: sending `language=auto` just forwards the string to the LLM, which usually figures the language out on its own but without confidence guarantees. A comparison of candidate detectors (fastText/langid/cld3/langdetect) for a future implementation lives in [`docs/research/QA_LANGUAGE_DETECTION.md`](./docs/research/QA_LANGUAGE_DETECTION.md).\n\nTo get production-quality results in another language you'll need:\n\n1. A **dedicated set of prompt blocks for that language** — not a translation of the English ones. Russian, German, Spanish each have their own grammar concerns (cases, no articles, ser/estar, agreement patterns) that English rules simply don't cover. Add a per-language overlay alongside the English one (see [`docs/prompt_rules.md`](./docs/prompt_rules.md) for the forbid/allow structure that each language module follows).\n2. A small gold-suite (`qa-results/quality/\u003clang\u003e/`) for the target language so you can compare model outputs and catch regressions.\n\nSee also [`docs/LEVEL_MODES.md`](./docs/LEVEL_MODES.md) for how `picky`/`default` modes inherit rules, and [`docs/style_prompt_blocks.md`](./docs/style_prompt_blocks.md) for style/toneTags presets.\n\nA concrete plan for splitting prompts into per-language modules (`app/prompts/en.py`, `ru.py`, `de.py`, ...) with a `get_prompt(language, mode)` dispatcher lives in [`ROADMAP.md`](./ROADMAP.md#language-support).\n\n## Privacy\n\nYour text is sent to whichever LLM provider you configure. If you'd rather keep everything on your machine, point the Bridge at a local Ollama (or similar) instance — same `OPENAI_BASE_URL` / `LLM_MODEL` pair, no external traffic.\n\n## License\n\n[MIT](./LICENSE) © loglux. Forks, PRs, and issue reports welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floglux%2Fgrammar-llm-bridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floglux%2Fgrammar-llm-bridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floglux%2Fgrammar-llm-bridge/lists"}