mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 03:43:39 +00:00
Merge branch 'sub' into feature/subscription
This commit is contained in:
@@ -817,6 +817,10 @@ type OpenAIResponsesRequest struct {
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
// qwen
|
||||
EnableThinking json.RawMessage `json:"enable_thinking,omitempty"`
|
||||
// perplexity
|
||||
Preset json.RawMessage `json:"preset,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -84,6 +84,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/embeddings", info.ChannelBaseUrl)
|
||||
case constant.RelayModeRerank:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.ChannelBaseUrl)
|
||||
case constant.RelayModeResponses:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v2/apps/protocols/compatible-mode/v1/responses", info.ChannelBaseUrl)
|
||||
case constant.RelayModeImagesGenerations:
|
||||
if isSyncImageModel(info.OriginModelName) {
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
|
||||
@@ -210,8 +212,7 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
|
||||
@@ -98,6 +98,19 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
|
||||
return headerOverride, nil
|
||||
}
|
||||
|
||||
func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]string) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
for key, value := range headerOverride {
|
||||
req.Header.Set(key, value)
|
||||
// set Host in req
|
||||
if strings.EqualFold(key, "Host") {
|
||||
req.Host = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
|
||||
fullRequestURL, err := a.GetRequestURL(info)
|
||||
if err != nil {
|
||||
@@ -121,9 +134,7 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range headerOverride {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
applyHeaderOverrideToRequest(req, headerOverride)
|
||||
resp, err := doRequest(c, req, info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("do request failed: %w", err)
|
||||
@@ -156,9 +167,7 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range headerOverride {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
applyHeaderOverrideToRequest(req, headerOverride)
|
||||
resp, err := doRequest(c, req, info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("do request failed: %w", err)
|
||||
|
||||
@@ -437,8 +437,10 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse
|
||||
}
|
||||
} else {
|
||||
if claudeResponse.Type == "message_start" {
|
||||
response.Id = claudeResponse.Message.Id
|
||||
response.Model = claudeResponse.Message.Model
|
||||
if claudeResponse.Message != nil {
|
||||
response.Id = claudeResponse.Message.Id
|
||||
response.Model = claudeResponse.Message.Model
|
||||
}
|
||||
//claudeUsage = &claudeResponse.Message.Usage
|
||||
choice.Delta.SetContentString("")
|
||||
choice.Delta.Role = "assistant"
|
||||
@@ -589,35 +591,63 @@ type ClaudeResponseInfo struct {
|
||||
}
|
||||
|
||||
func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
|
||||
if claudeInfo == nil {
|
||||
return false
|
||||
}
|
||||
if claudeInfo.Usage == nil {
|
||||
claudeInfo.Usage = &dto.Usage{}
|
||||
}
|
||||
if requestMode == RequestModeCompletion {
|
||||
claudeInfo.ResponseText.WriteString(claudeResponse.Completion)
|
||||
} else {
|
||||
if claudeResponse.Type == "message_start" {
|
||||
claudeInfo.ResponseId = claudeResponse.Message.Id
|
||||
claudeInfo.Model = claudeResponse.Message.Model
|
||||
if claudeResponse.Message != nil {
|
||||
claudeInfo.ResponseId = claudeResponse.Message.Id
|
||||
claudeInfo.Model = claudeResponse.Message.Model
|
||||
}
|
||||
|
||||
// message_start, 获取usage
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
||||
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
|
||||
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
||||
} else if claudeResponse.Type == "content_block_delta" {
|
||||
if claudeResponse.Delta.Text != nil {
|
||||
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
|
||||
if claudeResponse.Message != nil && claudeResponse.Message.Usage != nil {
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
||||
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
|
||||
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
||||
}
|
||||
if claudeResponse.Delta.Thinking != nil {
|
||||
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking)
|
||||
} else if claudeResponse.Type == "content_block_delta" {
|
||||
if claudeResponse.Delta != nil {
|
||||
if claudeResponse.Delta.Text != nil {
|
||||
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
|
||||
}
|
||||
if claudeResponse.Delta.Thinking != nil {
|
||||
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking)
|
||||
}
|
||||
}
|
||||
} else if claudeResponse.Type == "message_delta" {
|
||||
// 最终的usage获取
|
||||
if claudeResponse.Usage.InputTokens > 0 {
|
||||
// 不叠加,只取最新的
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||||
if claudeResponse.Usage != nil {
|
||||
if claudeResponse.Usage.InputTokens > 0 {
|
||||
// 不叠加,只取最新的
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||||
}
|
||||
if claudeResponse.Usage.CacheReadInputTokens > 0 {
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
||||
}
|
||||
if claudeResponse.Usage.CacheCreationInputTokens > 0 {
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
||||
}
|
||||
if cacheCreation5m := claudeResponse.Usage.GetCacheCreation5mTokens(); cacheCreation5m > 0 {
|
||||
claudeInfo.Usage.ClaudeCacheCreation5mTokens = cacheCreation5m
|
||||
}
|
||||
if cacheCreation1h := claudeResponse.Usage.GetCacheCreation1hTokens(); cacheCreation1h > 0 {
|
||||
claudeInfo.Usage.ClaudeCacheCreation1hTokens = cacheCreation1h
|
||||
}
|
||||
if claudeResponse.Usage.OutputTokens > 0 {
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||||
}
|
||||
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
|
||||
}
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||||
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
|
||||
|
||||
// 判断是否完整
|
||||
claudeInfo.Done = true
|
||||
@@ -657,7 +687,9 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
} else {
|
||||
if claudeResponse.Type == "message_start" {
|
||||
// message_start, 获取usage
|
||||
info.UpstreamModelName = claudeResponse.Message.Model
|
||||
if claudeResponse.Message != nil {
|
||||
info.UpstreamModelName = claudeResponse.Message.Model
|
||||
}
|
||||
} else if claudeResponse.Type == "content_block_delta" {
|
||||
} else if claudeResponse.Type == "message_delta" {
|
||||
}
|
||||
@@ -745,13 +777,18 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
if requestMode == RequestModeCompletion {
|
||||
claudeInfo.Usage = service.ResponseText2Usage(c, claudeResponse.Completion, info.UpstreamModelName, info.GetEstimatePromptTokens())
|
||||
} else {
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||||
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
||||
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
|
||||
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
|
||||
if claudeInfo.Usage == nil {
|
||||
claudeInfo.Usage = &dto.Usage{}
|
||||
}
|
||||
if claudeResponse.Usage != nil {
|
||||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||||
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
||||
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
|
||||
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
|
||||
}
|
||||
}
|
||||
var responseData []byte
|
||||
switch info.RelayFormat {
|
||||
@@ -766,7 +803,7 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
responseData = data
|
||||
}
|
||||
|
||||
if claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
|
||||
if claudeResponse.Usage != nil && claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
|
||||
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openai"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -42,6 +43,9 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
if info.RelayMode == relayconstant.RelayModeResponses {
|
||||
return fmt.Sprintf("%s/v1/responses", info.ChannelBaseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/chat/completions", info.ChannelBaseUrl), nil
|
||||
}
|
||||
|
||||
@@ -71,8 +75,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
// TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
RequestModeClaude = 1
|
||||
RequestModeGemini = 2
|
||||
RequestModeLlama = 3
|
||||
RequestModeClaude = 1
|
||||
RequestModeGemini = 2
|
||||
RequestModeOpenSource = 3
|
||||
)
|
||||
|
||||
var claudeModelMap = map[string]string{
|
||||
@@ -115,7 +115,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
} else if strings.Contains(info.UpstreamModelName, "llama") ||
|
||||
// open source models
|
||||
strings.Contains(info.UpstreamModelName, "-maas") {
|
||||
a.RequestMode = RequestModeLlama
|
||||
a.RequestMode = RequestModeOpenSource
|
||||
} else {
|
||||
a.RequestMode = RequestModeGemini
|
||||
}
|
||||
@@ -166,10 +166,9 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s
|
||||
suffix,
|
||||
), nil
|
||||
}
|
||||
} else if a.RequestMode == RequestModeLlama {
|
||||
} else if a.RequestMode == RequestModeOpenSource {
|
||||
return fmt.Sprintf(
|
||||
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
|
||||
region,
|
||||
"https://aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
|
||||
adc.ProjectID,
|
||||
region,
|
||||
), nil
|
||||
@@ -242,7 +241,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
model = v
|
||||
}
|
||||
return a.getRequestUrl(info, model, suffix)
|
||||
} else if a.RequestMode == RequestModeLlama {
|
||||
} else if a.RequestMode == RequestModeOpenSource {
|
||||
return a.getRequestUrl(info, "", "")
|
||||
}
|
||||
return "", errors.New("unsupported request mode")
|
||||
@@ -340,7 +339,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
}
|
||||
c.Set("request_model", request.Model)
|
||||
return geminiRequest, nil
|
||||
} else if a.RequestMode == RequestModeLlama {
|
||||
} else if a.RequestMode == RequestModeOpenSource {
|
||||
return request, nil
|
||||
}
|
||||
return nil, errors.New("unsupported request mode")
|
||||
@@ -375,7 +374,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
} else {
|
||||
return gemini.GeminiChatStreamHandler(c, info, resp)
|
||||
}
|
||||
case RequestModeLlama:
|
||||
case RequestModeOpenSource:
|
||||
return openai.OaiStreamHandler(c, info, resp)
|
||||
}
|
||||
} else {
|
||||
@@ -391,7 +390,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
}
|
||||
return gemini.GeminiChatHandler(c, info, resp)
|
||||
}
|
||||
case RequestModeLlama:
|
||||
case RequestModeOpenSource:
|
||||
return openai.OpenaiHandler(c, info, resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +231,8 @@ func GetMimeTypeByExtension(ext string) string {
|
||||
return "image/png"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
case "jfif":
|
||||
return "image/jpeg"
|
||||
|
||||
// Audio files
|
||||
case "mp3":
|
||||
|
||||
@@ -33,11 +33,40 @@ type ChannelAffinitySetting struct {
|
||||
}
|
||||
|
||||
var channelAffinitySetting = ChannelAffinitySetting{
|
||||
Enabled: false,
|
||||
Enabled: true,
|
||||
SwitchOnSuccess: true,
|
||||
MaxEntries: 100_000,
|
||||
DefaultTTLSeconds: 3600,
|
||||
Rules: []ChannelAffinityRule{},
|
||||
Rules: []ChannelAffinityRule{
|
||||
{
|
||||
Name: "codex trace",
|
||||
ModelRegex: []string{"^gpt-.*$"},
|
||||
PathRegex: []string{"/v1/responses"},
|
||||
KeySources: []ChannelAffinityKeySource{
|
||||
{Type: "gjson", Path: "prompt_cache_key"},
|
||||
},
|
||||
ValueRegex: "",
|
||||
TTLSeconds: 0,
|
||||
SkipRetryOnFailure: false,
|
||||
IncludeUsingGroup: true,
|
||||
IncludeRuleName: true,
|
||||
UserAgentInclude: nil,
|
||||
},
|
||||
{
|
||||
Name: "claude code trace",
|
||||
ModelRegex: []string{"^claude-.*$"},
|
||||
PathRegex: []string{"/v1/messages"},
|
||||
KeySources: []ChannelAffinityKeySource{
|
||||
{Type: "gjson", Path: "metadata.user_id"},
|
||||
},
|
||||
ValueRegex: "",
|
||||
TTLSeconds: 0,
|
||||
SkipRetryOnFailure: false,
|
||||
IncludeUsingGroup: true,
|
||||
IncludeRuleName: true,
|
||||
UserAgentInclude: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -120,7 +120,7 @@ const ContentModal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'relative', height: '100%' }}>
|
||||
{isLoading && (
|
||||
<div
|
||||
style={{
|
||||
@@ -137,7 +137,13 @@ const ContentModal = ({
|
||||
<video
|
||||
src={modalContent}
|
||||
controls
|
||||
style={{ width: '100%' }}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
autoPlay
|
||||
crossOrigin='anonymous'
|
||||
onError={handleVideoError}
|
||||
@@ -155,11 +161,13 @@ const ContentModal = ({
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
closable={null}
|
||||
bodyStyle={{
|
||||
height: isVideo ? '450px' : '400px',
|
||||
height: isVideo ? '70vh' : '400px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
padding: isVideo && videoError ? '0' : '24px',
|
||||
}}
|
||||
width={800}
|
||||
width={isVideo ? '90vw' : 800}
|
||||
style={isVideo ? { maxWidth: 960 } : undefined}
|
||||
>
|
||||
{isVideo ? (
|
||||
renderVideoContent()
|
||||
|
||||
@@ -67,7 +67,7 @@ const KEY_SOURCE_TYPES = [
|
||||
|
||||
const RULE_TEMPLATES = {
|
||||
codex: {
|
||||
name: 'codex优选',
|
||||
name: 'codex trace',
|
||||
model_regex: ['^gpt-.*$'],
|
||||
path_regex: ['/v1/responses'],
|
||||
key_sources: [{ type: 'gjson', path: 'prompt_cache_key' }],
|
||||
@@ -78,7 +78,7 @@ const RULE_TEMPLATES = {
|
||||
include_rule_name: true,
|
||||
},
|
||||
claudeCode: {
|
||||
name: 'claude-code优选',
|
||||
name: 'claude-code trace',
|
||||
model_regex: ['^claude-.*$'],
|
||||
path_regex: ['/v1/messages'],
|
||||
key_sources: [{ type: 'gjson', path: 'metadata.user_id' }],
|
||||
|
||||
Reference in New Issue
Block a user