From 305916f5a994b708d2f9f88aeaab1c1cf9d44d1e Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 1 Sep 2025 10:07:33 +0800 Subject: [PATCH] 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. --- .../claude_openai-responses_request.go | 187 +++++++++++++++- .../gemini_openai-responses_request.go | 209 +++++++++++++++++- 2 files changed, 394 insertions(+), 2 deletions(-) diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index e1a2e431..a5505282 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -1,5 +1,190 @@ 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 { - 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) } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 86c6158d..e67f9f9d 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -1,5 +1,212 @@ package responses +import ( + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + 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) }