mirror of
https://github.com/githubhjs/CLIProxyAPIPlus.git
synced 2026-07-12 01:25:13 +00:00
320 lines
9.4 KiB
Go
320 lines
9.4 KiB
Go
// Package chat_completions provides request translation from OpenAI to Kiro format.
|
|
package chat_completions
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// ConvertOpenAIRequestToKiro transforms an OpenAI Chat Completions API request into Kiro (Claude) format.
|
|
// Kiro uses Claude-compatible format internally, so we primarily pass through to Claude format.
|
|
// Supports tool calling: OpenAI tools -> Claude tools, tool_calls -> tool_use, tool messages -> tool_result.
|
|
func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bool) []byte {
|
|
rawJSON := bytes.Clone(inputRawJSON)
|
|
root := gjson.ParseBytes(rawJSON)
|
|
|
|
// Build Claude-compatible request
|
|
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
|
|
|
// Set model
|
|
out, _ = sjson.Set(out, "model", modelName)
|
|
|
|
// Copy max_tokens if present
|
|
if v := root.Get("max_tokens"); v.Exists() {
|
|
out, _ = sjson.Set(out, "max_tokens", v.Int())
|
|
}
|
|
|
|
// Copy temperature if present
|
|
if v := root.Get("temperature"); v.Exists() {
|
|
out, _ = sjson.Set(out, "temperature", v.Float())
|
|
}
|
|
|
|
// Copy top_p if present
|
|
if v := root.Get("top_p"); v.Exists() {
|
|
out, _ = sjson.Set(out, "top_p", v.Float())
|
|
}
|
|
|
|
// Convert OpenAI tools to Claude tools format
|
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
|
claudeTools := make([]interface{}, 0)
|
|
for _, tool := range tools.Array() {
|
|
if tool.Get("type").String() == "function" {
|
|
fn := tool.Get("function")
|
|
claudeTool := map[string]interface{}{
|
|
"name": fn.Get("name").String(),
|
|
"description": fn.Get("description").String(),
|
|
}
|
|
// Convert parameters to input_schema
|
|
if params := fn.Get("parameters"); params.Exists() {
|
|
claudeTool["input_schema"] = params.Value()
|
|
} else {
|
|
claudeTool["input_schema"] = map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{},
|
|
}
|
|
}
|
|
claudeTools = append(claudeTools, claudeTool)
|
|
}
|
|
}
|
|
if len(claudeTools) > 0 {
|
|
out, _ = sjson.Set(out, "tools", claudeTools)
|
|
}
|
|
}
|
|
|
|
// Process messages
|
|
messages := root.Get("messages")
|
|
if messages.Exists() && messages.IsArray() {
|
|
claudeMessages := make([]interface{}, 0)
|
|
var systemPrompt string
|
|
|
|
// Track pending tool results to merge with next user message
|
|
var pendingToolResults []map[string]interface{}
|
|
|
|
for _, msg := range messages.Array() {
|
|
role := msg.Get("role").String()
|
|
content := msg.Get("content")
|
|
|
|
if role == "system" {
|
|
// Extract system message
|
|
if content.IsArray() {
|
|
for _, part := range content.Array() {
|
|
if part.Get("type").String() == "text" {
|
|
systemPrompt += part.Get("text").String() + "\n"
|
|
}
|
|
}
|
|
} else {
|
|
systemPrompt = content.String()
|
|
}
|
|
continue
|
|
}
|
|
|
|
if role == "tool" {
|
|
// OpenAI tool message -> Claude tool_result content block
|
|
toolCallID := msg.Get("tool_call_id").String()
|
|
toolContent := content.String()
|
|
|
|
toolResult := map[string]interface{}{
|
|
"type": "tool_result",
|
|
"tool_use_id": toolCallID,
|
|
}
|
|
|
|
// Handle content - can be string or structured
|
|
if content.IsArray() {
|
|
contentParts := make([]interface{}, 0)
|
|
for _, part := range content.Array() {
|
|
if part.Get("type").String() == "text" {
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "text",
|
|
"text": part.Get("text").String(),
|
|
})
|
|
}
|
|
}
|
|
toolResult["content"] = contentParts
|
|
} else {
|
|
toolResult["content"] = toolContent
|
|
}
|
|
|
|
pendingToolResults = append(pendingToolResults, toolResult)
|
|
continue
|
|
}
|
|
|
|
claudeMsg := map[string]interface{}{
|
|
"role": role,
|
|
}
|
|
|
|
// Handle assistant messages with tool_calls
|
|
if role == "assistant" && msg.Get("tool_calls").Exists() {
|
|
contentParts := make([]interface{}, 0)
|
|
|
|
// Add text content if present
|
|
if content.Exists() && content.String() != "" {
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "text",
|
|
"text": content.String(),
|
|
})
|
|
}
|
|
|
|
// Convert tool_calls to tool_use blocks
|
|
for _, toolCall := range msg.Get("tool_calls").Array() {
|
|
toolUseID := toolCall.Get("id").String()
|
|
fnName := toolCall.Get("function.name").String()
|
|
fnArgs := toolCall.Get("function.arguments").String()
|
|
|
|
// Parse arguments JSON
|
|
var argsMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(fnArgs), &argsMap); err != nil {
|
|
argsMap = map[string]interface{}{"raw": fnArgs}
|
|
}
|
|
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "tool_use",
|
|
"id": toolUseID,
|
|
"name": fnName,
|
|
"input": argsMap,
|
|
})
|
|
}
|
|
|
|
claudeMsg["content"] = contentParts
|
|
claudeMessages = append(claudeMessages, claudeMsg)
|
|
continue
|
|
}
|
|
|
|
// Handle user messages - may need to include pending tool results
|
|
if role == "user" && len(pendingToolResults) > 0 {
|
|
contentParts := make([]interface{}, 0)
|
|
|
|
// Add pending tool results first
|
|
for _, tr := range pendingToolResults {
|
|
contentParts = append(contentParts, tr)
|
|
}
|
|
pendingToolResults = nil
|
|
|
|
// Add user content
|
|
if content.IsArray() {
|
|
for _, part := range content.Array() {
|
|
partType := part.Get("type").String()
|
|
if partType == "text" {
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "text",
|
|
"text": part.Get("text").String(),
|
|
})
|
|
} else if partType == "image_url" {
|
|
imageURL := part.Get("image_url.url").String()
|
|
|
|
// Check if it's base64 format (data:image/png;base64,xxxxx)
|
|
if strings.HasPrefix(imageURL, "data:") {
|
|
// Parse data URL format
|
|
// Format: data:image/png;base64,xxxxx
|
|
commaIdx := strings.Index(imageURL, ",")
|
|
if commaIdx != -1 {
|
|
// Extract media_type (e.g., "image/png")
|
|
header := imageURL[5:commaIdx] // Remove "data:" prefix
|
|
mediaType := header
|
|
if semiIdx := strings.Index(header, ";"); semiIdx != -1 {
|
|
mediaType = header[:semiIdx]
|
|
}
|
|
|
|
// Extract base64 data
|
|
base64Data := imageURL[commaIdx+1:]
|
|
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "image",
|
|
"source": map[string]interface{}{
|
|
"type": "base64",
|
|
"media_type": mediaType,
|
|
"data": base64Data,
|
|
},
|
|
})
|
|
}
|
|
} else {
|
|
// Regular URL format - keep original logic
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "image",
|
|
"source": map[string]interface{}{
|
|
"type": "url",
|
|
"url": imageURL,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
} else if content.String() != "" {
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "text",
|
|
"text": content.String(),
|
|
})
|
|
}
|
|
|
|
claudeMsg["content"] = contentParts
|
|
claudeMessages = append(claudeMessages, claudeMsg)
|
|
continue
|
|
}
|
|
|
|
// Handle regular content
|
|
if content.IsArray() {
|
|
contentParts := make([]interface{}, 0)
|
|
for _, part := range content.Array() {
|
|
partType := part.Get("type").String()
|
|
if partType == "text" {
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "text",
|
|
"text": part.Get("text").String(),
|
|
})
|
|
} else if partType == "image_url" {
|
|
imageURL := part.Get("image_url.url").String()
|
|
|
|
// Check if it's base64 format (data:image/png;base64,xxxxx)
|
|
if strings.HasPrefix(imageURL, "data:") {
|
|
// Parse data URL format
|
|
// Format: data:image/png;base64,xxxxx
|
|
commaIdx := strings.Index(imageURL, ",")
|
|
if commaIdx != -1 {
|
|
// Extract media_type (e.g., "image/png")
|
|
header := imageURL[5:commaIdx] // Remove "data:" prefix
|
|
mediaType := header
|
|
if semiIdx := strings.Index(header, ";"); semiIdx != -1 {
|
|
mediaType = header[:semiIdx]
|
|
}
|
|
|
|
// Extract base64 data
|
|
base64Data := imageURL[commaIdx+1:]
|
|
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "image",
|
|
"source": map[string]interface{}{
|
|
"type": "base64",
|
|
"media_type": mediaType,
|
|
"data": base64Data,
|
|
},
|
|
})
|
|
}
|
|
} else {
|
|
// Regular URL format - keep original logic
|
|
contentParts = append(contentParts, map[string]interface{}{
|
|
"type": "image",
|
|
"source": map[string]interface{}{
|
|
"type": "url",
|
|
"url": imageURL,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
claudeMsg["content"] = contentParts
|
|
} else {
|
|
claudeMsg["content"] = content.String()
|
|
}
|
|
|
|
claudeMessages = append(claudeMessages, claudeMsg)
|
|
}
|
|
|
|
// If there are pending tool results without a following user message,
|
|
// create a user message with just the tool results
|
|
if len(pendingToolResults) > 0 {
|
|
contentParts := make([]interface{}, 0)
|
|
for _, tr := range pendingToolResults {
|
|
contentParts = append(contentParts, tr)
|
|
}
|
|
claudeMessages = append(claudeMessages, map[string]interface{}{
|
|
"role": "user",
|
|
"content": contentParts,
|
|
})
|
|
}
|
|
|
|
out, _ = sjson.Set(out, "messages", claudeMessages)
|
|
|
|
if systemPrompt != "" {
|
|
out, _ = sjson.Set(out, "system", systemPrompt)
|
|
}
|
|
}
|
|
|
|
// Set stream
|
|
out, _ = sjson.Set(out, "stream", stream)
|
|
|
|
return []byte(out)
|
|
}
|