# CI code review

> A GitHub Actions workflow that reviews each pull request diff with MirAPI chat completions and posts the result as a comment.

Every pull request can get an automated review without anyone installing a local tool. A GitHub Actions job captures the PR diff, sends it to `POST https://api.mirapi.ai/v1/chat/completions`, and posts the model's findings back as a comment.

**Deliverable:** every pull request gets an automated review comment from MirAPI.

**Prerequisites:** a GitHub repository with Actions enabled; a `MIRAPI_API_KEY` stored as a repository secret.

## 1. Store the key

1. Create a dedicated key in https://console.mirapi.ai → **API Keys**.
2. Set a **spend quota** on it — CI runs unattended, and the quota caps the damage of a buggy workflow.
3. In GitHub: **Settings → Secrets and variables → Actions**, add `MIRAPI_API_KEY`.

Never put the key in the workflow file or in `.env` inside the repo. A dedicated CI key also keeps that spend visible in one place for reconciliation.

## 2. The workflow

Create `.github/workflows/mirapi-review.yml`:

```yaml
name: MirAPI code review

on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Capture the diff
        run: |
          git fetch origin "${{ github.event.pull_request.base.ref }}"
          git diff "origin/${{ github.event.pull_request.base.ref }}...HEAD" > /tmp/pr.diff
          wc -c /tmp/pr.diff

      - name: Review with MirAPI
        env:
          MIRAPI_API_KEY: ${{ secrets.MIRAPI_API_KEY }}
        run: |
          python3 - <<'PY'
          import os, sys, requests
          diff = open("/tmp/pr.diff", encoding="utf-8", errors="replace").read()[:30000]
          resp = requests.post(
              "https://api.mirapi.ai/v1/chat/completions",
              headers={"Authorization": f"Bearer {os.environ['MIRAPI_API_KEY']}"},
              json={
                  "model": "deepseek-chat",
                  "messages": [
                      {"role": "system", "content": "You are a careful code reviewer. Report bugs, security issues, and regressions concisely."},
                      {"role": "user", "content": f"Review this diff:\n\n{diff}"},
                  ],
                  "max_tokens": 1500,
              },
              timeout=120,
          )
          if resp.status_code != 200:
              print(f"MirAPI error {resp.status_code}: {resp.text}", file=sys.stderr)
              sys.exit(1)
          body = resp.json()["choices"][0]["message"]["content"]
          open("/tmp/review.txt", "w").write(body)
          PY

      - name: Post the comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/review.txt
```

The request body has three fields that matter here:

| Field | Purpose |
|---|---|
| `model` | The model that writes the review — `deepseek-chat` in the example. |
| `messages` | A `system` prompt with review instructions plus a `user` message carrying the diff. |
| `max_tokens` | Caps the review length, which also caps the token spend on each run. |

Optional tuning: set `reasoning_effort` to make a reasoning model think harder (reasoning tokens bill as output), or use `response_format` to have the model return JSON when it supports structured output.

## 3. Review request reference

The same key works across three protocol skins. The example above uses the OpenAI skin; the other two let you fire the same review from Anthropic- or Gemini-style clients.

| Protocol | Base URL | Endpoint | Auth header |
|---|---|---|---|
| OpenAI | `https://api.mirapi.ai/v1` | `POST /chat/completions` | `Authorization: Bearer` |
| Anthropic | `https://api.mirapi.ai` | `POST /v1/messages` | `x-api-key` or `Authorization: Bearer` |
| Gemini | `https://api.mirapi.ai` | `POST /v1beta/models/{model}:generateContent` | `x-goog-api-key` |

### OpenAI (chat completions)

```text
POST https://api.mirapi.ai/v1/chat/completions
Authorization: Bearer sk-...
```

```json
{
  "model": "deepseek-chat",
  "messages": [
    {"role": "system", "content": "You are a careful code reviewer."},
    {"role": "user", "content": "Review this diff:\n\n…"}
  ],
  "max_tokens": 1500
}
```

### Anthropic (messages)

```text
POST https://api.mirapi.ai/v1/messages
x-api-key: sk-...
```

```json
{
  "model": "deepseek-chat",
  "max_tokens": 1500,
  "system": "You are a careful code reviewer.",
  "messages": [
    {"role": "user", "content": "Review this diff:\n\n…"}
  ]
}
```

### Gemini (generateContent)

```text
POST https://api.mirapi.ai/v1beta/models/deepseek-chat:generateContent
x-goog-api-key: sk-...
```

```json
{
  "contents": [
    {
      "role": "user",
      "parts": [{"text": "You are a careful code reviewer.\n\nReview this diff:\n\n…"}]
    }
  ]
}
```

Not every model is reachable through every skin — check `GET /v1/models` for a model's capabilities before wiring it into CI.

## 4. Error handling

The workflow should fail loudly rather than post an empty comment, so the script exits non-zero on a non-200 response. The status codes you are most likely to meet in CI:

| Code | Meaning | What to do |
|---|---|---|
| 401 | Missing or invalid key | Fix the `MIRAPI_API_KEY` secret |
| 403 | Valid key, request rejected (balance, whitelist, quota, IP) | Resolve the cause, then retry |
| 429 | Rate limited | Back off with jitter (1s → 2s → 4s, capped ~30s) — the gateway does not retry for you |
| 500 | Gateway or upstream failure | Retry idempotent calls |

The error body uses the OpenAI envelope — `{"error": {"message", "type", "param", "code"}}` — and every message ends with a request ID. Save that ID from a failed run; it is what support needs to look up the call.

## 5. Verify

Open a pull request and check the review comment appears. Then confirm the request was billed:

```bash
curl https://api.mirapi.ai/api/log/token -H "Authorization: Bearer $MIRAPI_API_KEY"
```

`/api/log/token` lists per-request rows; `https://api.mirapi.ai/api/usage/token` gives account-level totals. A non-streaming response only reports token counts in `usage` — the dollar amount lives in the console and these logs.

## Pitfalls

- **Rate limits:** several commits in one push still produce one review (one `synchronize` event). If you run several models or longer prompts, pace them; MirAPI returns 429 when you exceed the limit.
- **Diff size:** the script truncates at 30 000 characters; split very large diffs across calls if needed.
- **403 with a valid key** means the quota is exhausted or the model is not whitelisted — raise the quota or fix the whitelist.

## Approximate cost

One review of a typical PR is a few thousand tokens — usually a fraction of a cent to a few cents. Spend draws from your prepaid balance, which never expires, so an idle workflow costs nothing. To keep reviews cheap, lower `max_tokens`, truncate the diff, and keep the system prompt identical across runs so it can benefit from automatic prompt caching.

## Related links

- [Automatic code review](/docs/tutorials/auto-code-review) — the same idea inside Claude Code.
- [First integration](/docs/tutorials/first-integration) — a minimal first call if this workflow is your first time using MirAPI.
- [Errors](/docs/api-reference/errors) — the full status-code and retry reference.
- [Configuration](/docs/configuration) — key constraints (whitelist, quota, validity, IP allowlist).
- [Billing & top-ups](/docs/billing) — prepaid balance and usage reconciliation.