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
7c6e6624
authored
Oct 03, 2025
by
bubblepipe42
Browse files
Options
Browse Files
Download
Plain Diff
merge
parents
20055ca0
e9aa45a5
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
362 additions
and
71 deletions
+362
-71
.github/workflows/electron-build.yml
+4
-3
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
+0
-0
relay/channel/ollama/stream.go
+210
-0
relay/channel/openai/relay-openai.go
+18
-0
relay/channel/openrouter/dto.go
+7
-0
relay/channel/xunfei/relay-xunfei.go
+3
-4
web/src/components/table/channels/modals/EditChannelModal.jsx
+42
-0
web/src/hooks/channels/useChannelsData.jsx
+0
-0
No files found.
.github/workflows/electron-build.yml
View file @
7c6e6624
...
@@ -35,8 +35,9 @@ jobs:
...
@@ -35,8 +35,9 @@ jobs:
npm run build
npm run build
env
:
env
:
DISABLE_ESLINT_PLUGIN
:
'
true'
DISABLE_ESLINT_PLUGIN
:
'
true'
NODE_OPTIONS
:
'
--max_old_space_size=4096'
-
name
:
Build Go binary (
darwin
/Linux)
-
name
:
Build Go binary (
macos
/Linux)
if
:
runner.os != 'Windows'
if
:
runner.os != 'Windows'
run
:
|
run
:
|
go build -ldflags="-s -w" -o new-api
go build -ldflags="-s -w" -o new-api
...
@@ -69,7 +70,7 @@ jobs:
...
@@ -69,7 +70,7 @@ jobs:
if
:
runner.os == 'macOS'
if
:
runner.os == 'macOS'
uses
:
actions/upload-artifact@v4
uses
:
actions/upload-artifact@v4
with
:
with
:
name
:
darwin
-build
name
:
macos
-build
path
:
|
path
:
|
electron/dist/*.dmg
electron/dist/*.dmg
electron/dist/*.zip
electron/dist/*.zip
...
@@ -95,7 +96,7 @@ jobs:
...
@@ -95,7 +96,7 @@ jobs:
uses
:
softprops/action-gh-release@v1
uses
:
softprops/action-gh-release@v1
with
:
with
:
files
:
|
files
:
|
darwin
-build/*
macos
-build/*
windows-build/*
windows-build/*
draft
:
false
draft
:
false
prerelease
:
false
prerelease
:
false
...
...
dto/channel_settings.go
View file @
7c6e6624
...
@@ -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 @
7c6e6624
...
@@ -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 @
7c6e6624
...
@@ -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 @
7c6e6624
...
@@ -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 @
7c6e6624
This diff is collapsed.
Click to expand it.
relay/channel/ollama/stream.go
0 → 100644
View file @
7c6e6624
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 @
7c6e6624
...
@@ -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 @
7c6e6624
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/xunfei/relay-xunfei.go
View file @
7c6e6624
...
@@ -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 @
7c6e6624
...
@@ -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
({
...
@@ -437,15 +440,19 @@ const EditChannelModal = (props) => {
...
@@ -437,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
);
...
@@ -457,6 +464,8 @@ const EditChannelModal = (props) => {
...
@@ -457,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
({
...
@@ -716,6 +725,8 @@ const EditChannelModal = (props) => {
...
@@ -716,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
);
...
@@ -879,6 +890,21 @@ const EditChannelModal = (props) => {
...
@@ -879,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
;
...
@@ -886,6 +912,7 @@ const EditChannelModal = (props) => {
...
@@ -886,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
;
...
@@ -1203,6 +1230,21 @@ const EditChannelModal = (props) => {
...
@@ -1203,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
(
'名称'
)
}
...
...
web/src/hooks/channels/useChannelsData.jsx
View file @
7c6e6624
This diff is collapsed.
Click to expand it.
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