# graphify-gemini-bridge

OpenAI-compatible shim so `graphify`'s `--backend openai` can reach Google Gemini via
the ADR-114 Tier-1 OAuth CLI path, instead of the deprecated static `GEMINI_API_KEY`
(GCP project `933872821357`, `CONSUMER_SUSPENDED` since 2026-07-12).

**Deployed at**: `/opt/graphify-gemini-bridge/shim.py` (this copy is for version control —
the live file is deployed, not symlinked; re-copy after editing here).

**Runs as**: systemd unit `graphify-gemini-bridge.service` (installed at
`/etc/systemd/system/`, copy also kept here), listening on `127.0.0.1:8793`.

**Nightly cron** (`crontab -l`, 30 20 * * * = ~02:00 IST) now reads:
```
cd /var/www/360lm && OPENAI_BASE_URL=http://127.0.0.1:8793/v1 OPENAI_API_KEY=unused GEMINI_API_KEY= \
  /root/.local/bin/graphify . --update --backend openai --model gemini-2.5-flash --max-concurrency 1 >> /var/log/graphify-update.log 2>&1
```
**Note the argument order**: `graphify <path> --update ...` (path first) accepts `--backend`/`--model`/`--max-concurrency`.
`graphify update <path> ...` is a DIFFERENT, older subcommand that only accepts `--force`/`--no-cluster` — passing
`--backend` to it fails with `error: unknown update option: --backend`. The first cron fix (2026-07-17) used the
wrong form and was still silently broken until caught and corrected on 2026-07-18.

## Manual invocation
```
OPENAI_BASE_URL=http://127.0.0.1:8793/v1 OPENAI_API_KEY=unused GEMINI_API_KEY= \
  graphify . --update --backend openai --model gemini-2.5-flash --max-concurrency 1
```

## Design notes / incident history (2026-07-17)

- Gemini CLI calls are serialized (one at a time) — the free OAuth tier's daily quota
  is unknown, and graphify can fire several chunks in parallel.
- Large chunks (up to graphify's 60k-token budget) are piped via **stdin**, not passed
  as a `-p` CLI argument — argv-based prompts hit `Argument list too long` for both
  large text chunks and image chunks with sizeable accompanying context. `@file` image
  references still have to live inside `-p` itself (stdin-only doesn't parse), so `-p`
  stays a short fixed string and the real content always goes over stdin.
- **Timeout handling kills the whole process group** (`start_new_session=True` +
  `os.killpg` on `TimeoutExpired`), not just the immediate child. `gemini` CLI spawns a
  heavier grandchild node worker; a plain `subprocess.run(timeout=...)` only kills the
  lightweight wrapper, orphaning the grandchild. On 2026-07-17 this leaked one ~1GB+
  zombie per timed-out chunk — 11 piled up over ~20 minutes and swapped the box solid
  (137MB free, 4/4GB swap) while other sessions/services were running. Fixed by killing
  the whole process group.
- **Ollama fallback is OFF by default** (`BRIDGE_OLLAMA_FALLBACK=false`). `llava`
  (CPU inference) competing for the same 2 cores as the Gemini CLI node process caused
  Gemini calls to slow past their own timeout, triggering more fallback, in a feedback
  loop — observed twice before being disabled. Gemini-only + fully serial is the
  current safe default; a failed chunk is skipped and picked up on the next
  `graphify update` (it retries anything not in the semantic cache). Re-enable via
  `BRIDGE_OLLAMA_FALLBACK=true` only after resolving the CPU contention (e.g. core
  pinning) — do not flip it back on casually.
- `GEMINI_TIMEOUT_S = 240` — large multi-file chunks (graphify bisects/batches up to
  ~17 files per chunk) can genuinely take longer than a single-file call; 120s was too
  tight and caused spurious failures even without contention.
- This VPS has a hard 2-concurrent-`claude`-session RAM cap (see
  `parallel_sessions.md`) and runs postgres + every 360lm PWA + other containers on
  2 cores / 7.8GB — any change here should stay conservative on concurrency and
  resource footprint by default.

See ADR-114 (`docs/adr/ADR-114-google-ai-oauth-vertex-ai.md`) for the OAuth/ADC
mandate this bridge implements, and `/opt/gemini-proxy/` for the sibling
media-analysis proxy that established the Tier-1 CLI env-var pattern
(`GOOGLE_CLOUD_PROJECT`, `GEMINI_CLI_TRUST_WORKSPACE=true`, explicit
`GEMINI_API_KEY` unset) this shim reuses.
