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
9c243d1f
authored
Dec 27, 2025
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(gemini): 支持 tool_choice 参数转换,优化错误处理
parent
7379b68f
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
121 additions
and
7 deletions
+121
-7
relay/channel/gemini/relay-gemini.go
+121
-7
No files found.
relay/channel/gemini/relay-gemini.go
View file @
9c243d1f
...
@@ -356,6 +356,13 @@ func CovertOpenAI2Gemini(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
...
@@ -356,6 +356,13 @@ func CovertOpenAI2Gemini(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
})
})
}
}
geminiRequest
.
SetTools
(
geminiTools
)
geminiRequest
.
SetTools
(
geminiTools
)
// [NEW] Convert OpenAI tool_choice to Gemini toolConfig.functionCallingConfig
// Mapping: "auto" -> "AUTO", "none" -> "NONE", "required" -> "ANY"
// Object format: {"type": "function", "function": {"name": "xxx"}} -> "ANY" + allowedFunctionNames
if
textRequest
.
ToolChoice
!=
nil
{
geminiRequest
.
ToolConfig
=
convertToolChoiceToGeminiConfig
(
textRequest
.
ToolChoice
)
}
}
}
if
textRequest
.
ResponseFormat
!=
nil
&&
(
textRequest
.
ResponseFormat
.
Type
==
"json_schema"
||
textRequest
.
ResponseFormat
.
Type
==
"json_object"
)
{
if
textRequest
.
ResponseFormat
!=
nil
&&
(
textRequest
.
ResponseFormat
.
Type
==
"json_schema"
||
textRequest
.
ResponseFormat
.
Type
==
"json_object"
)
{
...
@@ -960,6 +967,24 @@ func responseGeminiChat2OpenAI(c *gin.Context, response *dto.GeminiChatResponse)
...
@@ -960,6 +967,24 @@ func responseGeminiChat2OpenAI(c *gin.Context, response *dto.GeminiChatResponse)
choice
.
FinishReason
=
constant
.
FinishReasonStop
choice
.
FinishReason
=
constant
.
FinishReasonStop
case
"MAX_TOKENS"
:
case
"MAX_TOKENS"
:
choice
.
FinishReason
=
constant
.
FinishReasonLength
choice
.
FinishReason
=
constant
.
FinishReasonLength
case
"SAFETY"
:
// Safety filter triggered
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
case
"RECITATION"
:
// Recitation (citation) detected
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
case
"BLOCKLIST"
:
// Blocklist triggered
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
case
"PROHIBITED_CONTENT"
:
// Prohibited content detected
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
case
"SPII"
:
// Sensitive personally identifiable information
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
case
"OTHER"
:
// Other reasons
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
default
:
default
:
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
choice
.
FinishReason
=
constant
.
FinishReasonContentFilter
}
}
...
@@ -991,13 +1016,34 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d
...
@@ -991,13 +1016,34 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d
isTools
:=
false
isTools
:=
false
isThought
:=
false
isThought
:=
false
if
candidate
.
FinishReason
!=
nil
{
if
candidate
.
FinishReason
!=
nil
{
//
p := GeminiConvertFinishReason(*candidate.FinishReason)
//
Map Gemini FinishReason to OpenAI finish_reason
switch
*
candidate
.
FinishReason
{
switch
*
candidate
.
FinishReason
{
case
"STOP"
:
case
"STOP"
:
// Normal completion
choice
.
FinishReason
=
&
constant
.
FinishReasonStop
choice
.
FinishReason
=
&
constant
.
FinishReasonStop
case
"MAX_TOKENS"
:
case
"MAX_TOKENS"
:
// Reached maximum token limit
choice
.
FinishReason
=
&
constant
.
FinishReasonLength
choice
.
FinishReason
=
&
constant
.
FinishReasonLength
case
"SAFETY"
:
// Safety filter triggered
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
case
"RECITATION"
:
// Recitation (citation) detected
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
case
"BLOCKLIST"
:
// Blocklist triggered
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
case
"PROHIBITED_CONTENT"
:
// Prohibited content detected
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
case
"SPII"
:
// Sensitive personally identifiable information
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
case
"OTHER"
:
// Other reasons
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
default
:
default
:
// Unknown reason, treat as content filter
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
choice
.
FinishReason
=
&
constant
.
FinishReasonContentFilter
}
}
}
}
...
@@ -1214,12 +1260,20 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
...
@@ -1214,12 +1260,20 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
return
nil
,
types
.
NewOpenAIError
(
err
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
}
if
len
(
geminiResponse
.
Candidates
)
==
0
{
if
len
(
geminiResponse
.
Candidates
)
==
0
{
//return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
// [FIX] Return meaningful error when Candidates is empty
//if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil {
if
geminiResponse
.
PromptFeedback
!=
nil
&&
geminiResponse
.
PromptFeedback
.
BlockReason
!=
nil
{
// return nil, types.NewOpenAIError(errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest)
return
nil
,
types
.
NewOpenAIError
(
//} else {
errors
.
New
(
"request blocked by Gemini API: "
+*
geminiResponse
.
PromptFeedback
.
BlockReason
),
// return nil, types.NewOpenAIError(errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError)
types
.
ErrorCodePromptBlocked
,
//}
http
.
StatusBadRequest
,
)
}
else
{
return
nil
,
types
.
NewOpenAIError
(
errors
.
New
(
"empty response from Gemini API"
),
types
.
ErrorCodeEmptyResponse
,
http
.
StatusInternalServerError
,
)
}
}
}
fullTextResponse
:=
responseGeminiChat2OpenAI
(
c
,
&
geminiResponse
)
fullTextResponse
:=
responseGeminiChat2OpenAI
(
c
,
&
geminiResponse
)
fullTextResponse
.
Model
=
info
.
UpstreamModelName
fullTextResponse
.
Model
=
info
.
UpstreamModelName
...
@@ -1362,3 +1416,63 @@ func GeminiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
...
@@ -1362,3 +1416,63 @@ func GeminiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
return
usage
,
nil
return
usage
,
nil
}
}
// convertToolChoiceToGeminiConfig converts OpenAI tool_choice to Gemini toolConfig
// OpenAI tool_choice values:
// - "auto": Let the model decide (default)
// - "none": Don't call any tools
// - "required": Must call at least one tool
// - {"type": "function", "function": {"name": "xxx"}}: Call specific function
//
// Gemini functionCallingConfig.mode values:
// - "AUTO": Model decides whether to call functions
// - "NONE": Model won't call functions
// - "ANY": Model must call at least one function
func
convertToolChoiceToGeminiConfig
(
toolChoice
any
)
*
dto
.
ToolConfig
{
if
toolChoice
==
nil
{
return
nil
}
// Handle string values: "auto", "none", "required"
if
toolChoiceStr
,
ok
:=
toolChoice
.
(
string
);
ok
{
config
:=
&
dto
.
ToolConfig
{
FunctionCallingConfig
:
&
dto
.
FunctionCallingConfig
{},
}
switch
toolChoiceStr
{
case
"auto"
:
config
.
FunctionCallingConfig
.
Mode
=
"AUTO"
case
"none"
:
config
.
FunctionCallingConfig
.
Mode
=
"NONE"
case
"required"
:
config
.
FunctionCallingConfig
.
Mode
=
"ANY"
default
:
// Unknown string value, default to AUTO
config
.
FunctionCallingConfig
.
Mode
=
"AUTO"
}
return
config
}
// Handle object value: {"type": "function", "function": {"name": "xxx"}}
if
toolChoiceMap
,
ok
:=
toolChoice
.
(
map
[
string
]
interface
{});
ok
{
if
toolChoiceMap
[
"type"
]
==
"function"
{
config
:=
&
dto
.
ToolConfig
{
FunctionCallingConfig
:
&
dto
.
FunctionCallingConfig
{
Mode
:
"ANY"
,
},
}
// Extract function name if specified
if
function
,
ok
:=
toolChoiceMap
[
"function"
]
.
(
map
[
string
]
interface
{});
ok
{
if
name
,
ok
:=
function
[
"name"
]
.
(
string
);
ok
&&
name
!=
""
{
config
.
FunctionCallingConfig
.
AllowedFunctionNames
=
[]
string
{
name
}
}
}
return
config
}
// Unsupported map structure (type is not "function"), return nil
return
nil
}
// Unsupported type, return nil
return
nil
}
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