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
24151ab3
authored
Jul 18, 2024
by
CalciumIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support gemini tool calling (close #368)
parent
e6de0c0b
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
103 additions
and
93 deletions
+103
-93
dto/text_request.go
+10
-1
relay/channel/gemini/adaptor.go
+1
-1
relay/channel/gemini/dto.go
+8
-2
relay/channel/gemini/relay-gemini.go
+84
-89
No files found.
dto/text_request.go
View file @
24151ab3
...
@@ -148,7 +148,7 @@ func (m Message) ParseContent() []MediaMessage {
...
@@ -148,7 +148,7 @@ func (m Message) ParseContent() []MediaMessage {
if
ok
{
if
ok
{
subObj
[
"detail"
]
=
detail
.
(
string
)
subObj
[
"detail"
]
=
detail
.
(
string
)
}
else
{
}
else
{
subObj
[
"detail"
]
=
"
auto
"
subObj
[
"detail"
]
=
"
high
"
}
}
contentList
=
append
(
contentList
,
MediaMessage
{
contentList
=
append
(
contentList
,
MediaMessage
{
Type
:
ContentTypeImageURL
,
Type
:
ContentTypeImageURL
,
...
@@ -157,7 +157,16 @@ func (m Message) ParseContent() []MediaMessage {
...
@@ -157,7 +157,16 @@ func (m Message) ParseContent() []MediaMessage {
Detail
:
subObj
[
"detail"
]
.
(
string
),
Detail
:
subObj
[
"detail"
]
.
(
string
),
},
},
})
})
}
else
if
url
,
ok
:=
contentMap
[
"image_url"
]
.
(
string
);
ok
{
contentList
=
append
(
contentList
,
MediaMessage
{
Type
:
ContentTypeImageURL
,
ImageUrl
:
MessageImageUrl
{
Url
:
url
,
Detail
:
"high"
,
},
})
}
}
}
}
}
}
return
contentList
return
contentList
...
...
relay/channel/gemini/adaptor.go
View file @
24151ab3
...
@@ -47,7 +47,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
...
@@ -47,7 +47,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
action
:=
"generateContent"
action
:=
"generateContent"
if
info
.
IsStream
{
if
info
.
IsStream
{
action
=
"streamGenerateContent"
action
=
"streamGenerateContent
?alt=sse
"
}
}
return
fmt
.
Sprintf
(
"%s/%s/models/%s:%s"
,
info
.
BaseUrl
,
version
,
info
.
UpstreamModelName
,
action
),
nil
return
fmt
.
Sprintf
(
"%s/%s/models/%s:%s"
,
info
.
BaseUrl
,
version
,
info
.
UpstreamModelName
,
action
),
nil
}
}
...
...
relay/channel/gemini/dto.go
View file @
24151ab3
...
@@ -12,9 +12,15 @@ type GeminiInlineData struct {
...
@@ -12,9 +12,15 @@ type GeminiInlineData struct {
Data
string
`json:"data"`
Data
string
`json:"data"`
}
}
type
FunctionCall
struct
{
FunctionName
string
`json:"name"`
Arguments
any
`json:"args"`
}
type
GeminiPart
struct
{
type
GeminiPart
struct
{
Text
string
`json:"text,omitempty"`
Text
string
`json:"text,omitempty"`
InlineData
*
GeminiInlineData
`json:"inlineData,omitempty"`
InlineData
*
GeminiInlineData
`json:"inlineData,omitempty"`
FunctionCall
*
FunctionCall
`json:"functionCall,omitempty"`
}
}
type
GeminiChatContent
struct
{
type
GeminiChatContent
struct
{
...
...
relay/channel/gemini/relay-gemini.go
View file @
24151ab3
...
@@ -4,18 +4,14 @@ import (
...
@@ -4,18 +4,14 @@ import (
"bufio"
"bufio"
"encoding/json"
"encoding/json"
"fmt"
"fmt"
"github.com/gin-gonic/gin"
"io"
"io"
"log"
"net/http"
"net/http"
"one-api/common"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/dto"
relaycommon
"one-api/relay/common"
relaycommon
"one-api/relay/common"
"one-api/service"
"one-api/service"
"strings"
"strings"
"time"
"github.com/gin-gonic/gin"
)
)
// Setting safety to the lowest possible values since Gemini is already powerless enough
// Setting safety to the lowest possible values since Gemini is already powerless enough
...
@@ -46,7 +42,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
...
@@ -46,7 +42,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
MaxOutputTokens
:
textRequest
.
MaxTokens
,
MaxOutputTokens
:
textRequest
.
MaxTokens
,
},
},
}
}
if
textRequest
.
Functions
!=
nil
{
if
textRequest
.
Tools
!=
nil
{
functions
:=
make
([]
dto
.
FunctionCall
,
0
,
len
(
textRequest
.
Tools
))
for
_
,
tool
:=
range
textRequest
.
Tools
{
functions
=
append
(
functions
,
tool
.
Function
)
}
geminiRequest
.
Tools
=
[]
GeminiChatTools
{
{
FunctionDeclarations
:
functions
,
},
}
}
else
if
textRequest
.
Functions
!=
nil
{
geminiRequest
.
Tools
=
[]
GeminiChatTools
{
geminiRequest
.
Tools
=
[]
GeminiChatTools
{
{
{
FunctionDeclarations
:
textRequest
.
Functions
,
FunctionDeclarations
:
textRequest
.
Functions
,
...
@@ -126,6 +132,30 @@ func (g *GeminiChatResponse) GetResponseText() string {
...
@@ -126,6 +132,30 @@ func (g *GeminiChatResponse) GetResponseText() string {
return
""
return
""
}
}
func
getToolCalls
(
candidate
*
GeminiChatCandidate
)
[]
dto
.
ToolCall
{
var
toolCalls
[]
dto
.
ToolCall
item
:=
candidate
.
Content
.
Parts
[
0
]
if
item
.
FunctionCall
==
nil
{
return
toolCalls
}
argsBytes
,
err
:=
json
.
Marshal
(
item
.
FunctionCall
.
Arguments
)
if
err
!=
nil
{
//common.SysError("getToolCalls failed: " + err.Error())
return
toolCalls
}
toolCall
:=
dto
.
ToolCall
{
ID
:
fmt
.
Sprintf
(
"call_%s"
,
common
.
GetUUID
()),
Type
:
"function"
,
Function
:
dto
.
FunctionCall
{
Arguments
:
string
(
argsBytes
),
Name
:
item
.
FunctionCall
.
FunctionName
,
},
}
toolCalls
=
append
(
toolCalls
,
toolCall
)
return
toolCalls
}
func
responseGeminiChat2OpenAI
(
response
*
GeminiChatResponse
)
*
dto
.
OpenAITextResponse
{
func
responseGeminiChat2OpenAI
(
response
*
GeminiChatResponse
)
*
dto
.
OpenAITextResponse
{
fullTextResponse
:=
dto
.
OpenAITextResponse
{
fullTextResponse
:=
dto
.
OpenAITextResponse
{
Id
:
fmt
.
Sprintf
(
"chatcmpl-%s"
,
common
.
GetUUID
()),
Id
:
fmt
.
Sprintf
(
"chatcmpl-%s"
,
common
.
GetUUID
()),
...
@@ -144,8 +174,11 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
...
@@ -144,8 +174,11 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
FinishReason
:
relaycommon
.
StopFinishReason
,
FinishReason
:
relaycommon
.
StopFinishReason
,
}
}
if
len
(
candidate
.
Content
.
Parts
)
>
0
{
if
len
(
candidate
.
Content
.
Parts
)
>
0
{
content
,
_
=
json
.
Marshal
(
candidate
.
Content
.
Parts
[
0
]
.
Text
)
if
candidate
.
Content
.
Parts
[
0
]
.
FunctionCall
!=
nil
{
choice
.
Message
.
Content
=
content
choice
.
Message
.
ToolCalls
=
getToolCalls
(
&
candidate
)
}
else
{
choice
.
Message
.
SetStringContent
(
candidate
.
Content
.
Parts
[
0
]
.
Text
)
}
}
}
fullTextResponse
.
Choices
=
append
(
fullTextResponse
.
Choices
,
choice
)
fullTextResponse
.
Choices
=
append
(
fullTextResponse
.
Choices
,
choice
)
}
}
...
@@ -154,7 +187,17 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
...
@@ -154,7 +187,17 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
func
streamResponseGeminiChat2OpenAI
(
geminiResponse
*
GeminiChatResponse
)
*
dto
.
ChatCompletionsStreamResponse
{
func
streamResponseGeminiChat2OpenAI
(
geminiResponse
*
GeminiChatResponse
)
*
dto
.
ChatCompletionsStreamResponse
{
var
choice
dto
.
ChatCompletionsStreamResponseChoice
var
choice
dto
.
ChatCompletionsStreamResponseChoice
choice
.
Delta
.
SetContentString
(
geminiResponse
.
GetResponseText
())
//choice.Delta.SetContentString(geminiResponse.GetResponseText())
if
len
(
geminiResponse
.
Candidates
)
>
0
&&
len
(
geminiResponse
.
Candidates
[
0
]
.
Content
.
Parts
)
>
0
{
respFirst
:=
geminiResponse
.
Candidates
[
0
]
.
Content
.
Parts
[
0
]
if
respFirst
.
FunctionCall
!=
nil
{
// function response
choice
.
Delta
.
ToolCalls
=
getToolCalls
(
&
geminiResponse
.
Candidates
[
0
])
}
else
{
// text response
choice
.
Delta
.
SetContentString
(
respFirst
.
Text
)
}
}
choice
.
FinishReason
=
&
relaycommon
.
StopFinishReason
choice
.
FinishReason
=
&
relaycommon
.
StopFinishReason
var
response
dto
.
ChatCompletionsStreamResponse
var
response
dto
.
ChatCompletionsStreamResponse
response
.
Object
=
"chat.completion.chunk"
response
.
Object
=
"chat.completion.chunk"
...
@@ -165,92 +208,47 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.Ch
...
@@ -165,92 +208,47 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.Ch
func
geminiChatStreamHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
func
geminiChatStreamHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
responseText
:=
""
responseText
:=
""
responseJson
:=
""
id
:=
fmt
.
Sprintf
(
"chatcmpl-%s"
,
common
.
GetUUID
())
id
:=
fmt
.
Sprintf
(
"chatcmpl-%s"
,
common
.
GetUUID
())
createAt
:=
common
.
GetTimestamp
()
createAt
:=
common
.
GetTimestamp
()
var
usage
=
&
dto
.
Usage
{}
var
usage
=
&
dto
.
Usage
{}
dataChan
:=
make
(
chan
string
,
5
)
stopChan
:=
make
(
chan
bool
,
2
)
scanner
:=
bufio
.
NewScanner
(
resp
.
Body
)
scanner
:=
bufio
.
NewScanner
(
resp
.
Body
)
scanner
.
Split
(
func
(
data
[]
byte
,
atEOF
bool
)
(
advance
int
,
token
[]
byte
,
err
error
)
{
scanner
.
Split
(
bufio
.
ScanLines
)
if
atEOF
&&
len
(
data
)
==
0
{
return
0
,
nil
,
nil
service
.
SetEventStreamHeaders
(
c
)
}
for
scanner
.
Scan
()
{
if
i
:=
strings
.
Index
(
string
(
data
),
"
\n
"
);
i
>=
0
{
data
:=
scanner
.
Text
()
return
i
+
1
,
data
[
0
:
i
],
nil
info
.
SetFirstResponseTime
()
data
=
strings
.
TrimSpace
(
data
)
if
!
strings
.
HasPrefix
(
data
,
"data: "
)
{
continue
}
}
if
atEOF
{
data
=
strings
.
TrimPrefix
(
data
,
"data: "
)
return
len
(
data
),
data
,
nil
data
=
strings
.
TrimSuffix
(
data
,
"
\"
"
)
var
geminiResponse
GeminiChatResponse
err
:=
json
.
Unmarshal
([]
byte
(
data
),
&
geminiResponse
)
if
err
!=
nil
{
common
.
LogError
(
c
,
"error unmarshalling stream response: "
+
err
.
Error
())
continue
}
}
return
0
,
nil
,
nil
})
response
:=
streamResponseGeminiChat2OpenAI
(
&
geminiResponse
)
go
func
()
{
if
response
==
nil
{
for
scanner
.
Scan
()
{
continue
data
:=
scanner
.
Text
()
responseJson
+=
data
data
=
strings
.
TrimSpace
(
data
)
if
!
strings
.
HasPrefix
(
data
,
"
\"
text
\"
:
\"
"
)
{
continue
}
data
=
strings
.
TrimPrefix
(
data
,
"
\"
text
\"
:
\"
"
)
data
=
strings
.
TrimSuffix
(
data
,
"
\"
"
)
if
!
common
.
SafeSendStringTimeout
(
dataChan
,
data
,
constant
.
StreamingTimeout
)
{
// send data timeout, stop the stream
common
.
LogError
(
c
,
"send data timeout, stop the stream"
)
break
}
}
}
stopChan
<-
true
response
.
Id
=
id
}()
response
.
Created
=
createAt
isFirst
:=
true
responseText
+=
response
.
Choices
[
0
]
.
Delta
.
GetContentString
()
service
.
SetEventStreamHeaders
(
c
)
if
geminiResponse
.
UsageMetadata
.
TotalTokenCount
!=
0
{
c
.
Stream
(
func
(
w
io
.
Writer
)
bool
{
usage
.
PromptTokens
=
geminiResponse
.
UsageMetadata
.
PromptTokenCount
select
{
usage
.
CompletionTokens
=
geminiResponse
.
UsageMetadata
.
CandidatesTokenCount
case
data
:=
<-
dataChan
:
if
isFirst
{
isFirst
=
false
info
.
FirstResponseTime
=
time
.
Now
()
}
// this is used to prevent annoying \ related format bug
data
=
fmt
.
Sprintf
(
"{
\"
content
\"
:
\"
%s
\"
}"
,
data
)
type
dummyStruct
struct
{
Content
string
`json:"content"`
}
var
dummy
dummyStruct
err
:=
json
.
Unmarshal
([]
byte
(
data
),
&
dummy
)
responseText
+=
dummy
.
Content
var
choice
dto
.
ChatCompletionsStreamResponseChoice
choice
.
Delta
.
SetContentString
(
dummy
.
Content
)
response
:=
dto
.
ChatCompletionsStreamResponse
{
Id
:
id
,
Object
:
"chat.completion.chunk"
,
Created
:
createAt
,
Model
:
info
.
UpstreamModelName
,
Choices
:
[]
dto
.
ChatCompletionsStreamResponseChoice
{
choice
},
}
jsonResponse
,
err
:=
json
.
Marshal
(
response
)
if
err
!=
nil
{
common
.
SysError
(
"error marshalling stream response: "
+
err
.
Error
())
return
true
}
c
.
Render
(
-
1
,
common
.
CustomEvent
{
Data
:
"data: "
+
string
(
jsonResponse
)})
return
true
case
<-
stopChan
:
return
false
}
}
})
err
=
service
.
ObjectData
(
c
,
response
)
var
geminiChatResponses
[]
GeminiChatResponse
if
err
!=
nil
{
err
:=
json
.
Unmarshal
([]
byte
(
responseJson
),
&
geminiChatResponses
)
common
.
LogError
(
c
,
err
.
Error
())
if
err
!=
nil
{
log
.
Printf
(
"cannot get gemini usage: %s"
,
err
.
Error
())
usage
,
_
=
service
.
ResponseText2Usage
(
responseText
,
info
.
UpstreamModelName
,
info
.
PromptTokens
)
}
else
{
for
_
,
response
:=
range
geminiChatResponses
{
usage
.
PromptTokens
=
response
.
UsageMetadata
.
PromptTokenCount
usage
.
CompletionTokens
=
response
.
UsageMetadata
.
CandidatesTokenCount
}
}
usage
.
TotalTokens
=
usage
.
PromptTokens
+
usage
.
CompletionTokens
}
}
usage
.
TotalTokens
=
usage
.
PromptTokens
+
usage
.
CompletionTokens
if
info
.
ShouldIncludeUsage
{
if
info
.
ShouldIncludeUsage
{
response
:=
service
.
GenerateFinalUsageResponse
(
id
,
createAt
,
info
.
UpstreamModelName
,
*
usage
)
response
:=
service
.
GenerateFinalUsageResponse
(
id
,
createAt
,
info
.
UpstreamModelName
,
*
usage
)
err
:=
service
.
ObjectData
(
c
,
response
)
err
:=
service
.
ObjectData
(
c
,
response
)
...
@@ -259,10 +257,7 @@ func geminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
...
@@ -259,10 +257,7 @@ func geminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
}
}
}
}
service
.
Done
(
c
)
service
.
Done
(
c
)
err
=
resp
.
Body
.
Close
()
resp
.
Body
.
Close
()
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"close_response_body_failed"
,
http
.
StatusInternalServerError
),
usage
}
return
nil
,
usage
return
nil
,
usage
}
}
...
...
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