* fix(openai): harden Chat-to-Responses compatibility Add a shared Responses-to-Chat stream state machine and use it from the OpenAI relay path. Preserve assistant text alongside tool calls, bind tool argument deltas by output_index, map incomplete finish reasons, support reasoning/custom tool events, and buffer upstream SSE for non-stream Chat clients. Add deterministic service tests and relay SSE tests for the conversion path. Related to #5745. * refactor: rename openaicompat to relayconvert for improved clarity * feat(gemini): support responses request conversion * feat: add responses to chat conversion support * fix: harden responses chat conversion edge cases
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
package gemini
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
)
|
|
|
|
const (
|
|
geminiResponsesInputTypeCustomToolCall = "custom_tool_call"
|
|
geminiResponsesInputTypeCustomToolCallOutput = "custom_tool_call_output"
|
|
geminiResponsesInputTypeFunctionCallOutput = "function_call_output"
|
|
)
|
|
|
|
func preprocessGeminiOpenAIResponsesRequest(request dto.OpenAIResponsesRequest) (dto.OpenAIResponsesRequest, error) {
|
|
tools, err := filterGeminiResponsesTools(request.Tools)
|
|
if err != nil {
|
|
return request, err
|
|
}
|
|
request.Tools = tools
|
|
|
|
input, err := filterGeminiResponsesInput(request.Input)
|
|
if err != nil {
|
|
return request, err
|
|
}
|
|
request.Input = input
|
|
|
|
return request, nil
|
|
}
|
|
|
|
func filterGeminiResponsesTools(raw []byte) ([]byte, error) {
|
|
if !geminiRawJSONPresent(raw) || common.GetJsonType(raw) != "array" {
|
|
return raw, nil
|
|
}
|
|
|
|
var tools []map[string]any
|
|
if err := common.Unmarshal(raw, &tools); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filtered := make([]map[string]any, 0, len(tools))
|
|
for _, tool := range tools {
|
|
if strings.TrimSpace(common.Interface2String(tool["type"])) != "function" {
|
|
// TODO: Support Responses custom/freeform tools when Gemini has a safe equivalent representation.
|
|
continue
|
|
}
|
|
filtered = append(filtered, tool)
|
|
}
|
|
if len(filtered) == 0 {
|
|
return nil, nil
|
|
}
|
|
return common.Marshal(filtered)
|
|
}
|
|
|
|
func filterGeminiResponsesInput(raw []byte) ([]byte, error) {
|
|
if !geminiRawJSONPresent(raw) || common.GetJsonType(raw) != "array" {
|
|
return raw, nil
|
|
}
|
|
|
|
var items []map[string]any
|
|
if err := common.Unmarshal(raw, &items); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
skippedCustomCallIDs := make(map[string]struct{})
|
|
for _, item := range items {
|
|
if strings.TrimSpace(common.Interface2String(item["type"])) != geminiResponsesInputTypeCustomToolCall {
|
|
continue
|
|
}
|
|
if callID := strings.TrimSpace(common.Interface2String(item["call_id"])); callID != "" {
|
|
skippedCustomCallIDs[callID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
filtered := make([]map[string]any, 0, len(items))
|
|
for _, item := range items {
|
|
itemType := strings.TrimSpace(common.Interface2String(item["type"]))
|
|
switch itemType {
|
|
case geminiResponsesInputTypeCustomToolCall, geminiResponsesInputTypeCustomToolCallOutput:
|
|
// TODO: Support Responses custom/freeform tool calls once Gemini can preserve their semantics.
|
|
continue
|
|
case geminiResponsesInputTypeFunctionCallOutput:
|
|
if _, ok := skippedCustomCallIDs[strings.TrimSpace(common.Interface2String(item["call_id"]))]; ok {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, item)
|
|
}
|
|
|
|
return common.Marshal(filtered)
|
|
}
|
|
|
|
func geminiRawJSONPresent(raw []byte) bool {
|
|
if len(raw) == 0 {
|
|
return false
|
|
}
|
|
return common.GetJsonType(raw) != "null"
|
|
}
|