Add Qwen support

This commit is contained in:
Luis Pater
2025-08-21 05:11:21 +08:00
parent d58cc55cb2
commit aa2f37d54d
20 changed files with 2888 additions and 9 deletions

View File

@@ -0,0 +1,253 @@
// Package claude provides request translation functionality for Anthropic to OpenAI API.
// It handles parsing and transforming Anthropic API requests into OpenAI Chat Completions API format,
// extracting model information, system instructions, message contents, and tool declarations.
// The package performs JSON data transformation to ensure compatibility
// between Anthropic API format and OpenAI API's expected format.
package claude
import (
"encoding/json"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// ConvertAnthropicRequestToOpenAI parses and transforms an Anthropic API request into OpenAI Chat Completions API format.
// It extracts the model name, system instruction, message contents, and tool declarations
// from the raw JSON request and returns them in the format expected by the OpenAI API.
func ConvertAnthropicRequestToOpenAI(rawJSON []byte) string {
// Base OpenAI Chat Completions API template
out := `{"model":"","messages":[]}`
root := gjson.ParseBytes(rawJSON)
// Model mapping
if model := root.Get("model"); model.Exists() {
modelStr := model.String()
out, _ = sjson.Set(out, "model", modelStr)
}
// Max tokens
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
}
// Temperature
if temp := root.Get("temperature"); temp.Exists() {
out, _ = sjson.Set(out, "temperature", temp.Float())
}
// Top P
if topP := root.Get("top_p"); topP.Exists() {
out, _ = sjson.Set(out, "top_p", topP.Float())
}
// Stop sequences -> stop
if stopSequences := root.Get("stop_sequences"); stopSequences.Exists() {
if stopSequences.IsArray() {
var stops []string
stopSequences.ForEach(func(_, value gjson.Result) bool {
stops = append(stops, value.String())
return true
})
if len(stops) > 0 {
if len(stops) == 1 {
out, _ = sjson.Set(out, "stop", stops[0])
} else {
out, _ = sjson.Set(out, "stop", stops)
}
}
}
}
// Stream
if stream := root.Get("stream"); stream.Exists() {
out, _ = sjson.Set(out, "stream", stream.Bool())
}
// Process messages and system
var openAIMessages []interface{}
// Handle system message first
if system := root.Get("system"); system.Exists() && system.String() != "" {
systemMsg := map[string]interface{}{
"role": "system",
"content": system.String(),
}
openAIMessages = append(openAIMessages, systemMsg)
}
// Process Anthropic messages
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
messages.ForEach(func(_, message gjson.Result) bool {
role := message.Get("role").String()
contentResult := message.Get("content")
msg := map[string]interface{}{
"role": role,
}
// Handle content
if contentResult.Exists() && contentResult.IsArray() {
var textParts []string
var toolCalls []interface{}
var toolResults []interface{}
contentResult.ForEach(func(_, part gjson.Result) bool {
partType := part.Get("type").String()
switch partType {
case "text":
textParts = append(textParts, part.Get("text").String())
case "image":
// Convert Anthropic image format to OpenAI format
if source := part.Get("source"); source.Exists() {
sourceType := source.Get("type").String()
if sourceType == "base64" {
mediaType := source.Get("media_type").String()
data := source.Get("data").String()
imageURL := "data:" + mediaType + ";base64," + data
// For now, add as text since OpenAI image handling is complex
// In a real implementation, you'd need to handle this properly
textParts = append(textParts, "[Image: "+imageURL+"]")
}
}
case "tool_use":
// Convert to OpenAI tool call format
toolCall := map[string]interface{}{
"id": part.Get("id").String(),
"type": "function",
"function": map[string]interface{}{
"name": part.Get("name").String(),
},
}
// Convert input to arguments JSON string
if input := part.Get("input"); input.Exists() {
if inputJSON, err := json.Marshal(input.Value()); err == nil {
if function, ok := toolCall["function"].(map[string]interface{}); ok {
function["arguments"] = string(inputJSON)
}
} else {
if function, ok := toolCall["function"].(map[string]interface{}); ok {
function["arguments"] = "{}"
}
}
} else {
if function, ok := toolCall["function"].(map[string]interface{}); ok {
function["arguments"] = "{}"
}
}
toolCalls = append(toolCalls, toolCall)
case "tool_result":
// Convert to OpenAI tool message format
toolResult := map[string]interface{}{
"role": "tool",
"tool_call_id": part.Get("tool_use_id").String(),
"content": part.Get("content").String(),
}
toolResults = append(toolResults, toolResult)
}
return true
})
// Set content
if len(textParts) > 0 {
msg["content"] = strings.Join(textParts, "")
} else {
msg["content"] = ""
}
// Set tool calls for assistant messages
if role == "assistant" && len(toolCalls) > 0 {
msg["tool_calls"] = toolCalls
}
openAIMessages = append(openAIMessages, msg)
// Add tool result messages separately
for _, toolResult := range toolResults {
openAIMessages = append(openAIMessages, toolResult)
}
} else if contentResult.Exists() && contentResult.Type == gjson.String {
// Simple string content
msg["content"] = contentResult.String()
openAIMessages = append(openAIMessages, msg)
}
return true
})
}
// Set messages
if len(openAIMessages) > 0 {
messagesJSON, _ := json.Marshal(openAIMessages)
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
}
// Process tools - convert Anthropic tools to OpenAI functions
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
var openAITools []interface{}
tools.ForEach(func(_, tool gjson.Result) bool {
openAITool := map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": tool.Get("name").String(),
"description": tool.Get("description").String(),
},
}
// Convert Anthropic input_schema to OpenAI function parameters
if inputSchema := tool.Get("input_schema"); inputSchema.Exists() {
if function, ok := openAITool["function"].(map[string]interface{}); ok {
function["parameters"] = inputSchema.Value()
}
}
openAITools = append(openAITools, openAITool)
return true
})
if len(openAITools) > 0 {
toolsJSON, _ := json.Marshal(openAITools)
out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
}
}
// Tool choice mapping - convert Anthropic tool_choice to OpenAI format
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
switch toolChoice.Get("type").String() {
case "auto":
out, _ = sjson.Set(out, "tool_choice", "auto")
case "any":
out, _ = sjson.Set(out, "tool_choice", "required")
case "tool":
// Specific tool choice
toolName := toolChoice.Get("name").String()
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": toolName,
},
})
default:
// Default to auto if not specified
out, _ = sjson.Set(out, "tool_choice", "auto")
}
}
// Handle user parameter (for tracking)
if user := root.Get("user"); user.Exists() {
out, _ = sjson.Set(out, "user", user.String())
}
return out
}

