mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
feat: implement Gemini to OpenAI chat stream conversion with state management and terminal handling
This commit is contained in:
@@ -282,6 +282,107 @@ func StreamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d
|
||||
return &response, isStop
|
||||
}
|
||||
|
||||
type GeminiToChatStreamState struct {
|
||||
id string
|
||||
created int64
|
||||
sawToolCall bool
|
||||
finishEmitted bool
|
||||
latestUsage *dto.Usage
|
||||
}
|
||||
|
||||
func NewGeminiToChatStreamState(id string, created int64) *GeminiToChatStreamState {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID())
|
||||
}
|
||||
if created == 0 {
|
||||
created = kitutil.GetTimestamp()
|
||||
}
|
||||
return &GeminiToChatStreamState{id: id, created: created}
|
||||
}
|
||||
|
||||
func (s *GeminiToChatStreamState) ConvertChunk(geminiResponse *dto.GeminiChatResponse, model string, usage *dto.Usage) []*dto.ChatCompletionsStreamResponse {
|
||||
if s == nil || geminiResponse == nil {
|
||||
return nil
|
||||
}
|
||||
hasNonStopFinish := false
|
||||
for _, candidate := range geminiResponse.Candidates {
|
||||
if candidate.FinishReason != nil && *candidate.FinishReason != "" && *candidate.FinishReason != "STOP" {
|
||||
hasNonStopFinish = true
|
||||
break
|
||||
}
|
||||
}
|
||||
response, isStop := StreamResponseGeminiChat2OpenAI(geminiResponse)
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
response.Id = s.id
|
||||
response.Created = s.created
|
||||
response.Model = model
|
||||
response.Usage = usage
|
||||
|
||||
if response.IsToolCall() {
|
||||
s.sawToolCall = true
|
||||
if !hasNonStopFinish {
|
||||
for i := range response.Choices {
|
||||
if response.Choices[i].FinishReason != nil && *response.Choices[i].FinishReason == types.FinishReasonToolCalls {
|
||||
response.Choices[i].FinishReason = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if usage != nil {
|
||||
s.latestUsage = usage
|
||||
}
|
||||
for _, choice := range response.Choices {
|
||||
if choice.FinishReason != nil && *choice.FinishReason != "" {
|
||||
s.finishEmitted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
responses := []*dto.ChatCompletionsStreamResponse{response}
|
||||
if isStop && !s.finishEmitted {
|
||||
responses = append(responses, s.terminalChunk(model))
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
func (s *GeminiToChatStreamState) Finalize(model string) []*dto.ChatCompletionsStreamResponse {
|
||||
if s == nil || s.finishEmitted {
|
||||
return nil
|
||||
}
|
||||
return []*dto.ChatCompletionsStreamResponse{s.terminalChunk(model)}
|
||||
}
|
||||
|
||||
func (s *GeminiToChatStreamState) Usage() *dto.Usage {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.latestUsage
|
||||
}
|
||||
|
||||
func (s *GeminiToChatStreamState) terminalChunk(model string) *dto.ChatCompletionsStreamResponse {
|
||||
finishReason := types.FinishReasonStop
|
||||
if s.sawToolCall {
|
||||
finishReason = types.FinishReasonToolCalls
|
||||
}
|
||||
s.finishEmitted = true
|
||||
return &dto.ChatCompletionsStreamResponse{
|
||||
Id: s.id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: s.created,
|
||||
Model: model,
|
||||
Choices: []dto.ChatCompletionsStreamResponseChoice{
|
||||
{
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{},
|
||||
FinishReason: &finishReason,
|
||||
},
|
||||
},
|
||||
Usage: s.latestUsage,
|
||||
}
|
||||
}
|
||||
|
||||
func geminiResponseToolCall(item *dto.GeminiPart) *dto.ToolCallResponse {
|
||||
argsBytes, err := kitutil.Marshal(item.FunctionCall.Arguments)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,24 @@ func generateStopBlock(index int) *dto.ClaudeResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func stopOpenBlocks(state *convmeta.ClaudeConvertInfo) []*dto.ClaudeResponse {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
switch state.LastMessagesType {
|
||||
case convmeta.LastMessageTypeText, convmeta.LastMessageTypeThinking:
|
||||
return []*dto.ClaudeResponse{generateStopBlock(state.Index)}
|
||||
case convmeta.LastMessageTypeTools:
|
||||
responses := make([]*dto.ClaudeResponse, 0, state.ToolCallMaxIndexOffset+1)
|
||||
for offset := 0; offset <= state.ToolCallMaxIndexOffset; offset++ {
|
||||
responses = append(responses, generateStopBlock(state.ToolCallBaseIndex+offset))
|
||||
}
|
||||
return responses
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
|
||||
if oaiUsage == nil {
|
||||
return nil
|
||||
@@ -89,16 +107,8 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
// For text/thinking, there is at most one open block at state.Index.
|
||||
// For tools, OpenAI tool_calls can stream multiple parallel tool_use blocks (indexed from 0),
|
||||
// so we may have multiple open blocks and must stop each one explicitly.
|
||||
stopOpenBlocks := func() {
|
||||
switch state.LastMessagesType {
|
||||
case convmeta.LastMessageTypeText, convmeta.LastMessageTypeThinking:
|
||||
claudeResponses = append(claudeResponses, generateStopBlock(state.Index))
|
||||
case convmeta.LastMessageTypeTools:
|
||||
base := state.ToolCallBaseIndex
|
||||
for offset := 0; offset <= state.ToolCallMaxIndexOffset; offset++ {
|
||||
claudeResponses = append(claudeResponses, generateStopBlock(base+offset))
|
||||
}
|
||||
}
|
||||
appendStopOpenBlocks := func() {
|
||||
claudeResponses = append(claudeResponses, stopOpenBlocks(state)...)
|
||||
}
|
||||
// stopOpenBlocksAndAdvance closes the currently open block(s) and advances the content block index
|
||||
// to the next available slot for subsequent content_block_start events.
|
||||
@@ -109,7 +119,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
if state.LastMessagesType == convmeta.LastMessageTypeNone {
|
||||
return
|
||||
}
|
||||
stopOpenBlocks()
|
||||
appendStopOpenBlocks()
|
||||
switch state.LastMessagesType {
|
||||
case convmeta.LastMessageTypeTools:
|
||||
state.Index = state.ToolCallBaseIndex + state.ToolCallMaxIndexOffset + 1
|
||||
@@ -234,23 +244,24 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
}
|
||||
}
|
||||
|
||||
// 如果首块就带 finish_reason,需要立即发送停止块
|
||||
// A first chunk can carry finish_reason before usage; defer terminal events until usage arrives.
|
||||
if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" {
|
||||
state.FinishReason = *openAIResponse.Choices[0].FinishReason
|
||||
stopOpenBlocks()
|
||||
oaiUsage := openAIResponse.Usage
|
||||
if oaiUsage == nil {
|
||||
oaiUsage = state.Usage
|
||||
}
|
||||
if oaiUsage != nil {
|
||||
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
||||
Type: "message_delta",
|
||||
Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
|
||||
Delta: &dto.ClaudeMediaMessage{
|
||||
StopReason: kitutil.GetPointer[string](stopReasonOpenAI2Claude(state.FinishReason)),
|
||||
},
|
||||
})
|
||||
if oaiUsage == nil {
|
||||
return claudeResponses
|
||||
}
|
||||
appendStopOpenBlocks()
|
||||
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
||||
Type: "message_delta",
|
||||
Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
|
||||
Delta: &dto.ClaudeMediaMessage{
|
||||
StopReason: kitutil.GetPointer[string](stopReasonOpenAI2Claude(state.FinishReason)),
|
||||
},
|
||||
})
|
||||
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
||||
Type: "message_stop",
|
||||
})
|
||||
@@ -266,7 +277,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
oaiUsage = state.Usage
|
||||
}
|
||||
if oaiUsage != nil {
|
||||
stopOpenBlocks()
|
||||
appendStopOpenBlocks()
|
||||
stopReason := stopReasonOpenAI2Claude(state.FinishReason)
|
||||
if stopReason == "" {
|
||||
stopReason = "end_turn"
|
||||
@@ -403,7 +414,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
}
|
||||
|
||||
if doneChunk || state.Done {
|
||||
stopOpenBlocks()
|
||||
appendStopOpenBlocks()
|
||||
oaiUsage := openAIResponse.Usage
|
||||
if oaiUsage == nil {
|
||||
oaiUsage = state.Usage
|
||||
@@ -428,6 +439,34 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
||||
return claudeResponses
|
||||
}
|
||||
|
||||
func FinalizeStreamResponseOpenAI2Claude(info convmeta.Meta) []*dto.ClaudeResponse {
|
||||
if info == nil {
|
||||
info = &convmeta.Values{}
|
||||
}
|
||||
state := info.EnsureClaudeConvertInfo()
|
||||
if state.Done {
|
||||
return nil
|
||||
}
|
||||
|
||||
stopReason := stopReasonOpenAI2Claude(state.FinishReason)
|
||||
if stopReason == "" {
|
||||
stopReason = "end_turn"
|
||||
}
|
||||
responses := stopOpenBlocks(state)
|
||||
responses = append(responses,
|
||||
&dto.ClaudeResponse{
|
||||
Type: "message_delta",
|
||||
Usage: buildClaudeUsageFromOpenAIUsage(state.Usage),
|
||||
Delta: &dto.ClaudeMediaMessage{
|
||||
StopReason: kitutil.GetPointer[string](stopReason),
|
||||
},
|
||||
},
|
||||
&dto.ClaudeResponse{Type: "message_stop"},
|
||||
)
|
||||
state.Done = true
|
||||
return responses
|
||||
}
|
||||
|
||||
func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info convmeta.Meta) *dto.ClaudeResponse {
|
||||
var stopReason string
|
||||
contents := make([]dto.ClaudeMediaMessage, 0)
|
||||
|
||||
Reference in New Issue
Block a user