Commit Graph
27 Commits
Author SHA1 Message Date
Yevhenii Shcherbina dae41eb711 fix: detect out-of-range AI Gateway costs instead of wrapping silently (#27602)
Implements:
https://linear.app/codercom/issue/AIGOV-448/use-decimal-for-cost-computation
Follow-up to https://github.com/coder/coder/pull/26229

Follow-up to the AI Gateway cost-control work. Cost is computed per
token category as `tokens × price / 1_000_000` in `int64`, then summed.
This change makes an unrepresentable result a defined outcome instead of
an accident of integer wrap-around.

## Motivation

The intermediate `tokens × price` can exceed `int64`. Real usage cannot
get there: at a $75/M model the product overflows at roughly 123 billion
tokens in a single response, about six orders of magnitude above a
maxed-out Opus request, so this is not a live incident. The problem is
what happens if it ever does, because the sign of the wrapped value
silently selects between two different failure modes, neither of which
was chosen:

1. **Wraps positive.** A plausible-looking cost is stored, incremented
into the user's daily spend, and enforced against their AI budget. No
error, no signal, wrong number.
2. **Wraps negative.** The value violates `CHECK (cost_micros >= 0)`,
the insert fails, the surrounding transaction rolls back, and
`RecordTokenUsage` returns a Postgres constraint error that says nothing
about overflow. The token usage record is lost entirely, along with its
token counts.

So the same class of bad input either corrupts budget accounting or
discards an audit record, depending on arithmetic that nobody reasoned
about. That is the undefined behaviour.

## Decision

**An unrepresentable cost is treated as bad input, not a large bill.**
Since real usage cannot produce one, it can only mean a wrong price row
or implausible provider-reported token counts. In both cases the true
cost is unknowable, so no number is stored.

**Detect rather than avoid.** `computeCost` now evaluates in `decimal`,
so nothing wraps, and range-checks the total against `[0, MaxInt64]`
before converting back. Out of range returns `errCostOutOfRange`.
Rejecting negatives in the same check also keeps them away from the
non-negative column constraint, which would otherwise discard the
record.

**Log, do not block.** The error is swallowed at the call site: the
record is written with token counts intact and `cost_micros` NULL, the
spend update is skipped, and the condition is logged at ERROR.

**Per-category truncation is unchanged.** Each category is still
truncated independently rather than the total being rounded once, so a
per-category breakdown recomputed from the snapshotted price columns
sums exactly to the stored total. Every existing `computeCost` test case
passes unmodified.
2026-08-05 14:47:40 -04:00
Susana Ferreira db88ec3f6a fix: price AI usage by configured provider type (#27836)
## Problem

AI Gateway records the aibridge provider on each interception, which is
the upstream wire format and only ever `anthropic`, `openai`, or
`copilot`. Prices are matched on exact provider and model equality, so a
provider configured as Azure, Bedrock, Google, OpenRouter, or Vercel is
priced as if it were native OpenAI or Anthropic, matching either the
wrong price or no price at all.

## Changes

- Resolve the configured provider type from `ai_providers` by provider
name, which is unique among live providers, and key the price lookup on
it instead of the aibridge provider. No schema change is needed.
- Label `unpriced_token_usage_records_total` with the same provider
value used for the lookup, so it names a provider an operator actually
configured.
- Treat a provider that cannot be resolved as unpriced, consistent with
how a missing price is handled today.

Closes
https://linear.app/codercom/issue/AIGOV-570/resolve-ai-model-prices-using-the-configured-provider-type
Depends on the follow-up that extends the shipped price book to the
remaining provider types:
https://linear.app/codercom/issue/AIGOV-571/ship-prices-for-all-ai-governance-provider-types

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
2026-08-04 14:44:27 +01:00
Susana Ferreira 7b104b6a98 fix: add info log level to unpriced models message (#27693)
Change the unpriced models log to info level.

Refs https://github.com/coder/coder/pull/27678
2026-07-30 14:47:23 +00:00
Susana Ferreira 3660ffecdd fix: add warn log level to unpriced models message (#27678)
When a model is missing from the price table, token usage is still
recorded but with a NULL cost, so AI spend for that model goes
unattributed. This was logged at debug level, which means it is
invisible in a default deployment.
Log it at warn level instead so admins can see which provider/model
pairs need a price row and act on it.
2026-07-30 12:03:22 +01:00
Susana Ferreira c351280a37 feat: add Prometheus metrics for AI Governance cost control (#27490)
## Description

Adds Prometheus metrics for AI budget cost control, emitted by the
aibridged server under the `cost_control` subsystem (full names are
prefixed `coder_ai_gateway_`).

- `blocked_requests_total` (counter) — labels: `group_id`
- `blocked_users` (gauge) — labels: `group_id`
- `unpriced_requests_total` (counter) — labels: `provider`, `model`
- `enforcement_duration_seconds` (histogram) — labels: `outcome`

## Changes

- Add `GetOverBudgetUsersPerGroup` query (plus dbauthz/dbmetrics/dbmock
wiring) to count over-budget users per effective group.
- Add a background collector that refreshes the `blocked_users` gauge on
an interval, started only when Prometheus is enabled.
- Wire `Metrics` through the aibridged server, coderd API,
`cli/server.go`, and the enterprise AI gateway handler; recording is
nil-safe when metrics are unset.

Closes
https://linear.app/codercom/issue/AIGOV-296/add-prometheus-metrics-for-cost-control

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-07-28 09:22:58 +01:00
J. Scott Miller 1ab4ed8db5 feat: exclude AI Bridge usage from AI Governance seat counting (#27280)
Under the new `ai-gateway-seat-exclusion` experiment, AI Bridge usage
stops counting toward AI Governance seats.

## Seat recording

Under the experiment, `RecordInterception` no longer records
`ai_seat_state` usage for the initiator: AI Gateway access is licensed
by the AI Governance add-on rather than per seat. This experiment is
independent of `workspace-capable-licensing` (#27279) so the two
licensing behaviors can be enabled separately. Task workspace builds
still claim AI Governance seats.

## Manual verification

Verified live on a dev deployment (provider chained to dev.coder.com's
gateway, model `gpt-5.6-luna`): with the experiment off, the first
bridge request from each identity type (admin, plain member, service
account) wrote an `ai_seat_state` row (`aibridge` reason); with it on,
requests recorded interceptions but left seat state untouched — no new
rows, and existing rows' `last_used_at` did not advance.

Part of the gateway-accounts feature.

## Stack

Part 2 of the gateway-accounts stack:

1. **#27279**: permission-based license seat counting. Behind the
`workspace-capable-licensing` experiment and gated on the AI Governance
add-on, `user_limit` counts only users the RBAC engine authorizes to
create workspaces.
2. **This PR**: stops AI Bridge usage from claiming AI Governance seats
under the new `ai-gateway-seat-exclusion` experiment.
3. ~~**#27281**: adds a `use_shared` capability precondition for
workspace ACL grants, so workspace sharing is ineffective for (and
rejected toward) users without workspace capabilities, evaluated live on
every authorization.~~ This will be done in follow-up work when we have
time to look into the performance impact.

Related but independent: **#27278** hides the Workspaces page create
CTAs for users without workspace-create permission.
2026-07-27 21:05:37 -05:00
Yevhenii Shcherbina 2574e6b785 feat: notify admins when a user crosses an AI budget threshold (#27415)
Implements:
https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached

Notify admins when a user crosses an AI budget threshold, complementing
the user-facing notifications from
https://github.com/coder/coder/pull/27346

When a priced interception pushes a user's period spend across the
warning (85%) or limit (100%) threshold, the Owners and User Admins now
receive an admin notification naming the affected user, alongside the
user's own notification. The affected user is excluded from the admin
recipients since they already get the user-facing copy. Delivery is
best-effort: a failure to enqueue is logged and never blocks recording
the interception.

The admin templates always show the effective group the spend is
attributed to, and note when the limit comes from a per-user override
rather than the group budget.

Depends on https://github.com/coder/coder/pull/27346

## Screenshots:
<img width="1101" height="440" alt="image"
src="https://github.com/user-attachments/assets/eb731088-05c8-47bd-9d06-fc9d07f63a08"
/>

<img width="468" height="391" alt="image"
src="https://github.com/user-attachments/assets/b89b76a6-3fa8-4735-99a2-43e119a7a7e3"
/>
2026-07-27 17:02:28 -04:00
Yevhenii ShcherbinaandCian Johnston ce4ee923c2 feat: notify users when AI spend crosses the budget threshold (#27346)
Implements:
https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached

Notify users when their AI spend crosses a budget threshold for their
effective group. Two thresholds are covered: a warning at 85%, and a
limit-reached notification at 100%.

Detection runs on the post-response path, right after the interception's
cost is added to the user's daily spend. It reads the user's AI spend on
the same transaction where token usage is recorded and AI daily spend is
incremented, and derives the pre-interception total by subtracting this
interception's cost. In case of `oldSpend < threshold && newSpend >=
threshold` - notification is sent. A single interception that crosses
both thresholds enqueues both notifications.

Detection and delivery are best-effort: a failure is logged and never
fails usage recording. The payload uses only stable values (the
threshold percentage and the spend limit, not the exact spend), so
duplicate enqueues are deduplicated by the notification system.

The two templates are added via migration and appear in each user's
notification settings under the "AI Budget" group.

Admin notifications (owners and user admins) are a follow-up: #27415.

## Screenshots:
<img width="1102" height="252" alt="image"
src="https://github.com/user-attachments/assets/62291510-09ca-4cdf-a1f5-4bdc11a1db4b"
/>

<img width="466" height="384" alt="image"
src="https://github.com/user-attachments/assets/030460ff-6fe2-4d59-b247-3550c543ef30"
/>

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-27 12:09:21 -04:00
Susana Ferreira c23f2c0223 feat: fall back to the Everyone group for AI spend attribution (#27364)
## 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
2026-07-23 09:26:25 +01:00
Susana Ferreira 101aee8ee0 refactor: use Options struct in aibridgedserver.NewServer (#27200)
Refactor `aibridgedserver.NewServer` to take an `Options` struct instead
of a long list of positional arguments. Follow-up to review feedback in
https://github.com/coder/coder/pull/27117#discussion_r3571535760
2026-07-16 16:09:25 +01:00
Yevhenii Shcherbina 63ec93a7ce feat: add AWS Bedrock mantle endpoint to AI Gateway (#26745)
Implements
https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider

# AWS Bedrock mantle support in AI Gateway

## Summary

Add support for the AWS Bedrock **mantle** endpoint
(`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway.
Mantle serves Claude through the native Anthropic Messages API. We model
it as a `protocol` field on the existing Bedrock provider settings
(`invoke-model` default, or `mantle`) rather than as a new provider
type, and we treat mantle as a pure passthrough: SigV4-sign and forward,
no body translation.

## Background

Claude on AWS Bedrock is reachable through two endpoints, each speaking
exactly one wire protocol:

1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`.
Model id in the URL path, request translated into Bedrock's InvokeModel
format, responses returned as a binary AWS eventstream. This is what AI
Gateway already supported for Bedrock.
2. **Mantle** (this doc):
`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native
Anthropic Messages API: model in the body, plain SSE streaming.

## Why a `protocol` field, not a new provider type

The alternative is to model mantle as its own `ai_provider_type`
(`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field
instead for two reasons:

1. Mantle reads more like a protocol of Bedrock than a separate
provider. It is the same AWS account, credentials, region, and IAM,
reached over a different wire protocol and host. One Bedrock provider
with two protocols (`invoke-model` default and `mantle`) models that
more organically than two provider types.
2. It avoids a database migration. The `protocol` field lives in the
settings JSON blob (empty resolves to `invoke-model`, so existing
providers are unaffected), whereas a new type means an enum value and
the `ALTER TYPE ... ADD VALUE` migration that goes with it.

## Why passthrough, not translation

The client already emits Bedrock-legal requests in mantle mode:

```sh
export CLAUDE_CODE_USE_MANTLE=1
export CLAUDE_CODE_SKIP_MANTLE_AUTH=1
export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name>
```

So the gateway just forwards the body and SigV4-signs it (service
`bedrock-mantle`), and skips all the InvokeModel body-translation (model
remap, thinking conversion, beta-flag allowlist, field stripping). This
keeps the mantle path thin and avoids a second copy of translation logic
to maintain.

## Consequences

- Protocol-dependent fields: `model` / `small_fast_model` are used by
InvokeModel but ignored by mantle (the client sends the model), and
`base_url` is required for mantle but optional for InvokeModel.
Validation is protocol-aware.
- No central model control on mantle: because it is a passthrough, the
operator cannot pin the model.
- `region` and the `base_url` host must name the same region (the SigV4
scope must match the endpoint); a mismatch surfaces as `Credential
should be scoped to a valid region`.

## Draft UI

<img width="1100" height="579" alt="image"
src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6"
/>

## Follow-up PRs:
- https://github.com/coder/coder/pull/27156
2026-07-13 19:44:36 -04:00
Susana Ferreira 6580cdcf7f refactor: use AI budget period from deployment config (#27117)
## Description

Read the AI budget period from the deployment config on both the RPC server and the `/users/{user}/ai/spend` endpoint, instead of hardcoding `month`. Drops the `period_start` RPC parameter that was incorrectly introduced in #26915: the period should have been derived from the deployment config from the start.

## Changes

- Add `codersdk.NewAIBudgetPeriodFromString`, mirroring `NewAIBudgetPolicyFromString`.
- `aibridgedserver.Server` takes a `quartz.Clock`, reads `BudgetPeriod` from the deployment config at construction, and computes the period window inside `IsBudgetExceeded`.
- Remove `period_start` from `IsBudgetExceededRequest` and stop sending it from the daemon.
- The `userAISpendStatus` endpoint reads the period from `AIBridgeConfig.BudgetPeriod` instead of hardcoding month.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
2026-07-13 17:52:45 +01:00
Danny Kopping ef0b5585d5 feat: record and expose terminal upstream interception errors (#26961)
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.*
2026-07-09 15:36:56 +02:00
Danny Kopping affb359d13 feat: synchronise provider changes with WatchAIProviders (#27091)
## 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.*
2026-07-08 15:32:17 +02:00
Susana Ferreira 64ca5ac9f8 refactor(coderd): add budget.CurrentPeriod for [start, end) windows (#26972)
## Description

Extracts the AI budget period computation into a shared
`budget.CurrentPeriod` helper. Pure refactor with no wire-value changes.

## Changes

- Add `budget.CurrentPeriod(now, period)` returning a
`PeriodWindow{Start, End}` in UTC. Unknown periods return an error,
matching the pattern used by `ResolveUserAIBudget` for unknown policies.
- Update the callers and respective tests to use `CurrentPeriod`.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-07-06 10:37:27 +01:00
Danny Kopping dd216b96fe feat: record provider_item_id for tool usage (#26856)
## 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._
2026-07-06 09:29:07 +02:00
Susana Ferreira 1989db0e2b feat(coderd): enforce ai budget on pre-request path (#26915)
## Description

Adds pre-request AI budget enforcement to `aibridged`. Requests are rejected with HTTP 403 when the user's aggregated spend for the current period has reached their effective limit.

## Changes

- Add `IsBudgetExceeded` RPC to `aibridgedserver`. Resolves the user's effective budget, aggregates spend over the caller-supplied `[period_start, now]` window, and returns whether the limit has been reached along with the effective limit.
- Wire the check into `aibridged`'s HTTP handler. The caller computes the period start (monthly for now) and passes it in the request.
- Reject exceeded requests with HTTP 403 Forbidden and a message directing the user to contact an administrator.
- Add `dbtime.StartOfMonth` alongside `StartOfDay` for period computation.
- Add real-DB tests covering the enforcement path: month-boundary excludes prior-period spend, and a new user override unblocks a previously-exceeded user.

Closes https://linear.app/codercom/issue/AIGOV-428/add-pre-request-budget-enforcement

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
2026-07-02 16:53:36 +01:00
Susana Ferreira be9c95c8f5 feat(coderd): accumulate user daily AI spend on token usage (#26741)
## Description

Adds post-response spend accumulation to `RecordTokenUsage`.

## Changes

- Wrap the token usage insert and daily spend increment in a single transaction.
- Skip the spend update when the user is unbudgeted, the model is unpriced, or the computed cost is non-positive.

Depends on #26562

Closes https://linear.app/codercom/issue/AIGOV-427/add-post-response-spend-accumulation

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
2026-07-02 16:42:37 +01:00
Yevhenii Shcherbina db7f4438b4 feat: generate STS external ID for Bedrock role assumption (#26869)
Implements:
https://linear.app/codercom/issue/AIGOV-495/add-externalid-to-prevent-confused-deputy-problem

When a Bedrock provider assumes an IAM role via STS, the gateway now
generates a unique external ID for it and sends that value on every
`AssumeRole` call. The external ID guards against the [confused deputy
problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html)
on cross-account role assumption. Per [AWS's
recommendation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html),
the gateway generates and owns the value rather than accepting one from
the operator; that ownership is what makes it effective, since a party
who knows another's external ID can't induce the gateway to send it.

The external ID is server-owned and read-only over the API. It is
generated once, when a provider first has a `role_arn`, and is stable
thereafter. Clients cannot set it: create rejects any supplied
`external_id`, and update rejects a value that differs from the stored
one. An update may echo the stored value back unchanged, so the normal
read-modify-write flow (GET the provider, change a field, PATCH the full
settings object) keeps working. The value is not a secret and is
returned on GET so operators can copy it into the target role's trust
policy as an `sts:ExternalId` condition.

It is persisted in the existing JSON settings blob, so there is no
migration or audit-table change.
2026-07-01 20:44:15 +00:00
Danny Kopping ce94d42e19 feat: fetch providers over DRPC (#26650)
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.
2026-06-29 13:34:58 +02:00
Paweł Banaszewski 6189d6e386 feat: add /api/v2/aibridge/serve endpoint (#26506)
Adds a new enterprise-only `GET /api/v2/ai-gateway/serve` endpoint that standalone AI Gateway replicas use to connect to `coderd` over a DRPC-over-WebSocket transport, mirroring the existing in-memory path used by the embedded AI Bridge daemon.

- The endpoint upgrades the HTTP connection to a WebSocket, multiplexes it with yamux, and finally serves the three DRPC services (Recorder, MCPConfigurator, Authorizer).
- The `X-AI-Governance-Gateway-Key` header is used for authentication.
    - The key is looked up by its hashed secret
    - Missing or revoked keys return `401`.
- API version negotiation is enforced via a new `aibridged/proto` version (`v1.0`).
    - Incompatible versions return `400`.
- `FeatureAIBridge` entitlement is required.
- Key liveness (`last_used_at`) is recorded immediately on connection and refreshed every 60 seconds while the session remains open.
  - When key liveness detects the key was deleted (no rows where updated) session is closed.

#### Small refactors

* The three DRPC service registrations are extracted into `aibridgedserver.Register`, shared by both the in-memory and WebSocket paths.

* The literal `256 * 1024` used as the yamux-aligned WebSocket read limit is replaced with the named constant `drpcsdk.YamuxDefaultStreamWindowSize` in all call sites.
  * as noted in review comment https://github.com/coder/coder/pull/26506#discussion_r3461905223 order of `SetReadLimit` and `WebsocketNetConn` calls was fixed.
2026-06-26 18:27:37 +02:00
Yevhenii Shcherbina b6fcb9a30a feat: record cost on aibridge token usages (#26229)
Implements
https://linear.app/codercom/issue/AIGOV-286/add-interception-cost-calculation-to-aibridge-token-usages

Adds spend attribution to AI Gateway. After the upstream response, each
token-usage record now captures the user's effective group, the
per-token prices in effect at that moment, and a computed cost — so
spend is recorded as an immutable, point-in-time snapshot.

Concretely, `aibridge_token_usages` gains `effective_group_id`,
`input_price_micros`, `output_price_micros`, `cache_read_price_micros`,
`cache_write_price_micros`, and `cost_micros`. When a usage record is
written, the effective group is resolved (per-user override, else the
deployment budget policy), the `(provider, model)` price is looked up
and snapshotted onto the row, and cost is computed from the
provider-reported token counts. A model that isn't in the price table
records its tokens with a `NULL` cost; any *other* resolution failure
fails the write, so a `NULL` cost unambiguously means "model not priced"
rather than "lookup errored."

All values are stored in micro-units (1 unit = 1,000,000 micro-units;
Phase 1 assumes USD, so 1 micro-unit = $0.000001). Prices are quoted per
million tokens.

This also grants the AI Bridge RBAC subject `read` on `ai_model_prices`
(the per-interception price lookup needs it; it previously only had
`update` for the startup seeder).

## Cost precision

Cost is computed per token category as `tokens × price / 1_000_000` with
integer division, then the four categories are summed. The division is
done **per category** (not once over the summed numerator) on purpose:
it keeps the per-category line items summing exactly to the stored total
— no "the parts don't add up to the whole" in reporting).

Integer division truncates sub-micro-unit fractions. For example, a
cheap model at $0.10 per million tokens is a price of `100_000`; 9
tokens cost `9 × 100_000 / 1_000_000 = 900_000 / 1_000_000 = 0` (the
true 0.9 micro-units floors to 0). At real list prices this rarely bites
— $3/M input is a price of `3_000_000`, so even a single token is 3
micro-units. The per-record under-count is bounded below 1 micro-unit
per category, so under $0.000004 total across the four categories, which
is acceptable for list-price-based cost approximation.

## Overflow safety

`cost_micros` is a `BIGINT` (int64), and the largest intermediate value
is a single category's `tokens × price` before division. int64's ceiling
is ≈ `9.223e18`.

- At a steep $75/M model (price `75_000_000`), overflow would require
~123 billion tokens in one response: `123e9 × 75e6 = 9.225e18`, just
over the limit. `122e9` stays under at `9.15e18`.
- A realistically maxed-out Opus 4.8 response (≈1M input + 128K output
at list prices) costs about $15, with a numerator around `1.5e13` —
roughly six orders of magnitude below the ceiling.

So overflow is unreachable from real token counts.

### Multi-currency support

In the future, we may encounter issues with multi-currency support,
especially when dealing with currencies that have very large exchange
rates relative to USD, for example:

IRR: ~1,300,000 IRR ≈ 1 USD
VND: ~26,000 VND ≈ 1 USD

For currencies with such large denominations, numeric overflow is
technically possible, considering that we have only about six orders of
magnitude of headroom before reaching the limit (see above).

## `effective_group_id` has no foreign key

`effective_group_id` records the group a spend was attributed to, as an
immutable historical fact. It is intentionally **not** a foreign key, so
the record survives deletion of the group.

Alternatives were considered and rejected:

- **`ON DELETE SET NULL`** would mutate an "immutable" record — deleting
a group silently erases that interception's attribution and under-counts
the group's historical spend.
- **`RESTRICT` / `NO ACTION`** would block group deletion entirely
(groups are hard-deleted).
- **`CASCADE`** would delete spend history when a group is deleted — the
worst outcome for an audit record.

There is also no insert-time check that the group still exists: the id
comes from a budget that was just resolved, meaning it was valid at some
point.

## Open question: group name snapshotting

Should we also snapshot the group *name* onto each record? Two options:

- **Denormalize it now** — readable in historical reports even after a
group is deleted, but the snapshot can drift from the current name on
rename, raising a "show point-in-time vs. current name" question.
- **Postpone until needed** — it's a purely additive column later, and
the name is display-only (not correctness-bearing like the price). The
cost: names of groups deleted before the column is added can't be
backfilled.

Leaning toward postponing until a concrete reporting need settles the
drift question.
2026-06-16 20:09:00 +00:00
Sas Swart e1c7e61eb9 feat(coderd): add Agent Firewall correlation columns to aibridge_interceptions (#24817)
Add `agent_firewall_session_id` (UUID NULL) and
`agent_firewall_sequence_number` (INT NULL) to `aibridge_interceptions`
with a partial index on `agent_firewall_session_id`. No FK to
`boundary_sessions` (soft reference, resolved at query time).
`RecordInterception` reads the new fields from the proto request (merged
in #25884) via `parseOptionalUUID` / `parseOptionalInt32` helpers.

> This PR was authored by Coder Agents.
2026-06-15 12:34:16 +02:00
Paweł Banaszewski 0d2c9f904a fix: check user user is active in aibridge auth (#26173)
Adds check that user is active when authenticating request in AI
Gateway.
2026-06-09 19:01:23 +02:00
Susana Ferreira 7b903cad73 fix: track credential hint across key failover attempts in aibridge (#25735)
## Problem

Centralized requests recorded *the first available key from the pool at
`CreateInterceptor` time* as `credential_hint`, so the interception
could be persisted in the database with a hint that didn't match the key
that actually served the request. The fix consists in storing, at
end-of-interception, the hint of the key that succeeded, or the last
attempted key if all keys are unavailable.

## Changes

- Add `Key.Hint()` and update `credential_hint` on every failover
attempt so it reflects the actually-used key.
- Stop pre-populating `credential_hint` at `CreateInterceptor`.
Centralized starts empty and is updated by the key failover loop.
- Persist the final hint via `RecordInterceptionEnded`; SQL updates
`credential_hint` only when `credential_kind = 'centralized'` so BYOK
keeps its start-time value.
- Log the actually-used hint on interception end/failure; start log uses
a `<keypool-pending>` placeholder for centralized.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-05-29 12:01:37 +01:00
Danny KoppingandClaude Opus 4.7 eddd4a8c2f feat(coderd): accept delegated API key ID from in-process aibridge callers (#25625)
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>
2026-05-25 11:08:07 +02:00
Danny Kopping ddec110b0e refactor: move aibridged out of enterprise to AGPL (#25570)
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.
2026-05-22 09:11:37 +02:00