View File

@@ -0,0 +1,389 @@
// Package claude provides response translation functionality for OpenAI to Anthropic API.
// This package handles the conversion of OpenAI Chat Completions API responses into Anthropic API-compatible
// JSON format, transforming streaming events and non-streaming responses into the format
// expected by Anthropic API clients. It supports both streaming and non-streaming modes,
// handling text content, tool calls, and usage metadata appropriately.
package claude
import (
"encoding/json"
"strings"
"github.com/tidwall/gjson"
)
// ConvertOpenAIResponseToAnthropicParams holds parameters for response conversion
type ConvertOpenAIResponseToAnthropicParams struct {
MessageID string
Model string
CreatedAt int64
// Content accumulator for streaming
ContentAccumulator strings.Builder
// Tool calls accumulator for streaming
ToolCallsAccumulator map[int]*ToolCallAccumulator
// Track if text content block has been started
TextContentBlockStarted bool
// Track finish reason for later use
FinishReason string
// Track if content blocks have been stopped
ContentBlocksStopped bool
// Track if message_delta has been sent
MessageDeltaSent bool
}
// ToolCallAccumulator holds the state for accumulating tool call data
type ToolCallAccumulator struct {
ID string
Name string
Arguments strings.Builder
}
// ConvertOpenAIResponseToAnthropic converts OpenAI streaming response format to Anthropic API format.
// This function processes OpenAI streaming chunks and transforms them into Anthropic-compatible JSON responses.
// It handles text content, tool calls, and usage metadata, outputting responses that match the Anthropic API format.
func ConvertOpenAIResponseToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string {
// Check if this is the [DONE] marker
rawStr := strings.TrimSpace(string(rawJSON))
if rawStr == "[DONE]" {
return convertOpenAIDoneToAnthropic(param)
}
root := gjson.ParseBytes(rawJSON)
// Check if this is a streaming chunk or non-streaming response
objectType := root.Get("object").String()
if objectType == "chat.completion.chunk" {
// Handle streaming response
return convertOpenAIStreamingChunkToAnthropic(rawJSON, param)
} else if objectType == "chat.completion" {
// Handle non-streaming response
return convertOpenAINonStreamingToAnthropic(rawJSON)
}
return []string{}
}
// convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events
func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string {
root := gjson.ParseBytes(rawJSON)
var results []string
// Initialize parameters if needed
if param.MessageID == "" {
param.MessageID = root.Get("id").String()
}
if param.Model == "" {
param.Model = root.Get("model").String()
}
if param.CreatedAt == 0 {
param.CreatedAt = root.Get("created").Int()
}
// Check if this is the first chunk (has role)
if delta := root.Get("choices.0.delta"); delta.Exists() {
if role := delta.Get("role"); role.Exists() && role.String() == "assistant" {
// Send message_start event
messageStart := map[string]interface{}{
"type": "message_start",
"message": map[string]interface{}{
"id": param.MessageID,
"type": "message",
"role": "assistant",
"model": param.Model,
"content": []interface{}{},
"stop_reason": nil,
"stop_sequence": nil,
"usage": map[string]interface{}{
"input_tokens": 0,
"output_tokens": 0,
},
},
}
messageStartJSON, _ := json.Marshal(messageStart)
results = append(results, "event: message_start\ndata: "+string(messageStartJSON)+"\n\n")
// Don't send content_block_start for text here - wait for actual content
}
// Handle content delta
if content := delta.Get("content"); content.Exists() && content.String() != "" {
// Send content_block_start for text if not already sent
if !param.TextContentBlockStarted {
contentBlockStart := map[string]interface{}{
"type": "content_block_start",
"index": 0,
"content_block": map[string]interface{}{
"type": "text",
"text": "",
},
}
contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
param.TextContentBlockStarted = true
}
contentDelta := map[string]interface{}{
"type": "content_block_delta",
"index": 0,
"delta": map[string]interface{}{
"type": "text_delta",
"text": content.String(),
},
}
contentDeltaJSON, _ := json.Marshal(contentDelta)
results = append(results, "event: content_block_delta\ndata: "+string(contentDeltaJSON)+"\n\n")
// Accumulate content
param.ContentAccumulator.WriteString(content.String())
}
// Handle tool calls
if toolCalls := delta.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
if param.ToolCallsAccumulator == nil {
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
}
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
index := int(toolCall.Get("index").Int())
// Initialize accumulator if needed
if _, exists := param.ToolCallsAccumulator[index]; !exists {
param.ToolCallsAccumulator[index] = &ToolCallAccumulator{}
}
accumulator := param.ToolCallsAccumulator[index]
// Handle tool call ID
if id := toolCall.Get("id"); id.Exists() {
accumulator.ID = id.String()
}
// Handle function name
if function := toolCall.Get("function"); function.Exists() {
if name := function.Get("name"); name.Exists() {
accumulator.Name = name.String()
// Send content_block_start for tool_use
contentBlockStart := map[string]interface{}{
"type": "content_block_start",
"index": index + 1, // Offset by 1 since text is at index 0
"content_block": map[string]interface{}{
"type": "tool_use",
"id": accumulator.ID,
"name": accumulator.Name,
"input": map[string]interface{}{},
},
}
contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
}
// Handle function arguments
if args := function.Get("arguments"); args.Exists() {
argsText := args.String()
accumulator.Arguments.WriteString(argsText)
// Send input_json_delta
inputDelta := map[string]interface{}{
"type": "content_block_delta",
"index": index + 1,
"delta": map[string]interface{}{
"type": "input_json_delta",
"partial_json": argsText,
},
}
inputDeltaJSON, _ := json.Marshal(inputDelta)
results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
}
}
return true
})
}
}
// Handle finish_reason (but don't send message_delta/message_stop yet)
if finishReason := root.Get("choices.0.finish_reason"); finishReason.Exists() && finishReason.String() != "" {
reason := finishReason.String()
param.FinishReason = reason
// Send content_block_stop for text if text content block was started
if param.TextContentBlockStarted && !param.ContentBlocksStopped {
contentBlockStop := map[string]interface{}{
"type": "content_block_stop",
"index": 0,
}
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
}
// Send content_block_stop for any tool calls
if !param.ContentBlocksStopped {
for index := range param.ToolCallsAccumulator {
contentBlockStop := map[string]interface{}{
"type": "content_block_stop",
"index": index + 1,
}
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
}
param.ContentBlocksStopped = true
}
// Don't send message_delta here - wait for usage info or [DONE]
}
// Handle usage information separately (this comes in a later chunk)
// Only process if usage has actual values (not null)
if usage := root.Get("usage"); usage.Exists() && usage.Type != gjson.Null && param.FinishReason != "" {
// Check if usage has actual token counts
promptTokens := usage.Get("prompt_tokens")
completionTokens := usage.Get("completion_tokens")
if promptTokens.Exists() && completionTokens.Exists() {
// Send message_delta with usage
messageDelta := map[string]interface{}{
"type": "message_delta",
"delta": map[string]interface{}{
"stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
"stop_sequence": nil,
},
"usage": map[string]interface{}{
"input_tokens": promptTokens.Int(),
"output_tokens": completionTokens.Int(),
},
}
messageDeltaJSON, _ := json.Marshal(messageDelta)
results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
param.MessageDeltaSent = true
}
}
return results
}
// convertOpenAIDoneToAnthropic handles the [DONE] marker and sends final events
func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) []string {
var results []string
// If we haven't sent message_delta yet (no usage info was received), send it now
if param.FinishReason != "" && !param.MessageDeltaSent {
messageDelta := map[string]interface{}{
"type": "message_delta",
"delta": map[string]interface{}{
"stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
"stop_sequence": nil,
},
}
messageDeltaJSON, _ := json.Marshal(messageDelta)
results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
param.MessageDeltaSent = true
}
// Send message_stop
results = append(results, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
return results
}
// convertOpenAINonStreamingToAnthropic converts OpenAI non-streaming response to Anthropic format
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
root := gjson.ParseBytes(rawJSON)
// Build Anthropic response
response := map[string]interface{}{
"id": root.Get("id").String(),
"type": "message",
"role": "assistant",
"model": root.Get("model").String(),
"content": []interface{}{},
"stop_reason": nil,
"stop_sequence": nil,
"usage": map[string]interface{}{
"input_tokens": 0,
"output_tokens": 0,
},
}
// Process message content and tool calls
var contentBlocks []interface{}
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
choice := choices.Array()[0] // Take first choice
// Handle text content
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
textBlock := map[string]interface{}{
"type": "text",
"text": content.String(),
}
contentBlocks = append(contentBlocks, textBlock)
}
// Handle tool calls
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
toolUseBlock := map[string]interface{}{
"type": "tool_use",
"id": toolCall.Get("id").String(),
"name": toolCall.Get("function.name").String(),
}
// Parse arguments
argsStr := toolCall.Get("function.arguments").String()
if argsStr != "" {
var args interface{}
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
toolUseBlock["input"] = args
} else {
toolUseBlock["input"] = map[string]interface{}{}
}
} else {
toolUseBlock["input"] = map[string]interface{}{}
}
contentBlocks = append(contentBlocks, toolUseBlock)
return true
})
}
// Set stop reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
}
}
response["content"] = contentBlocks
// Set usage information
if usage := root.Get("usage"); usage.Exists() {
response["usage"] = map[string]interface{}{
"input_tokens": usage.Get("prompt_tokens").Int(),
"output_tokens": usage.Get("completion_tokens").Int(),
}
}
responseJSON, _ := json.Marshal(response)
return []string{string(responseJSON)}
}
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
func mapOpenAIFinishReasonToAnthropic(openAIReason string) string {
switch openAIReason {
case "stop":
return "end_turn"
case "length":
return "max_tokens"
case "tool_calls":
return "tool_use"
case "content_filter":
return "end_turn" // Anthropic doesn't have direct equivalent
case "function_call": // Legacy OpenAI
return "tool_use"
default:
return "end_turn"
}
}

