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.
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.
| 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), 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-keyheader; other OpenAI-compatible endpoints getAuthorization: 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.
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:
- 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
askmode, the App sends arepository_dispatchevent of typeupseam-generatewith 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 toupseam/<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.