mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Refactor translator packages for OpenAI Chat Completions
- Renamed `openai` packages to `chat_completions` across translator modules. - Introduced `openai_responses_handlers` with handlers for `/v1/models` and OpenAI-compatible chat completions endpoints. - Updated constants and registry identifiers for OpenAI response type. - Simplified request/response conversions and added detailed retry/error handling. - Added `golang.org/x/crypto` for additional cryptographic functions.
This commit is contained in:
@@ -1,5 +1,190 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, rawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToClaude(modelName string, rawJSON []byte, stream bool) []byte {
|
||||||
return nil
|
// Base Claude message payload
|
||||||
|
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
||||||
|
|
||||||
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String && instr.String() != "" {
|
||||||
|
sysMsg := `{"role":"user","content":""}`
|
||||||
|
sysMsg, _ = sjson.Set(sysMsg, "content", instr.String())
|
||||||
|
out, _ = sjson.SetRaw(out, "messages.-1", sysMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// input array processing
|
||||||
|
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
||||||
|
input.ForEach(func(_, item gjson.Result) bool {
|
||||||
|
typ := item.Get("type").String()
|
||||||
|
switch typ {
|
||||||
|
case "message":
|
||||||
|
// Determine role from content type (input_text=user, output_text=assistant)
|
||||||
|
var role string
|
||||||
|
var text strings.Builder
|
||||||
|
if parts := item.Get("content"); parts.Exists() && parts.IsArray() {
|
||||||
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
|
ptype := part.Get("type").String()
|
||||||
|
if ptype == "input_text" || ptype == "output_text" {
|
||||||
|
if t := part.Get("text"); t.Exists() {
|
||||||
|
text.WriteString(t.String())
|
||||||
|
}
|
||||||
|
if ptype == "input_text" {
|
||||||
|
role = "user"
|
||||||
|
} else if ptype == "output_text" {
|
||||||
|
role = "assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 text.Len() > 0 || role == "system" {
|
||||||
|
msg := `{"role":"","content":""}`
|
||||||
|
msg, _ = sjson.Set(msg, "role", role)
|
||||||
|
if text.Len() > 0 {
|
||||||
|
msg, _ = sjson.Set(msg, "content", text.String())
|
||||||
|
} else {
|
||||||
|
msg, _ = sjson.Set(msg, "content", "")
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,212 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
func ConvertOpenAIResponsesRequestToGemini(modelName string, rawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToGemini(modelName string, rawJSON []byte, stream bool) []byte {
|
||||||
return nil
|
// Note: modelName and stream parameters are part of the fixed method signature
|
||||||
|
_ = modelName // Unused but required by interface
|
||||||
|
_ = stream // Unused but required by interface
|
||||||
|
|
||||||
|
// Base Gemini API template
|
||||||
|
out := `{"contents":[]}`
|
||||||
|
|
||||||
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
// Extract system instruction from OpenAI "instructions" field
|
||||||
|
if instructions := root.Get("instructions"); instructions.Exists() {
|
||||||
|
systemInstr := `{"parts":[{"text":""}]}`
|
||||||
|
systemInstr, _ = sjson.Set(systemInstr, "parts.0.text", instructions.String())
|
||||||
|
out, _ = sjson.SetRaw(out, "system_instruction", systemInstr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert input messages to Gemini contents format
|
||||||
|
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
||||||
|
input.ForEach(func(_, item gjson.Result) bool {
|
||||||
|
itemType := item.Get("type").String()
|
||||||
|
|
||||||
|
switch itemType {
|
||||||
|
case "message":
|
||||||
|
// Handle regular messages
|
||||||
|
role := item.Get("role").String()
|
||||||
|
// Map OpenAI roles to Gemini roles
|
||||||
|
if role == "assistant" {
|
||||||
|
role = "model"
|
||||||
|
}
|
||||||
|
|
||||||
|
content := `{"role":"","parts":[]}`
|
||||||
|
content, _ = sjson.Set(content, "role", role)
|
||||||
|
|
||||||
|
if contentArray := item.Get("content"); contentArray.Exists() && contentArray.IsArray() {
|
||||||
|
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
|
contentType := contentItem.Get("type").String()
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case "input_text":
|
||||||
|
// Convert input_text to text part
|
||||||
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
|
textPart := `{"text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", text.String())
|
||||||
|
content, _ = sjson.SetRaw(content, "parts.-1", textPart)
|
||||||
|
}
|
||||||
|
case "output_text":
|
||||||
|
// Convert output_text to text part (for multi-turn conversations)
|
||||||
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
|
textPart := `{"text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", text.String())
|
||||||
|
content, _ = sjson.SetRaw(content, "parts.-1", textPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add content if it has parts
|
||||||
|
if parts := gjson.Get(content, "parts"); parts.Exists() && len(parts.Array()) > 0 {
|
||||||
|
out, _ = sjson.SetRaw(out, "contents.-1", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "function_call":
|
||||||
|
// Handle function calls - convert to model message with functionCall
|
||||||
|
name := item.Get("name").String()
|
||||||
|
arguments := item.Get("arguments").String()
|
||||||
|
|
||||||
|
modelContent := `{"role":"model","parts":[]}`
|
||||||
|
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
||||||
|
functionCall, _ = sjson.Set(functionCall, "functionCall.name", name)
|
||||||
|
|
||||||
|
// Parse arguments JSON string and set as args object
|
||||||
|
if arguments != "" {
|
||||||
|
argsResult := gjson.Parse(arguments)
|
||||||
|
functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsResult.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
modelContent, _ = sjson.SetRaw(modelContent, "parts.-1", functionCall)
|
||||||
|
out, _ = sjson.SetRaw(out, "contents.-1", modelContent)
|
||||||
|
|
||||||
|
case "function_call_output":
|
||||||
|
// Handle function call outputs - convert to function message with functionResponse
|
||||||
|
callID := item.Get("call_id").String()
|
||||||
|
output := item.Get("output").String()
|
||||||
|
|
||||||
|
functionContent := `{"role":"function","parts":[]}`
|
||||||
|
functionResponse := `{"functionResponse":{"name":"","response":{}}}`
|
||||||
|
|
||||||
|
// We need to extract the function name from the previous function_call
|
||||||
|
// For now, we'll use a placeholder or extract from context if available
|
||||||
|
functionName := "unknown" // This should ideally be matched with the corresponding function_call
|
||||||
|
|
||||||
|
// Find the corresponding function call name by matching call_id
|
||||||
|
// We need to look back through the input array to find the matching call
|
||||||
|
if inputArray := root.Get("input"); inputArray.Exists() && inputArray.IsArray() {
|
||||||
|
inputArray.ForEach(func(_, prevItem gjson.Result) bool {
|
||||||
|
if prevItem.Get("type").String() == "function_call" && prevItem.Get("call_id").String() == callID {
|
||||||
|
functionName = prevItem.Get("name").String()
|
||||||
|
return false // Stop iteration
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.name", functionName)
|
||||||
|
|
||||||
|
// Parse output JSON string and set as response content
|
||||||
|
if output != "" {
|
||||||
|
outputResult := gjson.Parse(output)
|
||||||
|
functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse)
|
||||||
|
out, _ = sjson.SetRaw(out, "contents.-1", functionContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert tools to Gemini functionDeclarations format
|
||||||
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||||
|
geminiTools := `[{"functionDeclarations":[]}]`
|
||||||
|
|
||||||
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
|
if tool.Get("type").String() == "function" {
|
||||||
|
funcDecl := `{"name":"","description":"","parameters":{}}`
|
||||||
|
|
||||||
|
if name := tool.Get("name"); name.Exists() {
|
||||||
|
funcDecl, _ = sjson.Set(funcDecl, "name", name.String())
|
||||||
|
}
|
||||||
|
if desc := tool.Get("description"); desc.Exists() {
|
||||||
|
funcDecl, _ = sjson.Set(funcDecl, "description", desc.String())
|
||||||
|
}
|
||||||
|
if params := tool.Get("parameters"); params.Exists() {
|
||||||
|
// Convert parameter types from OpenAI format to Gemini format
|
||||||
|
cleaned := params.Raw
|
||||||
|
// Convert type values to uppercase for Gemini
|
||||||
|
paramsResult := gjson.Parse(cleaned)
|
||||||
|
if properties := paramsResult.Get("properties"); properties.Exists() {
|
||||||
|
properties.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
if propType := value.Get("type"); propType.Exists() {
|
||||||
|
upperType := strings.ToUpper(propType.String())
|
||||||
|
cleaned, _ = sjson.Set(cleaned, "properties."+key.String()+".type", upperType)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Set the overall type to OBJECT
|
||||||
|
cleaned, _ = sjson.Set(cleaned, "type", "OBJECT")
|
||||||
|
funcDecl, _ = sjson.SetRaw(funcDecl, "parameters", cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only add tools if there are function declarations
|
||||||
|
if funcDecls := gjson.Get(geminiTools, "0.functionDeclarations"); funcDecls.Exists() && len(funcDecls.Array()) > 0 {
|
||||||
|
out, _ = sjson.SetRaw(out, "tools", geminiTools)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle generation config from OpenAI format
|
||||||
|
if maxOutputTokens := root.Get("max_output_tokens"); maxOutputTokens.Exists() {
|
||||||
|
genConfig := `{"maxOutputTokens":0}`
|
||||||
|
genConfig, _ = sjson.Set(genConfig, "maxOutputTokens", maxOutputTokens.Int())
|
||||||
|
out, _ = sjson.SetRaw(out, "generationConfig", genConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle temperature if present
|
||||||
|
if temperature := root.Get("temperature"); temperature.Exists() {
|
||||||
|
if !gjson.Get(out, "generationConfig").Exists() {
|
||||||
|
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
||||||
|
}
|
||||||
|
out, _ = sjson.Set(out, "generationConfig.temperature", temperature.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle top_p if present
|
||||||
|
if topP := root.Get("top_p"); topP.Exists() {
|
||||||
|
if !gjson.Get(out, "generationConfig").Exists() {
|
||||||
|
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
||||||
|
}
|
||||||
|
out, _ = sjson.Set(out, "generationConfig.topP", topP.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stop sequences
|
||||||
|
if stopSequences := root.Get("stop_sequences"); stopSequences.Exists() && stopSequences.IsArray() {
|
||||||
|
if !gjson.Get(out, "generationConfig").Exists() {
|
||||||
|
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
||||||
|
}
|
||||||
|
var sequences []string
|
||||||
|
stopSequences.ForEach(func(_, seq gjson.Result) bool {
|
||||||
|
sequences = append(sequences, seq.String())
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
out, _ = sjson.Set(out, "generationConfig.stopSequences", sequences)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(out)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user