View File

@@ -0,0 +1,359 @@
// Package gemini provides request translation functionality for Gemini to OpenAI API.
// It handles parsing and transforming Gemini API requests into OpenAI Chat Completions API format,
// extracting model information, generation config, message contents, and tool declarations.
// The package performs JSON data transformation to ensure compatibility
// between Gemini API format and OpenAI API's expected format.
package gemini
import (
"crypto/rand"
"encoding/json"
"math/big"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// ConvertGeminiRequestToOpenAI parses and transforms a Gemini API request into OpenAI Chat Completions API format.
// It extracts the model name, generation config, message contents, and tool declarations
// from the raw JSON request and returns them in the format expected by the OpenAI API.
func ConvertGeminiRequestToOpenAI(rawJSON []byte) string {
// Base OpenAI Chat Completions API template
out := `{"model":"","messages":[]}`
root := gjson.ParseBytes(rawJSON)
// Helper for generating tool call IDs in the form: call_<alphanum>
genToolCallID := func() string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var b strings.Builder
// 24 chars random suffix
for i := 0; i < 24; i++ {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
b.WriteByte(letters[n.Int64()])
}
return "call_" + b.String()
}
// Model mapping
if model := root.Get("model"); model.Exists() {
modelStr := model.String()
out, _ = sjson.Set(out, "model", modelStr)
}
// Generation config mapping
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
// Temperature
if temp := genConfig.Get("temperature"); temp.Exists() {
out, _ = sjson.Set(out, "temperature", temp.Float())
}
// Max tokens
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
}
// Top P
if topP := genConfig.Get("topP"); topP.Exists() {
out, _ = sjson.Set(out, "top_p", topP.Float())
}
// Top K (OpenAI doesn't have direct equivalent, but we can map it)
if topK := genConfig.Get("topK"); topK.Exists() {
// Store as custom parameter for potential use
out, _ = sjson.Set(out, "top_k", topK.Int())
}
// Stop sequences
if stopSequences := genConfig.Get("stopSequences"); stopSequences.Exists() && stopSequences.IsArray() {
var stops []string
stopSequences.ForEach(func(_, value gjson.Result) bool {
stops = append(stops, value.String())
return true
})
if len(stops) > 0 {
out, _ = sjson.Set(out, "stop", stops)
}
}
}
// Stream parameter
if stream := root.Get("stream"); stream.Exists() {
out, _ = sjson.Set(out, "stream", stream.Bool())
}
// Process contents (Gemini messages) -> OpenAI messages
var openAIMessages []interface{}
var toolCallIDs []string // Track tool call IDs for matching with tool results
if contents := root.Get("contents"); contents.Exists() && contents.IsArray() {
contents.ForEach(func(_, content gjson.Result) bool {
role := content.Get("role").String()
parts := content.Get("parts")
// Convert role: model -> assistant
if role == "model" {
role = "assistant"
}
// Create OpenAI message
msg := map[string]interface{}{
"role": role,
"content": "",
}
var contentParts []string
var toolCalls []interface{}
if parts.Exists() && parts.IsArray() {
parts.ForEach(func(_, part gjson.Result) bool {
// Handle text parts
if text := part.Get("text"); text.Exists() {
contentParts = append(contentParts, text.String())
}
// Handle function calls (Gemini) -> tool calls (OpenAI)
if functionCall := part.Get("functionCall"); functionCall.Exists() {
toolCallID := genToolCallID()
toolCallIDs = append(toolCallIDs, toolCallID)
toolCall := map[string]interface{}{
"id": toolCallID,
"type": "function",
"function": map[string]interface{}{
"name": functionCall.Get("name").String(),
},
}
// Convert args to arguments JSON string
if args := functionCall.Get("args"); args.Exists() {
argsJSON, _ := json.Marshal(args.Value())
toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
} else {
toolCall["function"].(map[string]interface{})["arguments"] = "{}"
}
toolCalls = append(toolCalls, toolCall)
}
// Handle function responses (Gemini) -> tool role messages (OpenAI)
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
// Create tool message for function response
toolMsg := map[string]interface{}{
"role": "tool",
"tool_call_id": "", // Will be set based on context
"content": "",
}
// Convert response.content to JSON string
if response := functionResponse.Get("response"); response.Exists() {
if content = response.Get("content"); content.Exists() {
// Use the content field from the response
contentJSON, _ := json.Marshal(content.Value())
toolMsg["content"] = string(contentJSON)
} else {
// Fallback to entire response
responseJSON, _ := json.Marshal(response.Value())
toolMsg["content"] = string(responseJSON)
}
}
// Try to match with previous tool call ID
_ = functionResponse.Get("name").String() // functionName not used for now
if len(toolCallIDs) > 0 {
// Use the last tool call ID (simple matching by function name)
// In a real implementation, you might want more sophisticated matching
toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
} else {
// Generate a tool call ID if none available
toolMsg["tool_call_id"] = genToolCallID()
}
openAIMessages = append(openAIMessages, toolMsg)
}
return true
})
}
// Set content
if len(contentParts) > 0 {
msg["content"] = strings.Join(contentParts, "")
}
// Set tool calls if any
if len(toolCalls) > 0 {
msg["tool_calls"] = toolCalls
}
openAIMessages = append(openAIMessages, msg)
// switch role {
// case "user", "model":
// // Convert role: model -> assistant
// if role == "model" {
// role = "assistant"
// }
//
// // Create OpenAI message
// msg := map[string]interface{}{
// "role": role,
// "content": "",
// }
//
// var contentParts []string
// var toolCalls []interface{}
//
// if parts.Exists() && parts.IsArray() {
// parts.ForEach(func(_, part gjson.Result) bool {
// // Handle text parts
// if text := part.Get("text"); text.Exists() {
// contentParts = append(contentParts, text.String())
// }
//
// // Handle function calls (Gemini) -> tool calls (OpenAI)
// if functionCall := part.Get("functionCall"); functionCall.Exists() {
// toolCallID := genToolCallID()
// toolCallIDs = append(toolCallIDs, toolCallID)
//
// toolCall := map[string]interface{}{
// "id": toolCallID,
// "type": "function",
// "function": map[string]interface{}{
// "name": functionCall.Get("name").String(),
// },
// }
//
// // Convert args to arguments JSON string
// if args := functionCall.Get("args"); args.Exists() {
// argsJSON, _ := json.Marshal(args.Value())
// toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
// } else {
// toolCall["function"].(map[string]interface{})["arguments"] = "{}"
// }
//
// toolCalls = append(toolCalls, toolCall)
// }
//
// return true
// })
// }
//
// // Set content
// if len(contentParts) > 0 {
// msg["content"] = strings.Join(contentParts, "")
// }
//
// // Set tool calls if any
// if len(toolCalls) > 0 {
// msg["tool_calls"] = toolCalls
// }
//
// openAIMessages = append(openAIMessages, msg)
//
// case "function":
// // Handle Gemini function role -> OpenAI tool role
// if parts.Exists() && parts.IsArray() {
// parts.ForEach(func(_, part gjson.Result) bool {
// // Handle function responses (Gemini) -> tool role messages (OpenAI)
// if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
// // Create tool message for function response
// toolMsg := map[string]interface{}{
// "role": "tool",
// "tool_call_id": "", // Will be set based on context
// "content": "",
// }
//
// // Convert response.content to JSON string
// if response := functionResponse.Get("response"); response.Exists() {
// if content = response.Get("content"); content.Exists() {
// // Use the content field from the response
// contentJSON, _ := json.Marshal(content.Value())
// toolMsg["content"] = string(contentJSON)
// } else {
// // Fallback to entire response
// responseJSON, _ := json.Marshal(response.Value())
// toolMsg["content"] = string(responseJSON)
// }
// }
//
// // Try to match with previous tool call ID
// _ = functionResponse.Get("name").String() // functionName not used for now
// if len(toolCallIDs) > 0 {
// // Use the last tool call ID (simple matching by function name)
// // In a real implementation, you might want more sophisticated matching
// toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
// } else {
// // Generate a tool call ID if none available
// toolMsg["tool_call_id"] = genToolCallID()
// }
//
// openAIMessages = append(openAIMessages, toolMsg)
// }
//
// return true
// })
// }
// }
return true
})
}
// Set messages
if len(openAIMessages) > 0 {
messagesJSON, _ := json.Marshal(openAIMessages)
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
}
// Tools mapping: Gemini tools -> OpenAI tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
var openAITools []interface{}
tools.ForEach(func(_, tool gjson.Result) bool {
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
openAITool := map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": funcDecl.Get("name").String(),
"description": funcDecl.Get("description").String(),
},
}
// Convert parameters schema
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
} else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
}
openAITools = append(openAITools, openAITool)
return true
})
}
return true
})
if len(openAITools) > 0 {
toolsJSON, _ := json.Marshal(openAITools)
out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
}
}
// Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)
if toolConfig := root.Get("toolConfig"); toolConfig.Exists() {
if functionCallingConfig := toolConfig.Get("functionCallingConfig"); functionCallingConfig.Exists() {
mode := functionCallingConfig.Get("mode").String()
switch mode {
case "NONE":
out, _ = sjson.Set(out, "tool_choice", "none")
case "AUTO":
out, _ = sjson.Set(out, "tool_choice", "auto")
case "ANY":
out, _ = sjson.Set(out, "tool_choice", "required")
}
}
}
return out
}

