544 lines · 19.9 KB · Go
RawDownload
package translator import (	"bytes"	"encoding/json"	"fmt"	"github.com/tidwall/sjson"	"strings" 	"github.com/luispater/CLIProxyAPI/internal/client"	log "github.com/sirupsen/logrus"	"github.com/tidwall/gjson") // PrepareRequest translates a raw JSON request from an OpenAI-compatible format// to the internal format expected by the backend client. It parses messages,// roles, content types (text, image, file), and tool calls.func PrepareRequest(rawJson []byte) (string, *client.Content, []client.Content, []client.ToolDeclaration) {	// Extract the model name from the request, defaulting to "gemini-2.5-pro".	modelName := "gemini-2.5-pro"	modelResult := gjson.GetBytes(rawJson, "model")	if modelResult.Type == gjson.String {		modelName = modelResult.String()	} 	// Initialize data structures for processing conversation messages	// contents: stores the processed conversation history	// systemInstruction: stores system-level instructions separate from conversation	contents := make([]client.Content, 0)	var systemInstruction *client.Content	messagesResult := gjson.GetBytes(rawJson, "messages") 	// Pre-process tool responses to create a lookup map	// This first pass collects all tool responses so they can be matched with their corresponding calls	toolItems := make(map[string]*client.FunctionResponse)	if messagesResult.IsArray() {		messagesResults := messagesResult.Array()		for i := 0; i < len(messagesResults); i++ {			messageResult := messagesResults[i]			roleResult := messageResult.Get("role")			if roleResult.Type != gjson.String {				continue			}			contentResult := messageResult.Get("content") 			// Extract tool responses for later matching with function calls			if roleResult.String() == "tool" {				toolCallID := messageResult.Get("tool_call_id").String()				if toolCallID != "" {					var responseData string					// Handle both string and object-based tool response formats					if contentResult.Type == gjson.String {						responseData = contentResult.String()					} else if contentResult.IsObject() && contentResult.Get("type").String() == "text" {						responseData = contentResult.Get("text").String()					} 					// Clean up tool call ID by removing timestamp suffix					// This normalizes IDs for consistent matching between calls and responses					toolCallIDs := strings.Split(toolCallID, "-")					strings.Join(toolCallIDs, "-")					newToolCallID := strings.Join(toolCallIDs[:len(toolCallIDs)-1], "-") 					// Create function response object with normalized ID and response data					functionResponse := client.FunctionResponse{Name: newToolCallID, Response: map[string]interface{}{"result": responseData}}					toolItems[toolCallID] = &functionResponse				}			}		}	} 	if messagesResult.IsArray() {		messagesResults := messagesResult.Array()		for i := 0; i < len(messagesResults); i++ {			messageResult := messagesResults[i]			roleResult := messageResult.Get("role")			contentResult := messageResult.Get("content")			if roleResult.Type != gjson.String {				continue			} 			switch roleResult.String() {			// System messages are converted to a user message followed by a model's acknowledgment.			case "system":				if contentResult.Type == gjson.String {					systemInstruction = &client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}}				} else if contentResult.IsObject() {					// Handle object-based system messages.					if contentResult.Get("type").String() == "text" {						systemInstruction = &client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.Get("text").String()}}}					}				}			// User messages can contain simple text or a multi-part body.			case "user":				if contentResult.Type == gjson.String {					contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})				} else if contentResult.IsArray() {					// Handle multi-part user messages (text, images, files).					contentItemResults := contentResult.Array()					parts := make([]client.Part, 0)					for j := 0; j < len(contentItemResults); j++ {						contentItemResult := contentItemResults[j]						contentTypeResult := contentItemResult.Get("type")						switch contentTypeResult.String() {						case "text":							parts = append(parts, client.Part{Text: contentItemResult.Get("text").String()})						case "image_url":							// Parse data URI for images.							imageURL := contentItemResult.Get("image_url.url").String()							if len(imageURL) > 5 {								imageURLs := strings.SplitN(imageURL[5:], ";", 2)								if len(imageURLs) == 2 && len(imageURLs[1]) > 7 {									parts = append(parts, client.Part{InlineData: &client.InlineData{										MimeType: imageURLs[0],										Data:     imageURLs[1][7:],									}})								}							}						case "file":							// Handle file attachments by determining MIME type from extension.							filename := contentItemResult.Get("file.filename").String()							fileData := contentItemResult.Get("file.file_data").String()							ext := ""							if split := strings.Split(filename, "."); len(split) > 1 {								ext = split[len(split)-1]							}							if mimeType, ok := MimeTypes[ext]; ok {								parts = append(parts, client.Part{InlineData: &client.InlineData{									MimeType: mimeType,									Data:     fileData,								}})							} else {								log.Warnf("Unknown file name extension '%s' at index %d, skipping file", ext, j)							}						}					}					contents = append(contents, client.Content{Role: "user", Parts: parts})				}			// Assistant messages can contain text responses or tool calls			// In the internal format, assistant messages are converted to "model" role			case "assistant":				if contentResult.Type == gjson.String {					// Simple text response from the assistant					contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: contentResult.String()}}})				} else if !contentResult.Exists() || contentResult.Type == gjson.Null {					// Handle complex tool calls made by the assistant					// This processes function calls and matches them with their responses					functionIDs := make([]string, 0)					toolCallsResult := messageResult.Get("tool_calls")					if toolCallsResult.IsArray() {						parts := make([]client.Part, 0)						tcsResult := toolCallsResult.Array() 						// Process each tool call in the assistant's message						for j := 0; j < len(tcsResult); j++ {							tcResult := tcsResult[j] 							// Extract function call details							functionID := tcResult.Get("id").String()							functionIDs = append(functionIDs, functionID) 							functionName := tcResult.Get("function.name").String()							functionArgs := tcResult.Get("function.arguments").String() 							// Parse function arguments from JSON string to map							var args map[string]any							if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {								parts = append(parts, client.Part{									FunctionCall: &client.FunctionCall{										Name: functionName,										Args: args,									},								})							}						} 						// Add the model's function calls to the conversation						if len(parts) > 0 {							contents = append(contents, client.Content{								Role: "model", Parts: parts,							}) 							// Create a separate tool response message with the collected responses							// This matches function calls with their corresponding responses							toolParts := make([]client.Part, 0)							for _, functionID := range functionIDs {								if functionResponse, ok := toolItems[functionID]; ok {									toolParts = append(toolParts, client.Part{FunctionResponse: functionResponse})								}							}							// Add the tool responses as a separate message in the conversation							contents = append(contents, client.Content{Role: "tool", Parts: toolParts})						}					}				}			}		}	} 	// Translate the tool declarations from the request.	var tools []client.ToolDeclaration	toolsResult := gjson.GetBytes(rawJson, "tools")	if toolsResult.IsArray() {		tools = make([]client.ToolDeclaration, 1)		tools[0].FunctionDeclarations = make([]any, 0)		toolsResults := toolsResult.Array()		for i := 0; i < len(toolsResults); i++ {			toolResult := toolsResults[i]			if toolResult.Get("type").String() == "function" {				functionTypeResult := toolResult.Get("function")				if functionTypeResult.Exists() && functionTypeResult.IsObject() {					var functionDeclaration any					if err := json.Unmarshal([]byte(functionTypeResult.Raw), &functionDeclaration); err == nil {						tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, functionDeclaration)					}				}			}		}	} else {		tools = make([]client.ToolDeclaration, 0)	} 	return modelName, systemInstruction, contents, tools} // FunctionCallGroup represents a group of function calls and their responsestype FunctionCallGroup struct {	ModelContent    map[string]interface{}	FunctionCalls   []gjson.Result	ResponsesNeeded int} // FixCLIToolResponse performs sophisticated tool response format conversion and grouping.// This function transforms the CLI tool response format by intelligently grouping function calls// with their corresponding responses, ensuring proper conversation flow and API compatibility.// It converts from a linear format (1.json) to a grouped format (2.json) where function calls// and their responses are properly associated and structured.func FixCLIToolResponse(input string) (string, error) {	// Parse the input JSON to extract the conversation structure	parsed := gjson.Parse(input) 	// Extract the contents array which contains the conversation messages	contents := parsed.Get("request.contents")	if !contents.Exists() {		return input, fmt.Errorf("contents not found in input")	} 	// Initialize data structures for processing and grouping	var newContents []interface{}          // Final processed contents array	var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses	var collectedResponses []gjson.Result  // Standalone responses to be matched 	// Process each content object in the conversation	// This iterates through messages and groups function calls with their responses	contents.ForEach(func(key, value gjson.Result) bool {		role := value.Get("role").String()		parts := value.Get("parts") 		// Check if this content has function responses		var responsePartsInThisContent []gjson.Result		parts.ForEach(func(_, part gjson.Result) bool {			if part.Get("functionResponse").Exists() {				responsePartsInThisContent = append(responsePartsInThisContent, part)			}			return true		}) 		// If this content has function responses, collect them		if len(responsePartsInThisContent) > 0 {			collectedResponses = append(collectedResponses, responsePartsInThisContent...) 			// Check if any pending groups can be satisfied			for i := len(pendingGroups) - 1; i >= 0; i-- {				group := pendingGroups[i]				if len(collectedResponses) >= group.ResponsesNeeded {					// Take the needed responses for this group					groupResponses := collectedResponses[:group.ResponsesNeeded]					collectedResponses = collectedResponses[group.ResponsesNeeded:] 					// Create merged function response content					var responseParts []interface{}					for _, response := range groupResponses {						var responseMap map[string]interface{}						errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)						if errUnmarshal != nil {							log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)							continue						}						responseParts = append(responseParts, responseMap)					} 					if len(responseParts) > 0 {						functionResponseContent := map[string]interface{}{							"parts": responseParts,							"role":  "function",						}						newContents = append(newContents, functionResponseContent)					} 					// Remove this group as it's been satisfied					pendingGroups = append(pendingGroups[:i], pendingGroups[i+1:]...)					break				}			} 			return true // Skip adding this content, responses are merged		} 		// If this is a model with function calls, create a new group		if role == "model" {			var functionCallsInThisModel []gjson.Result			parts.ForEach(func(_, part gjson.Result) bool {				if part.Get("functionCall").Exists() {					functionCallsInThisModel = append(functionCallsInThisModel, part)				}				return true			}) 			if len(functionCallsInThisModel) > 0 {				// Add the model content				var contentMap map[string]interface{}				errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)				if errUnmarshal != nil {					log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)					return true				}				newContents = append(newContents, contentMap) 				// Create a new group for tracking responses				group := &FunctionCallGroup{					ModelContent:    contentMap,					FunctionCalls:   functionCallsInThisModel,					ResponsesNeeded: len(functionCallsInThisModel),				}				pendingGroups = append(pendingGroups, group)			} else {				// Regular model content without function calls				var contentMap map[string]interface{}				errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)				if errUnmarshal != nil {					log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)					return true				}				newContents = append(newContents, contentMap)			}		} else {			// Non-model content (user, etc.)			var contentMap map[string]interface{}			errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)			if errUnmarshal != nil {				log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)				return true			}			newContents = append(newContents, contentMap)		} 		return true	}) 	// Handle any remaining pending groups with remaining responses	for _, group := range pendingGroups {		if len(collectedResponses) >= group.ResponsesNeeded {			groupResponses := collectedResponses[:group.ResponsesNeeded]			collectedResponses = collectedResponses[group.ResponsesNeeded:] 			var responseParts []interface{}			for _, response := range groupResponses {				var responseMap map[string]interface{}				errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)				if errUnmarshal != nil {					log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)					continue				}				responseParts = append(responseParts, responseMap)			} 			if len(responseParts) > 0 {				functionResponseContent := map[string]interface{}{					"parts": responseParts,					"role":  "function",				}				newContents = append(newContents, functionResponseContent)			}		}	} 	// Update the original JSON with the new contents	result := input	newContentsJSON, _ := json.Marshal(newContents)	result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON)) 	return result, nil} func PrepareClaudeRequest(rawJson []byte) (string, *client.Content, []client.Content, []client.ToolDeclaration) {	var pathsToDelete []string	root := gjson.ParseBytes(rawJson)	walk(root, "", "additionalProperties", &pathsToDelete)	walk(root, "", "$schema", &pathsToDelete) 	var err error	for _, p := range pathsToDelete {		rawJson, err = sjson.DeleteBytes(rawJson, p)		if err != nil {			continue		}	}	rawJson = bytes.Replace(rawJson, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) 	// log.Debug(string(rawJson))	modelName := "gemini-2.5-pro"	modelResult := gjson.GetBytes(rawJson, "model")	if modelResult.Type == gjson.String {		modelName = modelResult.String()	} 	contents := make([]client.Content, 0) 	var systemInstruction *client.Content 	systemResult := gjson.GetBytes(rawJson, "system")	if systemResult.IsArray() {		systemResults := systemResult.Array()		systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}		for i := 0; i < len(systemResults); i++ {			systemPromptResult := systemResults[i]			systemTypePromptResult := systemPromptResult.Get("type")			if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {				systemPrompt := systemPromptResult.Get("text").String()				systemPart := client.Part{Text: systemPrompt}				systemInstruction.Parts = append(systemInstruction.Parts, systemPart)			}		}		if len(systemInstruction.Parts) == 0 {			systemInstruction = nil		}	} 	messagesResult := gjson.GetBytes(rawJson, "messages")	if messagesResult.IsArray() {		messageResults := messagesResult.Array()		for i := 0; i < len(messageResults); i++ {			messageResult := messageResults[i]			roleResult := messageResult.Get("role")			if roleResult.Type != gjson.String {				continue			}			role := roleResult.String()			if role == "assistant" {				role = "model"			}			clientContent := client.Content{Role: role, Parts: []client.Part{}} 			contentsResult := messageResult.Get("content")			if contentsResult.IsArray() {				contentResults := contentsResult.Array()				for j := 0; j < len(contentResults); j++ {					contentResult := contentResults[j]					contentTypeResult := contentResult.Get("type")					if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {						prompt := contentResult.Get("text").String()						clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})					} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {						functionName := contentResult.Get("name").String()						functionArgs := contentResult.Get("input").String()						var args map[string]any						if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {							clientContent.Parts = append(clientContent.Parts, client.Part{								FunctionCall: &client.FunctionCall{									Name: functionName,									Args: args,								},							})						}					} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {						toolCallID := contentResult.Get("tool_use_id").String()						if toolCallID != "" {							funcName := toolCallID							toolCallIDs := strings.Split(toolCallID, "-")							if len(toolCallIDs) > 1 {								funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")							}							responseData := contentResult.Get("content").String()							functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}							clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})						}					}				}				contents = append(contents, clientContent)			} else if contentsResult.Type == gjson.String {				prompt := contentsResult.String()				contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})			}		}	} 	var tools []client.ToolDeclaration	toolsResult := gjson.GetBytes(rawJson, "tools")	if toolsResult.IsArray() {		tools = make([]client.ToolDeclaration, 1)

Lines 1–500 of 544.