feat: Refactor model configuration management with new config system
- Introduce a new configuration management approach for model-specific settings - Update Gemini settings to use the new config system with more flexible management - Add support for dynamic configuration updates in option handling - Modify Claude and Vertex adaptors to use new configuration methods - Enhance web interface to support namespaced configuration keys
This commit is contained in:
@@ -3,7 +3,7 @@ package model
|
|||||||
import (
|
import (
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
"one-api/setting/model_setting"
|
"one-api/setting/config"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,6 +24,8 @@ func AllOption() ([]*Option, error) {
|
|||||||
func InitOptionMap() {
|
func InitOptionMap() {
|
||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
common.OptionMap = make(map[string]string)
|
common.OptionMap = make(map[string]string)
|
||||||
|
|
||||||
|
// 添加原有的系统配置
|
||||||
common.OptionMap["FileUploadPermission"] = strconv.Itoa(common.FileUploadPermission)
|
common.OptionMap["FileUploadPermission"] = strconv.Itoa(common.FileUploadPermission)
|
||||||
common.OptionMap["FileDownloadPermission"] = strconv.Itoa(common.FileDownloadPermission)
|
common.OptionMap["FileDownloadPermission"] = strconv.Itoa(common.FileDownloadPermission)
|
||||||
common.OptionMap["ImageUploadPermission"] = strconv.Itoa(common.ImageUploadPermission)
|
common.OptionMap["ImageUploadPermission"] = strconv.Itoa(common.ImageUploadPermission)
|
||||||
@@ -111,13 +113,16 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["DemoSiteEnabled"] = strconv.FormatBool(setting.DemoSiteEnabled)
|
common.OptionMap["DemoSiteEnabled"] = strconv.FormatBool(setting.DemoSiteEnabled)
|
||||||
common.OptionMap["ModelRequestRateLimitEnabled"] = strconv.FormatBool(setting.ModelRequestRateLimitEnabled)
|
common.OptionMap["ModelRequestRateLimitEnabled"] = strconv.FormatBool(setting.ModelRequestRateLimitEnabled)
|
||||||
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
||||||
//common.OptionMap["CheckSensitiveOnCompletionEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnCompletionEnabled)
|
|
||||||
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
||||||
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||||
common.OptionMap["AutomaticDisableKeywords"] = setting.AutomaticDisableKeywordsToString()
|
common.OptionMap["AutomaticDisableKeywords"] = setting.AutomaticDisableKeywordsToString()
|
||||||
common.OptionMap["GeminiSafetySettings"] = model_setting.GeminiSafetySettingsJsonString()
|
|
||||||
common.OptionMap["GeminiVersionSettings"] = model_setting.GeminiVersionSettingsJsonString()
|
// 自动添加所有注册的模型配置
|
||||||
|
modelConfigs := config.GlobalConfig.ExportAllConfigs()
|
||||||
|
for k, v := range modelConfigs {
|
||||||
|
common.OptionMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
loadOptionsFromDatabase()
|
loadOptionsFromDatabase()
|
||||||
@@ -161,6 +166,13 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
defer common.OptionMapRWMutex.Unlock()
|
defer common.OptionMapRWMutex.Unlock()
|
||||||
common.OptionMap[key] = value
|
common.OptionMap[key] = value
|
||||||
|
|
||||||
|
// 检查是否是模型配置 - 使用更规范的方式处理
|
||||||
|
if handleConfigUpdate(key, value) {
|
||||||
|
return nil // 已由配置系统处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理传统配置项...
|
||||||
if strings.HasSuffix(key, "Permission") {
|
if strings.HasSuffix(key, "Permission") {
|
||||||
intValue, _ := strconv.Atoi(value)
|
intValue, _ := strconv.Atoi(value)
|
||||||
switch key {
|
switch key {
|
||||||
@@ -235,9 +247,6 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.CheckSensitiveOnPromptEnabled = boolValue
|
setting.CheckSensitiveOnPromptEnabled = boolValue
|
||||||
case "ModelRequestRateLimitEnabled":
|
case "ModelRequestRateLimitEnabled":
|
||||||
setting.ModelRequestRateLimitEnabled = boolValue
|
setting.ModelRequestRateLimitEnabled = boolValue
|
||||||
|
|
||||||
//case "CheckSensitiveOnCompletionEnabled":
|
|
||||||
// constant.CheckSensitiveOnCompletionEnabled = boolValue
|
|
||||||
case "StopOnSensitiveEnabled":
|
case "StopOnSensitiveEnabled":
|
||||||
setting.StopOnSensitiveEnabled = boolValue
|
setting.StopOnSensitiveEnabled = boolValue
|
||||||
case "SMTPSSLEnabled":
|
case "SMTPSSLEnabled":
|
||||||
@@ -354,12 +363,33 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.SensitiveWordsFromString(value)
|
setting.SensitiveWordsFromString(value)
|
||||||
case "AutomaticDisableKeywords":
|
case "AutomaticDisableKeywords":
|
||||||
setting.AutomaticDisableKeywordsFromString(value)
|
setting.AutomaticDisableKeywordsFromString(value)
|
||||||
case "GeminiSafetySettings":
|
|
||||||
model_setting.GeminiSafetySettingFromJsonString(value)
|
|
||||||
case "GeminiVersionSettings":
|
|
||||||
model_setting.GeminiVersionSettingFromJsonString(value)
|
|
||||||
case "StreamCacheQueueLength":
|
case "StreamCacheQueueLength":
|
||||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleConfigUpdate 处理分层配置更新,返回是否已处理
|
||||||
|
func handleConfigUpdate(key, value string) bool {
|
||||||
|
parts := strings.SplitN(key, ".", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false // 不是分层配置
|
||||||
|
}
|
||||||
|
|
||||||
|
configName := parts[0]
|
||||||
|
configKey := parts[1]
|
||||||
|
|
||||||
|
// 获取配置对象
|
||||||
|
cfg := config.GlobalConfig.Get(configName)
|
||||||
|
if cfg == nil {
|
||||||
|
return false // 未注册的配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
configMap := map[string]string{
|
||||||
|
configKey: value,
|
||||||
|
}
|
||||||
|
config.UpdateConfigFromMap(cfg, configMap)
|
||||||
|
|
||||||
|
return true // 已处理
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel/claude"
|
"one-api/relay/channel/claude"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
|
"one-api/setting/model_setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -38,6 +39,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||||
|
model_setting.GetClaudeSettings().WriteHeaders(req)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
|
"one-api/setting/model_setting"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
|
|||||||
anthropicVersion = "2023-06-01"
|
anthropicVersion = "2023-06-01"
|
||||||
}
|
}
|
||||||
req.Set("anthropic-version", anthropicVersion)
|
req.Set("anthropic-version", anthropicVersion)
|
||||||
|
model_setting.GetClaudeSettings().WriteHeaders(req)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
|
"one-api/setting/model_setting"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -93,9 +94,10 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
|
|||||||
Tools: claudeTools,
|
Tools: claudeTools,
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(textRequest.Model, "-thinking") {
|
if model_setting.GetClaudeSettings().ThinkingAdapterEnabled &&
|
||||||
|
strings.HasSuffix(textRequest.Model, "-thinking") {
|
||||||
if claudeRequest.MaxTokens == 0 {
|
if claudeRequest.MaxTokens == 0 {
|
||||||
claudeRequest.MaxTokens = 8192
|
claudeRequest.MaxTokens = uint(model_setting.GetClaudeSettings().ThinkingAdapterMaxTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 因为BudgetTokens 必须大于1024
|
// 因为BudgetTokens 必须大于1024
|
||||||
@@ -106,7 +108,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
|
|||||||
// BudgetTokens 为 max_tokens 的 80%
|
// BudgetTokens 为 max_tokens 的 80%
|
||||||
claudeRequest.Thinking = &Thinking{
|
claudeRequest.Thinking = &Thinking{
|
||||||
Type: "enabled",
|
Type: "enabled",
|
||||||
BudgetTokens: int(float64(claudeRequest.MaxTokens) * 0.8),
|
BudgetTokens: int(float64(claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage),
|
||||||
}
|
}
|
||||||
// TODO: 临时处理
|
// TODO: 临时处理
|
||||||
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking
|
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ var claudeModelMap = map[string]string{
|
|||||||
"claude-3-opus-20240229": "claude-3-opus@20240229",
|
"claude-3-opus-20240229": "claude-3-opus@20240229",
|
||||||
"claude-3-haiku-20240307": "claude-3-haiku@20240307",
|
"claude-3-haiku-20240307": "claude-3-haiku@20240307",
|
||||||
"claude-3-5-sonnet-20240620": "claude-3-5-sonnet@20240620",
|
"claude-3-5-sonnet-20240620": "claude-3-5-sonnet@20240620",
|
||||||
|
"claude-3-7-sonnet-20250219": "claude-3-7-sonnet@20250219",
|
||||||
}
|
}
|
||||||
|
|
||||||
const anthropicVersion = "vertex-2023-10-16"
|
const anthropicVersion = "vertex-2023-10-16"
|
||||||
@@ -156,7 +157,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
|||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||||
return channel.DoApiRequest(a, c, info, requestBody)
|
return channel.DoApiRequest(a, c, info, requestBody)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,52 @@
|
|||||||
package model_setting
|
package model_setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"one-api/setting/config"
|
||||||
"one-api/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var geminiSafetySettings = map[string]string{
|
// GeminiSettings 定义Gemini模型的配置
|
||||||
"default": "OFF",
|
type GeminiSettings struct {
|
||||||
"HARM_CATEGORY_CIVIC_INTEGRITY": "BLOCK_NONE",
|
SafetySettings map[string]string `json:"safety_settings"`
|
||||||
|
VersionSettings map[string]string `json:"version_settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var defaultGeminiSettings = GeminiSettings{
|
||||||
|
SafetySettings: map[string]string{
|
||||||
|
"default": "OFF",
|
||||||
|
"HARM_CATEGORY_CIVIC_INTEGRITY": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
VersionSettings: map[string]string{
|
||||||
|
"default": "v1beta",
|
||||||
|
"gemini-1.0-pro": "v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局实例
|
||||||
|
var geminiSettings = defaultGeminiSettings
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册到全局配置管理器
|
||||||
|
config.GlobalConfig.Register("gemini", &geminiSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGeminiSettings 获取Gemini配置
|
||||||
|
func GetGeminiSettings() *GeminiSettings {
|
||||||
|
return &geminiSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGeminiSafetySetting 获取安全设置
|
||||||
func GetGeminiSafetySetting(key string) string {
|
func GetGeminiSafetySetting(key string) string {
|
||||||
if value, ok := geminiSafetySettings[key]; ok {
|
if value, ok := geminiSettings.SafetySettings[key]; ok {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return geminiSafetySettings["default"]
|
return geminiSettings.SafetySettings["default"]
|
||||||
}
|
|
||||||
|
|
||||||
func GeminiSafetySettingFromJsonString(jsonString string) {
|
|
||||||
geminiSafetySettings = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(jsonString), &geminiSafetySettings)
|
|
||||||
if err != nil {
|
|
||||||
geminiSafetySettings = map[string]string{
|
|
||||||
"default": "OFF",
|
|
||||||
"HARM_CATEGORY_CIVIC_INTEGRITY": "BLOCK_NONE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check must have default
|
|
||||||
if _, ok := geminiSafetySettings["default"]; !ok {
|
|
||||||
geminiSafetySettings["default"] = common.GeminiSafetySetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GeminiSafetySettingsJsonString() string {
|
|
||||||
// check must have default
|
|
||||||
if _, ok := geminiSafetySettings["default"]; !ok {
|
|
||||||
geminiSafetySettings["default"] = common.GeminiSafetySetting
|
|
||||||
}
|
|
||||||
jsonString, err := json.Marshal(geminiSafetySettings)
|
|
||||||
if err != nil {
|
|
||||||
return "{}"
|
|
||||||
}
|
|
||||||
return string(jsonString)
|
|
||||||
}
|
|
||||||
|
|
||||||
var geminiVersionSettings = map[string]string{
|
|
||||||
"default": "v1beta",
|
|
||||||
"gemini-1.0-pro": "v1",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGeminiVersionSetting 获取版本设置
|
||||||
func GetGeminiVersionSetting(key string) string {
|
func GetGeminiVersionSetting(key string) string {
|
||||||
if value, ok := geminiVersionSettings[key]; ok {
|
if value, ok := geminiSettings.VersionSettings[key]; ok {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return geminiVersionSettings["default"]
|
return geminiSettings.VersionSettings["default"]
|
||||||
}
|
|
||||||
|
|
||||||
func GeminiVersionSettingFromJsonString(jsonString string) {
|
|
||||||
geminiVersionSettings = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(jsonString), &geminiVersionSettings)
|
|
||||||
if err != nil {
|
|
||||||
geminiVersionSettings = map[string]string{
|
|
||||||
"default": "v1beta",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check must have default
|
|
||||||
if _, ok := geminiVersionSettings["default"]; !ok {
|
|
||||||
geminiVersionSettings["default"] = "v1beta"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GeminiVersionSettingsJsonString() string {
|
|
||||||
// check must have default
|
|
||||||
if _, ok := geminiVersionSettings["default"]; !ok {
|
|
||||||
geminiVersionSettings["default"] = "v1beta"
|
|
||||||
}
|
|
||||||
jsonString, err := json.Marshal(geminiVersionSettings)
|
|
||||||
if err != nil {
|
|
||||||
return "{}"
|
|
||||||
}
|
|
||||||
return string(jsonString)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export default function SettingGeminiModel(props) {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
GeminiSafetySettings: '',
|
'gemini.safety_settings': '',
|
||||||
GeminiVersionSettings: '',
|
'gemini.version_settings': '',
|
||||||
});
|
});
|
||||||
const refForm = useRef();
|
const refForm = useRef();
|
||||||
const [inputsRow, setInputsRow] = useState(inputs);
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
@@ -90,7 +90,7 @@ export default function SettingGeminiModel(props) {
|
|||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label={t('Gemini安全设置')}
|
label={t('Gemini安全设置')}
|
||||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)}
|
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)}
|
||||||
field={'GeminiSafetySettings'}
|
field={'gemini.safety_settings'}
|
||||||
extraText={t('default为默认设置,可单独设置每个分类的安全等级')}
|
extraText={t('default为默认设置,可单独设置每个分类的安全等级')}
|
||||||
autosize={{ minRows: 6, maxRows: 12 }}
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
trigger='blur'
|
trigger='blur'
|
||||||
@@ -101,7 +101,7 @@ export default function SettingGeminiModel(props) {
|
|||||||
message: t('不是合法的 JSON 字符串')
|
message: t('不是合法的 JSON 字符串')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setInputs({ ...inputs, GeminiSafetySettings: value })}
|
onChange={(value) => setInputs({ ...inputs, 'gemini.safety_settings': value })}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -110,7 +110,7 @@ export default function SettingGeminiModel(props) {
|
|||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label={t('Gemini版本设置')}
|
label={t('Gemini版本设置')}
|
||||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)}
|
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)}
|
||||||
field={'GeminiVersionSettings'}
|
field={'gemini.version_settings'}
|
||||||
extraText={t('default为默认设置,可单独设置每个模型的版本')}
|
extraText={t('default为默认设置,可单独设置每个模型的版本')}
|
||||||
autosize={{ minRows: 6, maxRows: 12 }}
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
trigger='blur'
|
trigger='blur'
|
||||||
@@ -121,7 +121,7 @@ export default function SettingGeminiModel(props) {
|
|||||||
message: t('不是合法的 JSON 字符串')
|
message: t('不是合法的 JSON 字符串')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setInputs({ ...inputs, GeminiVersionSettings: value })}
|
onChange={(value) => setInputs({ ...inputs, 'gemini.version_settings': value })}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
Reference in New Issue
Block a user