View File

@@ -0,0 +1,353 @@
// Package gemini provides response translation functionality for OpenAI to Gemini API.
// This package handles the conversion of OpenAI Chat Completions API responses into Gemini API-compatible
// JSON format, transforming streaming events and non-streaming responses into the format
// expected by Gemini API clients. It supports both streaming and non-streaming modes,
// handling text content, tool calls, and usage metadata appropriately.
package gemini
import (
"encoding/json"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// ConvertOpenAIResponseToGeminiParams holds parameters for response conversion
type ConvertOpenAIResponseToGeminiParams struct {
// Tool calls accumulator for streaming
ToolCallsAccumulator map[int]*ToolCallAccumulator
// Content accumulator for streaming
ContentAccumulator strings.Builder
// Track if this is the first chunk
IsFirstChunk bool
}
// ToolCallAccumulator holds the state for accumulating tool call data
type ToolCallAccumulator struct {
ID string
Name string
Arguments strings.Builder
}
// ConvertOpenAIResponseToGemini converts OpenAI Chat Completions streaming response format to Gemini API format.
// This function processes OpenAI streaming chunks and transforms them into Gemini-compatible JSON responses.
// It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini API format.
func ConvertOpenAIResponseToGemini(rawJSON []byte, param *ConvertOpenAIResponseToGeminiParams) []string {
// Handle [DONE] marker
if strings.TrimSpace(string(rawJSON)) == "[DONE]" {
return []string{}
}
root := gjson.ParseBytes(rawJSON)
// Initialize accumulators if needed
if param.ToolCallsAccumulator == nil {
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
}
// Process choices
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
// Handle empty choices array (usage-only chunk)
if len(choices.Array()) == 0 {
// This is a usage-only chunk, handle usage and return
if usage := root.Get("usage"); usage.Exists() {
template := `{"candidates":[],"usageMetadata":{}}`
// Set model if available
if model := root.Get("model"); model.Exists() {
template, _ = sjson.Set(template, "model", model.String())
}
usageObj := map[string]interface{}{
"promptTokenCount": usage.Get("prompt_tokens").Int(),
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
"totalTokenCount": usage.Get("total_tokens").Int(),
}
template, _ = sjson.Set(template, "usageMetadata", usageObj)
return []string{template}
}
return []string{}
}
var results []string
choices.ForEach(func(choiceIndex, choice gjson.Result) bool {
// Base Gemini response template
template := `{"candidates":[{"content":{"parts":[],"role":"model"},"finishReason":"STOP","index":0}]}`
// Set model if available
if model := root.Get("model"); model.Exists() {
template, _ = sjson.Set(template, "model", model.String())
}
_ = int(choice.Get("index").Int()) // choiceIdx not used in streaming
delta := choice.Get("delta")
// Handle role (only in first chunk)
if role := delta.Get("role"); role.Exists() && param.IsFirstChunk {
// OpenAI assistant -> Gemini model
if role.String() == "assistant" {
template, _ = sjson.Set(template, "candidates.0.content.role", "model")
}
param.IsFirstChunk = false
results = append(results, template)
return true
}
// Handle content delta
if content := delta.Get("content"); content.Exists() && content.String() != "" {
contentText := content.String()
param.ContentAccumulator.WriteString(contentText)
// Create text part for this delta
parts := []interface{}{
map[string]interface{}{
"text": contentText,
},
}
template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
results = append(results, template)
return true
}
// Handle tool calls delta
if toolCalls := delta.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
toolIndex := int(toolCall.Get("index").Int())
toolID := toolCall.Get("id").String()
toolType := toolCall.Get("type").String()
if toolType == "function" {
function := toolCall.Get("function")
functionName := function.Get("name").String()
functionArgs := function.Get("arguments").String()
// Initialize accumulator if needed
if _, exists := param.ToolCallsAccumulator[toolIndex]; !exists {
param.ToolCallsAccumulator[toolIndex] = &ToolCallAccumulator{
ID: toolID,
Name: functionName,
}
}
// Update ID if provided
if toolID != "" {
param.ToolCallsAccumulator[toolIndex].ID = toolID
}
// Update name if provided
if functionName != "" {
param.ToolCallsAccumulator[toolIndex].Name = functionName
}
// Accumulate arguments
if functionArgs != "" {
param.ToolCallsAccumulator[toolIndex].Arguments.WriteString(functionArgs)
}
}
return true
})
// Don't output anything for tool call deltas - wait for completion
return true
}
// Handle finish reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
template, _ = sjson.Set(template, "candidates.0.finishReason", geminiFinishReason)
// If we have accumulated tool calls, output them now
if len(param.ToolCallsAccumulator) > 0 {
var parts []interface{}
for _, accumulator := range param.ToolCallsAccumulator {
argsStr := accumulator.Arguments.String()
var argsMap map[string]interface{}
if argsStr != "" && argsStr != "{}" {
// Handle malformed JSON by trying to fix common issues
fixedArgs := argsStr
// Fix unquoted keys and values (common in the sample)
if strings.Contains(fixedArgs, "北京") && !strings.Contains(fixedArgs, "\"北京\"") {
fixedArgs = strings.ReplaceAll(fixedArgs, "北京", "\"北京\"")
}
if strings.Contains(fixedArgs, "celsius") && !strings.Contains(fixedArgs, "\"celsius\"") {
fixedArgs = strings.ReplaceAll(fixedArgs, "celsius", "\"celsius\"")
}
if err := json.Unmarshal([]byte(fixedArgs), &argsMap); err != nil {
// If still fails, try to parse as raw string
if err2 := json.Unmarshal([]byte("\""+argsStr+"\""), &argsMap); err2 != nil {
// Last resort: use empty object
argsMap = map[string]interface{}{}
}
}
} else {
argsMap = map[string]interface{}{}
}
functionCallPart := map[string]interface{}{
"functionCall": map[string]interface{}{
"name": accumulator.Name,
"args": argsMap,
},
}
parts = append(parts, functionCallPart)
}
if len(parts) > 0 {
template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
}
// Clear accumulators
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
}
results = append(results, template)
return true
}
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
usageObj := map[string]interface{}{
"promptTokenCount": usage.Get("prompt_tokens").Int(),
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
"totalTokenCount": usage.Get("total_tokens").Int(),
}
template, _ = sjson.Set(template, "usageMetadata", usageObj)
results = append(results, template)
return true
}
return true
})
return results
}
return []string{}
}
// mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons
func mapOpenAIFinishReasonToGemini(openAIReason string) string {
switch openAIReason {
case "stop":
return "STOP"
case "length":
return "MAX_TOKENS"
case "tool_calls":
return "STOP" // Gemini doesn't have a specific tool_calls finish reason
case "content_filter":
return "SAFETY"
default:
return "STOP"
}
}
// ConvertOpenAINonStreamResponseToGemini converts OpenAI non-streaming response to Gemini format
func ConvertOpenAINonStreamResponseToGemini(rawJSON []byte) string {
root := gjson.ParseBytes(rawJSON)
// Base Gemini response template
out := `{"candidates":[{"content":{"parts":[],"role":"model"},"finishReason":"STOP","index":0}]}`
// Set model if available
if model := root.Get("model"); model.Exists() {
out, _ = sjson.Set(out, "model", model.String())
}
// Process choices
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
choices.ForEach(func(choiceIndex, choice gjson.Result) bool {
choiceIdx := int(choice.Get("index").Int())
message := choice.Get("message")
// Set role
if role := message.Get("role"); role.Exists() {
if role.String() == "assistant" {
out, _ = sjson.Set(out, "candidates.0.content.role", "model")
}
}
var parts []interface{}
// Handle content first
if content := message.Get("content"); content.Exists() && content.String() != "" {
parts = append(parts, map[string]interface{}{
"text": content.String(),
})
}
// Handle tool calls
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
if toolCall.Get("type").String() == "function" {
function := toolCall.Get("function")
functionName := function.Get("name").String()
functionArgs := function.Get("arguments").String()
// Parse arguments
var argsMap map[string]interface{}
if functionArgs != "" && functionArgs != "{}" {
// Handle malformed JSON by trying to fix common issues
fixedArgs := functionArgs
// Fix unquoted keys and values (common in the sample)
if strings.Contains(fixedArgs, "北京") && !strings.Contains(fixedArgs, "\"北京\"") {
fixedArgs = strings.ReplaceAll(fixedArgs, "北京", "\"北京\"")
}
if strings.Contains(fixedArgs, "celsius") && !strings.Contains(fixedArgs, "\"celsius\"") {
fixedArgs = strings.ReplaceAll(fixedArgs, "celsius", "\"celsius\"")
}
if err := json.Unmarshal([]byte(fixedArgs), &argsMap); err != nil {
// If still fails, try to parse as raw string
if err2 := json.Unmarshal([]byte("\""+functionArgs+"\""), &argsMap); err2 != nil {
// Last resort: use empty object
argsMap = map[string]interface{}{}
}
}
} else {
argsMap = map[string]interface{}{}
}
functionCallPart := map[string]interface{}{
"functionCall": map[string]interface{}{
"name": functionName,
"args": argsMap,
},
}
parts = append(parts, functionCallPart)
}
return true
})
}
// Set parts
if len(parts) > 0 {
out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
}
// Handle finish reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
out, _ = sjson.Set(out, "candidates.0.finishReason", geminiFinishReason)
}
// Set index
out, _ = sjson.Set(out, "candidates.0.index", choiceIdx)
return true
})
}
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
usageObj := map[string]interface{}{
"promptTokenCount": usage.Get("prompt_tokens").Int(),
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
"totalTokenCount": usage.Get("total_tokens").Int(),
}
out, _ = sjson.Set(out, "usageMetadata", usageObj)
}
return out
}