# Connect a model

Upseam writes fixes only with a model you connect and pay for. It has no
model keys of its own. What happens before you connect one is described in
[Without a model](#without-a-model).

There are two ways to connect a model, and you choose one per installation:
the **stored-key method** (your key is kept, encrypted, by Upseam) or
**generation in your GitHub Actions** (your key stays in your GitHub
secrets).

To have Claude Code, Codex or OpenCode write the fixes instead of a model key, see
[Bring your own agent](own-agent.md).

|                         | Key stored with Upseam (recommended)             | Generate in your GitHub Actions                        |
| ----------------------- | ------------------------------------------------ | ------------------------------------------------------ |
| Setup                   | Paste the key on the settings page.              | A workflow file and a secret in each repository, or an organization secret. |
| Where the key lives     | Encrypted in Upseam's database.                  | Your GitHub secrets. Upseam never sees it.             |
| Where the model is called | Upseam's patch step.                           | Your runner, by the Upseam Action.                     |
| What the model vendor gets | The change data and the files with matches, plus your `.editorconfig` and Prettier settings as style hints. | The change data and the files with matches. |
| Revoke                  | **Disconnect model** on the settings page.       | **Stop generating in Actions**, then delete the secret. |

## Without a model

Until a model is connected, generation in your GitHub Actions is chosen, or
the repository names its own agent in `.github/upseam.yml` (see
[Bring your own agent](own-agent.md)), Upseam writes no fixes. You still see
what it would fix:

- **Ready to fix.** Fixable findings are listed in the dashboard issue under
  **Ready to fix: connect a model or your agent**, with a link to the
  settings page. There is no approval checkbox yet, because nothing can be
  written.
- **Slack.** If Slack is connected, each such finding is posted once as an
  information message with an **Open settings** button and no approval
  buttons. It is not posted again when the finding is matched again.
- Findings that need you are listed in the dashboard issue as usual.
- **Exception: Dependabot and Renovate bumps.** These always wait for
  approval, so they appear under **Waiting for approval**, in Slack and as a
  comment on the bot's pull request even without a model. Approving one
  without a model leaves it waiting until you connect one.
- **Exception: successor models that need you.** When a successor-model
  finding needs you, for example because the vendor names several
  replacements, the Slack message about it is posted without a model too.

When you connect a model or choose generation in your GitHub Actions, the
ready findings go on without a new scan, exactly as if the model had been
there from the start: in the `ask` mode they move to **Waiting for approval**
and the same Slack message gains the approval buttons; in the `auto` mode
Upseam writes the patch and the message follows the pull request. Setting
`agent.runner` takes effect with the push that changes `.github/upseam.yml`.

## Key stored with Upseam

On the settings page, under **Model**, pick the vendor, enter the model and
the key, and press **Connect model**.

| Vendor              | Fields                                                    |
| ------------------- | --------------------------------------------------------- |
| `openai`            | Model, API key.                                           |
| `anthropic`         | Model, API key.                                           |
| `openai-compatible` | Model, base URL, API key. For OpenRouter, Azure OpenAI or your own server. Only with a stored key. |

- **Model** is the id as your vendor names it. Upseam keeps no list of models.
- **Base URL** must be `https`, without credentials, and resolve only to
  public addresses. Private, loopback and link-local addresses are refused,
  and redirects are not followed. A self-hosted server must be reachable from
  the internet.
- Azure OpenAI hosts get the key in the `api-key` header; other
  OpenAI-compatible endpoints get `Authorization: Bearer`.

Upseam makes one test call before it saves the key; the key is stored only if
the call succeeds. **Test** decrypts the stored key and repeats the call. An installation gets at
most five connection attempts in ten minutes.

How the key is kept:

- It is encrypted with AES-256-GCM under a data key of its own, and the data
  key is wrapped with a master key. The installation id is bound into the
  encryption, so the ciphertext cannot be moved to another installation.
- It is decrypted only for **Test** and inside the patch step, and is never
  logged or shown again. For keys of 20 characters or more, the settings page
  shows the last four characters, only to people who can change the model.
- Anyone who holds both the master key and a copy of the database can
  decrypt it. If that is not acceptable, generate fixes in your own GitHub
  Actions instead.
- **Disconnect model** deletes it. We also recommend revoking the key with your
  vendor.

## Generate in your GitHub Actions

Choose this if the key must never leave your GitHub secrets. On the settings
page, under **Generate in my GitHub Actions (key stays in my secrets)**, name
the vendor (`anthropic` or `openai`) and the model your workflow uses, and
press **Use GitHub Actions**. Switching to this method deletes a key stored with Upseam.

### Generation for the Upseam App

Add this workflow to the default branch of each repository. The `vendor` and
`model` inputs must name the same vendor and model as the settings page: the
App does not send them.

```yaml
name: upseam-generate
on:
  repository_dispatch:
    types: [upseam-generate]
permissions:
  contents: write
concurrency: upseam-generate-${{ github.event.client_payload.group }}
jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          persist-credentials: false
      - uses: upseam/action@<40-character commit SHA> # pin a release
        with:
          vendor: anthropic
          model: <model id>
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
```

For OpenAI, set `vendor: openai` and pass `OPENAI_API_KEY` instead. For an
OpenAI-compatible endpoint, also set `base-url`:

```yaml
      - uses: upseam/action@<40-character commit SHA> # pin a release
        with:
          vendor: openai
          model: <model id>
          base-url: https://<your endpoint>/v1
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
```

The `upseam/action` Action is published with the first release; until then
use the stored key.

- **Pin the Action to a full commit SHA.** This step holds your key and pushes
  with `contents: write`.
- **How it runs.** When a fix is due, after approval in the `ask` mode, the App sends a
  `repository_dispatch` event of type `upseam-generate` with the group key,
  the commit to start from, the expected branch head, the proposal id and the
  change event ids. No code and no key. The Action finds the group again in
  your code, calls your model, checks the answer with the same gates as the
  App and pushes one commit to `upseam/<group>`. It opens no pull request.
- **Server-side check.** The App accepts the push only from
  `github-actions[bot]`, as one commit on the expected base that modifies 1 to
  50 existing files, and only after its own gates pass on the diff. Then it
  opens the pull request.
- **Without the workflow** the App sends nothing and says what to add in the
  dashboard issue, in Slack and on the settings page. If no push arrives
  within an hour, the finding goes to **Needs you**.
- Internal contract changes are not generated this way.
