refactor: make user AI budget and spend endpoints consistent (#27611)

## 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
This commit is contained in:
Susana Ferreira
2026-07-29 14:01:32 +01:00
committed by GitHub
parent e71249a821
commit d6a5c8e9f8
14 changed files with 194 additions and 181 deletions
+21 -25
View File
@@ -9950,7 +9950,7 @@ const docTemplate = `{
]
}
},
"/api/v2/users/{user}/ai/budget": {
"/api/v2/users/{user}/ai/budget/override": {
"get": {
"produces": [
"application/json"
@@ -15784,6 +15784,17 @@ const docTemplate = `{
}
}
},
"codersdk.AIBudgetLimit": {
"type": "object",
"properties": {
"limit_source": {
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
},
"spend_limit_micros": {
"type": "integer"
}
}
},
"codersdk.AIBudgetLimitSource": {
"type": "string",
"enum": [
@@ -15832,17 +15843,6 @@ const docTemplate = `{
}
}
},
"codersdk.AIGroupBudget": {
"type": "object",
"properties": {
"limit_source": {
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
},
"spend_limit_micros": {
"type": "integer"
}
}
},
"codersdk.AIProvider": {
"type": "object",
"properties": {
@@ -20838,7 +20838,7 @@ const docTemplate = `{
"description": "GroupBudget is the budget when the queried group is this user's\neffective budget source. Null when the user's budget resolves to another\ngroup or no budget applies to the user.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIGroupBudget"
"$ref": "#/definitions/codersdk.AIBudgetLimit"
}
]
},
@@ -26529,19 +26529,19 @@ const docTemplate = `{
"description": "CurrentSpendMicros is the user's spend on their effective group over\nthe current budget period.",
"type": "integer"
},
"effective_budget": {
"description": "EffectiveBudget is the spend limit that applies to the user, whether it\ncame from a group budget or a user override. Null when no budget\napplies, leaving the user's spend unlimited.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIBudgetLimit"
}
]
},
"effective_group_id": {
"description": "EffectiveGroupID is the group the spend is attributed to, falling back to\nthe Everyone group when no budget applies. Null only when the user has no\norganization membership.",
"type": "string",
"format": "uuid"
},
"limit_source": {
"description": "LimitSource identifies which tier produced the limit. Null when no\nbudget applies.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
}
]
},
"period_end": {
"description": "PeriodEnd is the exclusive upper bound of the current budget\nperiod.",
"type": "string",
@@ -26552,10 +26552,6 @@ const docTemplate = `{
"type": "string",
"format": "date-time"
},
"spend_limit_micros": {
"description": "SpendLimitMicros is the effective spend limit in micro-units.\nNull when no budget applies to the user (unlimited).",
"type": "integer"
},
"user_id": {
"type": "string",
"format": "uuid"
+21 -25
View File
@@ -8823,7 +8823,7 @@
]
}
},
"/api/v2/users/{user}/ai/budget": {
"/api/v2/users/{user}/ai/budget/override": {
"get": {
"produces": ["application/json"],
"tags": ["Enterprise"],
@@ -14078,6 +14078,17 @@
}
}
},
"codersdk.AIBudgetLimit": {
"type": "object",
"properties": {
"limit_source": {
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
},
"spend_limit_micros": {
"type": "integer"
}
}
},
"codersdk.AIBudgetLimitSource": {
"type": "string",
"enum": ["user_override", "group"],
@@ -14123,17 +14134,6 @@
}
}
},
"codersdk.AIGroupBudget": {
"type": "object",
"properties": {
"limit_source": {
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
},
"spend_limit_micros": {
"type": "integer"
}
}
},
"codersdk.AIProvider": {
"type": "object",
"properties": {
@@ -18972,7 +18972,7 @@
"description": "GroupBudget is the budget when the queried group is this user's\neffective budget source. Null when the user's budget resolves to another\ngroup or no budget applies to the user.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIGroupBudget"
"$ref": "#/definitions/codersdk.AIBudgetLimit"
}
]
},
@@ -24386,19 +24386,19 @@
"description": "CurrentSpendMicros is the user's spend on their effective group over\nthe current budget period.",
"type": "integer"
},
"effective_budget": {
"description": "EffectiveBudget is the spend limit that applies to the user, whether it\ncame from a group budget or a user override. Null when no budget\napplies, leaving the user's spend unlimited.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIBudgetLimit"
}
]
},
"effective_group_id": {
"description": "EffectiveGroupID is the group the spend is attributed to, falling back to\nthe Everyone group when no budget applies. Null only when the user has no\norganization membership.",
"type": "string",
"format": "uuid"
},
"limit_source": {
"description": "LimitSource identifies which tier produced the limit. Null when no\nbudget applies.",
"allOf": [
{
"$ref": "#/definitions/codersdk.AIBudgetLimitSource"
}
]
},
"period_end": {
"description": "PeriodEnd is the exclusive upper bound of the current budget\nperiod.",
"type": "string",
@@ -24409,10 +24409,6 @@
"type": "string",
"format": "date-time"
},
"spend_limit_micros": {
"description": "SpendLimitMicros is the effective spend limit in micro-units.\nNull when no budget applies to the user (unlimited).",
"type": "integer"
},
"user_id": {
"type": "string",
"format": "uuid"
+1 -1
View File
@@ -1490,7 +1490,7 @@ func GroupMemberAISpend(row database.GetGroupMembersAISpendRow) codersdk.GroupMe
member.EffectiveGroupID = &row.EffectiveGroupID.UUID
}
if row.SpendLimitMicros.Valid {
member.GroupBudget = &codersdk.AIGroupBudget{
member.GroupBudget = &codersdk.AIBudgetLimit{
SpendLimitMicros: row.SpendLimitMicros.Int64,
LimitSource: codersdk.AIBudgetLimitSource(row.LimitSource.String),
}