Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
cbbe4cb4
authored
Sep 28, 2025
by
comeback01
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'upstream/main' into traduction
parents
05d3ddb3
1ba2cb0b
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
762 additions
and
339 deletions
+762
-339
dto/channel_settings.go
+8
-0
relay/channel/api_request.go
+1
-0
relay/channel/ollama/adaptor.go
+18
-34
relay/channel/ollama/dto.go
+51
-30
relay/channel/ollama/relay-ollama.go
+164
-108
relay/channel/ollama/stream.go
+210
-0
relay/channel/openai/relay-openai.go
+18
-0
relay/channel/openrouter/dto.go
+7
-0
relay/channel/volcengine/adaptor.go
+13
-6
relay/channel/volcengine/constants.go
+5
-0
relay/channel/xunfei/relay-xunfei.go
+3
-4
web/src/components/table/channels/modals/EditChannelModal.jsx
+75
-1
web/src/hooks/channels/useChannelsData.jsx
+189
-156
No files found.
dto/channel_settings.go
View file @
cbbe4cb4
...
@@ -19,4 +19,12 @@ const (
...
@@ -19,4 +19,12 @@ const (
type
ChannelOtherSettings
struct
{
type
ChannelOtherSettings
struct
{
AzureResponsesVersion
string
`json:"azure_responses_version,omitempty"`
AzureResponsesVersion
string
`json:"azure_responses_version,omitempty"`
VertexKeyType
VertexKeyType
`json:"vertex_key_type,omitempty"`
// "json" or "api_key"
VertexKeyType
VertexKeyType
`json:"vertex_key_type,omitempty"`
// "json" or "api_key"
OpenRouterEnterprise
*
bool
`json:"openrouter_enterprise,omitempty"`
}
func
(
s
*
ChannelOtherSettings
)
IsOpenRouterEnterprise
()
bool
{
if
s
==
nil
||
s
.
OpenRouterEnterprise
==
nil
{
return
false
}
return
*
s
.
OpenRouterEnterprise
}
}
relay/channel/api_request.go
View file @
cbbe4cb4
...
@@ -265,6 +265,7 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
...
@@ -265,6 +265,7 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
resp
,
err
:=
client
.
Do
(
req
)
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
if
err
!=
nil
{
logger
.
LogError
(
c
,
"do request failed: "
+
err
.
Error
())
return
nil
,
types
.
NewError
(
err
,
types
.
ErrorCodeDoRequestFailed
,
types
.
ErrOptionWithHideErrMsg
(
"upstream error: do request failed"
))
return
nil
,
types
.
NewError
(
err
,
types
.
ErrorCodeDoRequestFailed
,
types
.
ErrOptionWithHideErrMsg
(
"upstream error: do request failed"
))
}
}
if
resp
==
nil
{
if
resp
==
nil
{
...
...
relay/channel/ollama/adaptor.go
View file @
cbbe4cb4
...
@@ -10,6 +10,7 @@ import (
...
@@ -10,6 +10,7 @@ import (
relaycommon
"one-api/relay/common"
relaycommon
"one-api/relay/common"
relayconstant
"one-api/relay/constant"
relayconstant
"one-api/relay/constant"
"one-api/types"
"one-api/types"
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
)
...
@@ -17,10 +18,7 @@ import (
...
@@ -17,10 +18,7 @@ import (
type
Adaptor
struct
{
type
Adaptor
struct
{
}
}
func
(
a
*
Adaptor
)
ConvertGeminiRequest
(
*
gin
.
Context
,
*
relaycommon
.
RelayInfo
,
*
dto
.
GeminiChatRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertGeminiRequest
(
*
gin
.
Context
,
*
relaycommon
.
RelayInfo
,
*
dto
.
GeminiChatRequest
)
(
any
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
}
//TODO implement me
return
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
a
*
Adaptor
)
ConvertClaudeRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
ClaudeRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertClaudeRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
ClaudeRequest
)
(
any
,
error
)
{
openaiAdaptor
:=
openai
.
Adaptor
{}
openaiAdaptor
:=
openai
.
Adaptor
{}
...
@@ -31,32 +29,21 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
...
@@ -31,32 +29,21 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
openaiRequest
.
(
*
dto
.
GeneralOpenAIRequest
)
.
StreamOptions
=
&
dto
.
StreamOptions
{
openaiRequest
.
(
*
dto
.
GeneralOpenAIRequest
)
.
StreamOptions
=
&
dto
.
StreamOptions
{
IncludeUsage
:
true
,
IncludeUsage
:
true
,
}
}
return
requestOpenAI2Ollama
(
c
,
openaiRequest
.
(
*
dto
.
GeneralOpenAIRequest
))
// map to ollama chat request (Claude -> OpenAI -> Ollama chat)
return
openAIChatToOllamaChat
(
c
,
openaiRequest
.
(
*
dto
.
GeneralOpenAIRequest
))
}
}
func
(
a
*
Adaptor
)
ConvertAudioRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
AudioRequest
)
(
io
.
Reader
,
error
)
{
func
(
a
*
Adaptor
)
ConvertAudioRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
AudioRequest
)
(
io
.
Reader
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
}
//TODO implement me
return
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
a
*
Adaptor
)
ConvertImageRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
ImageRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertImageRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
ImageRequest
)
(
any
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
}
//TODO implement me
return
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
a
*
Adaptor
)
Init
(
info
*
relaycommon
.
RelayInfo
)
{
func
(
a
*
Adaptor
)
Init
(
info
*
relaycommon
.
RelayInfo
)
{
}
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
if
info
.
RelayFormat
==
types
.
RelayFormatClaude
{
if
info
.
RelayMode
==
relayconstant
.
RelayModeEmbeddings
{
return
info
.
ChannelBaseUrl
+
"/api/embed"
,
nil
}
return
info
.
ChannelBaseUrl
+
"/v1/chat/completions"
,
nil
if
strings
.
Contains
(
info
.
RequestURLPath
,
"/v1/completions"
)
||
info
.
RelayMode
==
relayconstant
.
RelayModeCompletions
{
return
info
.
ChannelBaseUrl
+
"/api/generate"
,
nil
}
}
return
info
.
ChannelBaseUrl
+
"/api/chat"
,
nil
switch
info
.
RelayMode
{
case
relayconstant
.
RelayModeEmbeddings
:
return
info
.
ChannelBaseUrl
+
"/api/embed"
,
nil
default
:
return
relaycommon
.
GetFullRequestURL
(
info
.
ChannelBaseUrl
,
info
.
RequestURLPath
,
info
.
ChannelType
),
nil
}
}
}
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
{
...
@@ -66,10 +53,12 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
...
@@ -66,10 +53,12 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
}
}
func
(
a
*
Adaptor
)
ConvertOpenAIRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeneralOpenAIRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertOpenAIRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeneralOpenAIRequest
)
(
any
,
error
)
{
if
request
==
nil
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
nil
,
errors
.
New
(
"request is nil"
)
// decide generate or chat
if
strings
.
Contains
(
info
.
RequestURLPath
,
"/v1/completions"
)
||
info
.
RelayMode
==
relayconstant
.
RelayModeCompletions
{
return
openAIToGenerate
(
c
,
request
)
}
}
return
requestOpenAI2Ollama
(
c
,
request
)
return
openAIChatToOllamaChat
(
c
,
request
)
}
}
func
(
a
*
Adaptor
)
ConvertRerankRequest
(
c
*
gin
.
Context
,
relayMode
int
,
request
dto
.
RerankRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertRerankRequest
(
c
*
gin
.
Context
,
relayMode
int
,
request
dto
.
RerankRequest
)
(
any
,
error
)
{
...
@@ -80,10 +69,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
...
@@ -80,10 +69,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
return
requestOpenAI2Embeddings
(
request
),
nil
return
requestOpenAI2Embeddings
(
request
),
nil
}
}
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
}
// TODO implement me
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
)
...
@@ -92,15 +78,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
...
@@ -92,15 +78,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func
(
a
*
Adaptor
)
DoResponse
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
usage
any
,
err
*
types
.
NewAPIError
)
{
func
(
a
*
Adaptor
)
DoResponse
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
usage
any
,
err
*
types
.
NewAPIError
)
{
switch
info
.
RelayMode
{
switch
info
.
RelayMode
{
case
relayconstant
.
RelayModeEmbeddings
:
case
relayconstant
.
RelayModeEmbeddings
:
usage
,
err
=
ollamaEmbeddingHandler
(
c
,
info
,
resp
)
return
ollamaEmbeddingHandler
(
c
,
info
,
resp
)
default
:
default
:
if
info
.
IsStream
{
if
info
.
IsStream
{
usage
,
err
=
openai
.
OaiStreamHandler
(
c
,
info
,
resp
)
return
ollamaStreamHandler
(
c
,
info
,
resp
)
}
else
{
usage
,
err
=
openai
.
OpenaiHandler
(
c
,
info
,
resp
)
}
}
return
ollamaChatHandler
(
c
,
info
,
resp
)
}
}
return
}
}
func
(
a
*
Adaptor
)
GetModelList
()
[]
string
{
func
(
a
*
Adaptor
)
GetModelList
()
[]
string
{
...
...
relay/channel/ollama/dto.go
View file @
cbbe4cb4
...
@@ -2,48 +2,69 @@ package ollama
...
@@ -2,48 +2,69 @@ package ollama
import
(
import
(
"encoding/json"
"encoding/json"
"one-api/dto"
)
)
type
OllamaRequest
struct
{
type
OllamaChatMessage
struct
{
Model
string
`json:"model,omitempty"`
Role
string
`json:"role"`
Messages
[]
dto
.
Message
`json:"messages,omitempty"`
Content
string
`json:"content,omitempty"`
Images
[]
string
`json:"images,omitempty"`
ToolCalls
[]
OllamaToolCall
`json:"tool_calls,omitempty"`
ToolName
string
`json:"tool_name,omitempty"`
Thinking
json
.
RawMessage
`json:"thinking,omitempty"`
}
type
OllamaToolFunction
struct
{
Name
string
`json:"name"`
Description
string
`json:"description,omitempty"`
Parameters
interface
{}
`json:"parameters,omitempty"`
}
type
OllamaTool
struct
{
Type
string
`json:"type"`
Function
OllamaToolFunction
`json:"function"`
}
type
OllamaToolCall
struct
{
Function
struct
{
Name
string
`json:"name"`
Arguments
interface
{}
`json:"arguments"`
}
`json:"function"`
}
type
OllamaChatRequest
struct
{
Model
string
`json:"model"`
Messages
[]
OllamaChatMessage
`json:"messages"`
Tools
interface
{}
`json:"tools,omitempty"`
Format
interface
{}
`json:"format,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
Options
map
[
string
]
any
`json:"options,omitempty"`
Seed
float64
`json:"seed,omitempty"`
KeepAlive
interface
{}
`json:"keep_alive,omitempty"`
Topp
float64
`json:"top_p,omitempty"`
TopK
int
`json:"top_k,omitempty"`
Stop
any
`json:"stop,omitempty"`
MaxTokens
uint
`json:"max_tokens,omitempty"`
Tools
[]
dto
.
ToolCallRequest
`json:"tools,omitempty"`
ResponseFormat
any
`json:"response_format,omitempty"`
FrequencyPenalty
float64
`json:"frequency_penalty,omitempty"`
PresencePenalty
float64
`json:"presence_penalty,omitempty"`
Suffix
any
`json:"suffix,omitempty"`
StreamOptions
*
dto
.
StreamOptions
`json:"stream_options,omitempty"`
Prompt
any
`json:"prompt,omitempty"`
Think
json
.
RawMessage
`json:"think,omitempty"`
Think
json
.
RawMessage
`json:"think,omitempty"`
}
}
type
Options
struct
{
type
OllamaGenerateRequest
struct
{
Seed
int
`json:"seed,omitempty"`
Model
string
`json:"model"`
Temperature
*
float64
`json:"temperature,omitempty"`
Prompt
string
`json:"prompt,omitempty"`
TopK
int
`json:"top_k,omitempty"`
Suffix
string
`json:"suffix,omitempty"`
TopP
float64
`json:"top_p,omitempty"`
Images
[]
string
`json:"images,omitempty"`
FrequencyPenalty
float64
`json:"frequency_penalty,omitempty"`
Format
interface
{}
`json:"format,omitempty"`
PresencePenalty
float64
`json:"presence_penalty,omitempty"`
Stream
bool
`json:"stream,omitempty"`
NumPredict
int
`json:"num_predict,omitempty"`
Options
map
[
string
]
any
`json:"options,omitempty"`
NumCtx
int
`json:"num_ctx,omitempty"`
KeepAlive
interface
{}
`json:"keep_alive,omitempty"`
Think
json
.
RawMessage
`json:"think,omitempty"`
}
}
type
OllamaEmbeddingRequest
struct
{
type
OllamaEmbeddingRequest
struct
{
Model
string
`json:"model,omitempty"`
Model
string
`json:"model"`
Input
[]
string
`json:"input"`
Input
interface
{}
`json:"input"`
Options
*
Options
`json:"options,omitempty"`
Options
map
[
string
]
any
`json:"options,omitempty"`
Dimensions
int
`json:"dimensions,omitempty"`
}
}
type
OllamaEmbeddingResponse
struct
{
type
OllamaEmbeddingResponse
struct
{
Error
string
`json:"error,omitempty"`
Error
string
`json:"error,omitempty"`
Model
string
`json:"model"`
Model
string
`json:"model"`
Embedding
[][]
float64
`json:"embeddings,omitempty"`
Embeddings
[][]
float64
`json:"embeddings"`
PromptEvalCount
int
`json:"prompt_eval_count,omitempty"`
}
}
relay/channel/ollama/relay-ollama.go
View file @
cbbe4cb4
package
ollama
package
ollama
import
(
import
(
"encoding/json"
"fmt"
"fmt"
"io"
"io"
"net/http"
"net/http"
...
@@ -14,121 +15,176 @@ import (
...
@@ -14,121 +15,176 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
)
func
requestOpenAI2Ollama
(
c
*
gin
.
Context
,
request
*
dto
.
GeneralOpenAIRequest
)
(
*
OllamaRequest
,
error
)
{
func
openAIChatToOllamaChat
(
c
*
gin
.
Context
,
r
*
dto
.
GeneralOpenAIRequest
)
(
*
OllamaChatRequest
,
error
)
{
messages
:=
make
([]
dto
.
Message
,
0
,
len
(
request
.
Messages
))
chatReq
:=
&
OllamaChatRequest
{
for
_
,
message
:=
range
request
.
Messages
{
Model
:
r
.
Model
,
if
!
message
.
IsStringContent
()
{
Stream
:
r
.
Stream
,
mediaMessages
:=
message
.
ParseContent
()
Options
:
map
[
string
]
any
{},
for
j
,
mediaMessage
:=
range
mediaMessages
{
Think
:
r
.
Think
,
if
mediaMessage
.
Type
==
dto
.
ContentTypeImageURL
{
}
imageUrl
:=
mediaMessage
.
GetImageMedia
()
if
r
.
ResponseFormat
!=
nil
{
// check if not base64
if
r
.
ResponseFormat
.
Type
==
"json"
{
if
strings
.
HasPrefix
(
imageUrl
.
Url
,
"http"
)
{
chatReq
.
Format
=
"json"
fileData
,
err
:=
service
.
GetFileBase64FromUrl
(
c
,
imageUrl
.
Url
,
"formatting image for Ollama"
)
}
else
if
r
.
ResponseFormat
.
Type
==
"json_schema"
{
if
err
!=
nil
{
if
len
(
r
.
ResponseFormat
.
JsonSchema
)
>
0
{
return
nil
,
err
var
schema
any
}
_
=
json
.
Unmarshal
(
r
.
ResponseFormat
.
JsonSchema
,
&
schema
)
imageUrl
.
Url
=
fmt
.
Sprintf
(
"data:%s;base64,%s"
,
fileData
.
MimeType
,
fileData
.
Base64Data
)
chatReq
.
Format
=
schema
}
}
mediaMessage
.
ImageUrl
=
imageUrl
}
mediaMessages
[
j
]
=
mediaMessage
}
}
}
// options mapping
message
.
SetMediaContent
(
mediaMessages
)
if
r
.
Temperature
!=
nil
{
chatReq
.
Options
[
"temperature"
]
=
r
.
Temperature
}
}
if
r
.
TopP
!=
0
{
chatReq
.
Options
[
"top_p"
]
=
r
.
TopP
}
messages
=
append
(
messages
,
dto
.
Message
{
if
r
.
TopK
!=
0
{
chatReq
.
Options
[
"top_k"
]
=
r
.
TopK
}
Role
:
message
.
Role
,
if
r
.
FrequencyPenalty
!=
0
{
chatReq
.
Options
[
"frequency_penalty"
]
=
r
.
FrequencyPenalty
}
Content
:
message
.
Content
,
if
r
.
PresencePenalty
!=
0
{
chatReq
.
Options
[
"presence_penalty"
]
=
r
.
PresencePenalty
}
ToolCalls
:
message
.
ToolCalls
,
if
r
.
Seed
!=
0
{
chatReq
.
Options
[
"seed"
]
=
int
(
r
.
Seed
)
}
ToolCallId
:
message
.
ToolCallId
,
if
mt
:=
r
.
GetMaxTokens
();
mt
!=
0
{
chatReq
.
Options
[
"num_predict"
]
=
int
(
mt
)
}
})
}
if
r
.
Stop
!=
nil
{
str
,
ok
:=
request
.
Stop
.
(
string
)
switch
v
:=
r
.
Stop
.
(
type
)
{
var
Stop
[]
string
case
string
:
if
ok
{
chatReq
.
Options
[
"stop"
]
=
[]
string
{
v
}
Stop
=
[]
string
{
str
}
case
[]
string
:
chatReq
.
Options
[
"stop"
]
=
v
case
[]
any
:
arr
:=
make
([]
string
,
0
,
len
(
v
))
for
_
,
i
:=
range
v
{
if
s
,
ok
:=
i
.
(
string
);
ok
{
arr
=
append
(
arr
,
s
)
}
}
if
len
(
arr
)
>
0
{
chatReq
.
Options
[
"stop"
]
=
arr
}
}
}
if
len
(
r
.
Tools
)
>
0
{
tools
:=
make
([]
OllamaTool
,
0
,
len
(
r
.
Tools
))
for
_
,
t
:=
range
r
.
Tools
{
tools
=
append
(
tools
,
OllamaTool
{
Type
:
"function"
,
Function
:
OllamaToolFunction
{
Name
:
t
.
Function
.
Name
,
Description
:
t
.
Function
.
Description
,
Parameters
:
t
.
Function
.
Parameters
}})
}
chatReq
.
Tools
=
tools
}
chatReq
.
Messages
=
make
([]
OllamaChatMessage
,
0
,
len
(
r
.
Messages
))
for
_
,
m
:=
range
r
.
Messages
{
var
textBuilder
strings
.
Builder
var
images
[]
string
if
m
.
IsStringContent
()
{
textBuilder
.
WriteString
(
m
.
StringContent
())
}
else
{
parts
:=
m
.
ParseContent
()
for
_
,
part
:=
range
parts
{
if
part
.
Type
==
dto
.
ContentTypeImageURL
{
img
:=
part
.
GetImageMedia
()
if
img
!=
nil
&&
img
.
Url
!=
""
{
var
base64Data
string
if
strings
.
HasPrefix
(
img
.
Url
,
"http"
)
{
fileData
,
err
:=
service
.
GetFileBase64FromUrl
(
c
,
img
.
Url
,
"fetch image for ollama chat"
)
if
err
!=
nil
{
return
nil
,
err
}
base64Data
=
fileData
.
Base64Data
}
else
if
strings
.
HasPrefix
(
img
.
Url
,
"data:"
)
{
if
idx
:=
strings
.
Index
(
img
.
Url
,
","
);
idx
!=
-
1
&&
idx
+
1
<
len
(
img
.
Url
)
{
base64Data
=
img
.
Url
[
idx
+
1
:
]
}
}
else
{
}
else
{
Stop
,
_
=
request
.
Stop
.
([]
string
)
base64Data
=
img
.
Url
}
}
ollamaRequest
:=
&
OllamaRequest
{
if
base64Data
!=
""
{
images
=
append
(
images
,
base64Data
)
}
Model
:
request
.
Model
,
}
Messages
:
messages
,
}
else
if
part
.
Type
==
dto
.
ContentTypeText
{
Stream
:
request
.
Stream
,
textBuilder
.
WriteString
(
part
.
Text
)
Temperature
:
request
.
Temperature
,
}
Seed
:
request
.
Seed
,
}
Topp
:
request
.
TopP
,
}
TopK
:
request
.
TopK
,
cm
:=
OllamaChatMessage
{
Role
:
m
.
Role
,
Content
:
textBuilder
.
String
()}
Stop
:
Stop
,
if
len
(
images
)
>
0
{
cm
.
Images
=
images
}
Tools
:
request
.
Tools
,
if
m
.
Role
==
"tool"
&&
m
.
Name
!=
nil
{
cm
.
ToolName
=
*
m
.
Name
}
MaxTokens
:
request
.
GetMaxTokens
(),
if
m
.
ToolCalls
!=
nil
&&
len
(
m
.
ToolCalls
)
>
0
{
ResponseFormat
:
request
.
ResponseFormat
,
parsed
:=
m
.
ParseToolCalls
()
FrequencyPenalty
:
request
.
FrequencyPenalty
,
if
len
(
parsed
)
>
0
{
PresencePenalty
:
request
.
PresencePenalty
,
calls
:=
make
([]
OllamaToolCall
,
0
,
len
(
parsed
))
Prompt
:
request
.
Prompt
,
for
_
,
tc
:=
range
parsed
{
StreamOptions
:
request
.
StreamOptions
,
var
args
interface
{}
Suffix
:
request
.
Suffix
,
if
tc
.
Function
.
Arguments
!=
""
{
_
=
json
.
Unmarshal
([]
byte
(
tc
.
Function
.
Arguments
),
&
args
)
}
}
if
args
==
nil
{
args
=
map
[
string
]
any
{}
}
ollamaRequest
.
Think
=
request
.
Think
oc
:=
OllamaToolCall
{}
return
ollamaRequest
,
nil
oc
.
Function
.
Name
=
tc
.
Function
.
Name
oc
.
Function
.
Arguments
=
args
calls
=
append
(
calls
,
oc
)
}
cm
.
ToolCalls
=
calls
}
}
chatReq
.
Messages
=
append
(
chatReq
.
Messages
,
cm
)
}
return
chatReq
,
nil
}
}
func
requestOpenAI2Embeddings
(
request
dto
.
EmbeddingRequest
)
*
OllamaEmbeddingRequest
{
// openAIToGenerate converts OpenAI completions request to Ollama generate
return
&
OllamaEmbeddingRequest
{
func
openAIToGenerate
(
c
*
gin
.
Context
,
r
*
dto
.
GeneralOpenAIRequest
)
(
*
OllamaGenerateRequest
,
error
)
{
Model
:
request
.
Model
,
gen
:=
&
OllamaGenerateRequest
{
Input
:
request
.
ParseInput
(),
Model
:
r
.
Model
,
Options
:
&
Options
{
Stream
:
r
.
Stream
,
Seed
:
int
(
request
.
Seed
),
Options
:
map
[
string
]
any
{},
Temperature
:
request
.
Temperature
,
Think
:
r
.
Think
,
TopP
:
request
.
TopP
,
}
FrequencyPenalty
:
request
.
FrequencyPenalty
,
// Prompt may be in r.Prompt (string or []any)
PresencePenalty
:
request
.
PresencePenalty
,
if
r
.
Prompt
!=
nil
{
},
switch
v
:=
r
.
Prompt
.
(
type
)
{
}
case
string
:
gen
.
Prompt
=
v
case
[]
any
:
var
sb
strings
.
Builder
for
_
,
it
:=
range
v
{
if
s
,
ok
:=
it
.
(
string
);
ok
{
sb
.
WriteString
(
s
)
}
}
gen
.
Prompt
=
sb
.
String
()
default
:
gen
.
Prompt
=
fmt
.
Sprintf
(
"%v"
,
r
.
Prompt
)
}
}
if
r
.
Suffix
!=
nil
{
if
s
,
ok
:=
r
.
Suffix
.
(
string
);
ok
{
gen
.
Suffix
=
s
}
}
if
r
.
ResponseFormat
!=
nil
{
if
r
.
ResponseFormat
.
Type
==
"json"
{
gen
.
Format
=
"json"
}
else
if
r
.
ResponseFormat
.
Type
==
"json_schema"
{
var
schema
any
;
_
=
json
.
Unmarshal
(
r
.
ResponseFormat
.
JsonSchema
,
&
schema
);
gen
.
Format
=
schema
}
}
if
r
.
Temperature
!=
nil
{
gen
.
Options
[
"temperature"
]
=
r
.
Temperature
}
if
r
.
TopP
!=
0
{
gen
.
Options
[
"top_p"
]
=
r
.
TopP
}
if
r
.
TopK
!=
0
{
gen
.
Options
[
"top_k"
]
=
r
.
TopK
}
if
r
.
FrequencyPenalty
!=
0
{
gen
.
Options
[
"frequency_penalty"
]
=
r
.
FrequencyPenalty
}
if
r
.
PresencePenalty
!=
0
{
gen
.
Options
[
"presence_penalty"
]
=
r
.
PresencePenalty
}
if
r
.
Seed
!=
0
{
gen
.
Options
[
"seed"
]
=
int
(
r
.
Seed
)
}
if
mt
:=
r
.
GetMaxTokens
();
mt
!=
0
{
gen
.
Options
[
"num_predict"
]
=
int
(
mt
)
}
if
r
.
Stop
!=
nil
{
switch
v
:=
r
.
Stop
.
(
type
)
{
case
string
:
gen
.
Options
[
"stop"
]
=
[]
string
{
v
}
case
[]
string
:
gen
.
Options
[
"stop"
]
=
v
case
[]
any
:
arr
:=
make
([]
string
,
0
,
len
(
v
));
for
_
,
i
:=
range
v
{
if
s
,
ok
:=
i
.
(
string
);
ok
{
arr
=
append
(
arr
,
s
)
}
};
if
len
(
arr
)
>
0
{
gen
.
Options
[
"stop"
]
=
arr
}
}
}
return
gen
,
nil
}
func
requestOpenAI2Embeddings
(
r
dto
.
EmbeddingRequest
)
*
OllamaEmbeddingRequest
{
opts
:=
map
[
string
]
any
{}
if
r
.
Temperature
!=
nil
{
opts
[
"temperature"
]
=
r
.
Temperature
}
if
r
.
TopP
!=
0
{
opts
[
"top_p"
]
=
r
.
TopP
}
if
r
.
FrequencyPenalty
!=
0
{
opts
[
"frequency_penalty"
]
=
r
.
FrequencyPenalty
}
if
r
.
PresencePenalty
!=
0
{
opts
[
"presence_penalty"
]
=
r
.
PresencePenalty
}
if
r
.
Seed
!=
0
{
opts
[
"seed"
]
=
int
(
r
.
Seed
)
}
if
r
.
Dimensions
!=
0
{
opts
[
"dimensions"
]
=
r
.
Dimensions
}
input
:=
r
.
ParseInput
()
if
len
(
input
)
==
1
{
return
&
OllamaEmbeddingRequest
{
Model
:
r
.
Model
,
Input
:
input
[
0
],
Options
:
opts
,
Dimensions
:
r
.
Dimensions
}
}
return
&
OllamaEmbeddingRequest
{
Model
:
r
.
Model
,
Input
:
input
,
Options
:
opts
,
Dimensions
:
r
.
Dimensions
}
}
}
func
ollamaEmbeddingHandler
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
resp
*
http
.
Response
)
(
*
dto
.
Usage
,
*
types
.
NewAPIError
)
{
func
ollamaEmbeddingHandler
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
resp
*
http
.
Response
)
(
*
dto
.
Usage
,
*
types
.
NewAPIError
)
{
var
ollamaEmbeddingResponse
OllamaEmbeddingResponse
var
oResp
OllamaEmbeddingResponse
responseBody
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
service
.
CloseResponseBodyGracefully
(
resp
)
service
.
CloseResponseBodyGracefully
(
resp
)
err
=
common
.
Unmarshal
(
responseBody
,
&
ollamaEmbeddingResponse
)
if
err
=
common
.
Unmarshal
(
body
,
&
oResp
);
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
if
err
!=
nil
{
if
oResp
.
Error
!=
""
{
return
nil
,
types
.
NewOpenAIError
(
fmt
.
Errorf
(
"ollama error: %s"
,
oResp
.
Error
),
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
data
:=
make
([]
dto
.
OpenAIEmbeddingResponseItem
,
0
,
len
(
oResp
.
Embeddings
))
}
for
i
,
emb
:=
range
oResp
.
Embeddings
{
data
=
append
(
data
,
dto
.
OpenAIEmbeddingResponseItem
{
Index
:
i
,
Object
:
"embedding"
,
Embedding
:
emb
})
}
if
ollamaEmbeddingResponse
.
Error
!=
""
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
oResp
.
PromptEvalCount
,
CompletionTokens
:
0
,
TotalTokens
:
oResp
.
PromptEvalCount
}
return
nil
,
types
.
NewOpenAIError
(
fmt
.
Errorf
(
"ollama error: %s"
,
ollamaEmbeddingResponse
.
Error
),
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
embResp
:=
&
dto
.
OpenAIEmbeddingResponse
{
Object
:
"list"
,
Data
:
data
,
Model
:
info
.
UpstreamModelName
,
Usage
:*
usage
}
}
out
,
_
:=
common
.
Marshal
(
embResp
)
flattenedEmbeddings
:=
flattenEmbeddings
(
ollamaEmbeddingResponse
.
Embedding
)
service
.
IOCopyBytesGracefully
(
c
,
resp
,
out
)
data
:=
make
([]
dto
.
OpenAIEmbeddingResponseItem
,
0
,
1
)
data
=
append
(
data
,
dto
.
OpenAIEmbeddingResponseItem
{
Embedding
:
flattenedEmbeddings
,
Object
:
"embedding"
,
})
usage
:=
&
dto
.
Usage
{
TotalTokens
:
info
.
PromptTokens
,
CompletionTokens
:
0
,
PromptTokens
:
info
.
PromptTokens
,
}
embeddingResponse
:=
&
dto
.
OpenAIEmbeddingResponse
{
Object
:
"list"
,
Data
:
data
,
Model
:
info
.
UpstreamModelName
,
Usage
:
*
usage
,
}
doResponseBody
,
err
:=
common
.
Marshal
(
embeddingResponse
)
if
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
service
.
IOCopyBytesGracefully
(
c
,
resp
,
doResponseBody
)
return
usage
,
nil
return
usage
,
nil
}
}
func
flattenEmbeddings
(
embeddings
[][]
float64
)
[]
float64
{
flattened
:=
[]
float64
{}
for
_
,
row
:=
range
embeddings
{
flattened
=
append
(
flattened
,
row
...
)
}
return
flattened
}
relay/channel/ollama/stream.go
0 → 100644
View file @
cbbe4cb4
package
ollama
import
(
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/logger"
relaycommon
"one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
"one-api/types"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type
ollamaChatStreamChunk
struct
{
Model
string
`json:"model"`
CreatedAt
string
`json:"created_at"`
// chat
Message
*
struct
{
Role
string
`json:"role"`
Content
string
`json:"content"`
Thinking
json
.
RawMessage
`json:"thinking"`
ToolCalls
[]
struct
{
Function
struct
{
Name
string
`json:"name"`
Arguments
interface
{}
`json:"arguments"`
}
`json:"function"`
}
`json:"tool_calls"`
}
`json:"message"`
// generate
Response
string
`json:"response"`
Done
bool
`json:"done"`
DoneReason
string
`json:"done_reason"`
TotalDuration
int64
`json:"total_duration"`
LoadDuration
int64
`json:"load_duration"`
PromptEvalCount
int
`json:"prompt_eval_count"`
EvalCount
int
`json:"eval_count"`
PromptEvalDuration
int64
`json:"prompt_eval_duration"`
EvalDuration
int64
`json:"eval_duration"`
}
func
toUnix
(
ts
string
)
int64
{
if
ts
==
""
{
return
time
.
Now
()
.
Unix
()
}
// try time.RFC3339 or with nanoseconds
t
,
err
:=
time
.
Parse
(
time
.
RFC3339Nano
,
ts
)
if
err
!=
nil
{
t2
,
err2
:=
time
.
Parse
(
time
.
RFC3339
,
ts
);
if
err2
==
nil
{
return
t2
.
Unix
()
};
return
time
.
Now
()
.
Unix
()
}
return
t
.
Unix
()
}
func
ollamaStreamHandler
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
resp
*
http
.
Response
)
(
*
dto
.
Usage
,
*
types
.
NewAPIError
)
{
if
resp
==
nil
||
resp
.
Body
==
nil
{
return
nil
,
types
.
NewOpenAIError
(
fmt
.
Errorf
(
"empty response"
),
types
.
ErrorCodeBadResponse
,
http
.
StatusBadRequest
)
}
defer
service
.
CloseResponseBodyGracefully
(
resp
)
helper
.
SetEventStreamHeaders
(
c
)
scanner
:=
bufio
.
NewScanner
(
resp
.
Body
)
usage
:=
&
dto
.
Usage
{}
var
model
=
info
.
UpstreamModelName
var
responseId
=
common
.
GetUUID
()
var
created
=
time
.
Now
()
.
Unix
()
var
toolCallIndex
int
start
:=
helper
.
GenerateStartEmptyResponse
(
responseId
,
created
,
model
,
nil
)
if
data
,
err
:=
common
.
Marshal
(
start
);
err
==
nil
{
_
=
helper
.
StringData
(
c
,
string
(
data
))
}
for
scanner
.
Scan
()
{
line
:=
scanner
.
Text
()
line
=
strings
.
TrimSpace
(
line
)
if
line
==
""
{
continue
}
var
chunk
ollamaChatStreamChunk
if
err
:=
json
.
Unmarshal
([]
byte
(
line
),
&
chunk
);
err
!=
nil
{
logger
.
LogError
(
c
,
"ollama stream json decode error: "
+
err
.
Error
()
+
" line="
+
line
)
return
usage
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
if
chunk
.
Model
!=
""
{
model
=
chunk
.
Model
}
created
=
toUnix
(
chunk
.
CreatedAt
)
if
!
chunk
.
Done
{
// delta content
var
content
string
if
chunk
.
Message
!=
nil
{
content
=
chunk
.
Message
.
Content
}
else
{
content
=
chunk
.
Response
}
delta
:=
dto
.
ChatCompletionsStreamResponse
{
Id
:
responseId
,
Object
:
"chat.completion.chunk"
,
Created
:
created
,
Model
:
model
,
Choices
:
[]
dto
.
ChatCompletionsStreamResponseChoice
{
{
Index
:
0
,
Delta
:
dto
.
ChatCompletionsStreamResponseChoiceDelta
{
Role
:
"assistant"
},
}
},
}
if
content
!=
""
{
delta
.
Choices
[
0
]
.
Delta
.
SetContentString
(
content
)
}
if
chunk
.
Message
!=
nil
&&
len
(
chunk
.
Message
.
Thinking
)
>
0
{
raw
:=
strings
.
TrimSpace
(
string
(
chunk
.
Message
.
Thinking
))
if
raw
!=
""
&&
raw
!=
"null"
{
delta
.
Choices
[
0
]
.
Delta
.
SetReasoningContent
(
raw
)
}
}
// tool calls
if
chunk
.
Message
!=
nil
&&
len
(
chunk
.
Message
.
ToolCalls
)
>
0
{
delta
.
Choices
[
0
]
.
Delta
.
ToolCalls
=
make
([]
dto
.
ToolCallResponse
,
0
,
len
(
chunk
.
Message
.
ToolCalls
))
for
_
,
tc
:=
range
chunk
.
Message
.
ToolCalls
{
// arguments -> string
argBytes
,
_
:=
json
.
Marshal
(
tc
.
Function
.
Arguments
)
toolId
:=
fmt
.
Sprintf
(
"call_%d"
,
toolCallIndex
)
tr
:=
dto
.
ToolCallResponse
{
ID
:
toolId
,
Type
:
"function"
,
Function
:
dto
.
FunctionResponse
{
Name
:
tc
.
Function
.
Name
,
Arguments
:
string
(
argBytes
)}}
tr
.
SetIndex
(
toolCallIndex
)
toolCallIndex
++
delta
.
Choices
[
0
]
.
Delta
.
ToolCalls
=
append
(
delta
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
tr
)
}
}
if
data
,
err
:=
common
.
Marshal
(
delta
);
err
==
nil
{
_
=
helper
.
StringData
(
c
,
string
(
data
))
}
continue
}
// done frame
// finalize once and break loop
usage
.
PromptTokens
=
chunk
.
PromptEvalCount
usage
.
CompletionTokens
=
chunk
.
EvalCount
usage
.
TotalTokens
=
usage
.
PromptTokens
+
usage
.
CompletionTokens
finishReason
:=
chunk
.
DoneReason
if
finishReason
==
""
{
finishReason
=
"stop"
}
// emit stop delta
if
stop
:=
helper
.
GenerateStopResponse
(
responseId
,
created
,
model
,
finishReason
);
stop
!=
nil
{
if
data
,
err
:=
common
.
Marshal
(
stop
);
err
==
nil
{
_
=
helper
.
StringData
(
c
,
string
(
data
))
}
}
// emit usage frame
if
final
:=
helper
.
GenerateFinalUsageResponse
(
responseId
,
created
,
model
,
*
usage
);
final
!=
nil
{
if
data
,
err
:=
common
.
Marshal
(
final
);
err
==
nil
{
_
=
helper
.
StringData
(
c
,
string
(
data
))
}
}
// send [DONE]
helper
.
Done
(
c
)
break
}
if
err
:=
scanner
.
Err
();
err
!=
nil
&&
err
!=
io
.
EOF
{
logger
.
LogError
(
c
,
"ollama stream scan error: "
+
err
.
Error
())
}
return
usage
,
nil
}
// non-stream handler for chat/generate
func
ollamaChatHandler
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
resp
*
http
.
Response
)
(
*
dto
.
Usage
,
*
types
.
NewAPIError
)
{
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeReadResponseBodyFailed
,
http
.
StatusInternalServerError
)
}
service
.
CloseResponseBodyGracefully
(
resp
)
raw
:=
string
(
body
)
if
common
.
DebugEnabled
{
println
(
"ollama non-stream raw resp:"
,
raw
)
}
lines
:=
strings
.
Split
(
raw
,
"
\n
"
)
var
(
aggContent
strings
.
Builder
reasoningBuilder
strings
.
Builder
lastChunk
ollamaChatStreamChunk
parsedAny
bool
)
for
_
,
ln
:=
range
lines
{
ln
=
strings
.
TrimSpace
(
ln
)
if
ln
==
""
{
continue
}
var
ck
ollamaChatStreamChunk
if
err
:=
json
.
Unmarshal
([]
byte
(
ln
),
&
ck
);
err
!=
nil
{
if
len
(
lines
)
==
1
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
continue
}
parsedAny
=
true
lastChunk
=
ck
if
ck
.
Message
!=
nil
&&
len
(
ck
.
Message
.
Thinking
)
>
0
{
raw
:=
strings
.
TrimSpace
(
string
(
ck
.
Message
.
Thinking
))
if
raw
!=
""
&&
raw
!=
"null"
{
reasoningBuilder
.
WriteString
(
raw
)
}
}
if
ck
.
Message
!=
nil
&&
ck
.
Message
.
Content
!=
""
{
aggContent
.
WriteString
(
ck
.
Message
.
Content
)
}
else
if
ck
.
Response
!=
""
{
aggContent
.
WriteString
(
ck
.
Response
)
}
}
if
!
parsedAny
{
var
single
ollamaChatStreamChunk
if
err
:=
json
.
Unmarshal
(
body
,
&
single
);
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
lastChunk
=
single
if
single
.
Message
!=
nil
{
if
len
(
single
.
Message
.
Thinking
)
>
0
{
raw
:=
strings
.
TrimSpace
(
string
(
single
.
Message
.
Thinking
));
if
raw
!=
""
&&
raw
!=
"null"
{
reasoningBuilder
.
WriteString
(
raw
)
}
}
aggContent
.
WriteString
(
single
.
Message
.
Content
)
}
else
{
aggContent
.
WriteString
(
single
.
Response
)
}
}
model
:=
lastChunk
.
Model
if
model
==
""
{
model
=
info
.
UpstreamModelName
}
created
:=
toUnix
(
lastChunk
.
CreatedAt
)
usage
:=
&
dto
.
Usage
{
PromptTokens
:
lastChunk
.
PromptEvalCount
,
CompletionTokens
:
lastChunk
.
EvalCount
,
TotalTokens
:
lastChunk
.
PromptEvalCount
+
lastChunk
.
EvalCount
}
content
:=
aggContent
.
String
()
finishReason
:=
lastChunk
.
DoneReason
if
finishReason
==
""
{
finishReason
=
"stop"
}
msg
:=
dto
.
Message
{
Role
:
"assistant"
,
Content
:
contentPtr
(
content
)}
if
rc
:=
reasoningBuilder
.
String
();
rc
!=
""
{
msg
.
ReasoningContent
=
rc
}
full
:=
dto
.
OpenAITextResponse
{
Id
:
common
.
GetUUID
(),
Model
:
model
,
Object
:
"chat.completion"
,
Created
:
created
,
Choices
:
[]
dto
.
OpenAITextResponseChoice
{
{
Index
:
0
,
Message
:
msg
,
FinishReason
:
finishReason
,
}
},
Usage
:
*
usage
,
}
out
,
_
:=
common
.
Marshal
(
full
)
service
.
IOCopyBytesGracefully
(
c
,
resp
,
out
)
return
usage
,
nil
}
func
contentPtr
(
s
string
)
*
string
{
if
s
==
""
{
return
nil
};
return
&
s
}
relay/channel/openai/relay-openai.go
View file @
cbbe4cb4
...
@@ -12,6 +12,7 @@ import (
...
@@ -12,6 +12,7 @@ import (
"one-api/constant"
"one-api/constant"
"one-api/dto"
"one-api/dto"
"one-api/logger"
"one-api/logger"
"one-api/relay/channel/openrouter"
relaycommon
"one-api/relay/common"
relaycommon
"one-api/relay/common"
"one-api/relay/helper"
"one-api/relay/helper"
"one-api/service"
"one-api/service"
...
@@ -185,10 +186,27 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
...
@@ -185,10 +186,27 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
if
common
.
DebugEnabled
{
if
common
.
DebugEnabled
{
println
(
"upstream response body:"
,
string
(
responseBody
))
println
(
"upstream response body:"
,
string
(
responseBody
))
}
}
// Unmarshal to simpleResponse
if
info
.
ChannelType
==
constant
.
ChannelTypeOpenRouter
&&
info
.
ChannelOtherSettings
.
IsOpenRouterEnterprise
()
{
// 尝试解析为 openrouter enterprise
var
enterpriseResponse
openrouter
.
OpenRouterEnterpriseResponse
err
=
common
.
Unmarshal
(
responseBody
,
&
enterpriseResponse
)
if
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
if
enterpriseResponse
.
Success
{
responseBody
=
enterpriseResponse
.
Data
}
else
{
logger
.
LogError
(
c
,
fmt
.
Sprintf
(
"openrouter enterprise response success=false, data: %s"
,
enterpriseResponse
.
Data
))
return
nil
,
types
.
NewOpenAIError
(
fmt
.
Errorf
(
"openrouter response success=false"
),
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
}
err
=
common
.
Unmarshal
(
responseBody
,
&
simpleResponse
)
err
=
common
.
Unmarshal
(
responseBody
,
&
simpleResponse
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
}
if
oaiError
:=
simpleResponse
.
GetOpenAIError
();
oaiError
!=
nil
&&
oaiError
.
Type
!=
""
{
if
oaiError
:=
simpleResponse
.
GetOpenAIError
();
oaiError
!=
nil
&&
oaiError
.
Type
!=
""
{
return
nil
,
types
.
WithOpenAIError
(
*
oaiError
,
resp
.
StatusCode
)
return
nil
,
types
.
WithOpenAIError
(
*
oaiError
,
resp
.
StatusCode
)
}
}
...
...
relay/channel/openrouter/dto.go
View file @
cbbe4cb4
package
openrouter
package
openrouter
import
"encoding/json"
type
RequestReasoning
struct
{
type
RequestReasoning
struct
{
// One of the following (not both):
// One of the following (not both):
Effort
string
`json:"effort,omitempty"`
// Can be "high", "medium", or "low" (OpenAI-style)
Effort
string
`json:"effort,omitempty"`
// Can be "high", "medium", or "low" (OpenAI-style)
...
@@ -7,3 +9,8 @@ type RequestReasoning struct {
...
@@ -7,3 +9,8 @@ type RequestReasoning struct {
// Optional: Default is false. All models support this.
// Optional: Default is false. All models support this.
Exclude
bool
`json:"exclude,omitempty"`
// Set to true to exclude reasoning tokens from response
Exclude
bool
`json:"exclude,omitempty"`
// Set to true to exclude reasoning tokens from response
}
}
type
OpenRouterEnterpriseResponse
struct
{
Data
json
.
RawMessage
`json:"data"`
Success
bool
`json:"success"`
}
relay/channel/volcengine/adaptor.go
View file @
cbbe4cb4
...
@@ -9,6 +9,7 @@ import (
...
@@ -9,6 +9,7 @@ import (
"mime/multipart"
"mime/multipart"
"net/http"
"net/http"
"net/textproto"
"net/textproto"
channelconstant
"one-api/constant"
"one-api/dto"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel"
"one-api/relay/channel/openai"
"one-api/relay/channel/openai"
...
@@ -188,20 +189,26 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
...
@@ -188,20 +189,26 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
// 支持自定义域名,如果未设置则使用默认域名
baseUrl
:=
info
.
ChannelBaseUrl
if
baseUrl
==
""
{
baseUrl
=
channelconstant
.
ChannelBaseURLs
[
channelconstant
.
ChannelTypeVolcEngine
]
}
switch
info
.
RelayMode
{
switch
info
.
RelayMode
{
case
constant
.
RelayModeChatCompletions
:
case
constant
.
RelayModeChatCompletions
:
if
strings
.
HasPrefix
(
info
.
UpstreamModelName
,
"bot"
)
{
if
strings
.
HasPrefix
(
info
.
UpstreamModelName
,
"bot"
)
{
return
fmt
.
Sprintf
(
"%s/api/v3/bots/chat/completions"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/bots/chat/completions"
,
b
aseUrl
),
nil
}
}
return
fmt
.
Sprintf
(
"%s/api/v3/chat/completions"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/chat/completions"
,
b
aseUrl
),
nil
case
constant
.
RelayModeEmbeddings
:
case
constant
.
RelayModeEmbeddings
:
return
fmt
.
Sprintf
(
"%s/api/v3/embeddings"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/embeddings"
,
b
aseUrl
),
nil
case
constant
.
RelayModeImagesGenerations
:
case
constant
.
RelayModeImagesGenerations
:
return
fmt
.
Sprintf
(
"%s/api/v3/images/generations"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/images/generations"
,
b
aseUrl
),
nil
case
constant
.
RelayModeImagesEdits
:
case
constant
.
RelayModeImagesEdits
:
return
fmt
.
Sprintf
(
"%s/api/v3/images/edits"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/images/edits"
,
b
aseUrl
),
nil
case
constant
.
RelayModeRerank
:
case
constant
.
RelayModeRerank
:
return
fmt
.
Sprintf
(
"%s/api/v3/rerank"
,
info
.
ChannelB
aseUrl
),
nil
return
fmt
.
Sprintf
(
"%s/api/v3/rerank"
,
b
aseUrl
),
nil
default
:
default
:
}
}
return
""
,
fmt
.
Errorf
(
"unsupported relay mode: %d"
,
info
.
RelayMode
)
return
""
,
fmt
.
Errorf
(
"unsupported relay mode: %d"
,
info
.
RelayMode
)
...
...
relay/channel/volcengine/constants.go
View file @
cbbe4cb4
...
@@ -9,6 +9,11 @@ var ModelList = []string{
...
@@ -9,6 +9,11 @@ var ModelList = []string{
"Doubao-lite-4k"
,
"Doubao-lite-4k"
,
"Doubao-embedding"
,
"Doubao-embedding"
,
"doubao-seedream-4-0-250828"
,
"doubao-seedream-4-0-250828"
,
"seedream-4-0-250828"
,
"doubao-seedance-1-0-pro-250528"
,
"seedance-1-0-pro-250528"
,
"doubao-seed-1-6-thinking-250715"
,
"seed-1-6-thinking-250715"
,
}
}
var
ChannelName
=
"volcengine"
var
ChannelName
=
"volcengine"
relay/channel/xunfei/relay-xunfei.go
View file @
cbbe4cb4
...
@@ -207,10 +207,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
...
@@ -207,10 +207,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
return
nil
,
nil
,
err
return
nil
,
nil
,
err
}
}
defer
func
()
{
conn
.
Close
()
}()
data
:=
requestOpenAI2Xunfei
(
textRequest
,
appId
,
domain
)
data
:=
requestOpenAI2Xunfei
(
textRequest
,
appId
,
domain
)
err
=
conn
.
WriteJSON
(
data
)
err
=
conn
.
WriteJSON
(
data
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -220,6 +216,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
...
@@ -220,6 +216,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
dataChan
:=
make
(
chan
XunfeiChatResponse
)
dataChan
:=
make
(
chan
XunfeiChatResponse
)
stopChan
:=
make
(
chan
bool
)
stopChan
:=
make
(
chan
bool
)
go
func
()
{
go
func
()
{
defer
func
()
{
conn
.
Close
()
}()
for
{
for
{
_
,
msg
,
err
:=
conn
.
ReadMessage
()
_
,
msg
,
err
:=
conn
.
ReadMessage
()
if
err
!=
nil
{
if
err
!=
nil
{
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
cbbe4cb4
...
@@ -164,6 +164,8 @@ const EditChannelModal = (props) => {
...
@@ -164,6 +164,8 @@ const EditChannelModal = (props) => {
settings
:
''
,
settings
:
''
,
// 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
// 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
vertex_key_type
:
'json'
,
vertex_key_type
:
'json'
,
// 企业账户设置
is_enterprise_account
:
false
,
};
};
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
multiToSingle
,
setMultiToSingle
]
=
useState
(
false
);
const
[
multiToSingle
,
setMultiToSingle
]
=
useState
(
false
);
...
@@ -189,6 +191,7 @@ const EditChannelModal = (props) => {
...
@@ -189,6 +191,7 @@ const EditChannelModal = (props) => {
const
[
channelSearchValue
,
setChannelSearchValue
]
=
useState
(
''
);
const
[
channelSearchValue
,
setChannelSearchValue
]
=
useState
(
''
);
const
[
useManualInput
,
setUseManualInput
]
=
useState
(
false
);
// 是否使用手动输入模式
const
[
useManualInput
,
setUseManualInput
]
=
useState
(
false
);
// 是否使用手动输入模式
const
[
keyMode
,
setKeyMode
]
=
useState
(
'append'
);
// 密钥模式:replace(覆盖)或 append(追加)
const
[
keyMode
,
setKeyMode
]
=
useState
(
'append'
);
// 密钥模式:replace(覆盖)或 append(追加)
const
[
isEnterpriseAccount
,
setIsEnterpriseAccount
]
=
useState
(
false
);
// 是否为企业账户
// 2FA验证查看密钥相关状态
// 2FA验证查看密钥相关状态
const
[
twoFAState
,
setTwoFAState
]
=
useState
({
const
[
twoFAState
,
setTwoFAState
]
=
useState
({
...
@@ -235,7 +238,7 @@ const EditChannelModal = (props) => {
...
@@ -235,7 +238,7 @@ const EditChannelModal = (props) => {
pass_through_body_enabled
:
false
,
pass_through_body_enabled
:
false
,
system_prompt
:
''
,
system_prompt
:
''
,
});
});
const
showApiConfigCard
=
inputs
.
type
!==
45
;
// 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示)
const
showApiConfigCard
=
true
;
// 控制是否显示 API 配置卡片
const
getInitValues
=
()
=>
({
...
originInputs
});
const
getInitValues
=
()
=>
({
...
originInputs
});
// 处理渠道额外设置的更新
// 处理渠道额外设置的更新
...
@@ -342,6 +345,10 @@ const EditChannelModal = (props) => {
...
@@ -342,6 +345,10 @@ const EditChannelModal = (props) => {
case
36
:
case
36
:
localModels
=
[
'suno_music'
,
'suno_lyrics'
];
localModels
=
[
'suno_music'
,
'suno_lyrics'
];
break
;
break
;
case
45
:
localModels
=
getChannelModels
(
value
);
setInputs
((
prevInputs
)
=>
({
...
prevInputs
,
base_url
:
'https://ark.cn-beijing.volces.com'
}));
break
;
default
:
default
:
localModels
=
getChannelModels
(
value
);
localModels
=
getChannelModels
(
value
);
break
;
break
;
...
@@ -433,15 +440,19 @@ const EditChannelModal = (props) => {
...
@@ -433,15 +440,19 @@ const EditChannelModal = (props) => {
parsedSettings
.
azure_responses_version
||
''
;
parsedSettings
.
azure_responses_version
||
''
;
// 读取 Vertex 密钥格式
// 读取 Vertex 密钥格式
data
.
vertex_key_type
=
parsedSettings
.
vertex_key_type
||
'json'
;
data
.
vertex_key_type
=
parsedSettings
.
vertex_key_type
||
'json'
;
// 读取企业账户设置
data
.
is_enterprise_account
=
parsedSettings
.
openrouter_enterprise
===
true
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'解析其他设置失败:'
,
error
);
console
.
error
(
'解析其他设置失败:'
,
error
);
data
.
azure_responses_version
=
''
;
data
.
azure_responses_version
=
''
;
data
.
region
=
''
;
data
.
region
=
''
;
data
.
vertex_key_type
=
'json'
;
data
.
vertex_key_type
=
'json'
;
data
.
is_enterprise_account
=
false
;
}
}
}
else
{
}
else
{
// 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
// 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
data
.
vertex_key_type
=
'json'
;
data
.
vertex_key_type
=
'json'
;
data
.
is_enterprise_account
=
false
;
}
}
setInputs
(
data
);
setInputs
(
data
);
...
@@ -453,6 +464,8 @@ const EditChannelModal = (props) => {
...
@@ -453,6 +464,8 @@ const EditChannelModal = (props) => {
}
else
{
}
else
{
setAutoBan
(
true
);
setAutoBan
(
true
);
}
}
// 同步企业账户状态
setIsEnterpriseAccount
(
data
.
is_enterprise_account
||
false
);
setBasicModels
(
getChannelModels
(
data
.
type
));
setBasicModels
(
getChannelModels
(
data
.
type
));
// 同步更新channelSettings状态显示
// 同步更新channelSettings状态显示
setChannelSettings
({
setChannelSettings
({
...
@@ -712,6 +725,8 @@ const EditChannelModal = (props) => {
...
@@ -712,6 +725,8 @@ const EditChannelModal = (props) => {
});
});
// 重置密钥模式状态
// 重置密钥模式状态
setKeyMode
(
'append'
);
setKeyMode
(
'append'
);
// 重置企业账户状态
setIsEnterpriseAccount
(
false
);
// 清空表单中的key_mode字段
// 清空表单中的key_mode字段
if
(
formApiRef
.
current
)
{
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key_mode'
,
undefined
);
formApiRef
.
current
.
setValue
(
'key_mode'
,
undefined
);
...
@@ -842,6 +857,10 @@ const EditChannelModal = (props) => {
...
@@ -842,6 +857,10 @@ const EditChannelModal = (props) => {
showInfo
(
t
(
'请至少选择一个模型!'
));
showInfo
(
t
(
'请至少选择一个模型!'
));
return
;
return
;
}
}
if
(
localInputs
.
type
===
45
&&
(
!
localInputs
.
base_url
||
localInputs
.
base_url
.
trim
()
===
''
))
{
showInfo
(
t
(
'请输入API地址!'
));
return
;
}
if
(
if
(
localInputs
.
model_mapping
&&
localInputs
.
model_mapping
&&
localInputs
.
model_mapping
!==
''
&&
localInputs
.
model_mapping
!==
''
&&
...
@@ -871,6 +890,21 @@ const EditChannelModal = (props) => {
...
@@ -871,6 +890,21 @@ const EditChannelModal = (props) => {
};
};
localInputs
.
setting
=
JSON
.
stringify
(
channelExtraSettings
);
localInputs
.
setting
=
JSON
.
stringify
(
channelExtraSettings
);
// 处理type === 20的企业账户设置
if
(
localInputs
.
type
===
20
)
{
let
settings
=
{};
if
(
localInputs
.
settings
)
{
try
{
settings
=
JSON
.
parse
(
localInputs
.
settings
);
}
catch
(
error
)
{
console
.
error
(
'解析settings失败:'
,
error
);
}
}
// 设置企业账户标识,无论是true还是false都要传到后端
settings
.
openrouter_enterprise
=
localInputs
.
is_enterprise_account
===
true
;
localInputs
.
settings
=
JSON
.
stringify
(
settings
);
}
// 清理不需要发送到后端的字段
// 清理不需要发送到后端的字段
delete
localInputs
.
force_format
;
delete
localInputs
.
force_format
;
delete
localInputs
.
thinking_to_content
;
delete
localInputs
.
thinking_to_content
;
...
@@ -878,6 +912,7 @@ const EditChannelModal = (props) => {
...
@@ -878,6 +912,7 @@ const EditChannelModal = (props) => {
delete
localInputs
.
pass_through_body_enabled
;
delete
localInputs
.
pass_through_body_enabled
;
delete
localInputs
.
system_prompt
;
delete
localInputs
.
system_prompt
;
delete
localInputs
.
system_prompt_override
;
delete
localInputs
.
system_prompt_override
;
delete
localInputs
.
is_enterprise_account
;
// 顶层的 vertex_key_type 不应发送给后端
// 顶层的 vertex_key_type 不应发送给后端
delete
localInputs
.
vertex_key_type
;
delete
localInputs
.
vertex_key_type
;
...
@@ -1195,6 +1230,21 @@ const EditChannelModal = (props) => {
...
@@ -1195,6 +1230,21 @@ const EditChannelModal = (props) => {
onChange=
{
(
value
)
=>
handleInputChange
(
'type'
,
value
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'type'
,
value
)
}
/>
/>
{
inputs
.
type
===
20
&&
(
<
Form
.
Switch
field=
'is_enterprise_account'
label=
{
t
(
'是否为企业账户'
)
}
checkedText=
{
t
(
'是'
)
}
uncheckedText=
{
t
(
'否'
)
}
onChange=
{
(
value
)
=>
{
setIsEnterpriseAccount
(
value
);
handleInputChange
(
'is_enterprise_account'
,
value
);
}
}
extraText=
{
t
(
'企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选'
)
}
initValue=
{
inputs
.
is_enterprise_account
}
/>
)
}
<
Form
.
Input
<
Form
.
Input
field=
'name'
field=
'name'
label=
{
t
(
'名称'
)
}
label=
{
t
(
'名称'
)
}
...
@@ -1809,6 +1859,30 @@ const EditChannelModal = (props) => {
...
@@ -1809,6 +1859,30 @@ const EditChannelModal = (props) => {
/>
/>
</
div
>
</
div
>
)
}
)
}
{
inputs
.
type
===
45
&&
(
<
div
>
<
Form
.
Select
field=
'base_url'
label=
{
t
(
'API地址'
)
}
placeholder=
{
t
(
'请选择API地址'
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
optionList=
{
[
{
value
:
'https://ark.cn-beijing.volces.com'
,
label
:
'https://ark.cn-beijing.volces.com'
},
{
value
:
'https://ark.ap-southeast.bytepluses.com'
,
label
:
'https://ark.ap-southeast.bytepluses.com'
}
]
}
defaultValue=
'https://ark.cn-beijing.volces.com'
/>
</
div
>
)
}
</
Card
>
</
Card
>
)
}
)
}
...
...
web/src/hooks/channels/useChannelsData.jsx
View file @
cbbe4cb4
...
@@ -25,13 +25,9 @@ import {
...
@@ -25,13 +25,9 @@ import {
showInfo
,
showInfo
,
showSuccess
,
showSuccess
,
loadChannelModels
,
loadChannelModels
,
copy
,
copy
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
import
{
CHANNEL_OPTIONS
,
ITEMS_PER_PAGE
,
MODEL_TABLE_PAGE_SIZE
}
from
'../../constants'
;
CHANNEL_OPTIONS
,
ITEMS_PER_PAGE
,
MODEL_TABLE_PAGE_SIZE
,
}
from
'../../constants'
;
import
{
useIsMobile
}
from
'../common/useIsMobile'
;
import
{
useIsMobile
}
from
'../common/useIsMobile'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
...
@@ -68,7 +64,7 @@ export const useChannelsData = () => {
...
@@ -68,7 +64,7 @@ export const useChannelsData = () => {
// Status filter
// Status filter
const
[
statusFilter
,
setStatusFilter
]
=
useState
(
const
[
statusFilter
,
setStatusFilter
]
=
useState
(
localStorage
.
getItem
(
'channel-status-filter'
)
||
'all'
,
localStorage
.
getItem
(
'channel-status-filter'
)
||
'all'
);
);
// Type tabs states
// Type tabs states
...
@@ -83,10 +79,11 @@ export const useChannelsData = () => {
...
@@ -83,10 +79,11 @@ export const useChannelsData = () => {
const
[
testingModels
,
setTestingModels
]
=
useState
(
new
Set
());
const
[
testingModels
,
setTestingModels
]
=
useState
(
new
Set
());
const
[
selectedModelKeys
,
setSelectedModelKeys
]
=
useState
([]);
const
[
selectedModelKeys
,
setSelectedModelKeys
]
=
useState
([]);
const
[
isBatchTesting
,
setIsBatchTesting
]
=
useState
(
false
);
const
[
isBatchTesting
,
setIsBatchTesting
]
=
useState
(
false
);
const
[
testQueue
,
setTestQueue
]
=
useState
([]);
const
[
isProcessingQueue
,
setIsProcessingQueue
]
=
useState
(
false
);
const
[
modelTablePage
,
setModelTablePage
]
=
useState
(
1
);
const
[
modelTablePage
,
setModelTablePage
]
=
useState
(
1
);
// 使用 ref 来避免闭包问题,类似旧版实现
const
shouldStopBatchTestingRef
=
useRef
(
false
);
// Multi-key management states
// Multi-key management states
const
[
showMultiKeyManageModal
,
setShowMultiKeyManageModal
]
=
useState
(
false
);
const
[
showMultiKeyManageModal
,
setShowMultiKeyManageModal
]
=
useState
(
false
);
const
[
currentMultiKeyChannel
,
setCurrentMultiKeyChannel
]
=
useState
(
null
);
const
[
currentMultiKeyChannel
,
setCurrentMultiKeyChannel
]
=
useState
(
null
);
...
@@ -119,12 +116,9 @@ export const useChannelsData = () => {
...
@@ -119,12 +116,9 @@ export const useChannelsData = () => {
// Initialize from localStorage
// Initialize from localStorage
useEffect
(()
=>
{
useEffect
(()
=>
{
const
localIdSort
=
localStorage
.
getItem
(
'id-sort'
)
===
'true'
;
const
localIdSort
=
localStorage
.
getItem
(
'id-sort'
)
===
'true'
;
const
localPageSize
=
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
const
localEnableTagMode
=
localStorage
.
getItem
(
'enable-tag-mode'
)
===
'true'
;
const
localEnableTagMode
=
const
localEnableBatchDelete
=
localStorage
.
getItem
(
'enable-batch-delete'
)
===
'true'
;
localStorage
.
getItem
(
'enable-tag-mode'
)
===
'true'
;
const
localEnableBatchDelete
=
localStorage
.
getItem
(
'enable-batch-delete'
)
===
'true'
;
setIdSort
(
localIdSort
);
setIdSort
(
localIdSort
);
setPageSize
(
localPageSize
);
setPageSize
(
localPageSize
);
...
@@ -182,10 +176,7 @@ export const useChannelsData = () => {
...
@@ -182,10 +176,7 @@ export const useChannelsData = () => {
// Save column preferences
// Save column preferences
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
Object
.
keys
(
visibleColumns
).
length
>
0
)
{
if
(
Object
.
keys
(
visibleColumns
).
length
>
0
)
{
localStorage
.
setItem
(
localStorage
.
setItem
(
'channels-table-columns'
,
JSON
.
stringify
(
visibleColumns
));
'channels-table-columns'
,
JSON
.
stringify
(
visibleColumns
),
);
}
}
},
[
visibleColumns
]);
},
[
visibleColumns
]);
...
@@ -299,21 +290,14 @@ export const useChannelsData = () => {
...
@@ -299,21 +290,14 @@ export const useChannelsData = () => {
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
if
(
searchKeyword
!==
''
||
searchGroup
!==
''
||
searchModel
!==
''
)
{
if
(
searchKeyword
!==
''
||
searchGroup
!==
''
||
searchModel
!==
''
)
{
setLoading
(
true
);
setLoading
(
true
);
await
searchChannels
(
await
searchChannels
(
enableTagMode
,
typeKey
,
statusF
,
page
,
pageSize
,
idSort
);
enableTagMode
,
typeKey
,
statusF
,
page
,
pageSize
,
idSort
,
);
setLoading
(
false
);
setLoading
(
false
);
return
;
return
;
}
}
const
reqId
=
++
requestCounter
.
current
;
const
reqId
=
++
requestCounter
.
current
;
setLoading
(
true
);
setLoading
(
true
);
const
typeParam
=
typeKey
!==
'all'
?
`&type=
${
typeKey
}
`
:
''
;
const
typeParam
=
(
typeKey
!==
'all'
)
?
`&type=
${
typeKey
}
`
:
''
;
const
statusParam
=
statusF
!==
'all'
?
`&status=
${
statusF
}
`
:
''
;
const
statusParam
=
statusF
!==
'all'
?
`&status=
${
statusF
}
`
:
''
;
const
res
=
await
API
.
get
(
const
res
=
await
API
.
get
(
`/api/channel/?p=
${
page
}
&page_size=
${
pageSize
}
&id_sort=
${
idSort
}
&tag_mode=
${
enableTagMode
}${
typeParam
}${
statusParam
}
`
,
`/api/channel/?p=
${
page
}
&page_size=
${
pageSize
}
&id_sort=
${
idSort
}
&tag_mode=
${
enableTagMode
}${
typeParam
}${
statusParam
}
`
,
...
@@ -327,10 +311,7 @@ export const useChannelsData = () => {
...
@@ -327,10 +311,7 @@ export const useChannelsData = () => {
if
(
success
)
{
if
(
success
)
{
const
{
items
,
total
,
type_counts
}
=
data
;
const
{
items
,
total
,
type_counts
}
=
data
;
if
(
type_counts
)
{
if
(
type_counts
)
{
const
sumAll
=
Object
.
values
(
type_counts
).
reduce
(
const
sumAll
=
Object
.
values
(
type_counts
).
reduce
((
acc
,
v
)
=>
acc
+
v
,
0
);
(
acc
,
v
)
=>
acc
+
v
,
0
,
);
setTypeCounts
({
...
type_counts
,
all
:
sumAll
});
setTypeCounts
({
...
type_counts
,
all
:
sumAll
});
}
}
setChannelFormat
(
items
,
enableTagMode
);
setChannelFormat
(
items
,
enableTagMode
);
...
@@ -354,18 +335,11 @@ export const useChannelsData = () => {
...
@@ -354,18 +335,11 @@ export const useChannelsData = () => {
setSearching
(
true
);
setSearching
(
true
);
try
{
try
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
await
loadChannels
(
await
loadChannels
(
page
,
pageSz
,
sortFlag
,
enableTagMode
,
typeKey
,
statusF
);
page
,
pageSz
,
sortFlag
,
enableTagMode
,
typeKey
,
statusF
,
);
return
;
return
;
}
}
const
typeParam
=
typeKey
!==
'all'
?
`&type=
${
typeKey
}
`
:
''
;
const
typeParam
=
(
typeKey
!==
'all'
)
?
`&type=
${
typeKey
}
`
:
''
;
const
statusParam
=
statusF
!==
'all'
?
`&status=
${
statusF
}
`
:
''
;
const
statusParam
=
statusF
!==
'all'
?
`&status=
${
statusF
}
`
:
''
;
const
res
=
await
API
.
get
(
const
res
=
await
API
.
get
(
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
&id_sort=
${
sortFlag
}
&tag_mode=
${
enableTagMode
}
&p=
${
page
}
&page_size=
${
pageSz
}${
typeParam
}${
statusParam
}
`
,
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
&id_sort=
${
sortFlag
}
&tag_mode=
${
enableTagMode
}
&p=
${
page
}
&page_size=
${
pageSz
}${
typeParam
}${
statusParam
}
`
,
...
@@ -373,10 +347,7 @@ export const useChannelsData = () => {
...
@@ -373,10 +347,7 @@ export const useChannelsData = () => {
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
const
{
items
=
[],
total
=
0
,
type_counts
=
{}
}
=
data
;
const
{
items
=
[],
total
=
0
,
type_counts
=
{}
}
=
data
;
const
sumAll
=
Object
.
values
(
type_counts
).
reduce
(
const
sumAll
=
Object
.
values
(
type_counts
).
reduce
((
acc
,
v
)
=>
acc
+
v
,
0
);
(
acc
,
v
)
=>
acc
+
v
,
0
,
);
setTypeCounts
({
...
type_counts
,
all
:
sumAll
});
setTypeCounts
({
...
type_counts
,
all
:
sumAll
});
setChannelFormat
(
items
,
enableTagMode
);
setChannelFormat
(
items
,
enableTagMode
);
setChannelCount
(
total
);
setChannelCount
(
total
);
...
@@ -395,14 +366,7 @@ export const useChannelsData = () => {
...
@@ -395,14 +366,7 @@ export const useChannelsData = () => {
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
await
loadChannels
(
page
,
pageSize
,
idSort
,
enableTagMode
);
await
loadChannels
(
page
,
pageSize
,
idSort
,
enableTagMode
);
}
else
{
}
else
{
await
searchChannels
(
await
searchChannels
(
enableTagMode
,
activeTypeKey
,
statusFilter
,
page
,
pageSize
,
idSort
);
enableTagMode
,
activeTypeKey
,
statusFilter
,
page
,
pageSize
,
idSort
,
);
}
}
};
};
...
@@ -488,16 +452,9 @@ export const useChannelsData = () => {
...
@@ -488,16 +452,9 @@ export const useChannelsData = () => {
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
setActivePage
(
page
);
setActivePage
(
page
);
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
loadChannels
(
page
,
pageSize
,
idSort
,
enableTagMode
).
then
(()
=>
{});
loadChannels
(
page
,
pageSize
,
idSort
,
enableTagMode
).
then
(()
=>
{
});
}
else
{
}
else
{
searchChannels
(
searchChannels
(
enableTagMode
,
activeTypeKey
,
statusFilter
,
page
,
pageSize
,
idSort
);
enableTagMode
,
activeTypeKey
,
statusFilter
,
page
,
pageSize
,
idSort
,
);
}
}
};
};
...
@@ -513,14 +470,7 @@ export const useChannelsData = () => {
...
@@ -513,14 +470,7 @@ export const useChannelsData = () => {
showError
(
reason
);
showError
(
reason
);
});
});
}
else
{
}
else
{
searchChannels
(
searchChannels
(
enableTagMode
,
activeTypeKey
,
statusFilter
,
1
,
size
,
idSort
);
enableTagMode
,
activeTypeKey
,
statusFilter
,
1
,
size
,
idSort
,
);
}
}
};
};
...
@@ -551,10 +501,7 @@ export const useChannelsData = () => {
...
@@ -551,10 +501,7 @@ export const useChannelsData = () => {
showError
(
res
?.
data
?.
message
||
t
(
'渠道复制失败'
));
showError
(
res
?.
data
?.
message
||
t
(
'渠道复制失败'
));
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
showError
(
showError
(
t
(
'渠道复制失败: '
)
+
(
error
?.
response
?.
data
?.
message
||
error
?.
message
||
error
));
t
(
'渠道复制失败: '
)
+
(
error
?.
response
?.
data
?.
message
||
error
?.
message
||
error
),
);
}
}
};
};
...
@@ -593,11 +540,7 @@ export const useChannelsData = () => {
...
@@ -593,11 +540,7 @@ export const useChannelsData = () => {
data
.
priority
=
parseInt
(
data
.
priority
);
data
.
priority
=
parseInt
(
data
.
priority
);
break
;
break
;
case
'weight'
:
case
'weight'
:
if
(
if
(
data
.
weight
===
undefined
||
data
.
weight
<
0
||
data
.
weight
===
''
)
{
data
.
weight
===
undefined
||
data
.
weight
<
0
||
data
.
weight
===
''
)
{
showInfo
(
'权重必须是非负整数!'
);
showInfo
(
'权重必须是非负整数!'
);
return
;
return
;
}
}
...
@@ -740,136 +683,226 @@ export const useChannelsData = () => {
...
@@ -740,136 +683,226 @@ export const useChannelsData = () => {
const
res
=
await
API
.
post
(
`/api/channel/fix`
);
const
res
=
await
API
.
post
(
`/api/channel/fix`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
showSuccess
(
t
(
'已修复 ${success} 个通道,失败 ${fails} 个通道。'
).
replace
(
'${success}'
,
data
.
success
).
replace
(
'${fails}'
,
data
.
fails
));
t
(
'已修复 ${success} 个通道,失败 ${fails} 个通道。'
)
.
replace
(
'${success}'
,
data
.
success
)
.
replace
(
'${fails}'
,
data
.
fails
),
);
await
refresh
();
await
refresh
();
}
else
{
}
else
{
showError
(
message
);
showError
(
message
);
}
}
};
};
// Test channel
// Test channel
- 单个模型测试,参考旧版实现
const
testChannel
=
async
(
record
,
model
)
=>
{
const
testChannel
=
async
(
record
,
model
)
=>
{
setTestQueue
((
prev
)
=>
[...
prev
,
{
channel
:
record
,
model
}]);
const
testKey
=
`
${
record
.
id
}
-
${
model
}
`
;
if
(
!
isProcessingQueue
)
{
setIsProcessingQueue
(
true
);
// 检查是否应该停止批量测试
if
(
shouldStopBatchTestingRef
.
current
&&
isBatchTesting
)
{
return
Promise
.
resolve
();
}
}
};
// Process test queue
// 添加到正在测试的模型集合
const
processTestQueue
=
async
()
=>
{
setTestingModels
(
prev
=>
new
Set
([...
prev
,
model
]));
if
(
!
isProcessingQueue
||
testQueue
.
length
===
0
)
return
;
const
{
channel
,
model
,
indexInFiltered
}
=
testQueue
[
0
];
try
{
const
res
=
await
API
.
get
(
`/api/channel/test/
${
record
.
id
}
?model=
${
model
}
`
);
if
(
currentTestChannel
&&
currentTestChannel
.
id
===
channel
.
id
)
{
// 检查是否在请求期间被停止
let
pageNo
;
if
(
shouldStopBatchTestingRef
.
current
&&
isBatchTesting
)
{
if
(
indexInFiltered
!==
undefined
)
{
return
Promise
.
resolve
();
pageNo
=
Math
.
floor
(
indexInFiltered
/
MODEL_TABLE_PAGE_SIZE
)
+
1
;
}
else
{
const
filteredModelsList
=
currentTestChannel
.
models
.
split
(
','
)
.
filter
((
m
)
=>
m
.
toLowerCase
().
includes
(
modelSearchKeyword
.
toLowerCase
()),
);
const
modelIdx
=
filteredModelsList
.
indexOf
(
model
);
pageNo
=
modelIdx
!==
-
1
?
Math
.
floor
(
modelIdx
/
MODEL_TABLE_PAGE_SIZE
)
+
1
:
1
;
}
setModelTablePage
(
pageNo
);
}
}
try
{
setTestingModels
((
prev
)
=>
new
Set
([...
prev
,
model
]));
const
res
=
await
API
.
get
(
`/api/channel/test/
${
channel
.
id
}
?model=
${
model
}
`
,
);
const
{
success
,
message
,
time
}
=
res
.
data
;
const
{
success
,
message
,
time
}
=
res
.
data
;
setModelTestResults
((
prev
)
=>
({
// 更新测试结果
setModelTestResults
(
prev
=>
({
...
prev
,
...
prev
,
[
`
${
channel
.
id
}
-
${
model
}
`
]:
{
success
,
time
},
[
testKey
]:
{
success
,
message
,
time
:
time
||
0
,
timestamp
:
Date
.
now
()
}
}));
}));
if
(
success
)
{
if
(
success
)
{
updateChannelProperty
(
channel
.
id
,
(
ch
)
=>
{
// 更新渠道响应时间
ch
.
response_time
=
time
*
1000
;
updateChannelProperty
(
record
.
id
,
(
channel
)
=>
{
ch
.
test_time
=
Date
.
now
()
/
1000
;
channel
.
response_time
=
time
*
1000
;
channel
.
test_time
=
Date
.
now
()
/
1000
;
});
});
if
(
!
model
)
{
if
(
!
model
||
model
===
''
)
{
showInfo
(
showInfo
(
t
(
'通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。'
)
t
(
'通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。'
)
.
replace
(
'${name}'
,
channel
.
name
)
.
replace
(
'${name}'
,
record
.
name
)
.
replace
(
'${time.toFixed(2)}'
,
time
.
toFixed
(
2
)),
);
}
else
{
showInfo
(
t
(
'通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。'
)
.
replace
(
'${name}'
,
record
.
name
)
.
replace
(
'${model}'
,
model
)
.
replace
(
'${time.toFixed(2)}'
,
time
.
toFixed
(
2
)),
.
replace
(
'${time.toFixed(2)}'
,
time
.
toFixed
(
2
)),
);
);
}
}
}
else
{
}
else
{
showError
(
message
);
showError
(
`
${
t
(
'模型'
)}
${
model
}
:
${
message
}
`
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
showError
(
error
.
message
);
// 处理网络错误
const
testKey
=
`
${
record
.
id
}
-
${
model
}
`
;
setModelTestResults
(
prev
=>
({
...
prev
,
[
testKey
]:
{
success
:
false
,
message
:
error
.
message
||
t
(
'网络错误'
),
time
:
0
,
timestamp
:
Date
.
now
()
}
}));
showError
(
`
${
t
(
'模型'
)}
${
model
}
:
${
error
.
message
||
t
(
'测试失败'
)}
`
);
}
finally
{
}
finally
{
setTestingModels
((
prev
)
=>
{
// 从正在测试的模型集合中移除
setTestingModels
(
prev
=>
{
const
newSet
=
new
Set
(
prev
);
const
newSet
=
new
Set
(
prev
);
newSet
.
delete
(
model
);
newSet
.
delete
(
model
);
return
newSet
;
return
newSet
;
});
});
}
}
setTestQueue
((
prev
)
=>
prev
.
slice
(
1
));
};
};
// Monitor queue changes
// 批量测试单个渠道的所有模型,参考旧版实现
useEffect
(()
=>
{
const
batchTestModels
=
async
()
=>
{
if
(
testQueue
.
length
>
0
&&
isProcessingQueue
)
{
if
(
!
currentTestChannel
||
!
currentTestChannel
.
models
)
{
processTestQueue
();
showError
(
t
(
'渠道模型信息不完整'
));
}
else
if
(
testQueue
.
length
===
0
&&
isProcessingQueue
)
{
return
;
setIsProcessingQueue
(
false
);
setIsBatchTesting
(
false
);
}
}
},
[
testQueue
,
isProcessingQueue
]);
// Batch test models
const
models
=
currentTestChannel
.
models
.
split
(
','
).
filter
(
model
=>
const
batchTestModels
=
async
()
=>
{
model
.
toLowerCase
().
includes
(
modelSearchKeyword
.
toLowerCase
())
if
(
!
currentTestChannel
)
return
;
);
if
(
models
.
length
===
0
)
{
showError
(
t
(
'没有找到匹配的模型'
));
return
;
}
setIsBatchTesting
(
true
);
setIsBatchTesting
(
true
);
setModelTablePage
(
1
);
shouldStopBatchTestingRef
.
current
=
false
;
// 重置停止标志
// 清空该渠道之前的测试结果
setModelTestResults
(
prev
=>
{
const
newResults
=
{
...
prev
};
models
.
forEach
(
model
=>
{
const
testKey
=
`
${
currentTestChannel
.
id
}
-
${
model
}
`
;
delete
newResults
[
testKey
];
});
return
newResults
;
});
try
{
showInfo
(
t
(
'开始批量测试 ${count} 个模型,已清空上次结果...'
).
replace
(
'${count}'
,
models
.
length
));
// 提高并发数量以加快测试速度,参考旧版的并发限制
const
concurrencyLimit
=
5
;
const
results
=
[];
for
(
let
i
=
0
;
i
<
models
.
length
;
i
+=
concurrencyLimit
)
{
// 检查是否应该停止
if
(
shouldStopBatchTestingRef
.
current
)
{
showInfo
(
t
(
'批量测试已停止'
));
break
;
}
const
filteredModels
=
currentTestChannel
.
models
const
batch
=
models
.
slice
(
i
,
i
+
concurrencyLimit
);
.
split
(
','
)
showInfo
(
t
(
'正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)'
)
.
filter
((
model
)
=>
.
replace
(
'${current}'
,
i
+
1
)
model
.
toLowerCase
().
includes
(
modelSearchKeyword
.
toLowerCase
()),
.
replace
(
'${end}'
,
Math
.
min
(
i
+
concurrencyLimit
,
models
.
length
))
.
replace
(
'${total}'
,
models
.
length
)
);
);
setTestQueue
(
const
batchPromises
=
batch
.
map
(
model
=>
testChannel
(
currentTestChannel
,
model
));
filteredModels
.
map
((
model
,
idx
)
=>
({
const
batchResults
=
await
Promise
.
allSettled
(
batchPromises
);
channel
:
currentTestChannel
,
results
.
push
(...
batchResults
);
model
,
indexInFiltered
:
idx
,
// 再次检查是否应该停止
})),
if
(
shouldStopBatchTestingRef
.
current
)
{
showInfo
(
t
(
'批量测试已停止'
));
break
;
}
// 短暂延迟避免过于频繁的请求
if
(
i
+
concurrencyLimit
<
models
.
length
)
{
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
100
));
}
}
if
(
!
shouldStopBatchTestingRef
.
current
)
{
// 等待一小段时间确保所有结果都已更新
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
300
));
// 使用当前状态重新计算结果统计
setModelTestResults
(
currentResults
=>
{
let
successCount
=
0
;
let
failCount
=
0
;
models
.
forEach
(
model
=>
{
const
testKey
=
`
${
currentTestChannel
.
id
}
-
${
model
}
`
;
const
result
=
currentResults
[
testKey
];
if
(
result
&&
result
.
success
)
{
successCount
++
;
}
else
{
failCount
++
;
}
});
// 显示完成消息
setTimeout
(()
=>
{
showSuccess
(
t
(
'批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}'
)
.
replace
(
'${success}'
,
successCount
)
.
replace
(
'${fail}'
,
failCount
)
.
replace
(
'${total}'
,
models
.
length
)
);
);
setIsProcessingQueue
(
true
);
},
100
);
return
currentResults
;
// 不修改状态,只是为了获取最新值
});
}
}
catch
(
error
)
{
showError
(
t
(
'批量测试过程中发生错误: '
)
+
error
.
message
);
}
finally
{
setIsBatchTesting
(
false
);
}
};
// 停止批量测试
const
stopBatchTesting
=
()
=>
{
shouldStopBatchTestingRef
.
current
=
true
;
setIsBatchTesting
(
false
);
setTestingModels
(
new
Set
());
showInfo
(
t
(
'已停止批量测试'
));
};
// 清空测试结果
const
clearTestResults
=
()
=>
{
setModelTestResults
({});
showInfo
(
t
(
'已清空测试结果'
));
};
};
// Handle close modal
// Handle close modal
const
handleCloseModal
=
()
=>
{
const
handleCloseModal
=
()
=>
{
// 如果正在批量测试,先停止测试
if
(
isBatchTesting
)
{
if
(
isBatchTesting
)
{
setTestQueue
([]);
shouldStopBatchTestingRef
.
current
=
true
;
setIsProcessingQueue
(
false
);
showInfo
(
t
(
'关闭弹窗,已停止批量测试'
));
setIsBatchTesting
(
false
);
}
showSuccess
(
t
(
'已停止测试'
));
}
else
{
setShowModelTestModal
(
false
);
setShowModelTestModal
(
false
);
setModelSearchKeyword
(
''
);
setModelSearchKeyword
(
''
);
setIsBatchTesting
(
false
);
setTestingModels
(
new
Set
());
setSelectedModelKeys
([]);
setSelectedModelKeys
([]);
setModelTablePage
(
1
);
setModelTablePage
(
1
);
}
// 可选择性保留测试结果,这里不清空以便用户查看
};
};
// Type counts
// Type counts
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment