Files
coder/coderd/rbac/object_gen.go
T
Yevhenii Shcherbina 4124d1137d feat: add ai_model_prices table (#24932)
# Summary

Implements
https://linear.app/codercom/issue/AIGOV-282/add-ai-model-price-table-and-seed-generator

This PR lays the groundwork for AI Bridge cost controls (per the AI
Governance RFC). It adds the foundation needed for future cost tracking:
a place to store per-model token prices, a way to keep those prices in
sync with upstream pricing data, and a startup mechanism that ensures
every deployment has prices loaded before AI Bridge starts processing
requests.

The price data comes from [models.dev](https://models.dev/), a
community-maintained catalogue of AI provider pricing. A generator
script fetches the latest prices, filters to Anthropic and OpenAI for
now, and produces a seed file checked into the repository.

On every server startup the seed is applied to the database, so new
releases automatically pick up any price corrections that landed since
the previous one. Existing rows are overwritten with the latest prices;
rows for models no longer in the seed are left untouched.

# Batching the AI model price seed: three approaches

Context: at server startup we seed the `ai_model_prices` table from an
embedded JSON price book (~70 rows today, will grow as we add providers,
potentially 4000+).

Each row is:

```text
(provider, model, input_price, output_price, cache_read_price, cache_write_price)
```

Any of the four price columns can be:

- `NULL` → “price unknown for this dimension”
- explicit `0` → “free”

The batch must be an UPSERT so re-running is idempotent and existing
rows pick up new prices.

We considered three implementations.

---

## Approach 1 — Per-row UPSERT in a Go loop

```go
for _, row := range rows {
    if err := db.UpsertAIModelPrice(ctx, database.UpsertAIModelPriceParams{
        Provider:   row.Provider,
        Model:      row.Model,
        InputPrice: nullInt64(row.InputPrice),
        // ...
    }); err != nil {
        return err
    }
}
```

### Pros

- Trivial.
- NULL handling falls out naturally from `sql.NullInt64`.

### Cons

- `N` round-trips per seed.
- With ~70 rows that means ~70 statement executions on every startup,
even inside a transaction.
- Doesn't scale gracefully as the price book grows, potentially 4000+.

---

## Approach 2 — `UNNEST` with parallel arrays

Pass each column as a separate Go slice. Postgres unnests them in
parallel into a virtual table, then `INSERT ... SELECT`.

```sql
INSERT INTO ai_model_prices (
    provider,
    model,
    input_price,
    output_price,
    cache_read_price,
    cache_write_price
)
SELECT
    UNNEST(@providers::text[]),
    UNNEST(@models::text[]),
    NULLIF(UNNEST(@input_prices::bigint[]), -1),
    NULLIF(UNNEST(@output_prices::bigint[]), -1),
    NULLIF(UNNEST(@cache_read_prices::bigint[]), -1),
    NULLIF(UNNEST(@cache_write_prices::bigint[]), -1)
ON CONFLICT (provider, model) DO UPDATE SET
    input_price       = EXCLUDED.input_price,
    output_price      = EXCLUDED.output_price,
    cache_read_price  = EXCLUDED.cache_read_price,
    cache_write_price = EXCLUDED.cache_write_price,
    updated_at        = NOW();
```

Go side: flatten rows into six parallel slices.

Use a sentinel (`-1`) for “missing”, since `lib/pq` can't encode `NULL`
into a `bigint[]` element.

```go
providers := make([]string, len(rows))
models    := make([]string, len(rows))
inputs    := make([]int64,  len(rows))
outputs   := make([]int64,  len(rows))
cacheR    := make([]int64,  len(rows))
cacheW    := make([]int64,  len(rows))

for i, r := range rows {
    providers[i] = r.Provider
    models[i]    = r.Model

    inputs[i] = -1
    if r.InputPrice != nil {
        inputs[i] = *r.InputPrice
    }

    outputs[i] = -1
    if r.OutputPrice != nil {
        outputs[i] = *r.OutputPrice
    }

    cacheR[i] = -1
    if r.CacheReadPrice != nil {
        cacheR[i] = *r.CacheReadPrice
    }

    cacheW[i] = -1
    if r.CacheWritePrice != nil {
        cacheW[i] = *r.CacheWritePrice
    }
}

return db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
    Providers:        providers,
    Models:           models,
    InputPrices:      inputs,
    OutputPrices:     outputs,
    CacheReadPrices:  cacheR,
    CacheWritePrices: cacheW,
})
```

### Pros

- Single round-trip.

### Cons

- The generated `sqlc` params become plain `[]int64`, which can't
represent `NULL`.

---

## Approach 3 — `jsonb_array_elements` over a single `@seed::jsonb`
(chosen)

Pass the raw seed JSON as one parameter; let Postgres expand and parse
it.

```sql
INSERT INTO ai_model_prices (
    provider,
    model,
    input_price,
    output_price,
    cache_read_price,
    cache_write_price
)
SELECT
    elem->>'provider',
    elem->>'model',
    (elem->>'input_price')::bigint,
    (elem->>'output_price')::bigint,
    (elem->>'cache_read_price')::bigint,
    (elem->>'cache_write_price')::bigint
FROM jsonb_array_elements(@seed::jsonb) AS elem
ON CONFLICT (provider, model) DO UPDATE SET
    input_price       = EXCLUDED.input_price,
    output_price      = EXCLUDED.output_price,
    cache_read_price  = EXCLUDED.cache_read_price,
    cache_write_price = EXCLUDED.cache_write_price,
    updated_at        = NOW();
```

Go side reduces to:

```go
return db.UpsertAIModelPrices(ctx, seedJSON)
```

### Pros

- Single round-trip.
- NULLs fall out naturally:
  - `(elem->>'cache_write_price')::bigint` becomes `NULL`
  - no sentinels
- The seed is already JSON:
- Existing precedent:
  - `jsonb_array_elements` is already used elsewhere in the codebase

### Cons

- Less type-safe at the SQL boundary than `UNNEST`
- Slightly less standard than `UNNEST`
- Readers need familiarity with:
  - `jsonb_array_elements`
  - `->>` extraction syntax
- Postgres pays JSON parse cost
  - negligible at our scale

---

---

# Decision

We picked Approach 3.

It collapses the round-trips like `UNNEST` does, but without:

- nullable-array workarounds
- sentinel values
2026-05-08 16:45:14 -04:00

522 lines
15 KiB
Go

// Code generated by typegen/main.go. DO NOT EDIT.
package rbac
import "github.com/coder/coder/v2/coderd/rbac/policy"
// Objecter returns the RBAC object for itself.
type Objecter interface {
RBACObject() Object
}
var (
// ResourceWildcard
// Valid Actions
ResourceWildcard = Object{
Type: "*",
}
// ResourceAiModelPrice
// Valid Actions
// - "ActionRead" :: read AI model prices
// - "ActionUpdate" :: update AI model prices
ResourceAiModelPrice = Object{
Type: "ai_model_price",
}
// ResourceAiSeat
// Valid Actions
// - "ActionCreate" :: record AI seat usage
// - "ActionRead" :: read AI seat state
ResourceAiSeat = Object{
Type: "ai_seat",
}
// ResourceAibridgeInterception
// Valid Actions
// - "ActionCreate" :: create aibridge interceptions & related records
// - "ActionRead" :: read aibridge interceptions & related records
// - "ActionUpdate" :: update aibridge interceptions & related records
ResourceAibridgeInterception = Object{
Type: "aibridge_interception",
}
// ResourceApiKey
// Valid Actions
// - "ActionCreate" :: create an api key
// - "ActionDelete" :: delete an api key
// - "ActionRead" :: read api key details (secrets are not stored)
// - "ActionUpdate" :: update an api key, eg expires
ResourceApiKey = Object{
Type: "api_key",
}
// ResourceAssignOrgRole
// Valid Actions
// - "ActionAssign" :: assign org scoped roles
// - "ActionCreate" :: create/delete custom roles within an organization
// - "ActionDelete" :: delete roles within an organization
// - "ActionRead" :: view what roles are assignable within an organization
// - "ActionUnassign" :: unassign org scoped roles
// - "ActionUpdate" :: edit custom roles within an organization
ResourceAssignOrgRole = Object{
Type: "assign_org_role",
}
// ResourceAssignRole
// Valid Actions
// - "ActionAssign" :: assign user roles
// - "ActionRead" :: view what roles are assignable
// - "ActionUnassign" :: unassign user roles
ResourceAssignRole = Object{
Type: "assign_role",
}
// ResourceAuditLog
// Valid Actions
// - "ActionCreate" :: create new audit log entries
// - "ActionRead" :: read audit logs
ResourceAuditLog = Object{
Type: "audit_log",
}
// ResourceBoundaryUsage
// Valid Actions
// - "ActionDelete" :: delete boundary usage statistics
// - "ActionRead" :: read boundary usage statistics
// - "ActionUpdate" :: upsert boundary usage statistics
ResourceBoundaryUsage = Object{
Type: "boundary_usage",
}
// ResourceChat
// Valid Actions
// - "ActionCreate" :: create a new chat
// - "ActionDelete" :: delete a chat
// - "ActionRead" :: read chat messages and metadata
// - "ActionUpdate" :: update chat title or settings
ResourceChat = Object{
Type: "chat",
}
// ResourceConnectionLog
// Valid Actions
// - "ActionRead" :: read connection logs
// - "ActionUpdate" :: upsert connection log entries
ResourceConnectionLog = Object{
Type: "connection_log",
}
// ResourceCryptoKey
// Valid Actions
// - "ActionCreate" :: create crypto keys
// - "ActionDelete" :: delete crypto keys
// - "ActionRead" :: read crypto keys
// - "ActionUpdate" :: update crypto keys
ResourceCryptoKey = Object{
Type: "crypto_key",
}
// ResourceDebugInfo
// Valid Actions
// - "ActionRead" :: access to debug routes
ResourceDebugInfo = Object{
Type: "debug_info",
}
// ResourceDeploymentConfig
// Valid Actions
// - "ActionRead" :: read deployment config
// - "ActionUpdate" :: updating health information
ResourceDeploymentConfig = Object{
Type: "deployment_config",
}
// ResourceDeploymentStats
// Valid Actions
// - "ActionRead" :: read deployment stats
ResourceDeploymentStats = Object{
Type: "deployment_stats",
}
// ResourceFile
// Valid Actions
// - "ActionCreate" :: create a file
// - "ActionRead" :: read files
ResourceFile = Object{
Type: "file",
}
// ResourceGroup
// Valid Actions
// - "ActionCreate" :: create a group
// - "ActionDelete" :: delete a group
// - "ActionRead" :: read groups
// - "ActionUpdate" :: update a group
ResourceGroup = Object{
Type: "group",
}
// ResourceGroupMember
// Valid Actions
// - "ActionRead" :: read group members
ResourceGroupMember = Object{
Type: "group_member",
}
// ResourceIdpsyncSettings
// Valid Actions
// - "ActionRead" :: read IdP sync settings
// - "ActionUpdate" :: update IdP sync settings
ResourceIdpsyncSettings = Object{
Type: "idpsync_settings",
}
// ResourceInboxNotification
// Valid Actions
// - "ActionCreate" :: create inbox notifications
// - "ActionRead" :: read inbox notifications
// - "ActionUpdate" :: update inbox notifications
ResourceInboxNotification = Object{
Type: "inbox_notification",
}
// ResourceLicense
// Valid Actions
// - "ActionCreate" :: create a license
// - "ActionDelete" :: delete license
// - "ActionRead" :: read licenses
ResourceLicense = Object{
Type: "license",
}
// ResourceNotificationMessage
// Valid Actions
// - "ActionCreate" :: create notification messages
// - "ActionDelete" :: delete notification messages
// - "ActionRead" :: read notification messages
// - "ActionUpdate" :: update notification messages
ResourceNotificationMessage = Object{
Type: "notification_message",
}
// ResourceNotificationPreference
// Valid Actions
// - "ActionRead" :: read notification preferences
// - "ActionUpdate" :: update notification preferences
ResourceNotificationPreference = Object{
Type: "notification_preference",
}
// ResourceNotificationTemplate
// Valid Actions
// - "ActionRead" :: read notification templates
// - "ActionUpdate" :: update notification templates
ResourceNotificationTemplate = Object{
Type: "notification_template",
}
// ResourceOauth2App
// Valid Actions
// - "ActionCreate" :: make an OAuth2 app
// - "ActionDelete" :: delete an OAuth2 app
// - "ActionRead" :: read OAuth2 apps
// - "ActionUpdate" :: update the properties of the OAuth2 app
ResourceOauth2App = Object{
Type: "oauth2_app",
}
// ResourceOauth2AppCodeToken
// Valid Actions
// - "ActionCreate" :: create an OAuth2 app code token
// - "ActionDelete" :: delete an OAuth2 app code token
// - "ActionRead" :: read an OAuth2 app code token
ResourceOauth2AppCodeToken = Object{
Type: "oauth2_app_code_token",
}
// ResourceOauth2AppSecret
// Valid Actions
// - "ActionCreate" :: create an OAuth2 app secret
// - "ActionDelete" :: delete an OAuth2 app secret
// - "ActionRead" :: read an OAuth2 app secret
// - "ActionUpdate" :: update an OAuth2 app secret
ResourceOauth2AppSecret = Object{
Type: "oauth2_app_secret",
}
// ResourceOrganization
// Valid Actions
// - "ActionCreate" :: create an organization
// - "ActionDelete" :: delete an organization
// - "ActionRead" :: read organizations
// - "ActionUpdate" :: update an organization
ResourceOrganization = Object{
Type: "organization",
}
// ResourceOrganizationMember
// Valid Actions
// - "ActionCreate" :: create an organization member
// - "ActionDelete" :: delete member
// - "ActionRead" :: read member
// - "ActionUpdate" :: update an organization member
ResourceOrganizationMember = Object{
Type: "organization_member",
}
// ResourcePrebuiltWorkspace
// Valid Actions
// - "ActionDelete" :: delete prebuilt workspace
// - "ActionUpdate" :: update prebuilt workspace settings
ResourcePrebuiltWorkspace = Object{
Type: "prebuilt_workspace",
}
// ResourceProvisionerDaemon
// Valid Actions
// - "ActionCreate" :: create a provisioner daemon/key
// - "ActionDelete" :: delete a provisioner daemon/key
// - "ActionRead" :: read provisioner daemon
// - "ActionUpdate" :: update a provisioner daemon
ResourceProvisionerDaemon = Object{
Type: "provisioner_daemon",
}
// ResourceProvisionerJobs
// Valid Actions
// - "ActionCreate" :: create provisioner jobs
// - "ActionRead" :: read provisioner jobs
// - "ActionUpdate" :: update provisioner jobs
ResourceProvisionerJobs = Object{
Type: "provisioner_jobs",
}
// ResourceReplicas
// Valid Actions
// - "ActionRead" :: read replicas
ResourceReplicas = Object{
Type: "replicas",
}
// ResourceSystem
// Valid Actions
// - "ActionCreate" :: create system resources
// - "ActionDelete" :: delete system resources
// - "ActionRead" :: view system resources
// - "ActionUpdate" :: update system resources
// DEPRECATED: New resources should be created for new things, rather than adding them to System, which has become
// an unmanaged collection of things that don't relate to one another. We can't effectively enforce
// least privilege access control when unrelated resources are grouped together.
ResourceSystem = Object{
Type: "system",
}
// ResourceTailnetCoordinator
// Valid Actions
// - "ActionCreate" :: create a Tailnet coordinator
// - "ActionDelete" :: delete a Tailnet coordinator
// - "ActionRead" :: view info about a Tailnet coordinator
// - "ActionUpdate" :: update a Tailnet coordinator
ResourceTailnetCoordinator = Object{
Type: "tailnet_coordinator",
}
// ResourceTask
// Valid Actions
// - "ActionCreate" :: create a new task
// - "ActionDelete" :: delete task
// - "ActionRead" :: read task data or output to view on the UI or CLI
// - "ActionUpdate" :: edit task settings or send input to an existing task
ResourceTask = Object{
Type: "task",
}
// ResourceTemplate
// Valid Actions
// - "ActionCreate" :: create a template
// - "ActionDelete" :: delete a template
// - "ActionRead" :: read template
// - "ActionUpdate" :: update a template
// - "ActionUse" :: use the template to initially create a workspace, then workspace lifecycle permissions take over
// - "ActionViewInsights" :: view insights
ResourceTemplate = Object{
Type: "template",
}
// ResourceUsageEvent
// Valid Actions
// - "ActionCreate" :: create a usage event
// - "ActionRead" :: read usage events
// - "ActionUpdate" :: update usage events
ResourceUsageEvent = Object{
Type: "usage_event",
}
// ResourceUser
// Valid Actions
// - "ActionCreate" :: create a new user
// - "ActionDelete" :: delete an existing user
// - "ActionRead" :: read user data
// - "ActionReadPersonal" :: read personal user data like user settings and auth links
// - "ActionUpdate" :: update an existing user
// - "ActionUpdatePersonal" :: update personal data
ResourceUser = Object{
Type: "user",
}
// ResourceUserSecret
// Valid Actions
// - "ActionCreate" :: create a user secret
// - "ActionDelete" :: delete a user secret
// - "ActionRead" :: read user secret metadata and value
// - "ActionUpdate" :: update user secret metadata and value
ResourceUserSecret = Object{
Type: "user_secret",
}
// ResourceWebpushSubscription
// Valid Actions
// - "ActionCreate" :: create webpush subscriptions
// - "ActionDelete" :: delete webpush subscriptions
// - "ActionRead" :: read webpush subscriptions
ResourceWebpushSubscription = Object{
Type: "webpush_subscription",
}
// ResourceWorkspace
// Valid Actions
// - "ActionApplicationConnect" :: connect to workspace apps via browser
// - "ActionCreate" :: create a new workspace
// - "ActionCreateAgent" :: create a new workspace agent
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
// - "ActionUpdateAgent" :: update an existing workspace agent
ResourceWorkspace = Object{
Type: "workspace",
}
// ResourceWorkspaceAgentDevcontainers
// Valid Actions
// - "ActionCreate" :: create workspace agent devcontainers
ResourceWorkspaceAgentDevcontainers = Object{
Type: "workspace_agent_devcontainers",
}
// ResourceWorkspaceAgentResourceMonitor
// Valid Actions
// - "ActionCreate" :: create workspace agent resource monitor
// - "ActionRead" :: read workspace agent resource monitor
// - "ActionUpdate" :: update workspace agent resource monitor
ResourceWorkspaceAgentResourceMonitor = Object{
Type: "workspace_agent_resource_monitor",
}
// ResourceWorkspaceDormant
// Valid Actions
// - "ActionApplicationConnect" :: connect to workspace apps via browser
// - "ActionCreate" :: create a new workspace
// - "ActionCreateAgent" :: create a new workspace agent
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
// - "ActionUpdateAgent" :: update an existing workspace agent
ResourceWorkspaceDormant = Object{
Type: "workspace_dormant",
}
// ResourceWorkspaceProxy
// Valid Actions
// - "ActionCreate" :: create a workspace proxy
// - "ActionDelete" :: delete a workspace proxy
// - "ActionRead" :: read and use a workspace proxy
// - "ActionUpdate" :: update a workspace proxy
ResourceWorkspaceProxy = Object{
Type: "workspace_proxy",
}
)
func AllResources() []Objecter {
return []Objecter{
ResourceWildcard,
ResourceAiModelPrice,
ResourceAiSeat,
ResourceAibridgeInterception,
ResourceApiKey,
ResourceAssignOrgRole,
ResourceAssignRole,
ResourceAuditLog,
ResourceBoundaryUsage,
ResourceChat,
ResourceConnectionLog,
ResourceCryptoKey,
ResourceDebugInfo,
ResourceDeploymentConfig,
ResourceDeploymentStats,
ResourceFile,
ResourceGroup,
ResourceGroupMember,
ResourceIdpsyncSettings,
ResourceInboxNotification,
ResourceLicense,
ResourceNotificationMessage,
ResourceNotificationPreference,
ResourceNotificationTemplate,
ResourceOauth2App,
ResourceOauth2AppCodeToken,
ResourceOauth2AppSecret,
ResourceOrganization,
ResourceOrganizationMember,
ResourcePrebuiltWorkspace,
ResourceProvisionerDaemon,
ResourceProvisionerJobs,
ResourceReplicas,
ResourceSystem,
ResourceTailnetCoordinator,
ResourceTask,
ResourceTemplate,
ResourceUsageEvent,
ResourceUser,
ResourceUserSecret,
ResourceWebpushSubscription,
ResourceWorkspace,
ResourceWorkspaceAgentDevcontainers,
ResourceWorkspaceAgentResourceMonitor,
ResourceWorkspaceDormant,
ResourceWorkspaceProxy,
}
}
func AllActions() []policy.Action {
return []policy.Action{
policy.ActionApplicationConnect,
policy.ActionAssign,
policy.ActionCreate,
policy.ActionCreateAgent,
policy.ActionDelete,
policy.ActionDeleteAgent,
policy.ActionRead,
policy.ActionReadPersonal,
policy.ActionSSH,
policy.ActionShare,
policy.ActionUnassign,
policy.ActionUpdate,
policy.ActionUpdateAgent,
policy.ActionUpdatePersonal,
policy.ActionUse,
policy.ActionViewInsights,
policy.ActionWorkspaceStart,
policy.ActionWorkspaceStop,
}
}