## Summary
Adds an opt-in **"Expose /dev/kvm to the workspace"** bool parameter
(default `false`) to the dogfood template. When enabled, the host's
`/dev/kvm` device is mapped into the workspace container so it can run
hardware-accelerated VMs (e.g. nested microVM runtimes).
## Details
- New `coder_parameter` `enable_kvm` (bool, default off, mutable).
- A `dynamic "devices"` block on `docker_container.workspace` that maps
`/dev/kvm` only when the parameter is enabled.
It is gated behind a parameter on purpose: dogfood runs on GKE, where
most nodes do not expose `/dev/kvm`, and mapping a missing device fails
container creation. Keeping it off by default means existing workspaces
are unaffected; only users on a KVM-capable host opt in.
## Verifying KVM passthrough on a host
```bash
docker run --rm --runtime=sysbox-runc --device=/dev/kvm alpine sh -c \
'[ -c /dev/kvm ] && [ -r /dev/kvm ] && [ -w /dev/kvm ] && echo OK || echo FAIL'
```
## Notes
- No effect unless the host actually provides `/dev/kvm` and the
workspace user has access to it.
- Draft: still validating end-to-end on a KVM-capable host.
Categorises the terminal error of a failed interception and persists it
on the interception record, then surfaces it on the AI Gateway API.
- Categorise into an enum (`bad_request`, `unauthorized`,
`rate_limited`, `overloaded`, `server_error`, `unknown`), unwrapping
the ResponseError envelope, the upstream Anthropic/OpenAI SDK errors,
and key-pool exhaustion so blocking and streaming paths agree.
- Thread the type and raw message through the recorder dRPC into the
`aibridge_interceptions` row (optional proto fields; NULL on success).
- Expose the error on the AI Gateway thread API from the root
interception.
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
Adds a nullable `aibridge_interception_error_type` enum and an
`error_message` column to `aibridge_interceptions`, so a failed
interception's terminal upstream error can be persisted.
Schema only: the write path and API exposure land in the stacked
backend PR.
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
## Why
PR #26797 was accidentally merged into the stale `graphite-base/26797`
branch instead of `main` (Graphite picked the wrong base), so its
changes never landed on `main`. This PR re-lands that work as a clean
cherry-pick onto the current `main`.
## What
Adds a `WatchAIProviders` streaming RPC to the `ProviderConfigurator`
service so a running standalone AI Gateway refetches its provider set
when the provider configuration changes. The server subscribes to
`AIProvidersChangedChannel` (published by the provider CRUD endpoints)
and forwards each event as a payload-free signal, plus one signal on
subscribe; the gateway calls `GetAIProviders` on each signal to rebuild
its pool. The aibridged API is bumped to v1.2.
Env-seeded providers don't need a signal: seeding finishes before coderd
serves the gateway connection, so the gateway's initial fetch already
reflects the seeded set.
## For reviewers
The change is split into two commits to make review easy:
1. **`feat: synchronise provider changes with WatchAIProviders`** is a
faithful cherry-pick of #26797, identical to the originally reviewed PR.
It is committed without pre-commit hooks because it does not build
against current `main` on its own.
2. **`fix: resolve cherry-pick conflicts against main`** contains only
the deltas needed to re-land on current `main`, and passes the full
pre-commit suite:
- `coderd/aibridged/proto/aibridged.pb.go` regenerated via the proto
make target (the cherry-picked copy was generated against the older
proto).
- `enterprise/cli/aigatewaystart.go` import block unioned; `main` added
`os` and `strings` while the PR added `sync`.
- Three `aibridgedserver.NewServer` test call sites that landed on
`main` after the original branch diverged now pass the new `pubsub`
argument.
Refs https://linear.app/codercom/issue/AIGOV-465
*This PR was produced by opencode (agent) using the
`anthropic/claude-opus-4-8` model, under human direction and review.*
## Summary
Plumbs the Responses output item id (added as `ToolUsageRecord.ItemID` in #26855) through to the database, captured independently of the `provider_tool_call_id` correlation key. Hosted tools (`web_search_call`, etc.) only have an item id; agentic tools have both.
`provider_item_id` is specific to the OpenAI Responses API; it stays empty for chat completions and Anthropic messages, which have no separate item id.
## Changes
- Migration `000534`: nullable `provider_item_id` column on `aibridge_tool_usages`.
- Proto: `item_id` field 11 on `RecordToolUsageRequest`.
- Server handler: persists `provider_item_id` and adds it to structured logging.
- Translator: maps `ToolUsageRecord.ItemID` to the proto field.
## Tests
- `TestRecordToolUsageProviderItemID`: real-database round-trip asserting `provider_item_id` persists for both hosted and agentic tools, independently of `provider_tool_call_id`.
Stacked on #26855. Linear: AIGOV-96
---
_This PR was produced by opencode (agent) using the_ _`anthropic/claude-opus-4-8`_ _model, under human direction and review._
## Summary
The Responses interceptor previously recorded only `function_call` and `custom_tool_call` output items, so interceptions that did real work via built-in tools (`web_search_call`, `computer_call`, `shell_call`, `mcp_call`, etc.) recorded no tool usage at all.
`recordNonInjectedToolUsage` now whitelists every tool-call output type and records it, with the tool name falling back to the item type when none is set.
`ToolUsageRecord` also gains an `ItemID` field so the two distinct Responses identifiers are captured without conflation (addresses review feedback on coder/aibridge#273):
- `ItemID`: the output item's unique `id` (always present).
- `ToolCallID`: the `call_id` correlation id (empty for hosted tools the provider runs server-side).
## Tests
- Extends `TestRecordToolUsage` with cases for the new hosted/agentic tool types.
- Adds blocking and streaming `web_search` fixtures (scrubbed of credentials and identifying metadata) plus `TestResponsesOutputMatchesUpstream` cases asserting a hosted tool records with an empty `ToolCallID` and a populated `ItemID`.
Linear: AIGOV-96
---
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
Implements
[AIGOV-481](https://linear.app/codercom/issue/AIGOV-481/featai-gateway-support-anthropic-v1messages-route-on-the-copilot).
Adds the Anthropic-style `/v1/messages` route to the Copilot provider.
GitHub Copilot CLI (1.0.65) using the default `Claude Sonnet 4.6
(default)` model sends Anthropic-style `/v1/messages` requests through
`aibridgeproxyd` to the Copilot provider. The provider only registered
the OpenAI-compatible `/chat/completions` and `/responses` routes, so
these requests failed:
```
CAPIError: 404 404 404 route not supported: POST /copilot/v1/messages
```
*This PR was produced by opencode (agent) using the
`anthropic/claude-opus-4-8` model, under human direction and review.*
## Summary
Adds an operator-facing migration guide for the AI Bridge to AI Gateway
rebrand, as a new docs page under **AI Coder > AI Gateway** (last child
in the section).
The guide documents:
- **Config aliases** (env vars, CLI flags, YAML group keys): old
`aibridge` names still work as hidden, deprecated aliases; new
`ai_gateway` names are canonical. Includes full env-var mapping tables
and the mechanical substitution rules.
- **HTTP API**: canonical path is now `/api/v2/ai-gateway`; legacy
`/api/v2/aibridge` routes retained.
- **Metrics**: prefixes renamed `coder_aibridged_*` ->
`coder_ai_gateway_*` and `coder_aibridgeproxyd_*` ->
`coder_ai_gateway_proxy_*`. Both old and new names are emitted today, so
dashboards keep working; guidance to migrate before the old names are
removed, plus an optional `metric_relabel_configs` drop snippet.
- **No database changes** and no required config changes to upgrade.
Implements the docs/release-notes portion of
[AIGOV-240](https://linear.app/codercom/issue/AIGOV-240) (parent:
[AIGOV-207](https://linear.app/codercom/issue/AIGOV-207)).
## Notes
- Content reflects what actually shipped in the codebase (metrics are
*aliased*, not hard-renamed), which differs from the original RFC that
assumed a hard rename.
- Registered in `docs/manifest.json` with `state: ["ai governance
add-on"]` to match sibling pages.
---
*This PR was produced by opencode (agent) using the
`anthropic/claude-opus-4-8` model, under human direction and review.*
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Closes [AIGOV-455](https://linear.app/codercom/issue/AIGOV-455/extend-drpc-with-buildproviders).
## Why
The AI Gateway (`aibridged`) is being split into a standalone process that must not touch the database. `coderd` stays the source of truth and seeds the `ai_providers` / `ai_provider_keys` tables from the environment. This PR adds a DRPC call so the gateway fetches provider config from `coderd` instead of reading the DB, for both the embedded and standalone daemons.
## What
- **Proto:** new `ProviderConfigurator` service with a unary `GetAIProviders` RPC, plus `AIProvider` / `AIProviderBedrock` messages. `CurrentMinor` bumped to 1 (additive).
- **Server (`coderd/aibridgedserver`):** `GetAIProviders` runs a read-only `InTx` under `LockIDAIProvidersEnvSeed` so it never returns a mid-seed snapshot, reads providers (incl. disabled) plus keys for enabled ones, and maps to proto under `dbauthz.AsAIBridged`. Unmappable rows are skipped and logged; plaintext keys and Bedrock secrets are never logged.
- **Client:** `DRPCProviderConfiguratorClient` wired into the client union, `dialer.go`, and `CreateInMemoryAIBridgeServer`.
- **cli:** `BuildProvidersFromProto` maps the response through the existing DB-neutral `buildProvider`. A shared `poolRPCReloader` does the fetch/build/replace for both daemons: the embedded daemon reloads on every `ai_providers` change and fails startup if it cannot subscribe; the standalone gateway drives the same reloader once at startup, retrying until success and staying interruptible.
- **Dead code removed:** `BuildProvidersFromConfig`, `ProvidersFromConfig`, `AIProviderFromConfig`, and the DB-read `BuildProviders` path.
## Problem
The `lint/emdash` check fails on Graphite-stacked PRs. See [this failed
run](https://github.com/coder/coder/actions/runs/28225390084/job/83616080375?pr=26650):
```
Base ref origin/graphite-base/26650 not found locally, fetching graphite-base/26650...
ERROR: could not fetch base ref origin/graphite-base/26650.
ERROR: could not determine base ref.
make: *** [Makefile:768: lint/emdash] Error 1
```
`scripts/check_emdash.sh` resolved its diff base by fetching
`origin/$GITHUB_BASE_REF` and computing a merge-base. Graphite sets
`GITHUB_BASE_REF` to a `graphite-base/<n>` ref that is ephemeral (it is
not reliably present on origin), so the fetch fails and the check errors
out instead of running.
## Fix
`actions/checkout` checks out the PR **merge commit**
(`refs/pull/<n>/merge`), whose **first parent (`HEAD^1`) is the exact
base commit GitHub merged against**. Diffing `HEAD^1` against the
checkout yields every change the PR makes against its base branch, for
normal and Graphite-stacked PRs alike. No base-branch fetch, no
merge-base computation, no `gh`-based deepen dance.
- `scripts/check_emdash.sh`: use `HEAD^1` (the PR base commit) as the
diff base in CI. Drops `resolve_merge_base` and `fetch_base_ref`. Emits
a clear error if `HEAD^1` is missing (checkout too shallow).
- `.github/workflows/ci.yaml`: bump the `lint` job checkout to
`fetch-depth: 2` so `HEAD^1` is present with no runtime fetch.
Local dev behavior (merge-base against `origin/main`) is unchanged.
## Verification
- `make lint/emdash`, `make lint/shellcheck`, `make
lint/actions/actionlint` pass.
- Simulated the CI path with `GITHUB_BASE_REF` set: the check resolves
to `HEAD^1` without fetching and still flags an added line containing an
emdash.
<details>
<summary>Why the merge commit's first parent</summary>
For a `pull_request` checkout of `refs/pull/<n>/merge`:
- `HEAD` = GitHub's synthetic PR merge commit
- `HEAD^1` = the exact base commit used for the merge
- `HEAD^2` = the PR head commit
`git diff HEAD^1 HEAD` is the full-tree diff from the base snapshot to
the merged result, i.e. all of the PR's changes against its base. This
is immutable and always local (given depth >= 2), unlike base branch
refs which are mutable and, for Graphite stacks, ephemeral.
</details>
---
This PR was generated by Coder Agents on behalf of @dannykopping.
## Summary
Restores the step-by-step "Obtaining static Bedrock credentials"
walkthrough that was present on the v2.32.6 `ai-bridge/setup` page but
missing from the current `ai-gateway/providers` page.
The current page mentions static credentials in a single line but no
longer explains how to create the IAM user and access key in the AWS
console. This PR brings back that walkthrough, adapted to the current
database/dashboard-managed provider flow.
## Changes
- Add an `#### Obtaining static Bedrock credentials` subsection under
the Amazon Bedrock provider section in
`docs/ai-coder/ai-gateway/providers.md`.
- Keep the AWS console steps (choose region, generate API keys, create
access key) from v2.32.6.
- Replace the deprecated `CODER_AIBRIDGE_BEDROCK_*` environment-variable
configuration step with guidance to enter the credentials when
adding/editing the provider via the dashboard or AI Providers API,
matching the post-v2.34 database-managed model.
<details>
<summary>Context and decisions</summary>
- Source: [`docs/ai-coder/ai-bridge/setup.md` at
v2.32.6](https://coder.com/docs/@v2.32.6/ai-coder/ai-bridge/setup)
"Obtaining Bedrock credentials" section.
- The old flow set provider config via environment variables, which are
deprecated since v2.34 (providers are now stored in the database and
managed via dashboard/API). The restored content keeps the AWS-side
credential-creation steps but routes the final configuration step
through the current provider management flow rather than env vars.
- Open questions from
[AIGOV-432](https://linear.app/codercom/issue/AIGOV-432/restore-bedrock-static-credentials-docs-from-v2326)
(whether other pages also need this, and whether the content needs
further accuracy updates) are left for review.
</details>
Closes
[AIGOV-432](https://linear.app/codercom/issue/AIGOV-432/restore-bedrock-static-credentials-docs-from-v2326).
> [!NOTE]
> This PR was generated by Coder Agents on behalf of @dannykopping.
---------
Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com>
## Summary
Adds a Troubleshooting section to the Codex CLI AI Gateway client docs
covering the WebSocket-to-HTTPS transport fallback.
Recent Codex CLI versions default to the WebSocket runtime for the
Responses API. AI Gateway does not support WebSocket transport, so each
request attempts a WebSocket connection, fails, and falls back to HTTPS,
surfacing:
```text
Falling back from WebSockets to HTTPS transport.
```
The doc explains the cause and the fix: set `supports_websockets =
false` in the `[model_providers.ai_gateway]` block in
`~/.codex/config.toml` to force HTTPS directly and remove the fallback
delay.
Closes
[AIGOV-453](https://linear.app/codercom/issue/AIGOV-453/document-codex-cli-websocket-fallback-workaround).
<details>
<summary>Note on the config value</summary>
The original request and the Linear issue referenced enabling websocket
support / `support_websockets = false`. The authoritative Codex CLI
[config reference](https://developers.openai.com/codex/config-reference)
confirms:
- The key is `supports_websockets` (trailing "s").
- It declares whether a provider supports the Responses API WebSocket
transport.
- Setting it to `false` is the documented workaround to force HTTPS and
stop the fallback attempts.
Since AI Gateway does not support WebSockets, `supports_websockets =
false` is the correct value. `= true` would assert support that does not
exist and keep the fallback happening.
</details>
---
This PR was generated by Coder Agents on behalf of @dannykopping.
The Bedrock create form required both `access_key` and
`access_key_secret`, blocking deployments that authenticate against AWS
through an IAM role, instance profile, or `AWS_PROFILE`. The backend
already accepts a Bedrock provider that is configured by region alone
(see `codersdk.AIProviderBedrockSettings.IsConfigured`), so the UI was
the only thing standing between the operator and a working IAM-role
provider.
The Yup schema now treats both fields as optional while keeping the
cross-validation that forces the pair to travel together. A descriptive
note under the inputs tells the operator that leaving both blank falls
back to ambient AWS credentials, and links to the [Amazon Bedrock
section](https://coder.com/docs/ai-coder/ai-gateway/providers#amazon-bedrock)
of the AI Gateway providers docs for the credential chain and IAM
permissions. The mapping into `CreateAIProviderRequest` already omits
empty credential fields, so the wire payload sends only `region`,
`model`, and `small_fast_model`, which is enough for `IsConfigured()` on
the backend.
The model and small-fast model fields are now pre-filled with the modern
Sonnet 4.5 and Haiku 4.5 IDs from `codersdk.aiGatewayBedrockModel` /
`codersdk.aiGatewayBedrockSmallFastModel`, matching the legacy
environment seed path. A second docs note under those fields points at
the [AWS Bedrock model
cards](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html)
page so operators can find the canonical model IDs without leaving the
form. This also addresses the AIGOV-411 ask.
Adds `providerFormValuesToCreate` coverage for the no-credential and
whitespace-only paths, plus three new `ProviderForm` stories: one that
verifies the model fields pre-fill, one that submits without static
credentials, and one that verifies a half-typed credential pair stays
blocked.
Closes
[CODAGT-626](https://linear.app/codercom/issue/CODAGT-626/bedrock-ui-requires-access-keys-for-iam-role-setup).
Partial coverage for
[AIGOV-411](https://linear.app/codercom/issue/AIGOV-411/ai-gateway-providers-improve-bedrock-model-fields-in-ui)
(model pre-fill plus docs link; combobox, model ID pattern validation,
and docs site updates remain).
> The Slack thread also flagged a separate edit-time regression: "if I
go to edit an existing provider, all the fields I set are not on the
UI." I did not see that reproduce against the masked-credential edit
story, and the issue description focuses on the create flow, so I left
it for a separate investigation rather than bundling it into this fix.
<img width="1169" height="814" alt="image"
src="https://github.com/user-attachments/assets/08741641-da86-4acc-82ac-ef758f739f58"
/>
<sub>This PR was created by a Coder Agent on behalf of
@dannykopping.</sub>
Adds `ai` to sqlc's `gen.go.initialisms` in `coderd/database/sqlc.yaml`
so the generated DB code follows Go's initialism convention. Adds the
matching `ai` -> `AI` case to the dbgen PascalCase helper
(`scripts/dbgen/main.go`) so the corresponding `dbmem` / mock
identifiers stay in sync. `make gen` regenerates the rest; hand-written
call sites that consume DB-generated identifiers
(`enterprise/audit/table.go`, `coderd/database/modelmethods.go`,
`enterprise/coderd/aigatewaykeys.go`, `coderd/database/dbauthz/*`, etc.)
are updated to match.
Scope is deliberately limited to the database layer:
- `coderd/rbac/*` (resource and scope generators) is untouched —
`ResourceAi*` / `ScopeAi*` constants stay on main's casing.
- `codersdk/*` (Go SDK) is untouched — `codersdk.ResourceAi*` /
`codersdk.APIKeyScopeAi*` constants stay on main's casing, so external
Go SDK consumers see no source-level break.
- `Aibridge*` identifiers (one SQL token `aibridge`, not `ai_bridge`)
are out of scope.
On-the-wire values are unchanged: enum strings, RBAC resource type
strings, API key scope strings, and JSON tags all stay the same. The
HTTP/JSON surface is unaffected.
Refs:
[AIGOV-369](https://linear.app/codercom/issue/AIGOV-369/change-ai-references-in-coderddatabasemodelsgo-to-ai)
🤖 Generated with [Coder Agents](https://coder.com)
## Summary
Removes the deprecated `/api/v2/aibridge/interceptions` endpoint and the
Request Logs frontend page, both replaced by the session-based view.
Closes https://linear.app/codercom/issue/AIGOV-266
Closes https://linear.app/codercom/issue/AIGOV-324
## Changes
### Backend
- Remove `GET /api/v2/aibridge/interceptions` HTTP handler and route
- Remove SDK types and client method (`AIBridgeInterception`,
`AIBridgeTokenUsage`, `AIBridgeUserPrompt`, `AIBridgeToolUsage`,
`AIBridgeListInterceptionsResponse`, `AIBridgeListInterceptionsFilter`)
- Remove SQL queries `CountAIBridgeInterceptions` and
`ListAIBridgeInterceptions`
- Remove `searchquery.AIBridgeInterceptions` parser
- Remove dbauthz wrappers, in-memory implementations, metrics, and mocks
for the interceptions list queries
- Remove the `coder aibridge interceptions list` CLI command and golden
files
- Regenerate API docs, swagger, mocks, and metrics
The `/models`, `/clients`, and `/sessions` endpoints stay; the sessions
list page still consumes all three.
### Frontend
- Delete the entire `RequestLogsPage/` directory (page, view, row,
filter, stories, tests)
- Remove the `/aibridge/request-logs` route and its lazy import
- Remove the `getAIBridgeInterceptions` API method,
`paginatedInterceptions` query, and mock interception entities
- `git mv` the shared filter and icon components used by the sessions
pages:
- `RequestLogsPage/RequestLogsFilter/{Client,Model,Provider}Filter.tsx`
→ `AIBridgePage/filters/`
- `RequestLogsPage/icons/AIBridge{Client,Model,Provider}Icon.tsx` →
`AIBridgePage/icons/`
- Drop the `getProviderIconName` hack and the duplicate `anthropic-neue`
icon case now that the FIXME no longer applies
## Commits
1. `refactor: remove interceptions API and request logs view` — the bulk
removal, with explicit renames for the shared filter/icon files.
2. `refactor(site/src/pages/AIBridgePage): drop getProviderIconName
hack` — cleanup of the FIXME that depended on RequestLogsPage existing.
> [!NOTE]
> Generated by Coder Agents on behalf of @dannykopping
## Summary
Adds a `cherry-pick/v<version>` label to the cherry-pick PRs that the
`Cherry-pick to release` workflow creates automatically, so cherry-picks
for a specific release can be filtered and identified easily (for
example
`cherry-pick/v2.31`).
## Changes
- Compute `CHERRY_PICK_LABEL="cherry-pick/v${VERSION}"` from the
resolved
release branch.
- Create the label on demand with `gh label create --force` so the
workflow stays idempotent across re-runs and concurrent runs, and works
even when the label does not exist yet.
- Apply the label at PR creation via `gh pr create --label`.
- Grant `issues: write` permission, required to create the label.
- Document the new label convention in the workflow header.
## Notes
The version is derived from the existing release-branch resolution
(`release/2.X` -> `2.X`), so no new configuration is required. The label
name uses a `v` prefix to match the requested `cherry-pick/vX.YZ`
format.
<details>
<summary>Implementation context</summary>
The label is created before the existing-PR idempotency check and
applied
in the same `gh pr create` call already used for assignees/reviewers, so
it
fits the workflow's existing conventions (branch, title, body) without
changing control flow.
</details>
---
*This PR was created by Coder Agents on behalf of @dannykopping.*
Reverts coder/coder#26239
We cannot disable a feature which was previously enabled; this is a BC
break.
This is also using `AIGatewayRoutingEnabled` which will be removed in
the next release.
_Disclosure: produced using Opus 4.8_
I've noticed recently that some prompts are not displaying correctly.
<img width="1388" height="509" alt="image"
src="https://github.com/user-attachments/assets/fad3812f-e42d-4398-a16c-a0e7f924455d"
/>
The cause is the `mid-conversation-system-2026-04-07` beta. When
enabled, the client appends a trailing `role: "system"` message after
the user's text (e.g. an injected skills list):
```json
"messages": [
{ "role": "user", "content": [ "hey how are ya" ] },
{ "role": "system", "content": "The following skills are available..." }
]
```
Our prompt detection algo was being subverted since we only check the
last message if role=user.
## Summary
`X-OpenCode-Session` is only set by the OpenCode "Zen" provider. Other
providers use `x-session-affinity` instead. This change falls back to
`x-session-affinity` when `X-OpenCode-Session` is not present, so
sessions are correctly identified regardless of the provider used.
Ref: https://github.com/coder/coder/pull/26128
## Changes
- `aibridge/session.go`: Prefer `X-OpenCode-Session` (Zen), fall back to
`x-session-affinity` (other providers).
- `aibridge/session_test.go`: Add tests for precedence and fallback
behavior.
> Generated with [Coder Agents](https://coder.com) by @dannykopping
This follows #26098 by teaching AI Bridge to read OpenCode session IDs
from the X-OpenCode-Session header, so OpenCode requests are grouped
consistently in interception logs.
Adds unit and integration test coverage for the new header.
<details>
<summary>Coder Agents generated</summary>
This pull request was generated with Coder Agents assistance.
</details>
Adds OpenCode to AI Bridge client detection so requests with user agents
like `opencode/1.16.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`
show up as a first-class client instead of `Unknown`.
This also wires the existing OpenCode frontend asset into the AIBridge
UI, adds a Storybook story for the client icon, and updates the
monitoring docs list of supported client values.
<details>
<summary>Coder Agents generated</summary>
This pull request was generated by Coder Agents.
</details>
Adds a new **Provider Configuration** reference page (`providers.md`) covering:
- The migration from environment-variable-based provider config to database-backed management introduced in v2.34, including the one-time seeding behavior and deprecation of `CODER_AI_GATEWAY_PROVIDER_<N>_*` and related flags
- All supported provider types (`openai`, `anthropic`, `bedrock`, `copilot`, `azure`, `google`, `openrouter`, `vercel`, `openai-compat`) with setup notes for each
- Provider lifecycle statuses (`enabled`, `disabled`, `error`) and their effect on request handling
- Reload behavior and how configuration changes apply without restarting `coderd`
- Bring Your Own Key (BYOK) and failure mode reference table
Updates **Setup** (`setup.md`) to replace the environment-variable-based provider configuration instructions with dashboard-driven steps (Add provider form, provider list, edit/disable flow), referencing the new `providers.md` page for deeper detail. Screenshots of the provider list, add, and edit forms are included.
Adds a **Provider metrics** section to **Monitoring** (`monitoring.md`) documenting the `coder_aibridged_*` and `coder_aibridgeproxyd_*` Prometheus metrics for provider status and reload timestamps, along with two suggested PromQL alert queries.
Coder runs all migrations in a single transaction (`pgTxnDriver`).
Postgres forbids using an enum value added by `ALTER TYPE ... ADD VALUE`
within the same transaction that added it. Migration `000499` widened
`ai_provider_type` with `ADD VALUE`, and `000504` casts existing
`chat_providers` rows to that enum in the same transaction. On
deployments with a legacy provider using one of the new values (for
example `openai-compat`), the batch failed with `unsafe use of new
value` and the server could not start.
Recreate the type (create a new enum, alter the column, drop and rename)
instead of using `ADD VALUE`, matching the existing precedent in
`000144_user_status_dormant`. A freshly created enum's values are usable
immediately in the same transaction, so the cast in `000504` succeeds.
The resulting schema is identical, so `make gen` produces no `dump.sql`
diff and databases that already applied these migrations see no drift.
Added a regression test that seeds an `openai-compat` provider and
applies `000499` through `000504` in a single transaction, reproducing
the production path. The per-step `Stepper` used by the other migration
tests commits each migration separately and cannot surface this class of
bug.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot is the only AI provider type that could not be added through the `/ai/settings` UI. The aibridge runtime and the env-var seeding path already supported it, but the runtime CRUD API rejected `type=copilot` and the UI omitted it entirely. The root cause is that Copilot's auth model (a per-request GitHub OAuth token, with no pre-shared key) does not fit the credential-centric add-provider flow that every other provider uses.
## Backend
Allow `type=copilot` in `CreateAIProviderRequest.Validate()`, and reject `api_keys` for Copilot on both create (validation) and update (handler sentinel), mirroring the existing Bedrock guards. Copilot carries no stored credential.
## Frontend
Add Copilot to the provider type picker (with the `github-copilot.svg` icon) and give the form a credential-free branch: name, display name, and a free-text endpoint defaulting to `https://api.business.githubcopilot.com`, with copy explaining that authentication happens via the user's GitHub token at request time. Copilot maps to the distinct `copilot` wire type rather than collapsing to `openai`, and the edit flow recovers it correctly.
The endpoint stays required with a business-tier default; users on the individual or enterprise endpoints edit the field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Previously, `SeedAIProvidersFromEnv` only hashed provider-level fields,
so env var key changes were silently ignored once a provider already
existed in the database.
Include bearer keys and Bedrock credentials in the canonical drift hash,
and cover multi-key, multi-provider cases so restarts now fail loudly
when the configured credentials no longer match what is stored.
When changing a key, you'll now see this in the server startup logs:
```
2026-05-29 12:29:02.674 [info] api: Encountered an error running "coder server", see "coder server --help" for more information
2026-05-29 12:29:02.674 [info] api: error: create coder API:
2026-05-29 12:29:02.674 [info] api: github.com/coder/coder/v2/cli.(*RootCmd).Server.func2
2026-05-29 12:29:02.674 [info] api: /home/coder/coder/cli/server.go:1015
2026-05-29 12:29:02.674 [info] api: - seed ai providers from env:
2026-05-29 12:29:02.674 [info] api: github.com/coder/coder/v2/enterprise/cli.(*RootCmd).Server.func1
2026-05-29 12:29:02.674 [info] api: /home/coder/coder/enterprise/cli/server.go:187
2026-05-29 12:29:02.674 [info] api: - execute transaction:
2026-05-29 12:29:02.674 [info] api: github.com/coder/coder/v2/coderd/database.(*sqlQuerier).runTx
2026-05-29 12:29:02.674 [info] api: /home/coder/coder/coderd/database/db.go:212
---> 2026-05-29 12:29:02.674 [info] api: - AI provider "vercel" already exists in the database and differs from the current environment configuration; update the provider through the API or remove the CODER_AIBRIDGE_* env vars to stop seeding it:
2026-05-29 12:29:02.674 [info] api: github.com/coder/coder/v2/coderd.SeedAIProvidersFromEnv.func1
2026-05-29 12:29:02.674 [info] api: /home/coder/coder/coderd/ai_providers_migrate.go:139
2026-05-29 12:29:02.674 [info] api: slogjson: failed to write entry: io: read/write on closed pipe
2026-05-29 12:29:02.700 [info] dlv: Stop reason: exited
2026-05-29 12:29:02.825 [info] site: ELIFECYCLE Command failed.
error: running command "develop": server did not become ready in 1m0s:
main.waitForHealthy
/home/coder/coder/scripts/develop/main.go:877
- context canceled
```
_This PR was generated with Coder Agents._
_Disclosure: created with Coder Agents._
When providers are disabled, we should serve a sentinel error so the
requesting client (Claude Code, Coder Agents, etc) is informed. Coder
Agents can also conditionalize its display to show a helpful error
message.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add metrics for `aibridged` and `aibridgeproxyd`'s provider statuses. AI providers can be modified, and possibly misconfigured, at runtime. These metrics help operators understand the state of these provider definitions in case unexpected behaviour is observed.
The final step of `.github/workflows/cherry-pick.yaml` comments on the
original PR with a link to the cherry-pick PR. If the original PR is
locked, `gh pr comment` fails and the whole job exits with status 1,
even though the backport branch and PR were created successfully.
See
https://github.com/coder/coder/actions/runs/26559681779/job/78239379200
for an example.
Make the comment non-fatal: log a warning and continue.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactors the `aibridgeproxyd` provider reload mechanism which was unnecessarily complex.
Also ensures that providers are evaluated on each CONNECT request to prevent interception of requests to (newly) disabled providers; in this case the requests will passthrough unencrypted, by design.
_Disclosure:_ _produced_ _with_ _Claude_ _Opus_ _4\.7_
AI Gateway only supports Anthropic (+Bedrock), OpenAI, and Copilot providers at present. All other types (Vercel, Gemini, etc) will be mapped to OpenAI since they support OpenAI-compatible endpoints.
For `vercel`, `openrouter`, and `openai-compat`, the
`<provider>/<model>` slash is part of the upstream model ID rather than
a hint. `ResolveModelWithProviderHint` was running
`parseCanonicalModelRef` before honoring `providerHint`, so a config
like `(provider=vercel, model=anthropic/claude-4-5-sonnet)` resolved to
`provider=anthropic, model=claude-4-5-sonnet` and the prefix-less model
name was forwarded to Vercel, which returned `Model 'claude-4-5-sonnet'
not found`.
Honor an explicit gateway provider hint before attempting canonical-ref
parsing. Non-gateway hints (anthropic, openai, etc.) keep the existing
canonical-ref-first behavior so `anthropic/claude-...` still has its
prefix stripped when routed directly to Anthropic.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the in-process aibridge daemon and the enterprise aibridgeproxy daemon both snapshotted their provider routing once at boot. Any `ai_providers` or `ai_provider_keys` mutation required a restart for either to pick it up.
Add an `ai_providers_changed` pubsub channel that the CRUD handlers publish on after Create / Update / Delete. Both daemons subscribe:
- **aibridged** rebuilds its `[]aibridge.Provider` snapshot via `BuildProviders` and swaps it into the pool atomically. Inflight requests keep serving against the bridge they already acquired; new acquires build against the new snapshot. Per-provider construction errors stay scoped to the offending row.
- **aibridgeproxyd** rebuilds its routing snapshot from `GetAIProviders` and swaps the host→provider map atomically. The MITM listener picks up new providers without restart.
DB read for aibridgeproxyd uses the existing `AsAIProviderMetadataReader` subject for routing-only access.
## Problem
Two related symptoms of the same architectural issue: the `dbcrypt`
wrapper is installed inside `enterprise/coderd.New`, so any access to
`options.Database` that happens before `newAPI` runs bypasses
encryption.
**Symptom 1 (reads):** Provider keys added via the admin UI are
encrypted at rest. `BuildProviders` was running *before* `newAPI`,
against the unwrapped store, so the ciphertext was read as-is and shoved
into the keypool as the upstream credential. Anthropic/OpenAI reject it,
and the interception log shows:
```
coderd.aibridged.pool: interception failed ... error="all configured keys failed authentication"
credential_kind=centralized credential_hint=PaPb...4A== credential_length=184
```
**Symptom 2 (writes):** `SeedAIProvidersFromEnv` was also running before
`newAPI`, against the unwrapped store, so env-derived keys
(`CODER_AIBRIDGE_OPENAI_KEY`, indexed `CODER_AIBRIDGE_PROVIDER_<N>_KEY`,
etc.) landed in `ai_provider_keys` as plaintext with `ApiKeyKeyID =
null` even when `CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS` was set.
## Fix
Move both `SeedAIProvidersFromEnv` and `BuildProviders` to after
`newAPI`, where `options.Database` is the dbcrypt-wrapped store. Writes
encrypt correctly; reads decrypt correctly.
The enterprise closure (`enterprise/cli/server.go`) runs *inside*
`newAPI` and calls `BuildProviders` for the aibridgeproxyd at that
point. Once the agpl seed moves to after `newAPI`, the proxy on first
boot would see no env-seeded providers. Add a matching seed call inside
the enterprise closure before its `BuildProviders` to cover that case.
Seeding is idempotent, so the agpl-side seed running again post-`newAPI`
is a no-op when the rows already exist.
## Known shortcomings
The clean version of this fix would just inherit `ctx` like every other
startup step and place these calls naturally. It can't, for two reasons
that are both about the surrounding handler architecture rather than
this change:
1. **`dbcrypt` wrapping is positioned inside `newAPI`, not around
`options.Database` at creation.** That's why both seed and build have to
wait until after `newAPI` in the first place. The principled fix is to
install the wrapper at the point the store is created (behind a hook the
enterprise build supplies), so every consumer sees a single
authoritative view and the ordering stops mattering. This would also
collapse the duplicated seed call back to a single site.
2. **The handler's shutdown sequence is not deferred.**
`coderAPICloser.Close()` and the other teardown steps run only if
control reaches the `select` at the bottom of the handler. An early
`return` from anywhere in Phase 1 (e.g. seed/build returning
`context.Canceled` when the user hits ctrl-c during startup) skips that
block and orphans all the goroutines `newAPI` spawned — tailnet workers,
gitsync, telemetry batcher, etc. `goleak` then catches them at package
teardown and `TestServer_TelemetryDisabled_FinalReport` fails. Moving
the shutdown into deferred closers (with a `sync.Once`-guarded close to
avoid double-close from the explicit Phase 2 call) is the principled
fix.
For this PR I took the smallest change that fixes the reported bugs: a
detached context (`context.WithoutCancel(ctx)` + a 30s timeout) at the
seed and build call sites in both the agpl and enterprise paths. It lets
the calls complete even if the user cancels during startup, after which
the handler reaches its shutdown select naturally and tears down through
Phase 2. Both shortcomings above are worth addressing separately.
## Test plan
- `make test RUN=TestServer_TelemetryDisabled_FinalReport` with `-race`;
passes locally with `-count=3`.
- Manually verified on a deployment with
`CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS` set and env-configured providers:
`ai_provider_keys.api_key_key_id` is populated, `api_key` is base64
ciphertext, and upstream auth succeeds.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously we were only extracting the API when _not_ delegating auth;
this is incorrect.
We need to extract the key _always_ when BYOK is intended.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
## Summary
Renames "AI Bridge" to "AI Gateway" in the paywall card shown on the AI
Governance settings page when the feature is not entitled.
## Changes
In `PaywallAIGovernance.tsx`:
- Title: `AI Bridge` -> `AI Gateway`
- Description text: `AI Bridge provides...` / `AI Bridge requires...` ->
`AI Gateway provides...` / `AI Gateway requires...`
- Docs link label: `AI Bridge Docs` -> `AI Gateway Docs`
Replace the env-based `BuildProviders` with a DB-backed loader. The database is now the single source of truth for runtime provider configuration; env config arrives via `SeedAIProvidersFromEnv` (run at boot) and `BuildProviders` reads it back as `aibridge.Provider` instances. `cli/server.go` and `enterprise/cli/server.go` both call the same path, so aibridged and aibridgeproxyd see the same provider set.
Per-provider `DumpDir` is replaced by a top-level `CODER_AI_GATEWAY_DUMP_DIR` base; each provider's effective dump path is `<base>/<provider name>`.
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Adds a clause under **Writing Comments and Avoiding Unnecessary
Changes** in `AGENTS.md` specifying that comments must be:
- **Substantive** and **concise**
- Describe the **behaviour** of the code
- NOT describe the reasoning the agent used to produce the change
Includes concrete anti-pattern examples (`// Added per PR feedback`, `//
Refactored for clarity`) to make the expectation clear.
We weren't stripping the API base (`/api/v2/aibridge`), leading to 404s
when using the in-memory transport.
Signed-off-by: Danny Kopping <danny@coder.com>
Allows an `api_key_id` to be passed from a trusted in-memory transport
(currently: `chatd`) to `aibridged` for use in authenticating LLM
requests.
This value can _only_ be passed via context, and all users of the
in-memory transport _must_ provide it.
It can be used in conjunction with BYOK headers.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since AI Gateway is now enabled by default, and if the AI Gateway Proxy is enabled too it's possible the server can start without any configured providers. This would previously block startup, which is unacceptable.
In an upstack PR we will handle reloading the providers at runtime, so the server needs to be able to start up even if it can't handle any proxy requests to AI Gateway.
This change was necessitated because if there are providers configured in the environment they need to be seeded _before_ the proxy starts.
### TL;DR
Introduces an in-process `TransportFactory` for aibridge so that chatd (coder-agent LLM traffic) can route requests through the aibridged handler without crossing the HTTP route or requiring a license entitlement check.
### What changed?
- Added a new `coderd/aibridge` package with a `TransportFactory` interface and a `Source` type for tagging the call site on request contexts. `SourceAgents` is defined as the constant for coder-agent traffic.
- Implemented `NewTransportFactory` in `coderd/aibridged/transport.go`, which returns an `http.RoundTripper` that dispatches requests to the aibridged handler in-process. The response body is streamed through an `io.Pipe` so SSE/NDJSON/chunked responses propagate token-by-token. Handler panics are recovered and surfaced as 500 responses, and context cancellation closes the pipe with the appropriate error.
- `RegisterInMemoryAIBridgedHTTPHandler` now also constructs a `TransportFactory` from the registered handler and stores it on `API.AIBridgeTransportFactory` (an `atomic.Pointer`), making it available to chatd without going through the license-gated HTTP route.
- Added `API.AIBridgeTransportFactory` as a public `atomic.Pointer[aibridge.TransportFactory]` field on `coderd.API`.
### How to test?
- `coderd/aibridged/transport_test.go` covers: transport creation, nil-handler errors, source attachment to context, header/status passthrough, streaming (SSE-style chunked writes visible before handler completion), context cancellation closing the body with an error, concurrent requests, handler panics producing 500s, and handlers that return without writing.
- `coderd/aibridge_test.go` verifies that `AIBridgeTransportFactory` starts as nil on AGPL coderd, can be stored and loaded atomically, and that the stored factory correctly dispatches requests through the stub handler.
### Why make this change?
Chatd needs to send LLM requests through aibridge in-process rather than via the external HTTP route, which is license-gated. The `TransportFactory` abstraction provides a clean seam: the entitlement check remains on the HTTP route for external callers, while in-process coder-agent traffic bypasses it through the factory. The `Source` type allows downstream handlers and logs to attribute traffic without gating behavior on the caller identity.
In order to allow Coder Agents to use AI Gateway in OSS, we need to rehome the `aibridged`\-related code into the AGPL path.
The HTTP API is only registered under enterprise so will still require the AI Governance Add-on to be present in order to use it, whereas Coder Agents uses an in-memory pipe to the same handlers.
`CODER_AI_GATEWAY_ENABLED` / `CODER_AIBRIDGE_ENABLED` is now being defaulted to `true` now that it will be used by Coder Agents.
If you previously had this value disabled explicitly, that value will persist.
_Disclaimer: implemented by a Coder Agent using Claude Opus 4.7_
Part of the implementation of [RFC: Common AI Provider Configs](https://www.notion.so/coderhq/RFC-Common-AI-Provider-Configs-34bd579be59280ed958feffb82024797) (AIGOV-201).
## Note
This change can cause a previously working installation to fail to start should a conflict exist between the providers configured in the environment & those now migrated to the database.
I'll raise a PR upstack to document this process and workarounds should a startup fail.
## What this PR does
Reconciles environment-derived AI provider configuration with the `ai_providers` table at server startup. The seed runs **before** the aibridged daemon is initialized, so the runtime always reads providers from the database; the legacy `CODER_AIBRIDGE_*` environment variables become a one-shot migration source.
### Behavior
- Concurrent server starts are serialized through a Postgres advisory lock (`LockIDAIProvidersEnvSeed`).
- Missing rows are inserted with an audit entry attributed to the system actor.
- Existing rows whose canonical hash matches the env-derived hash are left alone (the common no-op restart path).
- Existing rows whose canonical hash does **not** match cause server startup to fail with a descriptive error so the operator can explicitly resolve the conflict in either env or DB.
- Soft-deleted rows are NOT resurrected from env; an explicit operator deletion is sticky across restarts.
- Indexed providers whose name conflicts with a legacy env var fail startup with a clear remediation message.
- Unknown provider types (e.g. `copilot`, until the DB enum is widened) are skipped with a log entry rather than failing startup.
### Canonical hashing
The `canonicalAIProvider` shape captures exactly the fields that determine runtime behavior — `type`, `base_url`, and the Bedrock subset of settings (access key, access key secret, region, model, small fast model) — and is hashed with SHA-256. The hash is **computed on demand from the row + env**, never persisted, so the database does not need a new column for it. API keys live in the separate `ai_provider_keys` table and are intentionally excluded from the hash so operators can rotate keys via the API without forcing a server restart.
<details>
<summary>Decision log</summary>
- The hash is intentionally not persisted in the database. The RFC discussed this trade-off; computing on demand keeps the schema minimal and lets the canonical shape evolve without a migration.
- The lock uses an `iota` slot in `coderd/database/lock.go` rather than `GenLockID` so it's stable, easy to audit, and matches the convention used for every other startup lock.
- A bearer-token Anthropic provider whose env vars also set Bedrock metadata but no AWS credentials does NOT store the Bedrock fields. Without credentials the discriminated settings would misrepresent the row as Bedrock auth.
- We deliberately do NOT publish to the `ai_providers_changed` pubsub channel from the seed because the seed completes before any subscriber is started; the follow-up PR introduces that channel.
</details>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The `reconciliation stats` log line runs on every reconciliation tick
(every 5 minutes by default), even when there are no presets to
reconcile. In a steady-state installation without prebuilds activity,
this is the only log line that persistently shows up at info level.
Demote it to `debug` so the steady-state log output stays quiet.
Before:
```
2026-05-14 15:01:25.085 [info] coderd.prebuilds: reconciliation stats elapsed=1.649153ms presets_total=0 presets_reconciled=0
```
After: same line is emitted at `debug` level and is suppressed at the
default info log level.
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The `noEmptyInterface` rule is enabled by default as a recommended Biome
rule. Its safe fix converts empty interfaces to type aliases (e.g.
`export interface X {}` becomes `export type X = {}`), which then
conflicts with `noBannedTypes` on the empty `{}` type. Since empty
interfaces are legitimately produced by Go-to-TypeScript code
generation, this disables the rule entirely.
<details><summary>Context</summary>
Discussion in Slack concluded that disabling the rule is preferable to
adding per-type exceptions in `scripts/apitypings/main.go`, since the
safe fix itself creates a worse lint violation.
</details>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6/4.7*
Fixes
[coder/aibridge#280](https://github.com/coder/aibridge/issues/280).
Claude Opus 4.7 (and future adaptive-only Bedrock models) reject the
legacy `thinking.type: "enabled"` + `budget_tokens` shape with a 400.
Claude Code falls back to that shape when it cannot read the upstream
model's capability metadata, which is exactly the case when AI Bridge
sits between the client and Bedrock. Pinning back to Opus 4.6 is the
only operator workaround today.
This is the counterpart to the `adaptive -> enabled` conversion added in
[coder/aibridge#225](https://github.com/coder/aibridge/pull/225) for
older Bedrock models.
## Behavior
- New `bedrockModelRequiresAdaptiveThinking()` helper matches Opus 4.7
(covers `us.anthropic.claude-opus-4-7`, ARN-style application inference
profile names that include the model ID, etc.).
- New `RequestPayload.convertEnabledThinkingForBedrock()` rewrites
`thinking: {type: enabled, budget_tokens: N}` to `thinking: {type:
adaptive}`. The budget hint is dropped; an explicit
`output_config.effort` from the caller is preserved naturally because we
never touch that field. We deliberately do **not** derive an effort
label from the budget (see decision log).
- `removeUnsupportedBedrockFields` learns a variadic `exemptFields`
parameter. Adaptive-only models support `output_config` natively (no
beta flag required), so `augmentRequestForBedrock` exempts that field
for those models.
- Bedrock Opus 4.7 accepts `output_config.effort` but rejects
`output_config.format` (structured outputs) with the same "Extra inputs
are not permitted" 400. The generic strip pass operates at top-level
granularity only, so a small targeted pass drops `output_config.format`
after the top-level strip for adaptive-only models.
The whole Bedrock thinking-type shim block carries a header comment
flagging it as temporary; a planned native Bedrock provider removes the
impedance mismatch and lets us delete it.
## Out of scope
The issue calls out a possible follow-up around `Anthropic-Beta:
interleaved-thinking-2025-05-14` for adaptive-only models; best evidence
is that Opus 4.7 still accepts those flags, so this PR is a no-op there.
<details>
<summary>Decision log</summary>
- `bedrockModelSupportsAdaptiveThinking` now also returns `true` for
adaptive-only models. That keeps the existing
`convertAdaptiveThinkingForBedrock` branch from running on Opus 4.7
(which would otherwise be incorrect; `adaptive` is the supported native
type there), and the new `convertEnabledThinkingForBedrock` runs only
for adaptive-only models via the explicit
`bedrockModelRequiresAdaptiveThinking` switch case. The two model sets
are disjoint by construction.
- The reverse conversion does **not** derive `output_config.effort` from
`budget_tokens / max_tokens`. The two thinking shapes encode different
intents (`enabled+budget` is "give me exactly N tokens,"
`adaptive[+effort]` is "model, pick a budget, optionally biased") and
there is no canonical mapping between them. An earlier draft of this PR
derived effort via midpoints of an invented anchor table; it was
symmetric-looking but lossy and required a lot of scaffolding (sorted
anchors, init-time invariant guard, round-trip tests) to keep two halves
consistent. The reverse direction now just rewrites the shape, which is
honest about the information loss and matches platform-defined adaptive
behavior when no effort hint is present.
- `output_config.format` is stripped only for adaptive-only models.
Other Bedrock models either don't get `output_config` through at all
(top-level strip handles them) or accept it via a beta flag that may
imply broader feature support. Easy to widen if the same 400 shows up
elsewhere.
- I chose `variadic exemptFields ...string` over passing the model down
to `removeUnsupportedBedrockFields`, to keep that function focused on
stripping and to localise the model-aware policy in
`augmentRequestForBedrock`.
</details>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Remove the "AI Bridge Logs" menu item from the Admin settings dropdown,
keeping only the "AI Bridge Sessions" link. The `/aibridge` index now
redirects to `/aibridge/sessions` instead of `/aibridge/request-logs`.
The request-logs route and view remain accessible via direct URL
(`/aibridge/request-logs`) for users who still need it.
Fixes https://linear.app/codercom/issue/AIGOV-205
<details><summary>Changes</summary>
- `DeploymentDropdown.tsx`: Removed "AI Bridge Logs" menu item; the
`canViewAIBridge` block now only renders the "AI Bridge Sessions" link
- `router.tsx`: Changed `/aibridge` index redirect from `request-logs`
to `/aibridge/sessions`
- All request-logs page files, route, and components are preserved for
direct URL access
</details>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Follow-up to https://github.com/coder/coder/pull/24630 which added
backend Charm Crush client detection.
## Summary
Adds a `"Charm Crush"` case to the `AIBridgeClientIcon` switch so the
client filter and request logs show an icon instead of the generic
question mark fallback.
Uses a 💘 emoji-based SVG as a placeholder, matching Crush's branding.
Replace with an official brand SVG when one becomes available from the
Charm team.
## Changes
- `site/static/icon/charm-crush.svg`: new emoji-based placeholder icon.
-
`site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.tsx`:
added `"Charm Crush"` case.
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Marks the `GET /api/v2/aibridge/interceptions` endpoint as deprecated in
favor of `/aibridge/sessions`, which provides richer session-level
aggregation including threads and agentic actions.
Changes:
- Add `@Deprecated` Swagger annotation to the endpoint handler
- Add deprecation notice to the
`codersdk.Client.AIBridgeListInterceptions` method
- Regenerated OpenAPI spec with `"deprecated": true` flag
The endpoint remains fully functional.
Fixes https://github.com/coder/internal/issues/1339
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The [release calendar](https://coder.com/docs/install/releases) was
missing a link and details for v2.32, which was released on April 14,
2026.
Changes:
- Add v2.32 as Mainline with changelog link and release date
- Add v2.33 as the next upcoming (Not Released) entry
- Update latest patch versions: v2.29.10, v2.30.7, v2.31.9
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Porting https://github.com/coder/aibridge/pull/277 to coder/coder after
the [aibridge code move](https://github.com/coder/coder/pull/24190).
## Summary
Fixes client detection and session ID tracking for the [Charm
Crush](https://github.com/charmbracelet/crush) AI coding client.
## Changes
### Bug fix: User-Agent matching
The actual Crush user-agent is `Charm-Crush/{version}
(https://charm.land/crush)` (hyphenated), but `GuessClient` only checked
for `charm crush/` (space-separated). After lowercasing,
`Charm-Crush/0.2.0` becomes `charm-crush/0.2.0`, which did not match the
`charm crush/` prefix.
Now matches both formats for backwards compatibility.
### Session ID tracking
Adds an explicit `ClientCrush` case to `GuessSessionID`. Crush does not
currently send a session ID header to upstream AI providers, so this
returns `nil` (consistent with how `ClientZed`, `ClientRoo`, and
`ClientCursor` are handled).
### Tests
- Added `charm_crush_hyphen` test case for `GuessClient` using the real
user-agent format.
- Added `crush_returns_empty` test case for `GuessSessionID`.
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
---
Adds a lightweight workflow that posts a docs preview link as a PR
comment
whenever a pull request touches files under `docs/`. The preview is
served
by coder.com's branch-preview feature at `/docs/@<branch>`.
The branch name is URL-encoded so names with slashes (e.g.
`user/feature`) produce correct links like
`/docs/@user%2Ffeature` instead of broken paths.
The comment is created on open and updated in-place on subsequent pushes
using the `peter-evans/find-comment` + `create-or-update-comment`
pattern
already used by the `pr-deploy` workflow.
---
Depends on https://github.com/coder/coder.com/pull/708
_Disclaimer: produced by Claude Opus 4.6_
Adds a `coder_build_info` metric which allows operators to see which
versions of Coder are currently running.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
_Disclaimer: produced mostly by Claude Opus 4.6 following detailed
planning._
## Summary
- Support multiple instances of the same AI Bridge provider type via
indexed env vars (`CODER_AIBRIDGE_PROVIDER_<N>_<KEY>`), following the
`CODER_EXTERNAL_AUTH_<N>_<KEY>` pattern
- Existing single-provider env vars (`CODER_AIBRIDGE_OPENAI_KEY`, etc.)
continue to work unchanged
- Setting both a legacy env var and an indexed provider with the same
name errors at startup to prevent silent misconfiguration
- Mark legacy provider fields (`OpenAI`, `Anthropic`, `Bedrock`) as
deprecated in `AIBridgeConfig` in favor of `Providers`
## Example
```sh
CODER_AIBRIDGE_PROVIDER_0_TYPE=anthropic
CODER_AIBRIDGE_PROVIDER_0_NAME=anthropic-corp
CODER_AIBRIDGE_PROVIDER_0_KEY=sk-ant-corp-xxx
CODER_AIBRIDGE_PROVIDER_0_BASE_URL=https://llm-proxy.internal.example.com/anthropic
CODER_AIBRIDGE_PROVIDER_1_TYPE=anthropic
CODER_AIBRIDGE_PROVIDER_1_NAME=anthropic-direct
CODER_AIBRIDGE_PROVIDER_1_KEY=sk-ant-direct-yyy
```
Each instance is routed by name:
- /api/v2/aibridge/**anthropic-corp**/v1/messages
- /api/v2/aibridge/**anthropic-direct**/v1/messages
Closes
[AIGOV-157](https://linear.app/codercom/issue/AIGOV-157/spike-to-understand-if-there-is-a-simple-way-to-handle-multi-api-key)
---------
Signed-off-by: Danny Kopping <danny@coder.com>
_Disclaimer: created using Claude Opus 4.6._
```
# Examples:
# ./scripts/backport-pr.sh 2.30 23969
# ./scripts/backport-pr.sh --dry-run 2.30 23969
```
Here's one I created: https://github.com/coder/coder/pull/23972
Signed-off-by: Danny Kopping <danny@coder.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
## Summary
Two changes on the AI Bridge sessions page
(`/aibridge/sessions/<session>`):
1. **Updated header subtitle and link** — replaced the generic
"Centralized auditing for LLM usage across your organization. More about
AI Governance" with auditing-specific copy and a link to the [AI Bridge
audit docs](https://coder.com/docs/ai-coder/ai-bridge/audit).
2. **Added prompt attribution tooltip** — each user prompt now shows an
info icon with a tooltip explaining that prompt origin cannot be
reliably determined (human vs. agent), linking to the [attribution
docs](https://coder.com/docs/ai-coder/ai-bridge/audit#human-vs-agent-attribution).
## Changes
| File | What changed |
|------|-------------|
| `AIBridgeSessionsLayout.tsx` | Updated subtitle text and link target |
| `SessionTimeline.tsx` | Added `InfoIcon` + `Tooltip` next to the
"Prompt" label in `ThreadItem` |
<img width="954" height="318" alt="image"
src="https://github.com/user-attachments/assets/db3ca443-cb0f-426a-8457-4625c82fd6ba"
/>
---------
Signed-off-by: Danny Kopping <danny@coder.com>
_Disclaimer: produced using Claude Opus 4.6, reviewed by me, and
validated against Dogfood dataset._
The `ListAIBridgeSessions` query materialized and aggregated all
matching interceptions before paginating, then ran expensive
token/prompt lookups across the full dataset. For a page of 25 sessions
against ~200k interceptions (our dogfood dataset), this meant:
- Three CTEs scanning all rows (filtered_interceptions, session_tokens,
session_root)
- ARRAY_AGG(fi.id) collecting every interception ID per session
- Lateral prompt lookup via ANY(array_of_all_ids) running for every
session, not just the page
- ~90MB of disk sorts and JIT compilation kicking in
The improvement is to restructure to paginate first and enrich after: a
single CTE groups interceptions into sessions with only cheap aggregates
(MIN, MAX, COUNT), applies cursor pagination and LIMIT, then lateral
joins fetch metadata, tokens, and prompts for just the ~25-row page.
Measured against 220k interceptions / 160k sessions:
| Metric | Before | After |
|--------------------|--------|-------|
| Execution time | 1800ms | 185ms |
| Shared buffer hits | 737k | 2.6k |
| Disk sort spill | 86MB | 16MB |
| Lateral loops | 160k | 25 |
https://grafana.dev.coder.com/goto/fbODPGtvR?orgId=1 the results are
identical, just _much_ faster.
---
Also includes some additional tests which I added prior to refactoring
the query to ensure no regressions on edge-cases.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6,
reviewed by me.*
Replace the transitional soft warning message:
> AI Bridge is now Generally Available in v2.30. In a future Coder
version, your deployment will require the AI Governance Add-On to
continue using this feature. Please reach out to your account team or
sales@coder.com to learn more.
with the definitive requirement message:
> The AI Governance Add-On is required to use AI Bridge. Please reach
out to your account team or sales@coder.com to learn more.
Updated in:
- `enterprise/coderd/license/license.go`
- `enterprise/coderd/license/license_test.go` (2 occurrences)
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Adds an info banner on the `/aibridge/request-logs` page encouraging
users to visit `/aibridge/sessions` for an improved audit experience.
This allows us to validate whether customers still find the raw request
logs view useful before removing it in a future release.
Fixes#23563
*Disclaimer: implemented by a Coder Agent and reviewed by me.*
Renames the env vars used by chatd integration tests from the canonical
`SOMEPROVIDER_API_KEY` (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) to
`SOMEPROVIDER_TEST_API_KEY` (e.g. `ANTHROPIC_TEST_API_KEY`,
`OPENAI_TEST_API_KEY`) so that test-specific keys don't collide with
production/canonical provider credentials.
Relates to https://github.com/coder/internal/issues/1425
See also:
https://codercom.slack.com/archives/C0AGTPWLA3U/p1774433646799499
_Disclaimer:_ _produced_ _by_ _Claude_ _Opus_ _4\.6,_ _reviewed_ _by_ _me._
**This is a breaking change.** Users who are not have `owner` or sitewide `auditor` roles will no longer be able to view interceptions.
Regular users should not need to view this information; in fact, it could be used by a malicious insider to see what information we track and don't track to exfiltrate data or perform actions unobserved.
---
Changed authorization for AI Bridge interception-related operations from system-level permissions to resource-specific permissions. The following functions now authorize against `rbac.ResourceAibridgeInterception` instead of `rbac.ResourceSystem`:
- `ListAIBridgeTokenUsagesByInterceptionIDs`
- `ListAIBridgeToolUsagesByInterceptionIDs`
- `ListAIBridgeUserPromptsByInterceptionIDs`
Updated RBAC roles to grant AI Bridge interception permissions:
- **User/Member roles**: Can create and update AI Bridge interceptions but cannot read them back
- **Service accounts**: Same create/update permissions without read access
- **Owners/Auditors**: Retain full read access to all interceptions
Removed system-level authorization bypass in `populatedAndConvertAIBridgeInterceptions` function, allowing proper resource-level authorization checks.
Updated tests to reflect the new permission model where members cannot view AI Bridge interceptions, even their own, while owners and auditors maintain full visibility.
<!--
If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.
-->
_Disclaimer:_ _initially_ _produced_ _by_ _Claude_ _Opus_ _4\.6,_ _heavily_ _modified_ _and_ _reviewed_ _by_ _me._
Closes https://github.com/coder/internal/issues/1360
Adds a new `/api/v2/aibridge/sessions` API which returns "sessions".
Sessions, as defined in the [RFC](https://www.notion.so/coderhq/AI-Bridge-Sessions-Threads-2ccd579be59280f28021d3baf7472fbe?source=copy_link), are a set of interceptions logically grouped by a session key issued by the client.
The API design for this endpoint was done in [this doc](https://github.com/coder/internal/issues/1360).
If the client has not provided a session ID, we will revert to the thread root ID, and if that's not present we use the interception's own ID (i.e. a session of a single interception - which is effectively what we show currently in our `/api/v2/aibridge/interceptions` API).
The SQL query looks gnarly but it's relatively simple, and seems to perform well (~200ms) even when I import dogfood's `aibridge_*` tables into my workspace. If we need to improve performance on this later we can investigate materialized views, perhaps, but for now I don't think it's warranted.
---
_The PR looks large but it's got a lot of generated code; the actual changes aren't huge._
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
Ref:
https://github.com/coder/coder/actions/runs/23251163568/job/67597250364
The deploy workflow Flux reconciliation step failed with no visibility
into what went wrong. Two changes:
- Add `--verbose` to every `flux reconcile` invocation to print
generated objects on failure
- Increase `--timeout` for the four `helmrelease` reconciliations from
the default 5m to 10m
_Disclaimer: implemented by a Coder Agent using Claude Opus 4.6._
Marks the injected MCP approach in AI Bridge as deprecated across the
codebase.
## Changes
- **`codersdk/deployment.go`**: Deprecated `ExternalAuthConfig.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex` fields; deprecated and hid the
`--aibridge-inject-coder-mcp-tools` server flag; deprecated
`AIBridgeConfig.InjectCoderMCPTools`.
- **`coderd/externalauth/externalauth.go`**: Deprecated `Config.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex`.
- **`enterprise/aibridgedserver/aibridgedserver.go`**: Added runtime
deprecation warning when `CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS` is
enabled; deprecated `getCoderMCPServerConfig`.
- **`enterprise/aibridged/mcp.go`**: Deprecated `MCPProxyBuilder`
interface and `MCPProxyFactory` struct.
- **`docs/ai-coder/ai-bridge/mcp.md`**: Added deprecation warning
banner.
_Disclaimer: implemented with Opus 4.6 and Coder Agents._
Follow-up to #22879.
## Problem
The `CODER_SESSION_TOKEN` guard added in #22879 blocks `coder login`
unconditionally when the env var is set. This conflicts with
`--use-token-as-session`, which intentionally uses the provided token
(including from the env var) directly as the session token.
## Fix
Add `&& !useTokenForSession` to the check so that `coder login
--use-token-as-session` still works when `CODER_SESSION_TOKEN` is set.
## Testing
Added `TestLogin/SessionTokenEnvVarWithUseTokenAsSession` — sets the env
var with a valid token and passes `--use-token-as-session`, verifying
login succeeds.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
_Disclaimer: created with Opus 4.6 and Coder Agents._
## Problem
When `CODER_SESSION_TOKEN` is set as an environment variable with an
invalid value, `coder login` fails with a confusing error:
```
error: Trace=[create api key: ]
You are signed out or your session has expired. Please sign in again to continue.
Suggestion: Try logging in using 'coder login'.
```
The suggestion to run `coder login` is what the user just did, making it
circular and unhelpful.
## Root cause
The `--token` flag is mapped to `CODER_SESSION_TOKEN` via serpent. When
the env var is set, `coder login` picks it up as the session token and
tries to use it to create a new API key, which fails because the token
is invalid. Even if login were to succeed and write a new token to disk,
subsequent commands would still use the env var (which takes precedence
over the on-disk token), so the user would remain stuck.
## Fix
Before attempting login, check if `CODER_SESSION_TOKEN` is set in the
environment. If so, return a clear error telling the user to unset it:
```
the environment variable CODER_SESSION_TOKEN is set, which takes precedence
over the session token stored on disk. Please unset it and try again.
unset CODER_SESSION_TOKEN
```
## Testing
Added `TestLogin/SessionTokenEnvVar` that verifies the error is returned
when the env var is set.
Adds database columns and server-side logic to track interception lineage via tool call IDs. When an interception ends, the server resolves the correlating tool call ID to find the parent interception and links them via `parent_id`.
New `provider_tool_call_id` column on `aibridge_tool_usages` and `parent_id` column on `aibridge_interceptions`, with indexes for lookup. `findParentInterceptionID` queries by tool call ID and filters out the current interception to find the parent.
Adapted from the [coder/coder `dk/prompt_provenance_poc`](https://github.com/coder/coder/compare/main...dk/prompt_provenance_poc) branch.
Depends on [coder/aibridge#188](https://github.com/coder/aibridge/pull/188).
Closes https://github.com/coder/internal/issues/1334
Adds database columns and server-side logic to track interception lineage via tool call IDs. When an interception ends, the server resolves the correlating tool call ID to find the parent interception and links them via `parent_id`.
New `provider_tool_call_id` column on `aibridge_tool_usages` and `parent_id` column on `aibridge_interceptions`, with indexes for lookup. `findParentInterceptionID` queries by tool call ID and filters out the current interception to find the parent.
Adapted from the [coder/coder `dk/prompt_provenance_poc`](https://github.com/coder/coder/compare/main...dk/prompt_provenance_poc) branch.
Depends on [coder/aibridge#188](https://github.com/coder/aibridge/pull/188).
Closes https://github.com/coder/internal/issues/1334
The comments generated are too noisy and not of sufficiently high signal
that we should automatically opt every PR in.
This PR moves the trigger to the `code-review` label _only_.
Signed-off-by: Danny Kopping <danny@coder.com>
On Windows, `pty.New()` was creating a `ConPTY` (`PseudoConsole`) even
when no process would be attached. `ConPTY` requires a real process to
function correctly - without one, the pipe handles become invalid
intermittently, causing flaky test failures like `read |0: The handle is
invalid.`
This affected tests using the `ptytest.New()` + `Attach()` pattern for
in-process CLI testing.
The fix splits Windows PTY creation into two paths:
- `newPty()` now returns a simple pipe-based PTY for the `Attach()` use
case
- `newConPty()` creates a real `ConPTY`, called by `Start()` when a
process will be attached
AFAICT this will result in no change in behaviour outside of tests.
Fixescoder/internal#1277
_Disclaimer: investigated and implemented by Claude Opus 4.5, reviewed
by me._
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Liveness checks are currently causing pods to be killed during
long-running migrations.
They are generally not advisable for our workloads; if a pod becomes
unresponsive we _need_ to know about it (due to a deadlock, etc) and not
paper over the issue by killing the pod.
I've also made all probe settings configurable.
---------
Signed-off-by: Danny Kopping <danny@coder.com>
Operators need to know which API key was used in HTTP requests.
For example, if a key is leaking and a DDOS is underway using that key, operators need a way to identify the key in use and take steps to expire the key (see https://github.com/coder/coder/issues/21782).
_Disclaimer: created using Claude Opus 4.5_