refactor: replace sjson.Set usage with sjson.SetBytes to optimize mutable JSON transformations
This commit is contained in:
@@ -19,23 +19,23 @@ import (
|
||||
func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
rawJSON := inputRawJSON
|
||||
// Base OpenAI Chat Completions API template
|
||||
out := `{"model":"","messages":[]}`
|
||||
out := []byte(`{"model":"","messages":[]}`)
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Model mapping
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||
|
||||
// Max tokens
|
||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if temp := root.Get("temperature"); temp.Exists() {
|
||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
||||
out, _ = sjson.SetBytes(out, "temperature", temp.Float())
|
||||
} else if topP := root.Get("top_p"); topP.Exists() { // Top P
|
||||
out, _ = sjson.Set(out, "top_p", topP.Float())
|
||||
out, _ = sjson.SetBytes(out, "top_p", topP.Float())
|
||||
}
|
||||
|
||||
// Stop sequences -> stop
|
||||
@@ -48,16 +48,16 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
})
|
||||
if len(stops) > 0 {
|
||||
if len(stops) == 1 {
|
||||
out, _ = sjson.Set(out, "stop", stops[0])
|
||||
out, _ = sjson.SetBytes(out, "stop", stops[0])
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "stop", stops)
|
||||
out, _ = sjson.SetBytes(out, "stop", stops)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
out, _ = sjson.SetBytes(out, "stream", stream)
|
||||
|
||||
// Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort
|
||||
if thinkingConfig := root.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||
@@ -67,12 +67,12 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() {
|
||||
budget := int(budgetTokens.Int())
|
||||
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
} else {
|
||||
// No budget_tokens specified, default to "auto" for enabled thinking
|
||||
if effort, ok := thinking.ConvertBudgetToLevel(-1); ok && effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
}
|
||||
case "adaptive", "auto":
|
||||
@@ -83,30 +83,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||
}
|
||||
if effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", string(thinking.LevelXHigh))
|
||||
}
|
||||
case "disabled":
|
||||
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process messages and system
|
||||
var messagesJSON = "[]"
|
||||
messagesJSON := []byte(`[]`)
|
||||
|
||||
// Handle system message first
|
||||
systemMsgJSON := `{"role":"system","content":[]}`
|
||||
systemMsgJSON := []byte(`{"role":"system","content":[]}`)
|
||||
hasSystemContent := false
|
||||
if system := root.Get("system"); system.Exists() {
|
||||
if system.Type == gjson.String {
|
||||
if system.String() != "" {
|
||||
oldSystem := `{"type":"text","text":""}`
|
||||
oldSystem, _ = sjson.Set(oldSystem, "text", system.String())
|
||||
systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", oldSystem)
|
||||
oldSystem := []byte(`{"type":"text","text":""}`)
|
||||
oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String())
|
||||
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
|
||||
hasSystemContent = true
|
||||
}
|
||||
} else if system.Type == gjson.JSON {
|
||||
@@ -114,7 +114,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
systemResults := system.Array()
|
||||
for i := 0; i < len(systemResults); i++ {
|
||||
if contentItem, ok := convertClaudeContentPart(systemResults[i]); ok {
|
||||
systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", contentItem)
|
||||
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", []byte(contentItem))
|
||||
hasSystemContent = true
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
// Only add system message if it has content
|
||||
if hasSystemContent {
|
||||
messagesJSON, _ = sjson.SetRaw(messagesJSON, "-1", systemMsgJSON)
|
||||
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", systemMsgJSON)
|
||||
}
|
||||
|
||||
// Process Anthropic messages
|
||||
@@ -134,10 +134,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
|
||||
// Handle content
|
||||
if contentResult.Exists() && contentResult.IsArray() {
|
||||
var contentItems []string
|
||||
contentItems := make([][]byte, 0)
|
||||
var reasoningParts []string // Accumulate thinking text for reasoning_content
|
||||
var toolCalls []interface{}
|
||||
var toolResults []string // Collect tool_result messages to emit after the main message
|
||||
toolResults := make([][]byte, 0) // Collect tool_result messages to emit after the main message
|
||||
|
||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||
partType := part.Get("type").String()
|
||||
@@ -159,35 +159,35 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
|
||||
case "text", "image":
|
||||
if contentItem, ok := convertClaudeContentPart(part); ok {
|
||||
contentItems = append(contentItems, contentItem)
|
||||
contentItems = append(contentItems, []byte(contentItem))
|
||||
}
|
||||
|
||||
case "tool_use":
|
||||
// Only allow tool_use -> tool_calls for assistant messages (security: prevent injection).
|
||||
if role == "assistant" {
|
||||
toolCallJSON := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "id", part.Get("id").String())
|
||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.name", part.Get("name").String())
|
||||
toolCallJSON := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "id", part.Get("id").String())
|
||||
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.name", part.Get("name").String())
|
||||
|
||||
// Convert input to arguments JSON string
|
||||
if input := part.Get("input"); input.Exists() {
|
||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
|
||||
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.arguments", input.Raw)
|
||||
} else {
|
||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
|
||||
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.arguments", "{}")
|
||||
}
|
||||
|
||||
toolCalls = append(toolCalls, gjson.Parse(toolCallJSON).Value())
|
||||
toolCalls = append(toolCalls, gjson.ParseBytes(toolCallJSON).Value())
|
||||
}
|
||||
|
||||
case "tool_result":
|
||||
// Collect tool_result to emit after the main message (ensures tool results follow tool_calls)
|
||||
toolResultJSON := `{"role":"tool","tool_call_id":"","content":""}`
|
||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
||||
toolResultJSON := []byte(`{"role":"tool","tool_call_id":"","content":""}`)
|
||||
toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
||||
toolResultContent, toolResultContentRaw := convertClaudeToolResultContent(part.Get("content"))
|
||||
if toolResultContentRaw {
|
||||
toolResultJSON, _ = sjson.SetRaw(toolResultJSON, "content", toolResultContent)
|
||||
toolResultJSON, _ = sjson.SetRawBytes(toolResultJSON, "content", []byte(toolResultContent))
|
||||
} else {
|
||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "content", toolResultContent)
|
||||
toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "content", toolResultContent)
|
||||
}
|
||||
toolResults = append(toolResults, toolResultJSON)
|
||||
}
|
||||
@@ -209,53 +209,53 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
// Therefore, we emit tool_result messages FIRST (they respond to the previous assistant's tool_calls),
|
||||
// then emit the current message's content.
|
||||
for _, toolResultJSON := range toolResults {
|
||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolResultJSON).Value())
|
||||
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", toolResultJSON)
|
||||
}
|
||||
|
||||
// For assistant messages: emit a single unified message with content, tool_calls, and reasoning_content
|
||||
// This avoids splitting into multiple assistant messages which breaks OpenAI tool-call adjacency
|
||||
if role == "assistant" {
|
||||
if hasContent || hasReasoning || hasToolCalls {
|
||||
msgJSON := `{"role":"assistant"}`
|
||||
msgJSON := []byte(`{"role":"assistant"}`)
|
||||
|
||||
// Add content (as array if we have items, empty string if reasoning-only)
|
||||
if hasContent {
|
||||
contentArrayJSON := "[]"
|
||||
contentArrayJSON := []byte(`[]`)
|
||||
for _, contentItem := range contentItems {
|
||||
contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem)
|
||||
contentArrayJSON, _ = sjson.SetRawBytes(contentArrayJSON, "-1", contentItem)
|
||||
}
|
||||
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
||||
msgJSON, _ = sjson.SetRawBytes(msgJSON, "content", contentArrayJSON)
|
||||
} else {
|
||||
// Ensure content field exists for OpenAI compatibility
|
||||
msgJSON, _ = sjson.Set(msgJSON, "content", "")
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "content", "")
|
||||
}
|
||||
|
||||
// Add reasoning_content if present
|
||||
if hasReasoning {
|
||||
msgJSON, _ = sjson.Set(msgJSON, "reasoning_content", reasoningContent)
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "reasoning_content", reasoningContent)
|
||||
}
|
||||
|
||||
// Add tool_calls if present (in same message as content)
|
||||
if hasToolCalls {
|
||||
msgJSON, _ = sjson.Set(msgJSON, "tool_calls", toolCalls)
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "tool_calls", toolCalls)
|
||||
}
|
||||
|
||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
||||
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON)
|
||||
}
|
||||
} else {
|
||||
// For non-assistant roles: emit content message if we have content
|
||||
// If the message only contains tool_results (no text/image), we still processed them above
|
||||
if hasContent {
|
||||
msgJSON := `{"role":""}`
|
||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
||||
msgJSON := []byte(`{"role":""}`)
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "role", role)
|
||||
|
||||
contentArrayJSON := "[]"
|
||||
contentArrayJSON := []byte(`[]`)
|
||||
for _, contentItem := range contentItems {
|
||||
contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem)
|
||||
contentArrayJSON, _ = sjson.SetRawBytes(contentArrayJSON, "-1", contentItem)
|
||||
}
|
||||
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
||||
msgJSON, _ = sjson.SetRawBytes(msgJSON, "content", contentArrayJSON)
|
||||
|
||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
||||
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON)
|
||||
} else if hasToolResults && !hasContent {
|
||||
// tool_results already emitted above, no additional user message needed
|
||||
}
|
||||
@@ -263,10 +263,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
|
||||
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
||||
// Simple string content
|
||||
msgJSON := `{"role":"","content":""}`
|
||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
||||
msgJSON, _ = sjson.Set(msgJSON, "content", contentResult.String())
|
||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
||||
msgJSON := []byte(`{"role":"","content":""}`)
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "role", role)
|
||||
msgJSON, _ = sjson.SetBytes(msgJSON, "content", contentResult.String())
|
||||
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -274,30 +274,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
|
||||
// Set messages
|
||||
if gjson.Parse(messagesJSON).IsArray() && len(gjson.Parse(messagesJSON).Array()) > 0 {
|
||||
out, _ = sjson.SetRaw(out, "messages", messagesJSON)
|
||||
if msgs := gjson.ParseBytes(messagesJSON); msgs.IsArray() && len(msgs.Array()) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "messages", messagesJSON)
|
||||
}
|
||||
|
||||
// Process tools - convert Anthropic tools to OpenAI functions
|
||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||
var toolsJSON = "[]"
|
||||
toolsJSON := []byte(`[]`)
|
||||
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
openAIToolJSON := `{"type":"function","function":{"name":"","description":""}}`
|
||||
openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.name", tool.Get("name").String())
|
||||
openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.description", tool.Get("description").String())
|
||||
openAIToolJSON := []byte(`{"type":"function","function":{"name":"","description":""}}`)
|
||||
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.name", tool.Get("name").String())
|
||||
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.description", tool.Get("description").String())
|
||||
|
||||
// Convert Anthropic input_schema to OpenAI function parameters
|
||||
if inputSchema := tool.Get("input_schema"); inputSchema.Exists() {
|
||||
openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.parameters", inputSchema.Value())
|
||||
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.parameters", inputSchema.Value())
|
||||
}
|
||||
|
||||
toolsJSON, _ = sjson.Set(toolsJSON, "-1", gjson.Parse(openAIToolJSON).Value())
|
||||
toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", openAIToolJSON)
|
||||
return true
|
||||
})
|
||||
|
||||
if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 {
|
||||
out, _ = sjson.SetRaw(out, "tools", toolsJSON)
|
||||
if parsed := gjson.ParseBytes(toolsJSON); parsed.IsArray() && len(parsed.Array()) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "tools", toolsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,27 +305,27 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
||||
switch toolChoice.Get("type").String() {
|
||||
case "auto":
|
||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||
case "any":
|
||||
out, _ = sjson.Set(out, "tool_choice", "required")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "required")
|
||||
case "tool":
|
||||
// Specific tool choice
|
||||
toolName := toolChoice.Get("name").String()
|
||||
toolChoiceJSON := `{"type":"function","function":{"name":""}}`
|
||||
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "function.name", toolName)
|
||||
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
||||
toolChoiceJSON := []byte(`{"type":"function","function":{"name":""}}`)
|
||||
toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "function.name", toolName)
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON)
|
||||
default:
|
||||
// Default to auto if not specified
|
||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||
}
|
||||
}
|
||||
|
||||
// Handle user parameter (for tracking)
|
||||
if user := root.Get("user"); user.Exists() {
|
||||
out, _ = sjson.Set(out, "user", user.String())
|
||||
out, _ = sjson.SetBytes(out, "user", user.String())
|
||||
}
|
||||
|
||||
return []byte(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
||||
@@ -337,9 +337,9 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return "", false
|
||||
}
|
||||
textContent := `{"type":"text","text":""}`
|
||||
textContent, _ = sjson.Set(textContent, "text", text)
|
||||
return textContent, true
|
||||
textContent := []byte(`{"type":"text","text":""}`)
|
||||
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||
return string(textContent), true
|
||||
|
||||
case "image":
|
||||
var imageURL string
|
||||
@@ -369,10 +369,10 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
imageContent := `{"type":"image_url","image_url":{"url":""}}`
|
||||
imageContent, _ = sjson.Set(imageContent, "image_url.url", imageURL)
|
||||
imageContent := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||
imageContent, _ = sjson.SetBytes(imageContent, "image_url.url", imageURL)
|
||||
|
||||
return imageContent, true
|
||||
return string(imageContent), true
|
||||
|
||||
default:
|
||||
return "", false
|
||||
@@ -390,26 +390,26 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
||||
|
||||
if content.IsArray() {
|
||||
var parts []string
|
||||
contentJSON := "[]"
|
||||
contentJSON := []byte(`[]`)
|
||||
hasImagePart := false
|
||||
content.ForEach(func(_, item gjson.Result) bool {
|
||||
switch {
|
||||
case item.Type == gjson.String:
|
||||
text := item.String()
|
||||
parts = append(parts, text)
|
||||
textContent := `{"type":"text","text":""}`
|
||||
textContent, _ = sjson.Set(textContent, "text", text)
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
||||
textContent := []byte(`{"type":"text","text":""}`)
|
||||
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent)
|
||||
case item.IsObject() && item.Get("type").String() == "text":
|
||||
text := item.Get("text").String()
|
||||
parts = append(parts, text)
|
||||
textContent := `{"type":"text","text":""}`
|
||||
textContent, _ = sjson.Set(textContent, "text", text)
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
||||
textContent := []byte(`{"type":"text","text":""}`)
|
||||
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent)
|
||||
case item.IsObject() && item.Get("type").String() == "image":
|
||||
contentItem, ok := convertClaudeContentPart(item)
|
||||
if ok {
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
||||
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem))
|
||||
hasImagePart = true
|
||||
} else {
|
||||
parts = append(parts, item.Raw)
|
||||
@@ -423,7 +423,7 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
||||
})
|
||||
|
||||
if hasImagePart {
|
||||
return contentJSON, true
|
||||
return string(contentJSON), true
|
||||
}
|
||||
|
||||
joined := strings.Join(parts, "\n\n")
|
||||
@@ -437,9 +437,9 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
||||
if content.Get("type").String() == "image" {
|
||||
contentItem, ok := convertClaudeContentPart(content)
|
||||
if ok {
|
||||
contentJSON := "[]"
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
||||
return contentJSON, true
|
||||
contentJSON := []byte(`[]`)
|
||||
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem))
|
||||
return string(contentJSON), true
|
||||
}
|
||||
}
|
||||
if text := content.Get("text"); text.Exists() && text.Type == gjson.String {
|
||||
|
||||
@@ -8,9 +8,9 @@ package claude
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
@@ -73,8 +73,8 @@ type ToolCallAccumulator struct {
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing an Anthropic-compatible JSON response.
|
||||
func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
||||
// - [][]byte: A slice of byte chunks, each containing an Anthropic-compatible JSON response.
|
||||
func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
if *param == nil {
|
||||
*param = &ConvertOpenAIResponseToAnthropicParams{
|
||||
MessageID: "",
|
||||
@@ -97,7 +97,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||
|
||||
@@ -106,8 +106,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
||||
}
|
||||
|
||||
// Check if this is the [DONE] marker
|
||||
rawStr := strings.TrimSpace(string(rawJSON))
|
||||
if rawStr == "[DONE]" {
|
||||
if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) {
|
||||
return convertOpenAIDoneToAnthropic((*param).(*ConvertOpenAIResponseToAnthropicParams))
|
||||
}
|
||||
|
||||
@@ -130,9 +129,9 @@ func effectiveOpenAIFinishReason(param *ConvertOpenAIResponseToAnthropicParams)
|
||||
}
|
||||
|
||||
// convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events
|
||||
func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string {
|
||||
func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) [][]byte {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
var results []string
|
||||
var results [][]byte
|
||||
|
||||
// Initialize parameters if needed
|
||||
if param.MessageID == "" {
|
||||
@@ -150,10 +149,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
if delta := root.Get("choices.0.delta"); delta.Exists() {
|
||||
if !param.MessageStarted {
|
||||
// Send message_start event
|
||||
messageStartJSON := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`
|
||||
messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID)
|
||||
messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
|
||||
results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
|
||||
messageStartJSON := []byte(`{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`)
|
||||
messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.id", param.MessageID)
|
||||
messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.model", param.Model)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_start", messageStartJSON, 2))
|
||||
param.MessageStarted = true
|
||||
|
||||
// Don't send content_block_start for text here - wait for actual content
|
||||
@@ -172,15 +171,17 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
param.NextContentBlockIndex++
|
||||
}
|
||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||
param.ThinkingContentBlockStarted = true
|
||||
}
|
||||
|
||||
thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
||||
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
|
||||
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
|
||||
results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
|
||||
thinkingDeltaJSONBytes := []byte(thinkingDeltaJSON)
|
||||
thinkingDeltaJSONBytes, _ = sjson.SetBytes(thinkingDeltaJSONBytes, "index", param.ThinkingContentBlockIndex)
|
||||
thinkingDeltaJSONBytes, _ = sjson.SetBytes(thinkingDeltaJSONBytes, "delta.thinking", reasoningText)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", thinkingDeltaJSONBytes, 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,15 +195,17 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
param.NextContentBlockIndex++
|
||||
}
|
||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
|
||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.TextContentBlockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||
param.TextContentBlockStarted = true
|
||||
}
|
||||
|
||||
contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
||||
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
|
||||
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
|
||||
results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
|
||||
contentDeltaJSONBytes := []byte(contentDeltaJSON)
|
||||
contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "index", param.TextContentBlockIndex)
|
||||
contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "delta.text", content.String())
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", contentDeltaJSONBytes, 2))
|
||||
|
||||
// Accumulate content
|
||||
param.ContentAccumulator.WriteString(content.String())
|
||||
@@ -242,10 +245,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
|
||||
// Send content_block_start for tool_use
|
||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
|
||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID))
|
||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
|
||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", blockIndex)
|
||||
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID))
|
||||
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.name", accumulator.Name)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||
}
|
||||
|
||||
// Handle function arguments
|
||||
@@ -273,9 +277,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
|
||||
// Send content_block_stop for thinking content if needed
|
||||
if param.ThinkingContentBlockStarted {
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
param.ThinkingContentBlockStarted = false
|
||||
param.ThinkingContentBlockIndex = -1
|
||||
}
|
||||
@@ -291,15 +295,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
|
||||
// Send complete input_json_delta with all accumulated arguments
|
||||
if accumulator.Arguments.Len() > 0 {
|
||||
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
||||
inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex)
|
||||
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2))
|
||||
}
|
||||
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
delete(param.ToolCallBlockIndexes, index)
|
||||
}
|
||||
param.ContentBlocksStopped = true
|
||||
@@ -316,14 +320,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
if usage.Exists() && usage.Type != gjson.Null {
|
||||
inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage)
|
||||
// Send message_delta with usage
|
||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
||||
messageDeltaJSON := []byte(`{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
||||
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
||||
if cachedTokens > 0 {
|
||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.cache_read_input_tokens", cachedTokens)
|
||||
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.cache_read_input_tokens", cachedTokens)
|
||||
}
|
||||
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2))
|
||||
param.MessageDeltaSent = true
|
||||
|
||||
emitMessageStopIfNeeded(param, &results)
|
||||
@@ -334,14 +338,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
||||
}
|
||||
|
||||
// convertOpenAIDoneToAnthropic handles the [DONE] marker and sends final events
|
||||
func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) []string {
|
||||
var results []string
|
||||
func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) [][]byte {
|
||||
var results [][]byte
|
||||
|
||||
// Ensure all content blocks are stopped before final events
|
||||
if param.ThinkingContentBlockStarted {
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
param.ThinkingContentBlockStarted = false
|
||||
param.ThinkingContentBlockIndex = -1
|
||||
}
|
||||
@@ -354,15 +358,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
||||
blockIndex := param.toolContentBlockIndex(index)
|
||||
|
||||
if accumulator.Arguments.Len() > 0 {
|
||||
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
||||
inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex)
|
||||
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2))
|
||||
}
|
||||
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex)
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
delete(param.ToolCallBlockIndexes, index)
|
||||
}
|
||||
param.ContentBlocksStopped = true
|
||||
@@ -370,9 +374,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
||||
|
||||
// If we haven't sent message_delta yet (no usage info was received), send it now
|
||||
if param.FinishReason != "" && !param.MessageDeltaSent {
|
||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
||||
messageDeltaJSON := []byte(`{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2))
|
||||
param.MessageDeltaSent = true
|
||||
}
|
||||
|
||||
@@ -382,12 +386,12 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
||||
}
|
||||
|
||||
// convertOpenAINonStreamingToAnthropic converts OpenAI non-streaming response to Anthropic format
|
||||
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
||||
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) [][]byte {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
||||
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
out, _ = sjson.SetBytes(out, "id", root.Get("id").String())
|
||||
out, _ = sjson.SetBytes(out, "model", root.Get("model").String())
|
||||
|
||||
// Process message content and tool calls
|
||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||
@@ -398,59 +402,59 @@ func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
||||
if reasoningText == "" {
|
||||
continue
|
||||
}
|
||||
block := `{"type":"thinking","thinking":""}`
|
||||
block, _ = sjson.Set(block, "thinking", reasoningText)
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||
block, _ = sjson.SetBytes(block, "thinking", reasoningText)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
}
|
||||
|
||||
// Handle text content
|
||||
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
|
||||
block := `{"type":"text","text":""}`
|
||||
block, _ = sjson.Set(block, "text", content.String())
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"text","text":""}`)
|
||||
block, _ = sjson.SetBytes(block, "text", content.String())
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
|
||||
toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", toolCall.Get("function.name").String())
|
||||
|
||||
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||
if argsStr != "" && gjson.Valid(argsStr) {
|
||||
argsJSON := gjson.Parse(argsStr)
|
||||
if argsJSON.IsObject() {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw))
|
||||
} else {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||
}
|
||||
} else {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", toolUseBlock)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Set stop reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||
out, _ = sjson.SetBytes(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// Set usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage)
|
||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||
if cachedTokens > 0 {
|
||||
out, _ = sjson.Set(out, "usage.cache_read_input_tokens", cachedTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
|
||||
}
|
||||
}
|
||||
|
||||
return []string{out}
|
||||
return [][]byte{out}
|
||||
}
|
||||
|
||||
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
|
||||
@@ -513,32 +517,32 @@ func collectOpenAIReasoningTexts(node gjson.Result) []string {
|
||||
return texts
|
||||
}
|
||||
|
||||
func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
||||
func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||
if !param.ThinkingContentBlockStarted {
|
||||
return
|
||||
}
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
param.ThinkingContentBlockStarted = false
|
||||
param.ThinkingContentBlockIndex = -1
|
||||
}
|
||||
|
||||
func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
||||
func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||
if param.MessageStopSent {
|
||||
return
|
||||
}
|
||||
*results = append(*results, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
|
||||
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "message_stop", []byte(`{"type":"message_stop"}`), 2))
|
||||
param.MessageStopSent = true
|
||||
}
|
||||
|
||||
func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
||||
func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||
if !param.TextContentBlockStarted {
|
||||
return
|
||||
}
|
||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
|
||||
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.TextContentBlockIndex)
|
||||
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||
param.TextContentBlockStarted = false
|
||||
param.TextContentBlockIndex = -1
|
||||
}
|
||||
@@ -552,15 +556,15 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - string: An Anthropic-compatible JSON response.
|
||||
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||
// - []byte: An Anthropic-compatible JSON response.
|
||||
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||
_ = requestRawJSON
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
||||
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
||||
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
out, _ = sjson.SetBytes(out, "id", root.Get("id").String())
|
||||
out, _ = sjson.SetBytes(out, "model", root.Get("model").String())
|
||||
|
||||
hasToolCall := false
|
||||
stopReasonSet := false
|
||||
@@ -569,7 +573,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
choice := choices.Array()[0]
|
||||
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||
out, _ = sjson.SetBytes(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||
stopReasonSet = true
|
||||
}
|
||||
|
||||
@@ -583,9 +587,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
if textBuilder.Len() == 0 {
|
||||
return
|
||||
}
|
||||
block := `{"type":"text","text":""}`
|
||||
block, _ = sjson.Set(block, "text", textBuilder.String())
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"text","text":""}`)
|
||||
block, _ = sjson.SetBytes(block, "text", textBuilder.String())
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
textBuilder.Reset()
|
||||
}
|
||||
|
||||
@@ -593,9 +597,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
if thinkingBuilder.Len() == 0 {
|
||||
return
|
||||
}
|
||||
block := `{"type":"thinking","thinking":""}`
|
||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
thinkingBuilder.Reset()
|
||||
}
|
||||
|
||||
@@ -611,23 +615,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
if toolCalls.IsArray() {
|
||||
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
||||
hasToolCall = true
|
||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||
toolUse, _ = sjson.Set(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String()))
|
||||
toolUse, _ = sjson.Set(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String()))
|
||||
toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
toolUse, _ = sjson.SetBytes(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String()))
|
||||
toolUse, _ = sjson.SetBytes(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String()))
|
||||
|
||||
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
||||
if argsStr != "" && gjson.Valid(argsStr) {
|
||||
argsJSON := gjson.Parse(argsStr)
|
||||
if argsJSON.IsObject() {
|
||||
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
||||
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw))
|
||||
} else {
|
||||
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(`{}`))
|
||||
}
|
||||
} else {
|
||||
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(`{}`))
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "content.-1", toolUse)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", toolUse)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -647,9 +651,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
} else if contentResult.Type == gjson.String {
|
||||
textContent := contentResult.String()
|
||||
if textContent != "" {
|
||||
block := `{"type":"text","text":""}`
|
||||
block, _ = sjson.Set(block, "text", textContent)
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"text","text":""}`)
|
||||
block, _ = sjson.SetBytes(block, "text", textContent)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -659,32 +663,32 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
if reasoningText == "" {
|
||||
continue
|
||||
}
|
||||
block := `{"type":"thinking","thinking":""}`
|
||||
block, _ = sjson.Set(block, "thinking", reasoningText)
|
||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||
block, _ = sjson.SetBytes(block, "thinking", reasoningText)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||
}
|
||||
}
|
||||
|
||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||
hasToolCall = true
|
||||
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String()))
|
||||
toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String()))
|
||||
|
||||
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||
if argsStr != "" && gjson.Valid(argsStr) {
|
||||
argsJSON := gjson.Parse(argsStr)
|
||||
if argsJSON.IsObject() {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw))
|
||||
} else {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||
}
|
||||
} else {
|
||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
|
||||
out, _ = sjson.SetRawBytes(out, "content.-1", toolUseBlock)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -693,26 +697,26 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
|
||||
if respUsage := root.Get("usage"); respUsage.Exists() {
|
||||
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(respUsage)
|
||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||
if cachedTokens > 0 {
|
||||
out, _ = sjson.Set(out, "usage.cache_read_input_tokens", cachedTokens)
|
||||
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
|
||||
}
|
||||
}
|
||||
|
||||
if !stopReasonSet {
|
||||
if hasToolCall {
|
||||
out, _ = sjson.Set(out, "stop_reason", "tool_use")
|
||||
out, _ = sjson.SetBytes(out, "stop_reason", "tool_use")
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
||||
out, _ = sjson.SetBytes(out, "stop_reason", "end_turn")
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
||||
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||
}
|
||||
|
||||
func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) {
|
||||
|
||||
@@ -7,10 +7,9 @@ package geminiCLI
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertOpenAIResponseToGeminiCLI converts OpenAI Chat Completions streaming response format to Gemini API format.
|
||||
@@ -24,14 +23,12 @@ import (
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
||||
// - [][]byte: A slice of Gemini-compatible JSON responses.
|
||||
func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
outputs := ConvertOpenAIResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||
newOutputs := make([]string, 0)
|
||||
newOutputs := make([][]byte, 0, len(outputs))
|
||||
for i := 0; i < len(outputs); i++ {
|
||||
json := `{"response": {}}`
|
||||
output, _ := sjson.SetRaw(json, "response", outputs[i])
|
||||
newOutputs = append(newOutputs, output)
|
||||
newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i]))
|
||||
}
|
||||
return newOutputs
|
||||
}
|
||||
@@ -45,14 +42,12 @@ func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, ori
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - string: A Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
||||
strJSON := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||
json := `{"response": {}}`
|
||||
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
|
||||
return strJSON
|
||||
// - []byte: A Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||
out := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||
return translatorcommon.WrapGeminiCLIResponse(out)
|
||||
}
|
||||
|
||||
func GeminiCLITokenCount(ctx context.Context, count int64) string {
|
||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
||||
func GeminiCLITokenCount(ctx context.Context, count int64) []byte {
|
||||
return translatorcommon.GeminiTokenCountJSON(count)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
rawJSON := inputRawJSON
|
||||
// Base OpenAI Chat Completions API template
|
||||
out := `{"model":"","messages":[]}`
|
||||
out := []byte(`{"model":"","messages":[]}`)
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
@@ -39,29 +39,29 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
|
||||
// Model mapping
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||
|
||||
// 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())
|
||||
out, _ = sjson.SetBytes(out, "temperature", temp.Float())
|
||||
}
|
||||
|
||||
// Max tokens
|
||||
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
|
||||
// Top P
|
||||
if topP := genConfig.Get("topP"); topP.Exists() {
|
||||
out, _ = sjson.Set(out, "top_p", topP.Float())
|
||||
out, _ = sjson.SetBytes(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())
|
||||
out, _ = sjson.SetBytes(out, "top_k", topK.Int())
|
||||
}
|
||||
|
||||
// Stop sequences
|
||||
@@ -72,13 +72,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
return true
|
||||
})
|
||||
if len(stops) > 0 {
|
||||
out, _ = sjson.Set(out, "stop", stops)
|
||||
out, _ = sjson.SetBytes(out, "stop", stops)
|
||||
}
|
||||
}
|
||||
|
||||
// Candidate count (OpenAI 'n' parameter)
|
||||
if candidateCount := genConfig.Get("candidateCount"); candidateCount.Exists() {
|
||||
out, _ = sjson.Set(out, "n", candidateCount.Int())
|
||||
out, _ = sjson.SetBytes(out, "n", candidateCount.Int())
|
||||
}
|
||||
|
||||
// Map Gemini thinkingConfig to OpenAI reasoning_effort.
|
||||
@@ -92,7 +92,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
if thinkingLevel.Exists() {
|
||||
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||
if effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
} else {
|
||||
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
||||
@@ -101,7 +101,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
if thinkingBudget.Exists() {
|
||||
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
|
||||
// Stream parameter
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
out, _ = sjson.SetBytes(out, "stream", stream)
|
||||
|
||||
// Process contents (Gemini messages) -> OpenAI messages
|
||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
||||
@@ -122,16 +122,16 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
if systemInstruction.Exists() {
|
||||
parts := systemInstruction.Get("parts")
|
||||
msg := `{"role":"system","content":[]}`
|
||||
msg := []byte(`{"role":"system","content":[]}`)
|
||||
hasContent := false
|
||||
|
||||
if parts.Exists() && parts.IsArray() {
|
||||
parts.ForEach(func(_, part gjson.Result) bool {
|
||||
// Handle text parts
|
||||
if text := part.Get("text"); text.Exists() {
|
||||
contentPart := `{"type":"text","text":""}`
|
||||
contentPart, _ = sjson.Set(contentPart, "text", text.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"text","text":""}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "text", text.String())
|
||||
msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart)
|
||||
hasContent = true
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
data := inlineData.Get("data").String()
|
||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||
|
||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||
msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart)
|
||||
hasContent = true
|
||||
}
|
||||
return true
|
||||
@@ -154,7 +154,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
}
|
||||
|
||||
if hasContent {
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,14 +168,14 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
role = "assistant"
|
||||
}
|
||||
|
||||
msg := `{"role":"","content":""}`
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
msg := []byte(`{"role":"","content":""}`)
|
||||
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||
|
||||
var textBuilder strings.Builder
|
||||
contentWrapper := `{"arr":[]}`
|
||||
contentWrapper := []byte(`{"arr":[]}`)
|
||||
contentPartsCount := 0
|
||||
onlyTextContent := true
|
||||
toolCallsWrapper := `{"arr":[]}`
|
||||
toolCallsWrapper := []byte(`{"arr":[]}`)
|
||||
toolCallsCount := 0
|
||||
|
||||
if parts.Exists() && parts.IsArray() {
|
||||
@@ -184,9 +184,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
if text := part.Get("text"); text.Exists() {
|
||||
formattedText := text.String()
|
||||
textBuilder.WriteString(formattedText)
|
||||
contentPart := `{"type":"text","text":""}`
|
||||
contentPart, _ = sjson.Set(contentPart, "text", formattedText)
|
||||
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"text","text":""}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "text", formattedText)
|
||||
contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart)
|
||||
contentPartsCount++
|
||||
}
|
||||
|
||||
@@ -201,9 +201,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
data := inlineData.Get("data").String()
|
||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||
|
||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
||||
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||
contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart)
|
||||
contentPartsCount++
|
||||
}
|
||||
|
||||
@@ -212,32 +212,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
toolCallID := genToolCallID()
|
||||
toolCallIDs = append(toolCallIDs, toolCallID)
|
||||
|
||||
toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
||||
toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
|
||||
toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
|
||||
toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "id", toolCallID)
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "function.name", functionCall.Get("name").String())
|
||||
|
||||
// Convert args to arguments JSON string
|
||||
if args := functionCall.Get("args"); args.Exists() {
|
||||
toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", args.Raw)
|
||||
} else {
|
||||
toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}")
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", "{}")
|
||||
}
|
||||
|
||||
toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall)
|
||||
toolCallsWrapper, _ = sjson.SetRawBytes(toolCallsWrapper, "arr.-1", toolCall)
|
||||
toolCallsCount++
|
||||
}
|
||||
|
||||
// Handle function responses (Gemini) -> tool role messages (OpenAI)
|
||||
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
||||
// Create tool message for function response
|
||||
toolMsg := `{"role":"tool","tool_call_id":"","content":""}`
|
||||
toolMsg := []byte(`{"role":"tool","tool_call_id":"","content":""}`)
|
||||
|
||||
// Convert response.content to JSON string
|
||||
if response := functionResponse.Get("response"); response.Exists() {
|
||||
if contentField := response.Get("content"); contentField.Exists() {
|
||||
toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
|
||||
toolMsg, _ = sjson.SetBytes(toolMsg, "content", contentField.Raw)
|
||||
} else {
|
||||
toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw)
|
||||
toolMsg, _ = sjson.SetBytes(toolMsg, "content", response.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,13 +246,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
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, _ = sjson.Set(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1])
|
||||
toolMsg, _ = sjson.SetBytes(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1])
|
||||
} else {
|
||||
// Generate a tool call ID if none available
|
||||
toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID())
|
||||
toolMsg, _ = sjson.SetBytes(toolMsg, "tool_call_id", genToolCallID())
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", toolMsg)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", toolMsg)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -262,18 +262,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
// Set content
|
||||
if contentPartsCount > 0 {
|
||||
if onlyTextContent {
|
||||
msg, _ = sjson.Set(msg, "content", textBuilder.String())
|
||||
msg, _ = sjson.SetBytes(msg, "content", textBuilder.String())
|
||||
} else {
|
||||
msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw)
|
||||
msg, _ = sjson.SetRawBytes(msg, "content", []byte(gjson.GetBytes(contentWrapper, "arr").Raw))
|
||||
}
|
||||
}
|
||||
|
||||
// Set tool calls if any
|
||||
if toolCallsCount > 0 {
|
||||
msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw)
|
||||
msg, _ = sjson.SetRawBytes(msg, "tool_calls", []byte(gjson.GetBytes(toolCallsWrapper, "arr").Raw))
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -283,18 +283,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
|
||||
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
|
||||
openAITool := `{"type":"function","function":{"name":"","description":""}}`
|
||||
openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
|
||||
openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
|
||||
openAITool := []byte(`{"type":"function","function":{"name":"","description":""}}`)
|
||||
openAITool, _ = sjson.SetBytes(openAITool, "function.name", funcDecl.Get("name").String())
|
||||
openAITool, _ = sjson.SetBytes(openAITool, "function.description", funcDecl.Get("description").String())
|
||||
|
||||
// Convert parameters schema
|
||||
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
|
||||
openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
|
||||
openAITool, _ = sjson.SetRawBytes(openAITool, "function.parameters", []byte(parameters.Raw))
|
||||
} else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
|
||||
openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
|
||||
openAITool, _ = sjson.SetRawBytes(openAITool, "function.parameters", []byte(parameters.Raw))
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "tools.-1", openAITool)
|
||||
out, _ = sjson.SetRawBytes(out, "tools.-1", openAITool)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -308,14 +308,14 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
||||
mode := functionCallingConfig.Get("mode").String()
|
||||
switch mode {
|
||||
case "NONE":
|
||||
out, _ = sjson.Set(out, "tool_choice", "none")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "none")
|
||||
case "AUTO":
|
||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||
case "ANY":
|
||||
out, _ = sjson.Set(out, "tool_choice", "required")
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", "required")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
@@ -44,8 +45,8 @@ type ToolCallAccumulator struct {
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
||||
// - [][]byte: A slice of Gemini-compatible JSON responses.
|
||||
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
if *param == nil {
|
||||
*param = &ConvertOpenAIResponseToGeminiParams{
|
||||
ToolCallsAccumulator: nil,
|
||||
@@ -55,8 +56,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
}
|
||||
|
||||
// Handle [DONE] marker
|
||||
if strings.TrimSpace(string(rawJSON)) == "[DONE]" {
|
||||
return []string{}
|
||||
if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) {
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||
@@ -76,51 +77,51 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
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":{}}`
|
||||
template := []byte(`{"candidates":[],"usageMetadata":{}}`)
|
||||
|
||||
// Set model if available
|
||||
if model := root.Get("model"); model.Exists() {
|
||||
template, _ = sjson.Set(template, "model", model.String())
|
||||
template, _ = sjson.SetBytes(template, "model", model.String())
|
||||
}
|
||||
|
||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
return []string{template}
|
||||
return [][]byte{template}
|
||||
}
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
var results []string
|
||||
var results [][]byte
|
||||
|
||||
choices.ForEach(func(choiceIndex, choice gjson.Result) bool {
|
||||
// Base Gemini response template without finishReason; set when known
|
||||
template := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`
|
||||
template := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`)
|
||||
|
||||
// Set model if available
|
||||
if model := root.Get("model"); model.Exists() {
|
||||
template, _ = sjson.Set(template, "model", model.String())
|
||||
template, _ = sjson.SetBytes(template, "model", model.String())
|
||||
}
|
||||
|
||||
_ = int(choice.Get("index").Int()) // choiceIdx not used in streaming
|
||||
delta := choice.Get("delta")
|
||||
baseTemplate := template
|
||||
baseTemplate := append([]byte(nil), template...)
|
||||
|
||||
// Handle role (only in first chunk)
|
||||
if role := delta.Get("role"); role.Exists() && (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk {
|
||||
// OpenAI assistant -> Gemini model
|
||||
if role.String() == "assistant" {
|
||||
template, _ = sjson.Set(template, "candidates.0.content.role", "model")
|
||||
template, _ = sjson.SetBytes(template, "candidates.0.content.role", "model")
|
||||
}
|
||||
(*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk = false
|
||||
results = append(results, template)
|
||||
return true
|
||||
}
|
||||
|
||||
var chunkOutputs []string
|
||||
var chunkOutputs [][]byte
|
||||
|
||||
// Handle reasoning/thinking delta
|
||||
if reasoning := delta.Get("reasoning_content"); reasoning.Exists() {
|
||||
@@ -128,9 +129,9 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
if reasoningText == "" {
|
||||
continue
|
||||
}
|
||||
reasoningTemplate := baseTemplate
|
||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
||||
reasoningTemplate := append([]byte(nil), baseTemplate...)
|
||||
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
||||
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
||||
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
||||
}
|
||||
}
|
||||
@@ -141,8 +142,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
||||
|
||||
// Create text part for this delta
|
||||
contentTemplate := baseTemplate
|
||||
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
||||
contentTemplate := append([]byte(nil), baseTemplate...)
|
||||
contentTemplate, _ = sjson.SetBytes(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
||||
chunkOutputs = append(chunkOutputs, contentTemplate)
|
||||
}
|
||||
|
||||
@@ -207,7 +208,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
// Handle finish reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", geminiFinishReason)
|
||||
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", geminiFinishReason)
|
||||
|
||||
// If we have accumulated tool calls, output them now
|
||||
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
||||
@@ -215,8 +216,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
||||
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||
template, _ = sjson.Set(template, namePath, accumulator.Name)
|
||||
template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
|
||||
template, _ = sjson.SetBytes(template, namePath, accumulator.Name)
|
||||
template, _ = sjson.SetRawBytes(template, argsPath, []byte(parseArgsToObjectRaw(accumulator.Arguments.String())))
|
||||
partIndex++
|
||||
}
|
||||
|
||||
@@ -230,11 +231,11 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
|
||||
// Handle usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
results = append(results, template)
|
||||
return true
|
||||
@@ -244,7 +245,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
})
|
||||
return results
|
||||
}
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
// mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons
|
||||
@@ -310,7 +311,7 @@ func tolerantParseJSONObjectRaw(s string) string {
|
||||
runes := []rune(content)
|
||||
n := len(runes)
|
||||
i := 0
|
||||
result := "{}"
|
||||
result := []byte(`{}`)
|
||||
|
||||
for i < n {
|
||||
// Skip whitespace and commas
|
||||
@@ -362,10 +363,10 @@ func tolerantParseJSONObjectRaw(s string) string {
|
||||
valToken, ni := parseJSONStringRunes(runes, i)
|
||||
if ni == -1 {
|
||||
// Malformed; treat as empty string
|
||||
result, _ = sjson.Set(result, sjsonKey, "")
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, "")
|
||||
i = n
|
||||
} else {
|
||||
result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
||||
i = ni
|
||||
}
|
||||
case '{', '[':
|
||||
@@ -375,9 +376,9 @@ func tolerantParseJSONObjectRaw(s string) string {
|
||||
i = n
|
||||
} else {
|
||||
if gjson.Valid(seg) {
|
||||
result, _ = sjson.SetRaw(result, sjsonKey, seg)
|
||||
result, _ = sjson.SetRawBytes(result, sjsonKey, []byte(seg))
|
||||
} else {
|
||||
result, _ = sjson.Set(result, sjsonKey, seg)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, seg)
|
||||
}
|
||||
i = ni
|
||||
}
|
||||
@@ -390,15 +391,15 @@ func tolerantParseJSONObjectRaw(s string) string {
|
||||
token := strings.TrimSpace(string(runes[i:j]))
|
||||
// Interpret common JSON atoms and numbers; otherwise treat as string
|
||||
if token == "true" {
|
||||
result, _ = sjson.Set(result, sjsonKey, true)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, true)
|
||||
} else if token == "false" {
|
||||
result, _ = sjson.Set(result, sjsonKey, false)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, false)
|
||||
} else if token == "null" {
|
||||
result, _ = sjson.Set(result, sjsonKey, nil)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, nil)
|
||||
} else if numVal, ok := tryParseNumber(token); ok {
|
||||
result, _ = sjson.Set(result, sjsonKey, numVal)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, numVal)
|
||||
} else {
|
||||
result, _ = sjson.Set(result, sjsonKey, token)
|
||||
result, _ = sjson.SetBytes(result, sjsonKey, token)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
@@ -412,7 +413,7 @@ func tolerantParseJSONObjectRaw(s string) string {
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// parseJSONStringRunes returns the JSON string token (including quotes) and the index just after it.
|
||||
@@ -531,16 +532,16 @@ func tryParseNumber(s string) (interface{}, bool) {
|
||||
// - param: A pointer to a parameter object for the conversion.
|
||||
//
|
||||
// Returns:
|
||||
// - string: A Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||
// - []byte: A Gemini-compatible JSON response.
|
||||
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Base Gemini response template without finishReason; set when known
|
||||
out := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`
|
||||
out := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`)
|
||||
|
||||
// Set model if available
|
||||
if model := root.Get("model"); model.Exists() {
|
||||
out, _ = sjson.Set(out, "model", model.String())
|
||||
out, _ = sjson.SetBytes(out, "model", model.String())
|
||||
}
|
||||
|
||||
// Process choices
|
||||
@@ -552,7 +553,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
// Set role
|
||||
if role := message.Get("role"); role.Exists() {
|
||||
if role.String() == "assistant" {
|
||||
out, _ = sjson.Set(out, "candidates.0.content.role", "model")
|
||||
out, _ = sjson.SetBytes(out, "candidates.0.content.role", "model")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,15 +565,15 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
if reasoningText == "" {
|
||||
continue
|
||||
}
|
||||
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
|
||||
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
|
||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
|
||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
|
||||
partIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content first
|
||||
if content := message.Get("content"); content.Exists() && content.String() != "" {
|
||||
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
|
||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
|
||||
partIndex++
|
||||
}
|
||||
|
||||
@@ -586,8 +587,8 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
|
||||
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||
out, _ = sjson.Set(out, namePath, functionName)
|
||||
out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
|
||||
out, _ = sjson.SetBytes(out, namePath, functionName)
|
||||
out, _ = sjson.SetRawBytes(out, argsPath, []byte(parseArgsToObjectRaw(functionArgs)))
|
||||
partIndex++
|
||||
}
|
||||
return true
|
||||
@@ -597,11 +598,11 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
// Handle finish reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||
out, _ = sjson.Set(out, "candidates.0.finishReason", geminiFinishReason)
|
||||
out, _ = sjson.SetBytes(out, "candidates.0.finishReason", geminiFinishReason)
|
||||
}
|
||||
|
||||
// Set index
|
||||
out, _ = sjson.Set(out, "candidates.0.index", choiceIdx)
|
||||
out, _ = sjson.SetBytes(out, "candidates.0.index", choiceIdx)
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -609,19 +610,19 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
|
||||
// Handle usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
out, _ = sjson.SetBytes(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||
out, _ = sjson.SetBytes(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||
out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
out, _ = sjson.SetBytes(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
||||
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||
return translatorcommon.GeminiTokenCountJSON(count)
|
||||
}
|
||||
|
||||
func reasoningTokensFromUsage(usage gjson.Result) int64 {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// Package openai provides response translation functionality for Gemini CLI to OpenAI API compatibility.
|
||||
// This package handles the conversion of Gemini CLI API responses into OpenAI Chat Completions-compatible
|
||||
// JSON format, transforming streaming events and non-streaming responses into the format
|
||||
// expected by OpenAI API clients. It supports both streaming and non-streaming modes,
|
||||
// handling text content, tool calls, reasoning content, and usage metadata appropriately.
|
||||
// Package chat_completions provides passthrough response translation for OpenAI Chat Completions.
|
||||
// It normalizes OpenAI-compatible SSE lines by stripping the "data:" prefix and dropping "[DONE]".
|
||||
package chat_completions
|
||||
|
||||
import (
|
||||
@@ -10,11 +7,9 @@ import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// ConvertOpenAIResponseToOpenAI translates a single chunk of a streaming response from the
|
||||
// Gemini CLI API format to the OpenAI Chat Completions streaming format.
|
||||
// It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses.
|
||||
// The function handles text content, tool calls, reasoning content, and usage metadata, outputting
|
||||
// responses that match the OpenAI API format. It supports incremental updates for streaming responses.
|
||||
// ConvertOpenAIResponseToOpenAI normalizes a single chunk of an OpenAI-compatible streaming response.
|
||||
// If the chunk is an SSE "data:" line, the prefix is stripped and the remaining JSON payload is returned.
|
||||
// The "[DONE]" marker yields no output.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
@@ -23,21 +18,18 @@ import (
|
||||
// - param: A pointer to a parameter object for maintaining state between calls
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
||||
func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
||||
// - [][]byte: A slice of JSON payload chunks in OpenAI format.
|
||||
func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||
}
|
||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
return []string{string(rawJSON)}
|
||||
return [][]byte{rawJSON}
|
||||
}
|
||||
|
||||
// ConvertOpenAIResponseToOpenAINonStream converts a non-streaming Gemini CLI response to a non-streaming OpenAI response.
|
||||
// This function processes the complete Gemini CLI response and transforms it into a single OpenAI-compatible
|
||||
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
||||
// the information into a single response that matches the OpenAI API format.
|
||||
// ConvertOpenAIResponseToOpenAINonStream passes through a non-streaming OpenAI response.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
@@ -46,7 +38,7 @@ func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
||||
// - param: A pointer to a parameter object for the conversion
|
||||
//
|
||||
// Returns:
|
||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
||||
func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
||||
return string(rawJSON)
|
||||
// - []byte: The OpenAI-compatible JSON response.
|
||||
func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
@@ -29,30 +29,30 @@ import (
|
||||
func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
rawJSON := inputRawJSON
|
||||
// Base OpenAI chat completions template with default values
|
||||
out := `{"model":"","messages":[],"stream":false}`
|
||||
out := []byte(`{"model":"","messages":[],"stream":false}`)
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Set model name
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||
|
||||
// Set stream configuration
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
out, _ = sjson.SetBytes(out, "stream", stream)
|
||||
|
||||
// Map generation parameters from responses format to chat completions format
|
||||
if maxTokens := root.Get("max_output_tokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
|
||||
if parallelToolCalls := root.Get("parallel_tool_calls"); parallelToolCalls.Exists() {
|
||||
out, _ = sjson.Set(out, "parallel_tool_calls", parallelToolCalls.Bool())
|
||||
out, _ = sjson.SetBytes(out, "parallel_tool_calls", parallelToolCalls.Bool())
|
||||
}
|
||||
|
||||
// Convert instructions to system message
|
||||
if instructions := root.Get("instructions"); instructions.Exists() {
|
||||
systemMessage := `{"role":"system","content":""}`
|
||||
systemMessage, _ = sjson.Set(systemMessage, "content", instructions.String())
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMessage)
|
||||
systemMessage := []byte(`{"role":"system","content":""}`)
|
||||
systemMessage, _ = sjson.SetBytes(systemMessage, "content", instructions.String())
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage)
|
||||
}
|
||||
|
||||
// Convert input array to messages
|
||||
@@ -70,8 +70,8 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
||||
if role == "developer" {
|
||||
role = "user"
|
||||
}
|
||||
message := `{"role":"","content":[]}`
|
||||
message, _ = sjson.Set(message, "role", role)
|
||||
message := []byte(`{"role":"","content":[]}`)
|
||||
message, _ = sjson.SetBytes(message, "role", role)
|
||||
|
||||
if content := item.Get("content"); content.Exists() && content.IsArray() {
|
||||
var messageContent string
|
||||
@@ -86,74 +86,74 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
||||
switch contentType {
|
||||
case "input_text", "output_text":
|
||||
text := contentItem.Get("text").String()
|
||||
contentPart := `{"type":"text","text":""}`
|
||||
contentPart, _ = sjson.Set(contentPart, "text", text)
|
||||
message, _ = sjson.SetRaw(message, "content.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"text","text":""}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "text", text)
|
||||
message, _ = sjson.SetRawBytes(message, "content.-1", contentPart)
|
||||
case "input_image":
|
||||
imageURL := contentItem.Get("image_url").String()
|
||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
||||
message, _ = sjson.SetRaw(message, "content.-1", contentPart)
|
||||
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||
message, _ = sjson.SetRawBytes(message, "content.-1", contentPart)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if messageContent != "" {
|
||||
message, _ = sjson.Set(message, "content", messageContent)
|
||||
message, _ = sjson.SetBytes(message, "content", messageContent)
|
||||
}
|
||||
|
||||
if len(toolCalls) > 0 {
|
||||
message, _ = sjson.Set(message, "tool_calls", toolCalls)
|
||||
message, _ = sjson.SetBytes(message, "tool_calls", toolCalls)
|
||||
}
|
||||
} else if content.Type == gjson.String {
|
||||
message, _ = sjson.Set(message, "content", content.String())
|
||||
message, _ = sjson.SetBytes(message, "content", content.String())
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", message)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", message)
|
||||
|
||||
case "function_call":
|
||||
// Handle function call conversion to assistant message with tool_calls
|
||||
assistantMessage := `{"role":"assistant","tool_calls":[]}`
|
||||
assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`)
|
||||
|
||||
toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
||||
toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||
|
||||
if callId := item.Get("call_id"); callId.Exists() {
|
||||
toolCall, _ = sjson.Set(toolCall, "id", callId.String())
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "id", callId.String())
|
||||
}
|
||||
|
||||
if name := item.Get("name"); name.Exists() {
|
||||
toolCall, _ = sjson.Set(toolCall, "function.name", name.String())
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "function.name", name.String())
|
||||
}
|
||||
|
||||
if arguments := item.Get("arguments"); arguments.Exists() {
|
||||
toolCall, _ = sjson.Set(toolCall, "function.arguments", arguments.String())
|
||||
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", arguments.String())
|
||||
}
|
||||
|
||||
assistantMessage, _ = sjson.SetRaw(assistantMessage, "tool_calls.0", toolCall)
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", assistantMessage)
|
||||
assistantMessage, _ = sjson.SetRawBytes(assistantMessage, "tool_calls.0", toolCall)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage)
|
||||
|
||||
case "function_call_output":
|
||||
// Handle function call output conversion to tool message
|
||||
toolMessage := `{"role":"tool","tool_call_id":"","content":""}`
|
||||
toolMessage := []byte(`{"role":"tool","tool_call_id":"","content":""}`)
|
||||
|
||||
if callId := item.Get("call_id"); callId.Exists() {
|
||||
toolMessage, _ = sjson.Set(toolMessage, "tool_call_id", callId.String())
|
||||
toolMessage, _ = sjson.SetBytes(toolMessage, "tool_call_id", callId.String())
|
||||
}
|
||||
|
||||
if output := item.Get("output"); output.Exists() {
|
||||
toolMessage, _ = sjson.Set(toolMessage, "content", output.String())
|
||||
toolMessage, _ = sjson.SetBytes(toolMessage, "content", output.String())
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", toolMessage)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
} else if input.Type == gjson.String {
|
||||
msg := "{}"
|
||||
msg, _ = sjson.Set(msg, "role", "user")
|
||||
msg, _ = sjson.Set(msg, "content", input.String())
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||
msg := []byte(`{}`)
|
||||
msg, _ = sjson.SetBytes(msg, "role", "user")
|
||||
msg, _ = sjson.SetBytes(msg, "content", input.String())
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
}
|
||||
|
||||
// Convert tools from responses format to chat completions format
|
||||
@@ -170,45 +170,45 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
||||
return true
|
||||
}
|
||||
|
||||
chatTool := `{"type":"function","function":{}}`
|
||||
chatTool := []byte(`{"type":"function","function":{}}`)
|
||||
|
||||
// Convert tool structure from responses format to chat completions format
|
||||
function := `{"name":"","description":"","parameters":{}}`
|
||||
function := []byte(`{"name":"","description":"","parameters":{}}`)
|
||||
|
||||
if name := tool.Get("name"); name.Exists() {
|
||||
function, _ = sjson.Set(function, "name", name.String())
|
||||
function, _ = sjson.SetBytes(function, "name", name.String())
|
||||
}
|
||||
|
||||
if description := tool.Get("description"); description.Exists() {
|
||||
function, _ = sjson.Set(function, "description", description.String())
|
||||
function, _ = sjson.SetBytes(function, "description", description.String())
|
||||
}
|
||||
|
||||
if parameters := tool.Get("parameters"); parameters.Exists() {
|
||||
function, _ = sjson.SetRaw(function, "parameters", parameters.Raw)
|
||||
function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw))
|
||||
}
|
||||
|
||||
chatTool, _ = sjson.SetRaw(chatTool, "function", function)
|
||||
chatCompletionsTools = append(chatCompletionsTools, gjson.Parse(chatTool).Value())
|
||||
chatTool, _ = sjson.SetRawBytes(chatTool, "function", function)
|
||||
chatCompletionsTools = append(chatCompletionsTools, gjson.ParseBytes(chatTool).Value())
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if len(chatCompletionsTools) > 0 {
|
||||
out, _ = sjson.Set(out, "tools", chatCompletionsTools)
|
||||
out, _ = sjson.SetBytes(out, "tools", chatCompletionsTools)
|
||||
}
|
||||
}
|
||||
|
||||
if reasoningEffort := root.Get("reasoning.effort"); reasoningEffort.Exists() {
|
||||
effort := strings.ToLower(strings.TrimSpace(reasoningEffort.String()))
|
||||
if effort != "" {
|
||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tool_choice if present
|
||||
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
||||
out, _ = sjson.Set(out, "tool_choice", toolChoice.String())
|
||||
out, _ = sjson.SetBytes(out, "tool_choice", toolChoice.String())
|
||||
}
|
||||
|
||||
return []byte(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
@@ -50,13 +51,13 @@ type oaiToResponsesState struct {
|
||||
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
|
||||
var responseIDCounter uint64
|
||||
|
||||
func emitRespEvent(event string, payload string) string {
|
||||
return fmt.Sprintf("event: %s\ndata: %s", event, payload)
|
||||
func emitRespEvent(event string, payload []byte) []byte {
|
||||
return translatorcommon.SSEEventData(event, payload)
|
||||
}
|
||||
|
||||
// ConvertOpenAIChatCompletionsResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
||||
// to OpenAI Responses SSE events (response.*).
|
||||
func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
||||
func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
if *param == nil {
|
||||
*param = &oaiToResponsesState{
|
||||
FuncArgsBuf: make(map[int]*strings.Builder),
|
||||
@@ -79,19 +80,19 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
|
||||
rawJSON = bytes.TrimSpace(rawJSON)
|
||||
if len(rawJSON) == 0 {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
obj := root.Get("object")
|
||||
if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
if !root.Get("choices").Exists() || !root.Get("choices").IsArray() {
|
||||
return []string{}
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
@@ -124,7 +125,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
}
|
||||
|
||||
nextSeq := func() int { st.Seq++; return st.Seq }
|
||||
var out []string
|
||||
var out [][]byte
|
||||
|
||||
if !st.Started {
|
||||
st.ResponseID = root.Get("id").String()
|
||||
@@ -149,39 +150,39 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
st.ReasoningTokens = 0
|
||||
st.UsageSeen = false
|
||||
// response.created
|
||||
created := `{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`
|
||||
created, _ = sjson.Set(created, "sequence_number", nextSeq())
|
||||
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
||||
created, _ = sjson.Set(created, "response.created_at", st.Created)
|
||||
created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`)
|
||||
created, _ = sjson.SetBytes(created, "sequence_number", nextSeq())
|
||||
created, _ = sjson.SetBytes(created, "response.id", st.ResponseID)
|
||||
created, _ = sjson.SetBytes(created, "response.created_at", st.Created)
|
||||
out = append(out, emitRespEvent("response.created", created))
|
||||
|
||||
inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`
|
||||
inprog, _ = sjson.Set(inprog, "sequence_number", nextSeq())
|
||||
inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID)
|
||||
inprog, _ = sjson.Set(inprog, "response.created_at", st.Created)
|
||||
inprog := []byte(`{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`)
|
||||
inprog, _ = sjson.SetBytes(inprog, "sequence_number", nextSeq())
|
||||
inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID)
|
||||
inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.Created)
|
||||
out = append(out, emitRespEvent("response.in_progress", inprog))
|
||||
st.Started = true
|
||||
}
|
||||
|
||||
stopReasoning := func(text string) {
|
||||
// Emit reasoning done events
|
||||
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
||||
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
|
||||
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
|
||||
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
||||
textDone, _ = sjson.Set(textDone, "text", text)
|
||||
textDone := []byte(`{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`)
|
||||
textDone, _ = sjson.SetBytes(textDone, "sequence_number", nextSeq())
|
||||
textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningID)
|
||||
textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex)
|
||||
textDone, _ = sjson.SetBytes(textDone, "text", text)
|
||||
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
|
||||
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
||||
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
|
||||
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
||||
partDone, _ = sjson.Set(partDone, "part.text", text)
|
||||
partDone := []byte(`{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`)
|
||||
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningID)
|
||||
partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex)
|
||||
partDone, _ = sjson.SetBytes(partDone, "part.text", text)
|
||||
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
|
||||
outputItemDone := `{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`
|
||||
outputItemDone, _ = sjson.Set(outputItemDone, "sequence_number", nextSeq())
|
||||
outputItemDone, _ = sjson.Set(outputItemDone, "item.id", st.ReasoningID)
|
||||
outputItemDone, _ = sjson.Set(outputItemDone, "output_index", st.ReasoningIndex)
|
||||
outputItemDone, _ = sjson.Set(outputItemDone, "item.summary.text", text)
|
||||
outputItemDone := []byte(`{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`)
|
||||
outputItemDone, _ = sjson.SetBytes(outputItemDone, "sequence_number", nextSeq())
|
||||
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.id", st.ReasoningID)
|
||||
outputItemDone, _ = sjson.SetBytes(outputItemDone, "output_index", st.ReasoningIndex)
|
||||
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.summary.text", text)
|
||||
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
|
||||
|
||||
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text})
|
||||
@@ -201,29 +202,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
st.ReasoningBuf.Reset()
|
||||
}
|
||||
if !st.MsgItemAdded[idx] {
|
||||
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`
|
||||
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
||||
item, _ = sjson.Set(item, "output_index", idx)
|
||||
item, _ = sjson.Set(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`)
|
||||
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
|
||||
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||
item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
out = append(out, emitRespEvent("response.output_item.added", item))
|
||||
st.MsgItemAdded[idx] = true
|
||||
}
|
||||
if !st.MsgContentAdded[idx] {
|
||||
part := `{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
||||
part, _ = sjson.Set(part, "sequence_number", nextSeq())
|
||||
part, _ = sjson.Set(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
part, _ = sjson.Set(part, "output_index", idx)
|
||||
part, _ = sjson.Set(part, "content_index", 0)
|
||||
part := []byte(`{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`)
|
||||
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||
part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
part, _ = sjson.SetBytes(part, "output_index", idx)
|
||||
part, _ = sjson.SetBytes(part, "content_index", 0)
|
||||
out = append(out, emitRespEvent("response.content_part.added", part))
|
||||
st.MsgContentAdded[idx] = true
|
||||
}
|
||||
|
||||
msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`
|
||||
msg, _ = sjson.Set(msg, "sequence_number", nextSeq())
|
||||
msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
msg, _ = sjson.Set(msg, "output_index", idx)
|
||||
msg, _ = sjson.Set(msg, "content_index", 0)
|
||||
msg, _ = sjson.Set(msg, "delta", c.String())
|
||||
msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`)
|
||||
msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq())
|
||||
msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
msg, _ = sjson.SetBytes(msg, "output_index", idx)
|
||||
msg, _ = sjson.SetBytes(msg, "content_index", 0)
|
||||
msg, _ = sjson.SetBytes(msg, "delta", c.String())
|
||||
out = append(out, emitRespEvent("response.output_text.delta", msg))
|
||||
// aggregate for response.output
|
||||
if st.MsgTextBuf[idx] == nil {
|
||||
@@ -238,24 +239,24 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if st.ReasoningID == "" {
|
||||
st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx)
|
||||
st.ReasoningIndex = idx
|
||||
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`
|
||||
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
||||
item, _ = sjson.Set(item, "output_index", idx)
|
||||
item, _ = sjson.Set(item, "item.id", st.ReasoningID)
|
||||
item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`)
|
||||
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
|
||||
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||
item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID)
|
||||
out = append(out, emitRespEvent("response.output_item.added", item))
|
||||
part := `{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
||||
part, _ = sjson.Set(part, "sequence_number", nextSeq())
|
||||
part, _ = sjson.Set(part, "item_id", st.ReasoningID)
|
||||
part, _ = sjson.Set(part, "output_index", st.ReasoningIndex)
|
||||
part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`)
|
||||
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||
part, _ = sjson.SetBytes(part, "item_id", st.ReasoningID)
|
||||
part, _ = sjson.SetBytes(part, "output_index", st.ReasoningIndex)
|
||||
out = append(out, emitRespEvent("response.reasoning_summary_part.added", part))
|
||||
}
|
||||
// Append incremental text to reasoning buffer
|
||||
st.ReasoningBuf.WriteString(rc.String())
|
||||
msg := `{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`
|
||||
msg, _ = sjson.Set(msg, "sequence_number", nextSeq())
|
||||
msg, _ = sjson.Set(msg, "item_id", st.ReasoningID)
|
||||
msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex)
|
||||
msg, _ = sjson.Set(msg, "delta", rc.String())
|
||||
msg := []byte(`{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`)
|
||||
msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq())
|
||||
msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningID)
|
||||
msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex)
|
||||
msg, _ = sjson.SetBytes(msg, "delta", rc.String())
|
||||
out = append(out, emitRespEvent("response.reasoning_summary_text.delta", msg))
|
||||
}
|
||||
|
||||
@@ -272,27 +273,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if b := st.MsgTextBuf[idx]; b != nil {
|
||||
fullText = b.String()
|
||||
}
|
||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
||||
done, _ = sjson.Set(done, "sequence_number", nextSeq())
|
||||
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
done, _ = sjson.Set(done, "output_index", idx)
|
||||
done, _ = sjson.Set(done, "content_index", 0)
|
||||
done, _ = sjson.Set(done, "text", fullText)
|
||||
done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`)
|
||||
done, _ = sjson.SetBytes(done, "sequence_number", nextSeq())
|
||||
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
done, _ = sjson.SetBytes(done, "output_index", idx)
|
||||
done, _ = sjson.SetBytes(done, "content_index", 0)
|
||||
done, _ = sjson.SetBytes(done, "text", fullText)
|
||||
out = append(out, emitRespEvent("response.output_text.done", done))
|
||||
|
||||
partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
||||
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
partDone, _ = sjson.Set(partDone, "output_index", idx)
|
||||
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
||||
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
||||
partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`)
|
||||
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
partDone, _ = sjson.SetBytes(partDone, "output_index", idx)
|
||||
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
|
||||
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
|
||||
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
||||
|
||||
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`
|
||||
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
||||
itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
|
||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||
st.MsgItemDone[idx] = true
|
||||
}
|
||||
@@ -314,13 +315,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
}
|
||||
|
||||
if shouldEmitItem && effectiveCallID != "" {
|
||||
o := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`
|
||||
o, _ = sjson.Set(o, "sequence_number", nextSeq())
|
||||
o, _ = sjson.Set(o, "output_index", idx)
|
||||
o, _ = sjson.Set(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
|
||||
o, _ = sjson.Set(o, "item.call_id", effectiveCallID)
|
||||
o := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`)
|
||||
o, _ = sjson.SetBytes(o, "sequence_number", nextSeq())
|
||||
o, _ = sjson.SetBytes(o, "output_index", idx)
|
||||
o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
|
||||
o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID)
|
||||
name := st.FuncNames[idx]
|
||||
o, _ = sjson.Set(o, "item.name", name)
|
||||
o, _ = sjson.SetBytes(o, "item.name", name)
|
||||
out = append(out, emitRespEvent("response.output_item.added", o))
|
||||
}
|
||||
|
||||
@@ -337,11 +338,11 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
refCallID = newCallID
|
||||
}
|
||||
if refCallID != "" {
|
||||
ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`
|
||||
ad, _ = sjson.Set(ad, "sequence_number", nextSeq())
|
||||
ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
|
||||
ad, _ = sjson.Set(ad, "output_index", idx)
|
||||
ad, _ = sjson.Set(ad, "delta", args.String())
|
||||
ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`)
|
||||
ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq())
|
||||
ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
|
||||
ad, _ = sjson.SetBytes(ad, "output_index", idx)
|
||||
ad, _ = sjson.SetBytes(ad, "delta", args.String())
|
||||
out = append(out, emitRespEvent("response.function_call_arguments.delta", ad))
|
||||
}
|
||||
st.FuncArgsBuf[idx].WriteString(args.String())
|
||||
@@ -372,27 +373,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if b := st.MsgTextBuf[i]; b != nil {
|
||||
fullText = b.String()
|
||||
}
|
||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
||||
done, _ = sjson.Set(done, "sequence_number", nextSeq())
|
||||
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
done, _ = sjson.Set(done, "output_index", i)
|
||||
done, _ = sjson.Set(done, "content_index", 0)
|
||||
done, _ = sjson.Set(done, "text", fullText)
|
||||
done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`)
|
||||
done, _ = sjson.SetBytes(done, "sequence_number", nextSeq())
|
||||
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
done, _ = sjson.SetBytes(done, "output_index", i)
|
||||
done, _ = sjson.SetBytes(done, "content_index", 0)
|
||||
done, _ = sjson.SetBytes(done, "text", fullText)
|
||||
out = append(out, emitRespEvent("response.output_text.done", done))
|
||||
|
||||
partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
||||
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
partDone, _ = sjson.Set(partDone, "output_index", i)
|
||||
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
||||
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
||||
partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`)
|
||||
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
partDone, _ = sjson.SetBytes(partDone, "output_index", i)
|
||||
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
|
||||
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
|
||||
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
||||
|
||||
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`
|
||||
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
||||
itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "output_index", i)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
|
||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||
st.MsgItemDone[i] = true
|
||||
}
|
||||
@@ -426,101 +427,101 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 {
|
||||
args = b.String()
|
||||
}
|
||||
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
||||
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
||||
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
|
||||
fcDone, _ = sjson.Set(fcDone, "output_index", i)
|
||||
fcDone, _ = sjson.Set(fcDone, "arguments", args)
|
||||
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
|
||||
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
|
||||
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
|
||||
fcDone, _ = sjson.SetBytes(fcDone, "output_index", i)
|
||||
fcDone, _ = sjson.SetBytes(fcDone, "arguments", args)
|
||||
out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone))
|
||||
|
||||
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`
|
||||
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
|
||||
itemDone, _ = sjson.Set(itemDone, "item.arguments", args)
|
||||
itemDone, _ = sjson.Set(itemDone, "item.call_id", callID)
|
||||
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[i])
|
||||
itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "output_index", i)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID)
|
||||
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[i])
|
||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||
st.FuncItemDone[i] = true
|
||||
st.FuncArgsDone[i] = true
|
||||
}
|
||||
}
|
||||
completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`
|
||||
completed, _ = sjson.Set(completed, "sequence_number", nextSeq())
|
||||
completed, _ = sjson.Set(completed, "response.id", st.ResponseID)
|
||||
completed, _ = sjson.Set(completed, "response.created_at", st.Created)
|
||||
completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`)
|
||||
completed, _ = sjson.SetBytes(completed, "sequence_number", nextSeq())
|
||||
completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID)
|
||||
completed, _ = sjson.SetBytes(completed, "response.created_at", st.Created)
|
||||
// Inject original request fields into response as per docs/response.completed.json
|
||||
if requestRawJSON != nil {
|
||||
req := gjson.ParseBytes(requestRawJSON)
|
||||
if v := req.Get("instructions"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.instructions", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.instructions", v.String())
|
||||
}
|
||||
if v := req.Get("max_output_tokens"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.max_output_tokens", v.Int())
|
||||
completed, _ = sjson.SetBytes(completed, "response.max_output_tokens", v.Int())
|
||||
}
|
||||
if v := req.Get("max_tool_calls"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.max_tool_calls", v.Int())
|
||||
completed, _ = sjson.SetBytes(completed, "response.max_tool_calls", v.Int())
|
||||
}
|
||||
if v := req.Get("model"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.model", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.model", v.String())
|
||||
}
|
||||
if v := req.Get("parallel_tool_calls"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.parallel_tool_calls", v.Bool())
|
||||
completed, _ = sjson.SetBytes(completed, "response.parallel_tool_calls", v.Bool())
|
||||
}
|
||||
if v := req.Get("previous_response_id"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.previous_response_id", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.previous_response_id", v.String())
|
||||
}
|
||||
if v := req.Get("prompt_cache_key"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.prompt_cache_key", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.prompt_cache_key", v.String())
|
||||
}
|
||||
if v := req.Get("reasoning"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.reasoning", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.reasoning", v.Value())
|
||||
}
|
||||
if v := req.Get("safety_identifier"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.safety_identifier", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.safety_identifier", v.String())
|
||||
}
|
||||
if v := req.Get("service_tier"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.service_tier", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.service_tier", v.String())
|
||||
}
|
||||
if v := req.Get("store"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.store", v.Bool())
|
||||
completed, _ = sjson.SetBytes(completed, "response.store", v.Bool())
|
||||
}
|
||||
if v := req.Get("temperature"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.temperature", v.Float())
|
||||
completed, _ = sjson.SetBytes(completed, "response.temperature", v.Float())
|
||||
}
|
||||
if v := req.Get("text"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.text", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.text", v.Value())
|
||||
}
|
||||
if v := req.Get("tool_choice"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.tool_choice", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.tool_choice", v.Value())
|
||||
}
|
||||
if v := req.Get("tools"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.tools", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.tools", v.Value())
|
||||
}
|
||||
if v := req.Get("top_logprobs"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.top_logprobs", v.Int())
|
||||
completed, _ = sjson.SetBytes(completed, "response.top_logprobs", v.Int())
|
||||
}
|
||||
if v := req.Get("top_p"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.top_p", v.Float())
|
||||
completed, _ = sjson.SetBytes(completed, "response.top_p", v.Float())
|
||||
}
|
||||
if v := req.Get("truncation"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.truncation", v.String())
|
||||
completed, _ = sjson.SetBytes(completed, "response.truncation", v.String())
|
||||
}
|
||||
if v := req.Get("user"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.user", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.user", v.Value())
|
||||
}
|
||||
if v := req.Get("metadata"); v.Exists() {
|
||||
completed, _ = sjson.Set(completed, "response.metadata", v.Value())
|
||||
completed, _ = sjson.SetBytes(completed, "response.metadata", v.Value())
|
||||
}
|
||||
}
|
||||
// Build response.output using aggregated buffers
|
||||
outputsWrapper := `{"arr":[]}`
|
||||
outputsWrapper := []byte(`{"arr":[]}`)
|
||||
if len(st.Reasonings) > 0 {
|
||||
for _, r := range st.Reasonings {
|
||||
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||
item, _ = sjson.Set(item, "id", r.ReasoningID)
|
||||
item, _ = sjson.Set(item, "summary.0.text", r.ReasoningData)
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||
item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
|
||||
item, _ = sjson.SetBytes(item, "id", r.ReasoningID)
|
||||
item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData)
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||
}
|
||||
}
|
||||
// Append message items in ascending index order
|
||||
@@ -541,10 +542,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if b := st.MsgTextBuf[i]; b != nil {
|
||||
txt = b.String()
|
||||
}
|
||||
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
item, _ = sjson.Set(item, "content.0.text", txt)
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
|
||||
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||
item, _ = sjson.SetBytes(item, "content.0.text", txt)
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||
}
|
||||
}
|
||||
if len(st.FuncArgsBuf) > 0 {
|
||||
@@ -567,29 +568,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
}
|
||||
callID := st.FuncCallIDs[i]
|
||||
name := st.FuncNames[i]
|
||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||
item, _ = sjson.Set(item, "arguments", args)
|
||||
item, _ = sjson.Set(item, "call_id", callID)
|
||||
item, _ = sjson.Set(item, "name", name)
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||
item, _ = sjson.SetBytes(item, "name", name)
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||
}
|
||||
}
|
||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||
completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||
}
|
||||
if st.UsageSeen {
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.CompletionTokens)
|
||||
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
||||
completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", st.CompletionTokens)
|
||||
if st.ReasoningTokens > 0 {
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
|
||||
completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
|
||||
}
|
||||
total := st.TotalTokens
|
||||
if total == 0 {
|
||||
total = st.PromptTokens + st.CompletionTokens
|
||||
}
|
||||
completed, _ = sjson.Set(completed, "response.usage.total_tokens", total)
|
||||
completed, _ = sjson.SetBytes(completed, "response.usage.total_tokens", total)
|
||||
}
|
||||
out = append(out, emitRespEvent("response.completed", completed))
|
||||
}
|
||||
@@ -603,103 +604,103 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
|
||||
// ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
||||
// from a non-streaming OpenAI Chat Completions response.
|
||||
func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||
func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Basic response scaffold
|
||||
resp := `{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`
|
||||
resp := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`)
|
||||
|
||||
// id: use provider id if present, otherwise synthesize
|
||||
id := root.Get("id").String()
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1))
|
||||
}
|
||||
resp, _ = sjson.Set(resp, "id", id)
|
||||
resp, _ = sjson.SetBytes(resp, "id", id)
|
||||
|
||||
// created_at: map from chat.completion created
|
||||
created := root.Get("created").Int()
|
||||
if created == 0 {
|
||||
created = time.Now().Unix()
|
||||
}
|
||||
resp, _ = sjson.Set(resp, "created_at", created)
|
||||
resp, _ = sjson.SetBytes(resp, "created_at", created)
|
||||
|
||||
// Echo request fields when available (aligns with streaming path behavior)
|
||||
if len(requestRawJSON) > 0 {
|
||||
req := gjson.ParseBytes(requestRawJSON)
|
||||
if v := req.Get("instructions"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "instructions", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "instructions", v.String())
|
||||
}
|
||||
if v := req.Get("max_output_tokens"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "max_output_tokens", v.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "max_output_tokens", v.Int())
|
||||
} else {
|
||||
// Also support max_tokens from chat completion style
|
||||
if v = req.Get("max_tokens"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "max_output_tokens", v.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "max_output_tokens", v.Int())
|
||||
}
|
||||
}
|
||||
if v := req.Get("max_tool_calls"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "max_tool_calls", v.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "max_tool_calls", v.Int())
|
||||
}
|
||||
if v := req.Get("model"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "model", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "model", v.String())
|
||||
} else if v = root.Get("model"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "model", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "model", v.String())
|
||||
}
|
||||
if v := req.Get("parallel_tool_calls"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "parallel_tool_calls", v.Bool())
|
||||
resp, _ = sjson.SetBytes(resp, "parallel_tool_calls", v.Bool())
|
||||
}
|
||||
if v := req.Get("previous_response_id"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "previous_response_id", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "previous_response_id", v.String())
|
||||
}
|
||||
if v := req.Get("prompt_cache_key"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "prompt_cache_key", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "prompt_cache_key", v.String())
|
||||
}
|
||||
if v := req.Get("reasoning"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "reasoning", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "reasoning", v.Value())
|
||||
}
|
||||
if v := req.Get("safety_identifier"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "safety_identifier", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "safety_identifier", v.String())
|
||||
}
|
||||
if v := req.Get("service_tier"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "service_tier", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "service_tier", v.String())
|
||||
}
|
||||
if v := req.Get("store"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "store", v.Bool())
|
||||
resp, _ = sjson.SetBytes(resp, "store", v.Bool())
|
||||
}
|
||||
if v := req.Get("temperature"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "temperature", v.Float())
|
||||
resp, _ = sjson.SetBytes(resp, "temperature", v.Float())
|
||||
}
|
||||
if v := req.Get("text"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "text", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "text", v.Value())
|
||||
}
|
||||
if v := req.Get("tool_choice"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "tool_choice", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "tool_choice", v.Value())
|
||||
}
|
||||
if v := req.Get("tools"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "tools", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "tools", v.Value())
|
||||
}
|
||||
if v := req.Get("top_logprobs"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "top_logprobs", v.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "top_logprobs", v.Int())
|
||||
}
|
||||
if v := req.Get("top_p"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "top_p", v.Float())
|
||||
resp, _ = sjson.SetBytes(resp, "top_p", v.Float())
|
||||
}
|
||||
if v := req.Get("truncation"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "truncation", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "truncation", v.String())
|
||||
}
|
||||
if v := req.Get("user"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "user", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "user", v.Value())
|
||||
}
|
||||
if v := req.Get("metadata"); v.Exists() {
|
||||
resp, _ = sjson.Set(resp, "metadata", v.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "metadata", v.Value())
|
||||
}
|
||||
} else if v := root.Get("model"); v.Exists() {
|
||||
// Fallback model from response
|
||||
resp, _ = sjson.Set(resp, "model", v.String())
|
||||
resp, _ = sjson.SetBytes(resp, "model", v.String())
|
||||
}
|
||||
|
||||
// Build output list from choices[...]
|
||||
outputsWrapper := `{"arr":[]}`
|
||||
outputsWrapper := []byte(`{"arr":[]}`)
|
||||
// Detect and capture reasoning content if present
|
||||
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
||||
includeReasoning := rcText != ""
|
||||
@@ -712,13 +713,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
||||
rid = strings.TrimPrefix(rid, "resp_")
|
||||
}
|
||||
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
||||
reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`
|
||||
reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
|
||||
reasoningItem := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`)
|
||||
reasoningItem, _ = sjson.SetBytes(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
|
||||
if rcText != "" {
|
||||
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
|
||||
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
|
||||
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.type", "summary_text")
|
||||
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rcText)
|
||||
}
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem)
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", reasoningItem)
|
||||
}
|
||||
|
||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
||||
@@ -727,10 +728,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
||||
if msg.Exists() {
|
||||
// Text message part
|
||||
if c := msg.Get("content"); c.Exists() && c.String() != "" {
|
||||
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
|
||||
item, _ = sjson.Set(item, "content.0.text", c.String())
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
|
||||
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
|
||||
item, _ = sjson.SetBytes(item, "content.0.text", c.String())
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||
}
|
||||
|
||||
// Function/tool calls
|
||||
@@ -739,12 +740,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
||||
callID := tc.Get("id").String()
|
||||
name := tc.Get("function.name").String()
|
||||
args := tc.Get("function.arguments").String()
|
||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||
item, _ = sjson.Set(item, "arguments", args)
|
||||
item, _ = sjson.Set(item, "call_id", callID)
|
||||
item, _ = sjson.Set(item, "name", name)
|
||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||
item, _ = sjson.SetBytes(item, "name", name)
|
||||
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -752,27 +753,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
||||
return true
|
||||
})
|
||||
}
|
||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||
resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||
resp, _ = sjson.SetRawBytes(resp, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||
}
|
||||
|
||||
// usage mapping
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
// Map common tokens
|
||||
if usage.Get("prompt_tokens").Exists() || usage.Get("completion_tokens").Exists() || usage.Get("total_tokens").Exists() {
|
||||
resp, _ = sjson.Set(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int())
|
||||
resp, _ = sjson.SetBytes(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int())
|
||||
if d := usage.Get("prompt_tokens_details.cached_tokens"); d.Exists() {
|
||||
resp, _ = sjson.Set(resp, "usage.input_tokens_details.cached_tokens", d.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "usage.input_tokens_details.cached_tokens", d.Int())
|
||||
}
|
||||
resp, _ = sjson.Set(resp, "usage.output_tokens", usage.Get("completion_tokens").Int())
|
||||
resp, _ = sjson.SetBytes(resp, "usage.output_tokens", usage.Get("completion_tokens").Int())
|
||||
// Reasoning tokens not available in Chat Completions; set only if present under output_tokens_details
|
||||
if d := usage.Get("output_tokens_details.reasoning_tokens"); d.Exists() {
|
||||
resp, _ = sjson.Set(resp, "usage.output_tokens_details.reasoning_tokens", d.Int())
|
||||
resp, _ = sjson.SetBytes(resp, "usage.output_tokens_details.reasoning_tokens", d.Int())
|
||||
}
|
||||
resp, _ = sjson.Set(resp, "usage.total_tokens", usage.Get("total_tokens").Int())
|
||||
resp, _ = sjson.SetBytes(resp, "usage.total_tokens", usage.Get("total_tokens").Int())
|
||||
} else {
|
||||
// Fallback to raw usage object if structure differs
|
||||
resp, _ = sjson.Set(resp, "usage", usage.Value())
|
||||
resp, _ = sjson.SetBytes(resp, "usage", usage.Value())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user