mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
- Introduced helper functions (`appendTextContent`, `appendImageContent`, etc.) for structured content construction. - Refactored message generation logic for better clarity, supporting mixed content scenarios (text, images, and function calls). - Added `flushMessage` to ensure proper grouping of message contents.
326 lines
10 KiB
Go
326 lines
10 KiB
Go
package responses
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
var (
|
|
user = ""
|
|
account = ""
|
|
session = ""
|
|
)
|
|
|
|
// ConvertOpenAIResponsesRequestToClaude transforms an OpenAI Responses API request
|
|
// into a Claude Messages API request using only gjson/sjson for JSON handling.
|
|
// It supports:
|
|
// - instructions -> system message
|
|
// - input[].type==message with input_text/output_text -> user/assistant messages
|
|
// - function_call -> assistant tool_use
|
|
// - function_call_output -> user tool_result
|
|
// - tools[].parameters -> tools[].input_schema
|
|
// - max_output_tokens -> max_tokens
|
|
// - stream passthrough via parameter
|
|
func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
|
rawJSON := bytes.Clone(inputRawJSON)
|
|
|
|
if account == "" {
|
|
u, _ := uuid.NewRandom()
|
|
account = u.String()
|
|
}
|
|
if session == "" {
|
|
u, _ := uuid.NewRandom()
|
|
session = u.String()
|
|
}
|
|
if user == "" {
|
|
sum := sha256.Sum256([]byte(account + session))
|
|
user = hex.EncodeToString(sum[:])
|
|
}
|
|
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
|
|
|
// Base Claude message payload
|
|
out := fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)
|
|
|
|
root := gjson.ParseBytes(rawJSON)
|
|
|
|
if v := root.Get("reasoning.effort"); v.Exists() {
|
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
|
|
|
switch v.String() {
|
|
case "none":
|
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
|
case "minimal":
|
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 1024)
|
|
case "low":
|
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 4096)
|
|
case "medium":
|
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 8192)
|
|
case "high":
|
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 24576)
|
|
}
|
|
}
|
|
|
|
// Helper for generating tool call IDs when missing
|
|
genToolCallID := func() string {
|
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
var b strings.Builder
|
|
for i := 0; i < 24; i++ {
|
|
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
|
b.WriteByte(letters[n.Int64()])
|
|
}
|
|
return "toolu_" + b.String()
|
|
}
|
|
|
|
// Model
|
|
out, _ = sjson.Set(out, "model", modelName)
|
|
|
|
// Max tokens
|
|
if mot := root.Get("max_output_tokens"); mot.Exists() {
|
|
out, _ = sjson.Set(out, "max_tokens", mot.Int())
|
|
}
|
|
|
|
// Stream
|
|
out, _ = sjson.Set(out, "stream", stream)
|
|
|
|
// instructions -> as a leading message (use role user for Claude API compatibility)
|
|
instructionsText := ""
|
|
extractedFromSystem := false
|
|
if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String {
|
|
instructionsText = instr.String()
|
|
if instructionsText != "" {
|
|
sysMsg := `{"role":"user","content":""}`
|
|
sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText)
|
|
out, _ = sjson.SetRaw(out, "messages.-1", sysMsg)
|
|
}
|
|
}
|
|
|
|
if instructionsText == "" {
|
|
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
|
input.ForEach(func(_, item gjson.Result) bool {
|
|
if strings.EqualFold(item.Get("role").String(), "system") {
|
|
var builder strings.Builder
|
|
if parts := item.Get("content"); parts.Exists() && parts.IsArray() {
|
|
parts.ForEach(func(_, part gjson.Result) bool {
|
|
text := part.Get("text").String()
|
|
if builder.Len() > 0 && text != "" {
|
|
builder.WriteByte('\n')
|
|
}
|
|
builder.WriteString(text)
|
|
return true
|
|
})
|
|
}
|
|
instructionsText = builder.String()
|
|
if instructionsText != "" {
|
|
sysMsg := `{"role":"user","content":""}`
|
|
sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText)
|
|
out, _ = sjson.SetRaw(out, "messages.-1", sysMsg)
|
|
extractedFromSystem = true
|
|
}
|
|
}
|
|
return instructionsText == ""
|
|
})
|
|
}
|
|
}
|
|
|
|
// input array processing
|
|
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
|
input.ForEach(func(_, item gjson.Result) bool {
|
|
if extractedFromSystem && strings.EqualFold(item.Get("role").String(), "system") {
|
|
return true
|
|
}
|
|
typ := item.Get("type").String()
|
|
if typ == "" && item.Get("role").String() != "" {
|
|
typ = "message"
|
|
}
|
|
switch typ {
|
|
case "message":
|
|
// Determine role and construct Claude-compatible content parts.
|
|
var role string
|
|
var textAggregate strings.Builder
|
|
var partsJSON []string
|
|
hasImage := false
|
|
if parts := item.Get("content"); parts.Exists() && parts.IsArray() {
|
|
parts.ForEach(func(_, part gjson.Result) bool {
|
|
ptype := part.Get("type").String()
|
|
switch ptype {
|
|
case "input_text", "output_text":
|
|
if t := part.Get("text"); t.Exists() {
|
|
txt := t.String()
|
|
textAggregate.WriteString(txt)
|
|
contentPart := `{"type":"text","text":""}`
|
|
contentPart, _ = sjson.Set(contentPart, "text", txt)
|
|
partsJSON = append(partsJSON, contentPart)
|
|
}
|
|
if ptype == "input_text" {
|
|
role = "user"
|
|
} else {
|
|
role = "assistant"
|
|
}
|
|
case "input_image":
|
|
url := part.Get("image_url").String()
|
|
if url == "" {
|
|
url = part.Get("url").String()
|
|
}
|
|
if url != "" {
|
|
var contentPart string
|
|
if strings.HasPrefix(url, "data:") {
|
|
trimmed := strings.TrimPrefix(url, "data:")
|
|
mediaAndData := strings.SplitN(trimmed, ";base64,", 2)
|
|
mediaType := "application/octet-stream"
|
|
data := ""
|
|
if len(mediaAndData) == 2 {
|
|
if mediaAndData[0] != "" {
|
|
mediaType = mediaAndData[0]
|
|
}
|
|
data = mediaAndData[1]
|
|
}
|
|
if data != "" {
|
|
contentPart = `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
|
contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType)
|
|
contentPart, _ = sjson.Set(contentPart, "source.data", data)
|
|
}
|
|
} else {
|
|
contentPart = `{"type":"image","source":{"type":"url","url":""}}`
|
|
contentPart, _ = sjson.Set(contentPart, "source.url", url)
|
|
}
|
|
if contentPart != "" {
|
|
partsJSON = append(partsJSON, contentPart)
|
|
if role == "" {
|
|
role = "user"
|
|
}
|
|
hasImage = true
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// Fallback to given role if content types not decisive
|
|
if role == "" {
|
|
r := item.Get("role").String()
|
|
switch r {
|
|
case "user", "assistant", "system":
|
|
role = r
|
|
default:
|
|
role = "user"
|
|
}
|
|
}
|
|
|
|
if len(partsJSON) > 0 {
|
|
msg := `{"role":"","content":[]}`
|
|
msg, _ = sjson.Set(msg, "role", role)
|
|
if len(partsJSON) == 1 && !hasImage {
|
|
// Preserve legacy behavior for single text content
|
|
msg, _ = sjson.Delete(msg, "content")
|
|
textPart := gjson.Parse(partsJSON[0])
|
|
msg, _ = sjson.Set(msg, "content", textPart.Get("text").String())
|
|
} else {
|
|
for _, partJSON := range partsJSON {
|
|
msg, _ = sjson.SetRaw(msg, "content.-1", partJSON)
|
|
}
|
|
}
|
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
|
} else if textAggregate.Len() > 0 || role == "system" {
|
|
msg := `{"role":"","content":""}`
|
|
msg, _ = sjson.Set(msg, "role", role)
|
|
msg, _ = sjson.Set(msg, "content", textAggregate.String())
|
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
|
}
|
|
|
|
case "function_call":
|
|
// Map to assistant tool_use
|
|
callID := item.Get("call_id").String()
|
|
if callID == "" {
|
|
callID = genToolCallID()
|
|
}
|
|
name := item.Get("name").String()
|
|
argsStr := item.Get("arguments").String()
|
|
|
|
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
|
toolUse, _ = sjson.Set(toolUse, "id", callID)
|
|
toolUse, _ = sjson.Set(toolUse, "name", name)
|
|
if argsStr != "" && gjson.Valid(argsStr) {
|
|
toolUse, _ = sjson.SetRaw(toolUse, "input", argsStr)
|
|
}
|
|
|
|
asst := `{"role":"assistant","content":[]}`
|
|
asst, _ = sjson.SetRaw(asst, "content.-1", toolUse)
|
|
out, _ = sjson.SetRaw(out, "messages.-1", asst)
|
|
|
|
case "function_call_output":
|
|
// Map to user tool_result
|
|
callID := item.Get("call_id").String()
|
|
outputStr := item.Get("output").String()
|
|
toolResult := `{"type":"tool_result","tool_use_id":"","content":""}`
|
|
toolResult, _ = sjson.Set(toolResult, "tool_use_id", callID)
|
|
toolResult, _ = sjson.Set(toolResult, "content", outputStr)
|
|
|
|
usr := `{"role":"user","content":[]}`
|
|
usr, _ = sjson.SetRaw(usr, "content.-1", toolResult)
|
|
out, _ = sjson.SetRaw(out, "messages.-1", usr)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// tools mapping: parameters -> input_schema
|
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
|
toolsJSON := "[]"
|
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
|
tJSON := `{"name":"","description":"","input_schema":{}}`
|
|
if n := tool.Get("name"); n.Exists() {
|
|
tJSON, _ = sjson.Set(tJSON, "name", n.String())
|
|
}
|
|
if d := tool.Get("description"); d.Exists() {
|
|
tJSON, _ = sjson.Set(tJSON, "description", d.String())
|
|
}
|
|
|
|
if params := tool.Get("parameters"); params.Exists() {
|
|
tJSON, _ = sjson.SetRaw(tJSON, "input_schema", params.Raw)
|
|
} else if params = tool.Get("parametersJsonSchema"); params.Exists() {
|
|
tJSON, _ = sjson.SetRaw(tJSON, "input_schema", params.Raw)
|
|
}
|
|
|
|
toolsJSON, _ = sjson.SetRaw(toolsJSON, "-1", tJSON)
|
|
return true
|
|
})
|
|
if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 {
|
|
out, _ = sjson.SetRaw(out, "tools", toolsJSON)
|
|
}
|
|
}
|
|
|
|
// Map tool_choice similar to Chat Completions translator (optional in docs, safe to handle)
|
|
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
|
switch toolChoice.Type {
|
|
case gjson.String:
|
|
switch toolChoice.String() {
|
|
case "auto":
|
|
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
|
case "none":
|
|
// Leave unset; implies no tools
|
|
case "required":
|
|
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
|
}
|
|
case gjson.JSON:
|
|
if toolChoice.Get("type").String() == "function" {
|
|
fn := toolChoice.Get("function.name").String()
|
|
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "tool", "name": fn})
|
|
}
|
|
default:
|
|
|
|
}
|
|
}
|
|
|
|
return []byte(out)
|
|
}
|