Review with AWS Bedrock (keyless OIDC)¶
Use this guide to run lgtmaybe with AWS Bedrock using GitHub's OIDC token — no static AWS credentials stored in secrets.
Contents¶
- How it works
- One-time AWS setup
- Workflow example
- Choosing a Bedrock model ID
- Running locally with ambient AWS credentials
- Troubleshooting
How it works¶
GitHub Actions issues a short-lived OIDC token. AWS STS exchanges that token
for temporary IAM credentials scoped to a role you control. The action performs
that exchange for you (pass aws_role_arn) and lgtmaybe picks up the ambient
credentials automatically — no AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY
in your secrets.
One-time AWS setup¶
This is the human-only part — do it once in your AWS account:
- Create an IAM OIDC identity provider for
token.actions.githubusercontent.com. - Create an IAM role with a trust policy scoped to your repo
(
repo:<org>/lgtmaybe:*). - Attach the least-privilege policy below.
- Confirm the models you want are enabled in the target region (model access request in the Bedrock console).
- Note the role ARN (e.g.
arn:aws:iam::123456789012:role/lgtmaybe-bedrock) — it becomes theaws_role_arnaction input. No static key is ever stored.
The role needs only:
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:*:<account-id>:inference-profile/*",
"arn:aws:bedrock:*::foundation-model/*"
]
}
Both ARNs are required for the recommended inference-profile model ids
(the us./eu./apac.-prefixed ids below). A cross-region inference profile
fans the call out to the foundation model in several regions, so the role needs
bedrock:InvokeModel* on both the inference-profile/* ARN and the
underlying foundation-model/* ARN. Granting only one of them still fails with
AccessDeniedException. A bare anthropic.… model id needs only the
foundation-model/* ARN, but most current Claude models are invocable only
through an inference profile — see the model table below.
Scope each Resource to specific model / inference-profile ARNs for tighter
least-privilege once it works, e.g.
arn:aws:bedrock:*:<account-id>:inference-profile/us.anthropic.claude-opus-4-8*
and arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-4-8*.
Workflow example¶
The action assumes the role for you — no separate configure-aws-credentials
step needed. Store the role ARN in an AWS_ROLE_ARN secret.
name: lgtmaybe
on:
pull_request_target:
issue_comment:
types: [created]
permissions:
id-token: write # required for the OIDC token exchange (keyless)
pull-requests: write # required to post review comments
contents: read
jobs:
review:
# Only trusted authors (repo owner / org member / collaborator) can trigger
# a review, so fork PRs and drive-by comments cannot spend your provider
# budget. A maintainer can still opt in to an external PR with /review.
if: >-
(github.event_name == 'pull_request_target' &&
contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)) ||
(github.event.issue.pull_request &&
contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
(contains(github.event.comment.body, '/review') ||
contains(github.event.comment.body, '/improve') ||
contains(github.event.comment.body, '/ask') ||
contains(github.event.comment.body, '/describe') ||
contains(github.event.comment.body, '/diagram')))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: MattJColes/lgtmaybe@v2
with:
provider: bedrock
model: us.anthropic.claude-sonnet-4-6
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region: us-east-1
Choosing a Bedrock model ID¶
The model must be a Bedrock model identifier. Bedrock hosts Anthropic
Claude, Amazon Nova/Titan, Meta Llama, Mistral, Cohere and others — it does
not host OpenAI's GPT models, so an id like openai.gpt-5.5 is rejected with
The provided model identifier is invalid. Use one of the Claude ids below
(prefixed with a cross-region inference profile — see the note):
| Model | Inference-profile ID (recommended) | Base model ID |
|---|---|---|
| Claude Opus 4.8 | us.anthropic.claude-opus-4-8 |
anthropic.claude-opus-4-8 |
| Claude Sonnet 4.6 | us.anthropic.claude-sonnet-4-6 |
anthropic.claude-sonnet-4-6 |
| Claude Haiku 4.5 | us.anthropic.claude-haiku-4-5 |
anthropic.claude-haiku-4-5 |
Prefer the inference-profile form. Most current Claude models on Bedrock are
invocable only through a cross-region inference profile, not via on-demand
throughput on the bare model id — so a bare id often fails with the same
invalid model identifier (or on-demand throughput isn't supported) error.
Prefix with your geography: us. (US), eu. (Europe) or apac. (Asia
Pacific), matching aws_region. Use the bare anthropic.… id only where
on-demand access to that model is enabled in your region.
Running locally with ambient AWS credentials¶
If your local shell has AWS credentials (via ~/.aws, SSO, or an assumed role),
you can review your current branch's changes with Bedrock. Bedrock signing needs
boto3, so install the extra (the Action image already bundles it):
pip install 'lgtmaybe[bedrock]'
lgtmaybe review \
--provider bedrock \
--model us.anthropic.claude-sonnet-4-6
lgtmaybe does not require or accept a static API key for Bedrock.
No matching distribution found for lgtmaybe[bedrock](from versions: none) — this is not a packaging problem; thebedrockextra is published on PyPI. It meanspipfound no version compatible with your environment. The usual cause is an interpreter older than the required Python 3.11+ (check withpython --version); install under 3.11+ (e.g. viapipx/uv). If your Python is fine, yourpipcan't reach PyPI — check your network/proxy or--index-url.
Troubleshooting¶
ExpiredTokenException — the OIDC exchange failed or the role session
expired. Check that id-token: write permission is present in the workflow and
that the IAM trust policy references the correct GitHub repository.
AccessDeniedException — the role lacks bedrock:InvokeModel for the
selected model, or the model is not enabled in the Bedrock console for your
account and region. For an inference-profile id (us./eu./apac.-prefixed),
the policy must also allow the inference-profile/* ARN, not just
foundation-model/*.
output_config.format: Extra inputs are not permitted, outputConfig,
or a toolConfig / "doesn't support tool choice" refusal — the model's Converse
route doesn't accept the structured-output shape lgtmaybe asks for, and Bedrock
rejects the whole request with a 400. No action is needed: enforcement degrades
one rung at a time rather than being switched off. A rejected response_format
is re-sent as the same schema through the mechanism Converse does implement —
a forced tool call, whose arguments are the findings — and the rest of the lens
fan-out uses that shape straight away. Only a route that refuses the tool too
falls back to prompt-instructed JSON (the prompt asks for JSON anyway and the
parser is lenient); that fallback is logged as a warning naming the model and
the reason. If you're on a build from before this (the symptom is every lens
failing at once, and the review reporting every review call failed), upgrade,
or pass --no-structured-output / set structured_output: false as a stopgap.
Every review comes back unstructured after one odd reply — an empty reply,
or one that parsed to something that wasn't findings, is evidence against the
mechanism in play, not against enforcement. It now steps the model down to the
forced tool call and keeps the schema; only a second such reply gives it up. On
a build from before this, one blip on one lens cost enforcement for every later
call of the run. The provider log names what happened:
structured output stepped down to a forced tool call is the middle rung and
costs you nothing, while structured output disabled for this model is the
floor and carries a reason (empty-response, unparseable-output,
tool-rejected, internal-tool-rejected) worth reading.
The provided model identifier is invalid — the model is not a Bedrock
model id. Two common causes: (1) it's a non-Bedrock id such as openai.gpt-5.5
or another provider's name — Bedrock hosts Claude / Nova / Llama / Mistral /
Cohere, not OpenAI GPT, so pick a Claude id from the table above; (2) the model
needs a cross-region inference profile — prefix with us./eu./apac. (e.g.
us.anthropic.claude-sonnet-4-6) rather than the bare anthropic.… id.