diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index ee26cb9d66..13a3c5614a 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -173,6 +173,15 @@ AI BRIDGE OPTIONS: Emit structured logs for AI Bridge interception records. Use this for exporting these records to external SIEM or observability systems. + --ai-budget-period month, $CODER_AI_BUDGET_PERIOD (default: month) + Determines when accumulated AI spend resets to zero, aligned to UTC + calendar boundaries. Only "month" is currently supported. + + --ai-budget-policy highest, $CODER_AI_BUDGET_POLICY (default: highest) + Determines the effective group when a user belongs to multiple groups + with AI budgets. "highest" selects the group with the largest spend + limit, and is currently the only supported value. + AI BRIDGE PROXY OPTIONS: --aibridge-proxy-dump-dir string, $CODER_AIBRIDGE_PROXY_DUMP_DIR Directory for dumping MITM request/response pairs to disk for diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 0779b5a9d1..c7535238b7 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -837,6 +837,15 @@ aibridge: # or re-open the circuit. # (default: 3, type: int) circuit_breaker_max_requests: 3 + # Determines the effective group when a user belongs to multiple groups with AI + # budgets. "highest" selects the group with the largest spend limit, and is + # currently the only supported value. + # (default: highest, type: enum[highest]) + budget_policy: highest + # Determines when accumulated AI spend resets to zero, aligned to UTC calendar + # boundaries. Only "month" is currently supported. + # (default: month, type: enum[month]) + budget_period: month aibridgeproxy: # Enable the AI Bridge MITM Proxy for intercepting and decrypting AI provider # requests. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3286d03eda..679fb97605 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -13802,6 +13802,13 @@ const docTemplate = `{ } ] }, + "budget_period": { + "type": "string" + }, + "budget_policy": { + "description": "Budget settings for AI Governance cost controls.", + "type": "string" + }, "circuit_breaker_enabled": { "description": "Circuit breaker protects against cascading failures from upstream AI\nprovider overload (503, 529).", "type": "boolean" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4c77d86943..be636fd5c8 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12278,6 +12278,13 @@ } ] }, + "budget_period": { + "type": "string" + }, + "budget_policy": { + "description": "Budget settings for AI Governance cost controls.", + "type": "string" + }, "circuit_breaker_enabled": { "description": "Circuit breaker protects against cascading failures from upstream AI\nprovider overload (503, 529).", "type": "boolean" diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 72eb37350a..2641005e4a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -574,6 +574,34 @@ var PostgresAuthDrivers = []string{ // based on max open connections. const PostgresConnMaxIdleAuto = "auto" +// AIBudgetPolicy determines how the effective group is selected when a user +// belongs to multiple groups with AI budgets configured. +type AIBudgetPolicy string + +const ( + // AIBudgetPolicyHighest selects the group with the highest spend limit. + AIBudgetPolicyHighest AIBudgetPolicy = "highest" +) + +// AIBudgetPolicies lists the supported AIBudgetPolicy values. +var AIBudgetPolicies = []string{ + string(AIBudgetPolicyHighest), +} + +// AIBudgetPeriod determines when accumulated AI spend resets to zero, +// aligned to UTC calendar boundaries. +type AIBudgetPeriod string + +const ( + // AIBudgetPeriodMonth resets spend at the start of each UTC calendar month. + AIBudgetPeriodMonth AIBudgetPeriod = "month" +) + +// AIBudgetPeriods lists the supported AIBudgetPeriod values. +var AIBudgetPeriods = []string{ + string(AIBudgetPeriodMonth), +} + // DeploymentValues is the central configuration values the coder server. type DeploymentValues struct { Verbose serpent.Bool `json:"verbose,omitempty"` @@ -3893,6 +3921,27 @@ Write out the current server config as YAML to stdout.`, YAML: "circuit_breaker_max_requests", }, + { + Name: "AI Budget Policy", + Description: "Determines the effective group when a user belongs to multiple groups with AI budgets. \"highest\" selects the group with the largest spend limit, and is currently the only supported value.", + Flag: "ai-budget-policy", + Env: "CODER_AI_BUDGET_POLICY", + Value: serpent.EnumOf(&c.AI.BridgeConfig.BudgetPolicy, AIBudgetPolicies...), + Default: string(AIBudgetPolicyHighest), + Group: &deploymentGroupAIBridge, + YAML: "budget_policy", + }, + { + Name: "AI Budget Period", + Description: "Determines when accumulated AI spend resets to zero, aligned to UTC calendar boundaries. Only \"month\" is currently supported.", + Flag: "ai-budget-period", + Env: "CODER_AI_BUDGET_PERIOD", + Value: serpent.EnumOf(&c.AI.BridgeConfig.BudgetPeriod, AIBudgetPeriods...), + Default: string(AIBudgetPeriodMonth), + Group: &deploymentGroupAIBridge, + YAML: "budget_period", + }, + // AI Bridge Proxy Options { Name: "AI Bridge Proxy Enabled", @@ -4107,6 +4156,9 @@ type AIBridgeConfig struct { StructuredLogging serpent.Bool `json:"structured_logging" typescript:",notnull"` SendActorHeaders serpent.Bool `json:"send_actor_headers" typescript:",notnull"` AllowBYOK serpent.Bool `json:"allow_byok" typescript:",notnull"` + // Budget settings for AI Governance cost controls. + BudgetPolicy string `json:"budget_policy,omitempty" typescript:",notnull"` + BudgetPeriod string `json:"budget_period,omitempty" typescript:",notnull"` // Circuit breaker protects against cascading failures from upstream AI // provider overload (503, 529). CircuitBreakerEnabled serpent.Bool `json:"circuit_breaker_enabled" typescript:",notnull"` diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index 24476d4a52..7bb0f2e422 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -768,6 +768,66 @@ func TestRetentionConfigParsing(t *testing.T) { } } +func TestAIBudgetConfigParsing(t *testing.T) { + t.Parallel() + + t.Run("Defaults", func(t *testing.T) { + t.Parallel() + + dv := codersdk.DeploymentValues{} + opts := dv.Options() + + require.NoError(t, opts.SetDefaults()) + + assert.Equal(t, string(codersdk.AIBudgetPolicyHighest), dv.AI.BridgeConfig.BudgetPolicy) + assert.Equal(t, string(codersdk.AIBudgetPeriodMonth), dv.AI.BridgeConfig.BudgetPeriod) + }) + + t.Run("AcceptsSupportedValues", func(t *testing.T) { + t.Parallel() + + dv := codersdk.DeploymentValues{} + opts := dv.Options() + + require.NoError(t, opts.SetDefaults()) + require.NoError(t, opts.ParseEnv([]serpent.EnvVar{ + {Name: "CODER_AI_BUDGET_POLICY", Value: string(codersdk.AIBudgetPolicyHighest)}, + {Name: "CODER_AI_BUDGET_PERIOD", Value: string(codersdk.AIBudgetPeriodMonth)}, + })) + + assert.Equal(t, string(codersdk.AIBudgetPolicyHighest), dv.AI.BridgeConfig.BudgetPolicy) + assert.Equal(t, string(codersdk.AIBudgetPeriodMonth), dv.AI.BridgeConfig.BudgetPeriod) + }) + + t.Run("RejectsUnsupportedPolicy", func(t *testing.T) { + t.Parallel() + + dv := codersdk.DeploymentValues{} + opts := dv.Options() + + require.NoError(t, opts.SetDefaults()) + err := opts.ParseEnv([]serpent.EnvVar{ + {Name: "CODER_AI_BUDGET_POLICY", Value: "invalid"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid choice") + }) + + t.Run("RejectsUnsupportedPeriod", func(t *testing.T) { + t.Parallel() + + dv := codersdk.DeploymentValues{} + opts := dv.Options() + + require.NoError(t, opts.SetDefaults()) + err := opts.ParseEnv([]serpent.EnvVar{ + {Name: "CODER_AI_BUDGET_PERIOD", Value: "invalid"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid choice") + }) +} + func TestComputeMaxIdleConns(t *testing.T) { t.Parallel() diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index d90eacc160..e7a30d1e69 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -193,6 +193,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "region": "string", "small_fast_model": "string" }, + "budget_period": "string", + "budget_policy": "string", "circuit_breaker_enabled": true, "circuit_breaker_failure_threshold": 0, "circuit_breaker_interval": 0, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 729b6946f0..30328f7d02 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -450,6 +450,8 @@ "region": "string", "small_fast_model": "string" }, + "budget_period": "string", + "budget_policy": "string", "circuit_breaker_enabled": true, "circuit_breaker_failure_threshold": 0, "circuit_breaker_interval": 0, @@ -487,6 +489,8 @@ | `allow_byok` | boolean | false | | | | `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | Deprecated: Use Providers with indexed CODER_AIBRIDGE_PROVIDER__* env vars instead. | | `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | Deprecated: Use Providers with indexed CODER_AIBRIDGE_PROVIDER__* env vars instead. | +| `budget_period` | string | false | | | +| `budget_policy` | string | false | | Budget settings for AI Governance cost controls. | | `circuit_breaker_enabled` | boolean | false | | Circuit breaker protects against cascading failures from upstream AI provider overload (503, 529). | | `circuit_breaker_failure_threshold` | integer | false | | | | `circuit_breaker_interval` | integer | false | | | @@ -1275,6 +1279,8 @@ "region": "string", "small_fast_model": "string" }, + "budget_period": "string", + "budget_policy": "string", "circuit_breaker_enabled": true, "circuit_breaker_failure_threshold": 0, "circuit_breaker_interval": 0, @@ -5288,6 +5294,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "region": "string", "small_fast_model": "string" }, + "budget_period": "string", + "budget_policy": "string", "circuit_breaker_enabled": true, "circuit_breaker_failure_threshold": 0, "circuit_breaker_interval": 0, @@ -5884,6 +5892,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "region": "string", "small_fast_model": "string" }, + "budget_period": "string", + "budget_policy": "string", "circuit_breaker_enabled": true, "circuit_breaker_failure_threshold": 0, "circuit_breaker_interval": 0, diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index ce8ddb1bd8..20145a509f 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1901,6 +1901,28 @@ Allow users to provide their own LLM API keys or subscriptions. When disabled, o Enable the circuit breaker to protect against cascading failures from upstream AI provider overload (503, 529). +### --ai-budget-policy + +| | | +|-------------|--------------------------------------| +| Type | highest | +| Environment | $CODER_AI_BUDGET_POLICY | +| YAML | aibridge.budget_policy | +| Default | highest | + +Determines the effective group when a user belongs to multiple groups with AI budgets. "highest" selects the group with the largest spend limit, and is currently the only supported value. + +### --ai-budget-period + +| | | +|-------------|--------------------------------------| +| Type | month | +| Environment | $CODER_AI_BUDGET_PERIOD | +| YAML | aibridge.budget_period | +| Default | month | + +Determines when accumulated AI spend resets to zero, aligned to UTC calendar boundaries. Only "month" is currently supported. + ### --aibridge-proxy-enabled | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 8eb543f63a..e5362ecf4f 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -174,6 +174,15 @@ AI BRIDGE OPTIONS: Emit structured logs for AI Bridge interception records. Use this for exporting these records to external SIEM or observability systems. + --ai-budget-period month, $CODER_AI_BUDGET_PERIOD (default: month) + Determines when accumulated AI spend resets to zero, aligned to UTC + calendar boundaries. Only "month" is currently supported. + + --ai-budget-policy highest, $CODER_AI_BUDGET_POLICY (default: highest) + Determines the effective group when a user belongs to multiple groups + with AI budgets. "highest" selects the group with the largest spend + limit, and is currently the only supported value. + AI BRIDGE PROXY OPTIONS: --aibridge-proxy-dump-dir string, $CODER_AIBRIDGE_PROXY_DUMP_DIR Directory for dumping MITM request/response pairs to disk for diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2f913e20a8..431ef9cbdf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -68,6 +68,11 @@ export interface AIBridgeConfig { readonly structured_logging: boolean; readonly send_actor_headers: boolean; readonly allow_byok: boolean; + /** + * Budget settings for AI Governance cost controls. + */ + readonly budget_policy?: string; + readonly budget_period?: string; /** * Circuit breaker protects against cascading failures from upstream AI * provider overload (503, 529). @@ -305,6 +310,16 @@ export interface AIBridgeUserPrompt { readonly created_at: string; } +// From codersdk/deployment.go +export type AIBudgetPeriod = "month"; + +export const AIBudgetPeriods: AIBudgetPeriod[] = ["month"]; + +export const AIBudgetPolicies: AIBudgetPolicy[] = ["highest"]; + +// From codersdk/deployment.go +export type AIBudgetPolicy = "highest"; + // From codersdk/deployment.go export interface AIConfig { readonly bridge?: AIBridgeConfig;