mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: add AWS Bedrock support (#20507)
Depends on https://github.com/coder/aibridge/pull/44 Closes https://github.com/coder/aibridge/issues/28 --------- Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
+20
-2
@@ -726,7 +726,25 @@ aibridge:
|
||||
openai_key: ""
|
||||
# The base URL of the Anthropic API.
|
||||
# (default: https://api.anthropic.com/, type: string)
|
||||
base_url: https://api.anthropic.com/
|
||||
anthropic_base_url: https://api.anthropic.com/
|
||||
# The key to authenticate against the Anthropic API.
|
||||
# (default: <unset>, type: string)
|
||||
key: ""
|
||||
anthropic_key: ""
|
||||
# The AWS Bedrock API region.
|
||||
# (default: <unset>, type: string)
|
||||
bedrock_region: ""
|
||||
# The access key to authenticate against the AWS Bedrock API.
|
||||
# (default: <unset>, type: string)
|
||||
bedrock_access_key: ""
|
||||
# The access key secret to use with the access key to authenticate against the AWS
|
||||
# Bedrock API.
|
||||
# (default: <unset>, type: string)
|
||||
bedrock_access_key_secret: ""
|
||||
# The model to use when making requests to the AWS Bedrock API.
|
||||
# (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0, type: string)
|
||||
bedrock_model: global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# The small fast model to use when making requests to the AWS Bedrock API. Claude
|
||||
# Code uses Haiku-class models to perform background tasks. See
|
||||
# https://docs.claude.com/en/docs/claude-code/settings#environment-variables.
|
||||
# (default: global.anthropic.claude-haiku-4-5-20251001-v1:0, type: string)
|
||||
bedrock_small_fast_model: global.anthropic.claude-haiku-4-5-20251001-v1:0
|
||||
|
||||
Generated
+23
@@ -11668,12 +11668,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeBedrockConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"access_key_secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
"small_fast_model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"anthropic": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeAnthropicConfig"
|
||||
},
|
||||
"bedrock": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeBedrockConfig"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
Generated
+23
@@ -10364,12 +10364,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeBedrockConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"access_key_secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
"small_fast_model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"anthropic": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeAnthropicConfig"
|
||||
},
|
||||
"bedrock": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeBedrockConfig"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+67
-3
@@ -3280,18 +3280,73 @@ Write out the current server config as YAML to stdout.`,
|
||||
Value: &c.AI.BridgeConfig.Anthropic.BaseURL,
|
||||
Default: "https://api.anthropic.com/",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "base_url",
|
||||
YAML: "anthropic_base_url",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Anthropic KEY",
|
||||
Name: "AIBridge Anthropic Key",
|
||||
Description: "The key to authenticate against the Anthropic API.",
|
||||
Flag: "aibridge-anthropic-key",
|
||||
Env: "CODER_AIBRIDGE_ANTHROPIC_KEY",
|
||||
Value: &c.AI.BridgeConfig.Anthropic.Key,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "key",
|
||||
YAML: "anthropic_key",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Bedrock Region",
|
||||
Description: "The AWS Bedrock API region.",
|
||||
Flag: "aibridge-bedrock-region",
|
||||
Env: "CODER_AIBRIDGE_BEDROCK_REGION",
|
||||
Value: &c.AI.BridgeConfig.Bedrock.Region,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "bedrock_region",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Bedrock Access Key",
|
||||
Description: "The access key to authenticate against the AWS Bedrock API.",
|
||||
Flag: "aibridge-bedrock-access-key",
|
||||
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY",
|
||||
Value: &c.AI.BridgeConfig.Bedrock.AccessKey,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "bedrock_access_key",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Bedrock Access Key Secret",
|
||||
Description: "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
|
||||
Flag: "aibridge-bedrock-access-key-secret",
|
||||
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET",
|
||||
Value: &c.AI.BridgeConfig.Bedrock.AccessKeySecret,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "bedrock_access_key_secret",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Bedrock Model",
|
||||
Description: "The model to use when making requests to the AWS Bedrock API.",
|
||||
Flag: "aibridge-bedrock-model",
|
||||
Env: "CODER_AIBRIDGE_BEDROCK_MODEL",
|
||||
Value: &c.AI.BridgeConfig.Bedrock.Model,
|
||||
Default: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock.
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "bedrock_model",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "AIBridge Bedrock Small Fast Model",
|
||||
Description: "The small fast model to use when making requests to the AWS Bedrock API. Claude Code uses Haiku-class models to perform background tasks. See https://docs.claude.com/en/docs/claude-code/settings#environment-variables.",
|
||||
Flag: "aibridge-bedrock-small-fastmodel",
|
||||
Env: "CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL",
|
||||
Value: &c.AI.BridgeConfig.Bedrock.SmallFastModel,
|
||||
Default: "global.anthropic.claude-haiku-4-5-20251001-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock.
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "bedrock_small_fast_model",
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
@@ -3316,6 +3371,7 @@ type AIBridgeConfig struct {
|
||||
Enabled serpent.Bool `json:"enabled" typescript:",notnull"`
|
||||
OpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"`
|
||||
Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"`
|
||||
Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type AIBridgeOpenAIConfig struct {
|
||||
@@ -3328,6 +3384,14 @@ type AIBridgeAnthropicConfig struct {
|
||||
Key serpent.String `json:"key" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type AIBridgeBedrockConfig struct {
|
||||
Region serpent.String `json:"region" typescript:",notnull"`
|
||||
AccessKey serpent.String `json:"access_key" typescript:",notnull"`
|
||||
AccessKeySecret serpent.String `json:"access_key_secret" typescript:",notnull"`
|
||||
Model serpent.String `json:"model" typescript:",notnull"`
|
||||
SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
BridgeConfig AIBridgeConfig `json:"bridge,omitempty"`
|
||||
}
|
||||
|
||||
Generated
+7
@@ -167,6 +167,13 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"bedrock": {
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
},
|
||||
"enabled": true,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
|
||||
Generated
+51
@@ -351,6 +351,28 @@
|
||||
| `base_url` | string | false | | |
|
||||
| `key` | string | false | | |
|
||||
|
||||
## codersdk.AIBridgeBedrockConfig
|
||||
|
||||
```json
|
||||
{
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------------------|--------|----------|--------------|-------------|
|
||||
| `access_key` | string | false | | |
|
||||
| `access_key_secret` | string | false | | |
|
||||
| `model` | string | false | | |
|
||||
| `region` | string | false | | |
|
||||
| `small_fast_model` | string | false | | |
|
||||
|
||||
## codersdk.AIBridgeConfig
|
||||
|
||||
```json
|
||||
@@ -359,6 +381,13 @@
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"bedrock": {
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
},
|
||||
"enabled": true,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
@@ -372,6 +401,7 @@
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-------------|----------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | |
|
||||
| `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | |
|
||||
|
||||
@@ -654,6 +684,13 @@
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"bedrock": {
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
},
|
||||
"enabled": true,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
@@ -2803,6 +2840,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"bedrock": {
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
},
|
||||
"enabled": true,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
@@ -3310,6 +3354,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"bedrock": {
|
||||
"access_key": "string",
|
||||
"access_key_secret": "string",
|
||||
"model": "string",
|
||||
"region": "string",
|
||||
"small_fast_model": "string"
|
||||
},
|
||||
"enabled": true,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/aibridge"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd"
|
||||
"github.com/coder/coder/v2/enterprise/x/aibridged"
|
||||
)
|
||||
@@ -20,14 +21,14 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) {
|
||||
|
||||
// Setup supported providers.
|
||||
providers := []aibridge.Provider{
|
||||
aibridge.NewOpenAIProvider(aibridge.ProviderConfig{
|
||||
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{
|
||||
BaseURL: coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(),
|
||||
Key: coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String(),
|
||||
}),
|
||||
aibridge.NewAnthropicProvider(aibridge.ProviderConfig{
|
||||
aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{
|
||||
BaseURL: coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(),
|
||||
Key: coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String(),
|
||||
}),
|
||||
}, getBedrockConfig(coderAPI.DeploymentValues.AI.BridgeConfig.Bedrock)),
|
||||
}
|
||||
|
||||
// Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user).
|
||||
@@ -45,3 +46,17 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) {
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func getBedrockConfig(cfg codersdk.AIBridgeBedrockConfig) *aibridge.AWSBedrockConfig {
|
||||
if cfg.Region.String() == "" && cfg.AccessKey.String() == "" && cfg.AccessKeySecret.String() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &aibridge.AWSBedrockConfig{
|
||||
Region: cfg.Region.String(),
|
||||
AccessKey: cfg.AccessKey.String(),
|
||||
AccessKeySecret: cfg.AccessKeySecret.String(),
|
||||
Model: cfg.Model.String(),
|
||||
SmallFastModel: cfg.SmallFastModel.String(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestIntegration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := testutil.Logger(t)
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.ProviderConfig{BaseURL: mockOpenAI.URL})}
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -288,8 +288,8 @@ func TestRouting(t *testing.T) {
|
||||
client := mock.NewMockDRPCClient(ctrl)
|
||||
|
||||
providers := []aibridge.Provider{
|
||||
aibridge.NewOpenAIProvider(aibridge.ProviderConfig{BaseURL: openaiSrv.URL}),
|
||||
aibridge.NewAnthropicProvider(aibridge.ProviderConfig{BaseURL: antSrv.URL}),
|
||||
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: openaiSrv.URL}),
|
||||
aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{BaseURL: antSrv.URL}, nil),
|
||||
}
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -476,7 +476,7 @@ require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.13.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.8.0
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aibridge v0.1.5
|
||||
github.com/coder/aibridge v0.1.6
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945
|
||||
github.com/coder/preview v1.0.4
|
||||
@@ -509,6 +509,7 @@ require (
|
||||
github.com/aquasecurity/trivy v0.61.1-0.20250407075540-f1329c7ea1aa // indirect
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
|
||||
@@ -764,6 +764,8 @@ github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.3 h1:RIb3yr/+PZ18YYNe6MDiG/3jVoJrPmdoCARwNkMGvco=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.3/go.mod h1:jjgx1n7x0FAKl6TnakqrpkHWWKcX3xfWtdnIJs5K9CE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.7 h1:zqg4OMrKj+t5HlswDApgvAHjxKtlduKS7KicXB+7RLg=
|
||||
@@ -915,8 +917,8 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4=
|
||||
github.com/coder/aibridge v0.1.5 h1:uSrltfLZWF2qOaq9RDzJW/26Ow1wMFwcwObBM0WikME=
|
||||
github.com/coder/aibridge v0.1.5/go.mod h1:Q5MCfKMcKYmYl4qH1Zd0rltmPaUBPKFvIPs2k9q6qeY=
|
||||
github.com/coder/aibridge v0.1.6 h1:Ax9hfAQeg+vLeE9M/IMmARuBuGyCuFdMw8bpy72/fD8=
|
||||
github.com/coder/aibridge v0.1.6/go.mod h1:7GhrLbzf6uM3sCA7OPaDzvq9QNrCjNuzMy+WgipYwfQ=
|
||||
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
|
||||
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945 h1:hDUf02kTX8EGR3+5B+v5KdYvORs4YNfDPci0zCs+pC0=
|
||||
|
||||
Generated
+10
@@ -16,11 +16,21 @@ export interface AIBridgeAnthropicConfig {
|
||||
readonly key: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface AIBridgeBedrockConfig {
|
||||
readonly region: string;
|
||||
readonly access_key: string;
|
||||
readonly access_key_secret: string;
|
||||
readonly model: string;
|
||||
readonly small_fast_model: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface AIBridgeConfig {
|
||||
readonly enabled: boolean;
|
||||
readonly openai: AIBridgeOpenAIConfig;
|
||||
readonly anthropic: AIBridgeAnthropicConfig;
|
||||
readonly bedrock: AIBridgeBedrockConfig;
|
||||
}
|
||||
|
||||
// From codersdk/aibridge.go
|
||||
|
||||
Reference in New Issue
Block a user