Skip to content

Commit e057c0e

Browse files
committed
添加gemini支持
1 parent b3f1da4 commit e057c0e

9 files changed

Lines changed: 372 additions & 0 deletions

File tree

common/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ const (
190190
ChannelTypeAIProxyLibrary = 21
191191
ChannelTypeFastGPT = 22
192192
ChannelTypeTencent = 23
193+
ChannelTypeGemini = 24
193194
)
194195

195196
var ChannelBaseURLs = []string{

common/model-ratio.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ var ModelRatio = map[string]float64{
6161
"ERNIE-Bot-4": 8.572, // ¥0.12 / 1k tokens
6262
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
6363
"PaLM-2": 1,
64+
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
6465
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
6566
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
6667
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens

controller/channel-test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ func testChannel(channel *model.Channel, request ChatRequest) (err error, openai
2929
fallthrough
3030
case common.ChannelType360:
3131
fallthrough
32+
case common.ChannelTypeGemini:
33+
fallthrough
3234
case common.ChannelTypeXunfei:
3335
return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
3436
case common.ChannelTypeAzure:

controller/model.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,15 @@ func init() {
423423
Root: "PaLM-2",
424424
Parent: nil,
425425
},
426+
{
427+
Id: "gemini-pro",
428+
Object: "model",
429+
Created: 1677649963,
430+
OwnedBy: "google",
431+
Permission: permission,
432+
Root: "gemini-pro",
433+
Parent: nil,
434+
},
426435
{
427436
Id: "chatglm_turbo",
428437
Object: "model",

controller/relay-gemini.go

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
package controller
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"one-api/common"
10+
"strings"
11+
12+
"github.com/gin-gonic/gin"
13+
)
14+
15+
type GeminiChatRequest struct {
16+
Contents []GeminiChatContent `json:"contents"`
17+
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
18+
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
19+
Tools []GeminiChatTools `json:"tools,omitempty"`
20+
}
21+
22+
type GeminiInlineData struct {
23+
MimeType string `json:"mimeType"`
24+
Data string `json:"data"`
25+
}
26+
27+
type GeminiPart struct {
28+
Text string `json:"text,omitempty"`
29+
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
30+
}
31+
32+
type GeminiChatContent struct {
33+
Role string `json:"role,omitempty"`
34+
Parts []GeminiPart `json:"parts"`
35+
}
36+
37+
type GeminiChatSafetySettings struct {
38+
Category string `json:"category"`
39+
Threshold string `json:"threshold"`
40+
}
41+
42+
type GeminiChatTools struct {
43+
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
44+
}
45+
46+
type GeminiChatGenerationConfig struct {
47+
Temperature float64 `json:"temperature,omitempty"`
48+
TopP float64 `json:"topP,omitempty"`
49+
TopK float64 `json:"topK,omitempty"`
50+
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
51+
CandidateCount int `json:"candidateCount,omitempty"`
52+
StopSequences []string `json:"stopSequences,omitempty"`
53+
}
54+
55+
// Setting safety to the lowest possible values since Gemini is already powerless enough
56+
func requestOpenAI2Gemini(textRequest GeneralOpenAIRequest) *GeminiChatRequest {
57+
geminiRequest := GeminiChatRequest{
58+
Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
59+
//SafetySettings: []GeminiChatSafetySettings{
60+
// {
61+
// Category: "HARM_CATEGORY_HARASSMENT",
62+
// Threshold: "BLOCK_ONLY_HIGH",
63+
// },
64+
// {
65+
// Category: "HARM_CATEGORY_HATE_SPEECH",
66+
// Threshold: "BLOCK_ONLY_HIGH",
67+
// },
68+
// {
69+
// Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
70+
// Threshold: "BLOCK_ONLY_HIGH",
71+
// },
72+
// {
73+
// Category: "HARM_CATEGORY_DANGEROUS_CONTENT",
74+
// Threshold: "BLOCK_ONLY_HIGH",
75+
// },
76+
//},
77+
GenerationConfig: GeminiChatGenerationConfig{
78+
Temperature: textRequest.Temperature,
79+
TopP: textRequest.TopP,
80+
MaxOutputTokens: textRequest.MaxTokens,
81+
},
82+
}
83+
if textRequest.Functions != nil {
84+
geminiRequest.Tools = []GeminiChatTools{
85+
{
86+
FunctionDeclarations: textRequest.Functions,
87+
},
88+
}
89+
}
90+
shouldAddDummyModelMessage := false
91+
for _, message := range textRequest.Messages {
92+
content := GeminiChatContent{
93+
Role: message.Role,
94+
Parts: []GeminiPart{
95+
{
96+
Text: string(message.Content),
97+
},
98+
},
99+
}
100+
// there's no assistant role in gemini and API shall vomit if Role is not user or model
101+
if content.Role == "assistant" {
102+
content.Role = "model"
103+
}
104+
// Converting system prompt to prompt from user for the same reason
105+
if content.Role == "system" {
106+
content.Role = "user"
107+
shouldAddDummyModelMessage = true
108+
}
109+
geminiRequest.Contents = append(geminiRequest.Contents, content)
110+
111+
// If a system message is the last message, we need to add a dummy model message to make gemini happy
112+
if shouldAddDummyModelMessage {
113+
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
114+
Role: "model",
115+
Parts: []GeminiPart{
116+
{
117+
Text: "Okay",
118+
},
119+
},
120+
})
121+
shouldAddDummyModelMessage = false
122+
}
123+
}
124+
125+
return &geminiRequest
126+
}
127+
128+
type GeminiChatResponse struct {
129+
Candidates []GeminiChatCandidate `json:"candidates"`
130+
PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"`
131+
}
132+
133+
func (g *GeminiChatResponse) GetResponseText() string {
134+
if g == nil {
135+
return ""
136+
}
137+
if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
138+
return g.Candidates[0].Content.Parts[0].Text
139+
}
140+
return ""
141+
}
142+
143+
type GeminiChatCandidate struct {
144+
Content GeminiChatContent `json:"content"`
145+
FinishReason string `json:"finishReason"`
146+
Index int64 `json:"index"`
147+
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
148+
}
149+
150+
type GeminiChatSafetyRating struct {
151+
Category string `json:"category"`
152+
Probability string `json:"probability"`
153+
}
154+
155+
type GeminiChatPromptFeedback struct {
156+
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
157+
}
158+
159+
func responseGeminiChat2OpenAI(response *GeminiChatResponse) *OpenAITextResponse {
160+
fullTextResponse := OpenAITextResponse{
161+
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
162+
Object: "chat.completion",
163+
Created: common.GetTimestamp(),
164+
Choices: make([]OpenAITextResponseChoice, 0, len(response.Candidates)),
165+
}
166+
content, _ := json.Marshal("")
167+
for i, candidate := range response.Candidates {
168+
choice := OpenAITextResponseChoice{
169+
Index: i,
170+
Message: Message{
171+
Role: "assistant",
172+
Content: content,
173+
},
174+
FinishReason: stopFinishReason,
175+
}
176+
content, _ = json.Marshal(candidate.Content.Parts[0].Text)
177+
if len(candidate.Content.Parts) > 0 {
178+
choice.Message.Content = content
179+
}
180+
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
181+
}
182+
return &fullTextResponse
183+
}
184+
185+
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *ChatCompletionsStreamResponse {
186+
var choice ChatCompletionsStreamResponseChoice
187+
choice.Delta.Content = geminiResponse.GetResponseText()
188+
choice.FinishReason = &stopFinishReason
189+
var response ChatCompletionsStreamResponse
190+
response.Object = "chat.completion.chunk"
191+
response.Model = "gemini"
192+
response.Choices = []ChatCompletionsStreamResponseChoice{choice}
193+
return &response
194+
}
195+
196+
func geminiChatStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
197+
responseText := ""
198+
dataChan := make(chan string)
199+
stopChan := make(chan bool)
200+
scanner := bufio.NewScanner(resp.Body)
201+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
202+
if atEOF && len(data) == 0 {
203+
return 0, nil, nil
204+
}
205+
if i := strings.Index(string(data), "\n"); i >= 0 {
206+
return i + 1, data[0:i], nil
207+
}
208+
if atEOF {
209+
return len(data), data, nil
210+
}
211+
return 0, nil, nil
212+
})
213+
go func() {
214+
for scanner.Scan() {
215+
data := scanner.Text()
216+
data = strings.TrimSpace(data)
217+
if !strings.HasPrefix(data, "\"text\": \"") {
218+
continue
219+
}
220+
data = strings.TrimPrefix(data, "\"text\": \"")
221+
data = strings.TrimSuffix(data, "\"")
222+
dataChan <- data
223+
}
224+
stopChan <- true
225+
}()
226+
setEventStreamHeaders(c)
227+
c.Stream(func(w io.Writer) bool {
228+
select {
229+
case data := <-dataChan:
230+
// this is used to prevent annoying \ related format bug
231+
data = fmt.Sprintf("{\"content\": \"%s\"}", data)
232+
type dummyStruct struct {
233+
Content string `json:"content"`
234+
}
235+
var dummy dummyStruct
236+
err := json.Unmarshal([]byte(data), &dummy)
237+
responseText += dummy.Content
238+
var choice ChatCompletionsStreamResponseChoice
239+
choice.Delta.Content = dummy.Content
240+
response := ChatCompletionsStreamResponse{
241+
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
242+
Object: "chat.completion.chunk",
243+
Created: common.GetTimestamp(),
244+
Model: "gemini-pro",
245+
Choices: []ChatCompletionsStreamResponseChoice{choice},
246+
}
247+
jsonResponse, err := json.Marshal(response)
248+
if err != nil {
249+
common.SysError("error marshalling stream response: " + err.Error())
250+
return true
251+
}
252+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
253+
return true
254+
case <-stopChan:
255+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
256+
return false
257+
}
258+
})
259+
err := resp.Body.Close()
260+
if err != nil {
261+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
262+
}
263+
return nil, responseText
264+
}
265+
266+
func geminiChatHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
267+
responseBody, err := io.ReadAll(resp.Body)
268+
if err != nil {
269+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
270+
}
271+
err = resp.Body.Close()
272+
if err != nil {
273+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
274+
}
275+
var geminiResponse GeminiChatResponse
276+
err = json.Unmarshal(responseBody, &geminiResponse)
277+
if err != nil {
278+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
279+
}
280+
if len(geminiResponse.Candidates) == 0 {
281+
return &OpenAIErrorWithStatusCode{
282+
OpenAIError: OpenAIError{
283+
Message: "No candidates returned",
284+
Type: "server_error",
285+
Param: "",
286+
Code: 500,
287+
},
288+
StatusCode: resp.StatusCode,
289+
}, nil
290+
}
291+
fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse)
292+
completionTokens := countTokenText(geminiResponse.GetResponseText(), model)
293+
usage := Usage{
294+
PromptTokens: promptTokens,
295+
CompletionTokens: completionTokens,
296+
TotalTokens: promptTokens + completionTokens,
297+
}
298+
fullTextResponse.Usage = usage
299+
jsonResponse, err := json.Marshal(fullTextResponse)
300+
if err != nil {
301+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
302+
}
303+
c.Writer.Header().Set("Content-Type", "application/json")
304+
c.Writer.WriteHeader(resp.StatusCode)
305+
_, err = c.Writer.Write(jsonResponse)
306+
return nil, &usage
307+
}

0 commit comments

Comments
 (0)