// Package openai provides request translation functionality for OpenAI to Claude Code API compatibility. // It handles parsing and transforming OpenAI Chat Completions API requests into Claude Code API format, // extracting model information, system instructions, message contents, and tool declarations. // The package performs JSON data transformation to ensure compatibility // between OpenAI API format and Claude Code API's expected format. package chat_completions import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "math/big" "strings" "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) var ( user = "" account = "" session = "" ) // ConvertOpenAIRequestToClaude parses and transforms an OpenAI Chat Completions API request into Claude Code API format. // It extracts the model name, system instruction, message contents, and tool declarations // from the raw JSON request and returns them in the format expected by the Claude Code API. // The function performs comprehensive transformation including: // 1. Model name mapping and parameter extraction (max_tokens, temperature, top_p, etc.) // 2. Message content conversion from OpenAI to Claude Code format // 3. Tool call and tool result handling with proper ID mapping // 4. Image data conversion from OpenAI data URLs to Claude Code base64 format // 5. Stop sequence and streaming configuration handling // // Parameters: // - modelName: The name of the model to use for the request // - rawJSON: The raw JSON request data from the OpenAI API // - stream: A boolean indicating if the request is for a streaming response // // Returns: // - []byte: The transformed request data in Claude Code API format func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := 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 Code API template with default max_tokens value out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)) root := gjson.ParseBytes(rawJSON) // Convert OpenAI reasoning_effort to Claude thinking config. if v := root.Get("reasoning_effort"); v.Exists() { effort := strings.ToLower(strings.TrimSpace(v.String())) if effort != "" { mi := registry.LookupModelInfo(modelName, "claude") supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid // validation errors since validate treats same-provider unsupported levels as errors. if supportsAdaptive { switch effort { case "none": out, _ = sjson.SetBytes(out, "thinking.type", "disabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.DeleteBytes(out, "output_config.effort") case "auto": out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.DeleteBytes(out, "output_config.effort") default: if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.SetBytes(out, "output_config.effort", effort) } } else { // Legacy/manual thinking (budget_tokens). budget, ok := thinking.ConvertLevelToBudget(effort) if ok { switch budget { case 0: out, _ = sjson.SetBytes(out, "thinking.type", "disabled") case -1: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") default: if budget > 0 { out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } } } } // Helper for generating tool call IDs in the form: toolu_ // This ensures unique identifiers for tool calls in the Claude Code format genToolCallID := func() string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var b strings.Builder // 24 chars random suffix for uniqueness 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 mapping to specify which Claude Code model to use out, _ = sjson.SetBytes(out, "model", modelName) // Max tokens configuration with fallback to default value if maxTokens := root.Get("max_tokens"); maxTokens.Exists() { out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Temperature setting for controlling response randomness if temp := root.Get("temperature"); temp.Exists() { out, _ = sjson.SetBytes(out, "temperature", temp.Float()) } else if topP := root.Get("top_p"); topP.Exists() { // Top P setting for nucleus sampling (filtered out if temperature is set) out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences configuration for custom termination conditions if stop := root.Get("stop"); stop.Exists() { if stop.IsArray() { var stopSequences []string stop.ForEach(func(_, value gjson.Result) bool { stopSequences = append(stopSequences, value.String()) return true }) if len(stopSequences) > 0 { out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences) } } else { out, _ = sjson.SetBytes(out, "stop_sequences", []string{stop.String()}) } } // Stream configuration to enable or disable streaming responses out, _ = sjson.SetBytes(out, "stream", stream) // Process messages and transform them to Claude Code format if messages := root.Get("messages"); messages.Exists() && messages.IsArray() { messageIndex := 0 messages.ForEach(func(_, message gjson.Result) bool { role := message.Get("role").String() contentResult := message.Get("content") switch role { case "system": if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" { textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String()) out, _ = sjson.SetRawBytes(out, "system.-1", textPart) } else if contentResult.Exists() && contentResult.IsArray() { contentResult.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String()) out, _ = sjson.SetRawBytes(out, "system.-1", textPart) } return true }) } case "user", "assistant": msg := []byte(`{"role":"","content":[]}`) msg, _ = sjson.SetBytes(msg, "role", role) // Handle content based on its type (string or array) if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" { part := []byte(`{"type":"text","text":""}`) part, _ = sjson.SetBytes(part, "text", contentResult.String()) msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } else if contentResult.Exists() && contentResult.IsArray() { contentResult.ForEach(func(_, part gjson.Result) bool { claudePart := convertOpenAIContentPartToClaudePart(part) if claudePart != "" { msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(claudePart)) } return true }) } // Handle tool calls (for assistant messages) if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" { toolCalls.ForEach(func(_, toolCall gjson.Result) bool { if toolCall.Get("type").String() == "function" { toolCallID := toolCall.Get("id").String() if toolCallID == "" { toolCallID = genToolCallID() } function := toolCall.Get("function") toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) toolUse, _ = sjson.SetBytes(toolUse, "id", toolCallID) toolUse, _ = sjson.SetBytes(toolUse, "name", function.Get("name").String()) // Parse arguments for the tool call if args := function.Get("arguments"); args.Exists() { argsStr := args.String() if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw)) } else { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } } else { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } } else { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } msg, _ = sjson.SetRawBytes(msg, "content.-1", toolUse) } return true }) } out, _ = sjson.SetRawBytes(out, "messages.-1", msg) messageIndex++ case "tool": // Handle tool result messages conversion toolCallID := message.Get("tool_call_id").String() toolContentResult := message.Get("content") msg := []byte(`{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`) msg, _ = sjson.SetBytes(msg, "content.0.tool_use_id", toolCallID) toolResultContent, toolResultContentRaw := convertOpenAIToolResultContent(toolContentResult) if toolResultContentRaw { msg, _ = sjson.SetRawBytes(msg, "content.0.content", []byte(toolResultContent)) } else { msg, _ = sjson.SetBytes(msg, "content.0.content", toolResultContent) } out, _ = sjson.SetRawBytes(out, "messages.-1", msg) messageIndex++ } return true }) // Preserve a minimal conversational turn for system-only inputs. // Claude payloads with top-level system instructions but no messages are risky for downstream validation. if messageIndex == 0 { system := gjson.GetBytes(out, "system") if system.Exists() && system.IsArray() && len(system.Array()) > 0 { fallbackMsg := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`) out, _ = sjson.SetRawBytes(out, "messages.-1", fallbackMsg) } } } // Tools mapping: OpenAI tools -> Claude Code tools if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 { hasAnthropicTools := false tools.ForEach(func(_, tool gjson.Result) bool { if tool.Get("type").String() == "function" { function := tool.Get("function") anthropicTool := []byte(`{"name":"","description":""}`) anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", function.Get("name").String()) anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", function.Get("description").String()) // Convert parameters schema for the tool if parameters := function.Get("parameters"); parameters.Exists() { anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", []byte(parameters.Raw)) } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() { anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", []byte(parameters.Raw)) } out, _ = sjson.SetRawBytes(out, "tools.-1", anthropicTool) hasAnthropicTools = true } return true }) if !hasAnthropicTools { out, _ = sjson.DeleteBytes(out, "tools") } } // Tool choice mapping from OpenAI format to Claude Code format if toolChoice := root.Get("tool_choice"); toolChoice.Exists() { switch toolChoice.Type { case gjson.String: choice := toolChoice.String() switch choice { case "none": // Don't set tool_choice, Claude Code will not use tools case "auto": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "required": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } case gjson.JSON: // Specific tool choice mapping if toolChoice.Get("type").String() == "function" { functionName := toolChoice.Get("function.name").String() toolChoiceJSON := []byte(`{"type":"tool","name":""}`) toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", functionName) out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON) } default: } } return out } func convertOpenAIContentPartToClaudePart(part gjson.Result) string { switch part.Get("type").String() { case "text": textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String()) return string(textPart) case "image_url": return convertOpenAIImageURLToClaudePart(part.Get("image_url.url").String()) case "file": fileData := part.Get("file.file_data").String() if strings.HasPrefix(fileData, "data:") { semicolonIdx := strings.Index(fileData, ";") commaIdx := strings.Index(fileData, ",") if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx { mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:") data := fileData[commaIdx+1:] docPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`) docPart, _ = sjson.SetBytes(docPart, "source.media_type", mediaType) docPart, _ = sjson.SetBytes(docPart, "source.data", data) return string(docPart) } } } return "" } func convertOpenAIImageURLToClaudePart(imageURL string) string { if imageURL == "" { return "" } if strings.HasPrefix(imageURL, "data:") { parts := strings.SplitN(imageURL, ",", 2) if len(parts) != 2 { return "" } mediaTypePart := strings.SplitN(parts[0], ";", 2)[0] mediaType := strings.TrimPrefix(mediaTypePart, "data:") if mediaType == "" { mediaType = "application/octet-stream" } imagePart := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) imagePart, _ = sjson.SetBytes(imagePart, "source.media_type", mediaType) imagePart, _ = sjson.SetBytes(imagePart, "source.data", parts[1]) return string(imagePart) } imagePart := []byte(`{"type":"image","source":{"type":"url","url":""}}`) imagePart, _ = sjson.SetBytes(imagePart, "source.url", imageURL) return string(imagePart) } func convertOpenAIToolResultContent(content gjson.Result) (string, bool) { if !content.Exists() { return "", false } if content.Type == gjson.String { return content.String(), false } if content.IsArray() { claudeContent := []byte("[]") partCount := 0 content.ForEach(func(_, part gjson.Result) bool { if part.Type == gjson.String { textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", part.String()) claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", textPart) partCount++ return true } claudePart := convertOpenAIContentPartToClaudePart(part) if claudePart != "" { claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart)) partCount++ } return true }) if partCount > 0 || len(content.Array()) == 0 { return string(claudeContent), true } return content.Raw, false } if content.IsObject() { claudePart := convertOpenAIContentPartToClaudePart(content) if claudePart != "" { claudeContent := []byte("[]") claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart)) return string(claudeContent), true } return content.Raw, false } return content.Raw, false }