The AI session threads API returned only a network call *summary*
(total/blocked counts + top domains). This adds the per-call list so the
session detail can render individual Agent Firewall network calls.
`ListAIBridgeSessionNetworkCalls` reuses the same sequence-number
windowing as the existing summary and includes all protocols. The list
is exposed as `network_call_logs` on the threads response and is capped
server-side at 100 rows. The summary (`network_calls.total`/`blocked`)
remains authoritative for whole-session totals: the list length and its
blocked count equal the summary only when a session has at most 100
calls, and are truncated beyond that.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary
2. #27418 — frontend summary rows
3. #27425 — backend per-call list `network_call_logs`
4. #27426 — frontend network-calls panel
Refs AIGOV-464
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
## Description
Closes
[AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable).
The AI Gateway cost control feature is planned for GA on the upcoming
release, so this removes the `ExperimentAIGatewayCostControl` experiment
and all of its gating. The cost control API endpoints remain gated by
the `FeatureAIBridge` license feature (the AI Governance add-on), so
this only drops the experiment layer.
## Changes
- **`codersdk/deployment.go`**: remove the
`ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and
its `ExperimentsKnown` entry.
- **`enterprise/coderd/coderd.go`**: remove the
`httpmw.RequireExperiment(...)` gating from the AI cost control routes.
They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected
endpoints:
- `GET /organizations/{organization}/groups/ai/spend`
- `GET
/organizations/{organization}/groups/{groupName}/members/ai/spend`
- `GET /organizations/{organization}/ai/spend/export`
- `GET /groups/{group}/members/ai/spend`
- `GET /groups/{group}/ai/spend`
- `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET
/users/{user}/ai/spend`
- **`enterprise/coderd/aibridge_test.go`**: drop the experiment from
test setup and remove the now-obsolete `RequiresExperiment`
negative-path tests.
- **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control`
experiment checks from the cost control UI (Groups pages, user dropdown)
and their stories/mocks. The feature is now driven solely by the
`aibridge` feature visibility.
- **Generated**: regenerated `coderd/apidoc/*`,
`docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`.
## Out of scope
The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo,
not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless:
unknown experiment values are logged as `"ignoring unknown experiment"`
at startup and otherwise ignored, so no ordering dependency or breakage.
That cleanup can be a follow-up.
<details>
<summary>Implementation notes</summary>
- Verified how unknown experiments are handled in `coderd/coderd.go`
`ReadExperiments`: unknown values produce a warning log and are inert,
so removing the definition before the dogfood config is updated is safe.
- Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`)
were already gated only by `FeatureAIBridge`, never by the experiment.
After this change all cost control routes are uniformly feature-gated,
resolving that inconsistency.
- Removed an obsolete `RequiresExperiment` subtest in
`TestUserAISpendStatus` that only asserted a 403 from the experiment
gate; with the gate gone it would no longer be blocked pre-RBAC.
</details>
---
_This PR was created by Coder Agents on behalf of @ssncferreira._
## Description
Makes the user AI cost control endpoints consistent.
## Changes
- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
## Problem
A configured AI spend limit was only validated as `gte=0`, with no upper
bound. The group spend query multiplies the per-member limit by the
number of attributed members, so a large enough limit overflows `bigint`
and fails the whole query, returning an error for every group in the
request rather than just the misconfigured one.
## Changes
- Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period.
- Reject group budgets and per-user overrides above the maximum with a
400 naming the limit.
- Bound both budget forms in the UI so they show the valid range before
submitting.
Follow-up
https://github.com/coder/coder/pull/27589#discussion_r3668956350
Depends on https://github.com/coder/coder/pull/27589
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
## Problem
The organization groups page showed each group's AI budget as the
group's per-member limit, so the total it displayed was effectively
group members × group budget. That ignores per-user budget overrides
charged to the group, so a group where one member has an override
reported a limit that doesn't match what its members can actually spend.
## Changes
- Add `total_spend_limit_micros` to the organization groups AI spend
payload, the combined budget of the members attributed to the group,
with each member's override replacing their share.
- Return `null` for the total when the group has no budget, since its
members spend without a cap.
- Both the organization groups and single group spend endpoints report
the new field, as they share the same query.
- Use the total as the denominator on the groups page AI budget column.
Depends on #27568
## Description
Adds `GET /api/v2/groups/{group}/ai/spend`, returning the AI spend limit
and aggregate spend for a single group over the current budget period.
The period is derived from the deployment's configured budget period
rather than being caller-specified, matching the other AI spend
endpoints.
## Changes
- Add the `groupAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature.
- Reuse the existing `GetOrganizationGroupsAISpend` query with a single
group ID, so no new query or authorization path is introduced.
- Add the `GroupAISpend` codersdk type and client method.
Closes
https://linear.app/codercom/issue/AIGOV-475/implement-apiv2groupsgroupaispend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
## Description
Adds `GET /api/v2/organizations/{organization}/ai/spend/export`,
returning `text/csv` with per-user, per-group, per-model, per-provider
aggregated AI spend. The data is built from the raw AI Gateway token
usage tables rather than the `ai_user_daily_spend` rollup, but stays
consistent with it: spend is attributed through the token usage's
effective group and bucketed by the token usage `created_at`, the same
values the daily rollup derives from.
The period defaults to the current UTC month, narrowed to the configured
AI Gateway retention window when the month begins before retained data
does. Explicit `period_start`/`period_end` params must be provided
together, are interpreted as UTC, and may span at most 31 days. Unlike
the default period, an explicit period that begins before the retention
window is rejected rather than narrowed. Every row echoes the applied
bounds, so a narrowed window is visible in the export.
The endpoint requires organization-level admin permissions.
## Changes
- Add the `ExportOrganizationAISpend` query aggregating
`aibridge_token_usages` joined to `aibridge_interceptions`, scoped to
the organization via the effective group, resolving the username, group
name, and organization name alongside their IDs.
- Add the `exportOrganizationAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature,
returning the CSV in a single response.
- Add the `ExportOrganizationAISpend` codersdk client method.
- Require organization-wide `ResourceGroupMember` read, since the export
aggregates every user in the organization. The per-row filter stays in
`dbauthz` as defence in depth.
- Escape leading formula characters in the free-text columns, so a model
or provider name recorded from an intercepted request cannot be
evaluated when the CSV is opened in a spreadsheet.
- Add an index on `aibridge_token_usages (effective_group_id,
created_at)`, which the period and group predicates otherwise cannot
use.
Closes
https://linear.app/codercom/issue/AIGOV-293/add-csv-export-for-ai-spend-data
> [!NOTE]
> Generated by Coder Agents on behalf of @ssncferreira
## Description
Previously, a user with no per-user override and no membership in a budgeted group had no effective group, so their AI spend was attributed nowhere and was, therefore, untracked. This change falls back to the organization's Everyone group when no override or group budget applies.
Since every user in an organization is implicitly a member of that org's Everyone group, spend is now attributed and tracked for any user with organization membership. A user with no organization membership resolves to no group, so their daily spend is not incremented and a warning is logged.
The fallback is unlimited, so enforcement is unaffected: only override and group budgets can block requests. For users in multiple organizations, an existing budget on any Everyone group is still chosen by the "highest" policy; when none is budgeted, the fallback prefers the default org, then orders by organization name.
## Changes
- Add `ResolveUserEffectiveGroup` and the `GetUserEveryoneFallbackGroup` query: resolve override → group budget → Everyone group fallback.
- Attribute token-usage spend and the user AI spend endpoint via the fallback, so unbudgeted users resolve to their Everyone group instead of null.
- Update `GetGroupMembersAISpend` to surface the Everyone fallback as the effective group.
- Update `GetHighestGroupAIBudgetByUser` to break ties by organization name then group name, keeping multi-org resolution deterministic and consistent with the fallback.
- For multi-org users with no budget anywhere, the fallback picks the Everyone group deterministically: prefer the default org, then order by organization name.
Closes https://linear.app/codercom/issue/AIGOV-509/fall-back-to-the-everyone-group-for-spend-attribution
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
Add a "Total/blocked network calls" column to the AIBridge sessions
table.
Update `ListAIBridgeSessions` query to calculate network called made and
blocked per session. See query plan
[here](https://explain.dalibo.com/plan/54355c90b165ggb4).
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Description
Adds `GET /api/v2/groups/{group}/members/ai/spend?user_ids=...` (also available org-scoped at `/api/v2/organizations/{org}/groups/{groupName}/members/ai/spend`) to return per-member AI spend attributed to a group, along with each member's effective budget group and the applied spend limit when the queried group is their effective budget source.
In the UI, this endpoint is used alongside the existing `/api/v2/groups/{group}/members` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (group members) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/groups/{group}/members` → returns the group's members.
2. Request `/api/v2/groups/{group}/members/ai/spend?user_ids=...` with the IDs from step 1.
**Note:** Only current members of the queried group are returned. `spend_limit_micros` and `limit_source` are populated only when the queried group is the member's effective budget source (its own limit or a user override). `effective_group_id` is null when the member's budget resolves to a group in another organization, since an organization is treated as a tenant boundary.
<img width="2880" height="1904" alt="image" src="https://github.com/user-attachments/assets/33ed395d-d1a3-4b46-bb04-c8d3f41c8886" />
## Changes
- Add `codersdk.GroupMembersAISpend` and `GroupMemberAISpend` types, reusing the shared `AISpendPeriodWindow`.
- Add `GetGroupMembersAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /api/v2/groups/{group}/members`.
- Add handler and routes under `/groups/{group}/members/ai/spend` (and the org-scoped alias) with a required `user_ids` query param (cap 100). Callers with more than 100 members are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-471/backend-group-members-endpoint-with-members-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
## Description
Adds `GET /api/v2/organizations/{org}/groups/ai/spend?group_ids=...` to return per-group AI spend and configured limits for a set of groups in an organization.
In the UI, this endpoint is used alongside the existing `/api/v2/organizations/{org}/groups` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (groups) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/organizations/{org}/groups` → returns the organization's groups.
2. Request `/api/v2/organizations/{org}/groups/ai/spend?group_ids=...` with the IDs from step 1.
The groups endpoint from 1) is currently not paginated, but if pagination is added later, this design keeps the two responses in sync. This spend endpoint intentionally takes `group_ids` rather than paginating on its own, since it depends on the group set from step 1. Pagination could be added in the future, especially for Cost Control-focused pages.
<img width="2880" height="1460" alt="image" src="https://github.com/user-attachments/assets/ea83b74d-6a4f-45a6-af2f-1024e019da07" />
## Changes
- Add `codersdk.OrganizationGroupsAISpend` and `OrganizationGroupAISpend` types, plus a shared `AISpendPeriodWindow` embedded in the spend response.
- Add `GetOrganizationGroupsAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /organizations/{org}/groups`.
- Add handler and route under `/organizations/{organization}/groups/ai/spend` with a required `group_ids` query param (cap 100). Callers with more than 100 groups are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-466/backend-organization-groups-endpoint-with-groups-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
## Description
Adds the `GET /api/v2/users/{user}/ai/spend` endpoint returning the
user's current AI spend, effective budget, and period bounds.
## Changes
- Add `userAISpendStatus` handler under the same feature/experiment gate
as `/api/v2/users/{user}/ai/budget`.
- Add `codersdk.UserAIBudgetSummary` (embedded into `UserAISpendStatus`)
and a `UserAISpendStatus` client method.
- Move `LimitSource` from `coderd/aibridge/budget` to `codersdk` so the
type is shared across endpoints.
Closes https://linear.app/codercom/issue/AIGOV-472
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
Closes
https://linear.app/codercom/issue/AIGOV-502/flake-testuseraibudgetoverrideauditupsertspendlimit
The `Audit/UpsertEverything` and `Audit/UpsertSpendLimit` subtests
grabbed `rows[0]` from `GetAuditLogsOffset`, assuming strict time-desc
order. Both upserts emit `AuditActionWrite` against the same override
with no ordering tiebreaker, so when the create and update entries share
a timestamp the create entry could sort first and fail the assertion.
Select the update entry by the spend limit it results in via a new
`auditLogByNewSpendLimit` helper instead of by row position.
<hr/>
We could make `auditLogByNewSpendLimit` more generic, but since we don't
have any other use cases for it right now, I think we can postpone that
until the need arises.
Related PR: https://github.com/coder/coder/pull/26630/changes
## Description
Registers `/api/v2/ai-gateway/*` as the new API path for AI Gateway, replacing `/api/v2/aibridge/*`. Both prefixes share the same route builder (`aiBridgeRoutes`) backed by a single in-memory handler, so existing `/aibridge` endpoints continue to work. New endpoints must be registered on the enterprise API handler under `/api/v2/ai-gateway` only.
Swagger annotations now point to `/api/v2/ai-gateway` paths with a backward-compatibility note referencing `/aibridge`. The legacy `/aibridge` routes are skipped in the swagger documentation test.
## Changes
- Store one raw handler (`aiGatewayHandler`) instead of two prefix-stripped handlers
- Register `/ai-gateway` and `/ai-gateway/proxy` route aliases alongside legacy `/aibridge` routes
- Move `/aibridge/keys` to `/ai-gateway/keys`
- Update in-process transport to use `/api/v2/ai-gateway` prefix
- Update SDK client URLs and proxy forwarding URL
- Swap `@Router` and `@Tags` annotations from `aibridge`/`AI Bridge` to `ai-gateway`/`AI Gateway`
- Rename user-facing error messages from "AI Bridge" to "AI Gateway"
- Define consts for route prefixes (`AIGatewayRootPath`, `AIBridgeRootPath`)
- Update tests and comments to use new paths
Note: the following will be addressed in follow-up PRs:
- Frontend API URLs
- Frontend routes and redirects
- Dogfood main.tf updates
- Hand-written documentation URL updates
- aibridge internal comments and nits
- Scale tests path updates
Refs https://linear.app/coder/issue/AIGOV-230
> Generated with the assistance of Coder Agents (@ssncferreira)
Add `agent_firewall_session_id` and `agent_firewall_sequence_number`
fields to `AIBridgeThread` in the `GET
/api/v2/aibridge/sessions/{session_id}` response. These fields link each
thread to its agent firewall confinement session so the frontend can
discover the boundary session and compute sequence ranges for
interleaving firewall events within the thread timeline.
The database columns already exist on `aibridge_interceptions`
(migration 000520) and are already selected by
`ListAIBridgeSessionThreads`. This PR surfaces them through the SDK type
and the `db2sdk` conversion.
Depends on #24814
**Naming note:** The RFC uses `boundary_session_id` /
`boundary_sequence_number`, but the codebase standardized on
`agent_firewall_*` naming in the DB migration. The API fields follow the
existing convention.
</details>
> [!NOTE]
> This PR was authored by Coder Agents.
Adds the `ai-gateway-cost-control` experiment flag to gate new cost
control endpoints and upcoming frontend UI behind an explicit opt-in.
Currently AI Gateway cost control supports the following endpoints:
- `GET/PUT/DELETE /api/v2/organizations/{org}/groups/{group}/ai/budget`
- `GET/PUT/DELETE /api/v2/users/{user}/ai/budget`
Note: the group-level endpoints were already released in v2.34.0 and
remain ungated. Only the user-level endpoints are gated behind this
experiment. Future cost control endpoints and UI should use this
experiment for gating until the feature is stable.
> Generated by Coder Agents on behalf of @ssncferreira
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
Relates to
https://linear.app/codercom/issue/AIGOV-285/add-user-budget-overrides-table-and-crud-api
Adds audit-log support for `user_ai_budget_override` mutations. Without
it, an admin could quietly change a user's per-user spend cap (e.g. from
`$500` to `$50`), reassign it to a different group, or delete it
entirely with no record of who did it.
Both write (`create-or-update`) and delete actions now generate audit
log entries. Unlike group AI budgets, which only track `spend_limit`,
overrides also track `group_name`: an override can be reassigned to a
different attributed group, so that change needs to show up in the diff.
The raw `spend_limit_micros`, IDs, and timestamps are ignored in favor
of the human-readable `spend_limit` and `group_name`.
Depends on #25439.
## Screenshot
<img width="1343" height="514" alt="image"
src="https://github.com/user-attachments/assets/aee30f58-6e81-435e-9bca-5bc98f49d8d3"
/>
Implements https://linear.app/codercom/issue/AIGOV-285
Follow the structure established in
https://github.com/coder/coder/pull/25203
## Summary
Adds the `user_ai_budget_overrides` table and CRUD API at
`/api/v2/users/{user}/ai/budget`. An override sets a custom per-user
spend cap that supersedes group-budget resolution, attributing spend to
a specific group.
## Schema
```sql
CREATE TABLE user_ai_budget_overrides (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
spend_limit_micros BIGINT NOT NULL CHECK (spend_limit_micros >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
## Membership lifecycle
The membership invariant — a user must be a member of the attributed
group, including when that group is "Everyone" — would naturally be
expressed as a composite FK on `(user_id, group_id) →
group_members_expanded(user_id, group_id)`. PostgreSQL doesn't allow
foreign keys to reference views, so enforcement is split across two
mechanisms:
- **Write-time check.** A CHECK constraint on the table
(`user_ai_budget_overrides_must_be_group_member`) calls a `STABLE`
function `is_group_member(user_id, group_id)` that queries
`group_members_expanded`. The view surfaces both regular group
memberships and the implicit "Everyone" group memberships from
`organization_members`. Any INSERT or UPDATE that violates the predicate
is rejected with a Postgres `check_violation`, which the handler maps to
a 400. `is_group_member` is defined as a general predicate, reusable by
any future table that needs the same check.
- **Cascade on removal.** Two `BEFORE DELETE` triggers handle membership
loss:
- `trigger_delete_user_ai_budget_overrides_on_group_member_delete` on
`group_members` — covers regular group removals (admin action, OIDC
sync).
- `trigger_delete_user_ai_budget_overrides_on_org_member_delete` on
`organization_members` — covers the "Everyone" group, whose membership
lives in `organization_members`.
The single-column FKs on `users(id)` and `groups(id)` remain to cascade
on user or group deletion (those paths don't pass through
`group_members`).
## Authorization
The dbauthz layer gates each operation against the `User` and (for
writes) `Group` resources:
| Operation | User resource | Group resource |
|-----------|----------------|----------------|
| `GET` | `ActionRead` | — |
| `PUT` | `ActionUpdate` | `ActionUpdate` |
| `DELETE` | `ActionUpdate` | `ActionUpdate` |
For `DELETE`, the dbauthz layer fetches the existing override first to
learn the attributed `group_id`, then runs both checks.
### Role matrix
| Role | GET | PUT | DELETE |
|--------------|-----|-----|--------|
| Owner | ✅ | ✅ | ✅ |
| UserAdmin | ✅ | ✅ | ✅ |
| OrgAdmin | ✅ | ❌ | ❌ |
| OrgUserAdmin | ✅ | ❌ | ❌ |
Internal discussion:
https://codercom.slack.com/archives/C096PFVBZKN/p1779392747885359
## Audit logs
Audit logs will be addressed in a follow-up PR.
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.
Closes
https://linear.app/codercom/issue/AIGOV-284/add-group-budgets-table-and-crud-api
## Summary
Adds the `group_ai_budgets` table and the following endpoints:
- `GET /api/v2/groups/{group}/ai/budget`
- `PUT /api/v2/groups/{group}/ai/budget`
- `DELETE /api/v2/groups/{group}/ai/budget`
Each group may have at most one budget row. If no row exists, no budget
is enforced.
### Feature gate
Added `RequireFeatureMW(FeatureAIBridge)` on the `/ai/budget` sub-route.
## RBAC
Authorization reuses `rbac.ResourceGroup` with the existing
`.InOrganization(...).WithID(...)` scoping model.
The `dbauthz` wrappers load the parent `groups` row and authorize
against it.
No new resource type is introduced. As a result, anyone with
`group:update` permissions (Owner, OrgAdmin, or UserAdmin within the
organization) can manage AI budgets for that group.
## Read access for group members
`database.Group.RBACObject()` grants `policy.ActionRead` to all members
of the group through the group ACL:
```go
func (g Group) RBACObject() rbac.Object {
return rbac.ResourceGroup.WithID(g.ID).
InOrg(g.OrganizationID).
// Group members can read the group.
WithGroupACL(map[string][]policy.Action{
g.ID.String(): {
policy.ActionRead,
},
})
}
```
Because the `GET` endpoint authorizes against the same loaded `Group`
object, any group member can call:
```text
GET /api/v2/groups/{group}/ai/budget
```
`PUT` and `DELETE` remain admin-only. The group ACL grants only
`ActionRead`, so write operations continue to require role-based
`group:update` permissions.
## Alternative considered
A dedicated `rbac.ResourceGroupAiBudget` resource would allow budget
management to be separated from general group administration.
We decided not to add that complexity for now.
Previously, the sessions list sorted by `MIN(started_at)` across
interceptions, so sessions with old start times but recent activity
would sink to the bottom of the list regardless of how recently they
were used.
`ListAIBridgeSessions` now sorts by `COALESCE(MAX(prompt.created_at),
MIN(started_at)) DESC`, exposed as the non-nullable `last_active_at`
field. Sessions with prompts surface by last activity; sessions with no
prompts fall back to their start time.
The original implementation used two separate columns (`last_active_at`
as a nullable prompt timestamp and `sort_at` as the non-nullable cursor
key). This revision collapses them into a single `last_active_at` that
is always set — simplifying the SQL, the Go conversion, the API type,
and the frontend.
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This PR merges code from `coder/aibridge` repository into `coder/coder`.
It was split into 4 PRs for easier review but stacked PRs will need to
be merged into this PR so all checks pass.
* https://github.com/coder/coder/pull/24190 -> raw code copy (this PR,
before merging PRs on top of it, it was just 1 commit:
https://github.com/coder/coder/commit/70d33f33200c7e77df910957595715f81f9bec24)
* https://github.com/coder/coder/pull/24570 -> update imports in
`coder/coder` to use copied code
* https://github.com/coder/coder/pull/24586 -> linter fixes and CI
integration (also added README.md)
* https://github.com/coder/coder/pull/24571 -> added exclude to
scripts/check_emdash.sh check
Original PR message (before PR squash):
Moves coder/aibridge code into coder/coder repository.
Omitted files:
- `go.mod`, `go.sum`, `.gitignore`, `.github/workflows/ci.yml,`
`Makefile`, `LICENSE`, `README.md` (modified README.md is added later)
- `.github`, `example`, `buildinfo,` `scripts` directories
Simple verification script (will list omitted files)
```
tmp=$(mktemp -d)
echo "$tmp"
git clone --depth=1 https://github.com/coder/aibridge "$tmp/aibridge"
git clone --depth=1 --branch pb/aibridge-code-move https://github.com/coder/coder "$tmp/coder"
diff -rq --exclude=.git "$tmp/aibridge" "$tmp/coder/aibridge"
# rm -rf "$tmp"
```
## Summary
Adds `--ai-gateway-allow-byok` deployment option to control whether
users can use Bring Your Own Key (BYOK) mode with AI Gateway.
When disabled (`--ai-gateway-allow-byok=false`), BYOK requests are
rejected with a 403 and a message directing the admin to enable the
flag. Centralized key authentication works regardless of this setting.
Defaults to `true` (BYOK allowed).
---------
Co-authored-by: Danny Kopping <danny@coder.com>
## Summary
Adds a `sanitizeCredentialHint` safety check in the db-to-SDK conversion
layer to ensure credential hints are always masked before being exposed
in the API. Also adds `credential_kind` and `credential_hint` assertions
to the session threads API test.
Two new columns added to aibridge_token_usages:
- cache_read_input_tokens (BIGINT, default 0)
- cache_write_input_tokens (BIGINT, default 0)
Migration backfills existing rows by extracting values from the metadata
JSONB column (cache_read_input, input_cached, prompt_cached for reads
(max value selected since only 1 should be set), cache_creation_input
for writes).
All references to data from metadata were updated to reference new
columns. No other changes then changing where data is extracted from.
Requires aibridge library version bump to include:
https://github.com/coder/aibridge/pull/229
Fixes: https://github.com/coder/aibridge/issues/150
_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>
Closes#22136
This pull-request implements a `<ClientFilter />` to our `Request Logs`
page for AI Bridge. This will allow the user to select a client which
they wish to filter against. Technically the backend is able to actually
filter against multiple clients at once however the frontend doesn't
currently have a nice way of supporting this (future improvement).
<img width="1447" height="831" alt="image"
src="https://github.com/user-attachments/assets/0be234e2-25f2-4a89-b971-d74817395da1"
/>
---------
Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
_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._
## Summary
AI Bridge is moving to General Availability in v2.30 and will require
the AI Governance Add-On license in future versions. This adds a soft
warning for deployments using AI Bridge via Premium/Enterprise
FeatureSet without an explicit AI Bridge add-on license.
Relates to: https://github.com/coder/internal/issues/1226
## Changes
- Track whether AI Bridge was explicitly granted via license Features
(add-on) vs inherited from FeatureSet
- Show soft warning when AI Bridge is enabled and entitled via
FeatureSet but not via explicit add-on
- Changed AI Bridge enablement from hardcoded `true` to check
`CODER_AIBRIDGE_ENABLED` deployment config
## Behavior Change
AI Bridge is now only marked as "enabled" in entitlements when
`CODER_AIBRIDGE_ENABLED=true` is set in the deployment config.
Previously, it was always enabled for Premium/Enterprise licenses
regardless of the config setting.
This change ensures that users who do not use AI Bridge will not see the
soft warning about the upcoming license requirement.
## 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.
## Behavior
| Condition | Warning Shown |
|-----------|---------------|
| AI Bridge disabled | ❌ No |
| AI Bridge enabled + explicit add-on license | ❌ No |
| AI Bridge enabled + Premium/Enterprise FeatureSet (no add-on) | ✅ Yes
|
## Screenshots
### 1. No license
<img width="1708" height="577" alt="image"
src="https://github.com/user-attachments/assets/cbdbfd4d-55de-4d70-8abf-2665f458e96f"
/>
### 2. No license + CODER_AIBRIDGE_ENABLED=true
<img width="1716" height="513" alt="image"
src="https://github.com/user-attachments/assets/344aae76-7703-485f-b568-1f13a1efa48f"
/>
### 3. Premium license + CODER_AIBRIDGE_ENABLED=false
<img width="1687" height="389" alt="image"
src="https://github.com/user-attachments/assets/c2be12b0-1c0f-438d-a293-f9ec9fe6a736"
/>
### 4. Premium license + CODER_AIBRIDGE_ENABLED=true
<img width="1707" height="525" alt="image"
src="https://github.com/user-attachments/assets/1a4640e1-e656-4f9b-bed0-9390cb5d6a84"
/>
## Notes
- TODO comments added to mark code that should be removed when AI Bridge
enforcement is added
- Feature continues to work - this is just a transitional warning (soft
enforcement)
## Summary
This adds configurable overload protection to the AI Bridge daemon to
prevent the server from being overwhelmed during periods of high load.
Partially addresses coder/internal#1153 (rate limits and concurrency
control; circuit breakers are deferred to a follow-up).
## New Configuration Options
| Option | Environment Variable | Description | Default |
|--------|---------------------|-------------|---------|
| `--aibridge-max-concurrency` | `CODER_AIBRIDGE_MAX_CONCURRENCY` |
Maximum number of concurrent AI Bridge requests. Set to 0 to disable
(unlimited). | `0` |
| `--aibridge-rate-limit` | `CODER_AIBRIDGE_RATE_LIMIT` | Maximum number
of AI Bridge requests per second. Set to 0 to disable rate limiting. |
`0` |
## Behavior
When limits are exceeded:
- **Concurrency limit**: Returns HTTP `503 Service Unavailable` with
message "AI Bridge is currently at capacity. Please try again later."
- **Rate limit**: Returns HTTP `429 Too Many Requests` with
`Retry-After` header.
Both protections are optional and disabled by default (0 values).
## Implementation
The overload protection is implemented as reusable middleware in
`coderd/httpmw/ratelimit.go`:
1. **`RateLimitByAuthToken`**: Per-user rate limiting that uses
`APITokenFromRequest` to extract the authentication token, with fallback
to `X-Api-Key` header for AI provider compatibility (e.g., Anthropic).
Falls back to IP-based rate limiting if no token is present. Includes
`Retry-After` header for backpressure signaling.
2. **`ConcurrencyLimit`**: Uses an atomic counter to track in-flight
requests and reject when at capacity.
The middleware is applied in `enterprise/coderd/aibridge.go` via
`r.Group` in the following order:
1. Concurrency check (faster rejection for load shedding)
2. Rate limit check
**Note**: Rate limiting currently applies to all AI Bridge requests,
including pass-through requests. Ideally only actual interceptions
should count, but this would require changes in the aibridge library.
## Testing
Added comprehensive tests for:
- Rate limiting by auth token (Bearer token, X-Api-Key, no token
fallback to IP)
- Different tokens not rate limited against each other
- Disabled when limit is zero
- Retry-After header is set on 429 responses
- Concurrency limiting (allows within limit, rejects over limit,
disabled when zero)
Addresses [`aibridge#54`](https://github.com/coder/aibridge/issues/54)
When querying against the values in the database for
`/api/experimental/aibridge/interceptions` we found strange behaviour
wherein there was interceptions that lacked prompting and other various
fields we want. Generally this was as a result of the data not actually
existing for these values (as they were inflight).
The simple solution to this was to hide them if they didn't exist. This
PR addresses that.
---------
Co-authored-by: Danny Kopping <danny@coder.com>
Thanks to the great work in #20393, we’ve successfully introduced
offset-based pagination for this endpoint. However, the frontend expects
a `count` field in the response rather than `total`. This PR updates the
response payload to rename the returned key to `count` for consistency
with frontend expectations and existing API patterns.
This is necessary to unblock the work in #20331