| |
| |
| |
| |
| |
| package claude |
|
|
| import ( |
| "bytes" |
| "fmt" |
| "strconv" |
| "strings" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" |
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte { |
| rawJSON := bytes.Clone(inputRawJSON) |
| userAgent := misc.ExtractCodexUserAgent(rawJSON) |
|
|
| template := `{"model":"","instructions":"","input":[]}` |
|
|
| _, instructions := misc.CodexInstructionsForModel(modelName, "", userAgent) |
| template, _ = sjson.Set(template, "instructions", instructions) |
|
|
| rootResult := gjson.ParseBytes(rawJSON) |
| template, _ = sjson.Set(template, "model", modelName) |
|
|
| |
| systemsResult := rootResult.Get("system") |
| if systemsResult.IsArray() { |
| systemResults := systemsResult.Array() |
| message := `{"type":"message","role":"developer","content":[]}` |
| for i := 0; i < len(systemResults); i++ { |
| systemResult := systemResults[i] |
| systemTypeResult := systemResult.Get("type") |
| if systemTypeResult.String() == "text" { |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", i), "input_text") |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", i), systemResult.Get("text").String()) |
| } |
| } |
| template, _ = sjson.SetRaw(template, "input.-1", message) |
| } |
|
|
| |
| messagesResult := rootResult.Get("messages") |
| if messagesResult.IsArray() { |
| messageResults := messagesResult.Array() |
|
|
| for i := 0; i < len(messageResults); i++ { |
| messageResult := messageResults[i] |
| messageRole := messageResult.Get("role").String() |
|
|
| newMessage := func() string { |
| msg := `{"type": "message","role":"","content":[]}` |
| msg, _ = sjson.Set(msg, "role", messageRole) |
| return msg |
| } |
|
|
| message := newMessage() |
| contentIndex := 0 |
| hasContent := false |
|
|
| flushMessage := func() { |
| if hasContent { |
| template, _ = sjson.SetRaw(template, "input.-1", message) |
| message = newMessage() |
| contentIndex = 0 |
| hasContent = false |
| } |
| } |
|
|
| appendTextContent := func(text string) { |
| partType := "input_text" |
| if messageRole == "assistant" { |
| partType = "output_text" |
| } |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), partType) |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text) |
| contentIndex++ |
| hasContent = true |
| } |
|
|
| appendImageContent := func(dataURL string) { |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image") |
| message, _ = sjson.Set(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL) |
| contentIndex++ |
| hasContent = true |
| } |
|
|
| messageContentsResult := messageResult.Get("content") |
| if messageContentsResult.IsArray() { |
| messageContentResults := messageContentsResult.Array() |
| for j := 0; j < len(messageContentResults); j++ { |
| messageContentResult := messageContentResults[j] |
| contentType := messageContentResult.Get("type").String() |
|
|
| switch contentType { |
| case "text": |
| appendTextContent(messageContentResult.Get("text").String()) |
| case "image": |
| sourceResult := messageContentResult.Get("source") |
| if sourceResult.Exists() { |
| data := sourceResult.Get("data").String() |
| if data == "" { |
| data = sourceResult.Get("base64").String() |
| } |
| if data != "" { |
| mediaType := sourceResult.Get("media_type").String() |
| if mediaType == "" { |
| mediaType = sourceResult.Get("mime_type").String() |
| } |
| if mediaType == "" { |
| mediaType = "application/octet-stream" |
| } |
| dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data) |
| appendImageContent(dataURL) |
| } |
| } |
| case "tool_use": |
| flushMessage() |
| functionCallMessage := `{"type":"function_call"}` |
| functionCallMessage, _ = sjson.Set(functionCallMessage, "call_id", messageContentResult.Get("id").String()) |
| { |
| name := messageContentResult.Get("name").String() |
| toolMap := buildReverseMapFromClaudeOriginalToShort(rawJSON) |
| if short, ok := toolMap[name]; ok { |
| name = short |
| } else { |
| name = shortenNameIfNeeded(name) |
| } |
| functionCallMessage, _ = sjson.Set(functionCallMessage, "name", name) |
| } |
| functionCallMessage, _ = sjson.Set(functionCallMessage, "arguments", messageContentResult.Get("input").Raw) |
| template, _ = sjson.SetRaw(template, "input.-1", functionCallMessage) |
| case "tool_result": |
| flushMessage() |
| functionCallOutputMessage := `{"type":"function_call_output"}` |
| functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String()) |
| functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String()) |
| template, _ = sjson.SetRaw(template, "input.-1", functionCallOutputMessage) |
| } |
| } |
| flushMessage() |
| } else if messageContentsResult.Type == gjson.String { |
| appendTextContent(messageContentsResult.String()) |
| flushMessage() |
| } |
| } |
|
|
| } |
|
|
| |
| toolsResult := rootResult.Get("tools") |
| if toolsResult.IsArray() { |
| template, _ = sjson.SetRaw(template, "tools", `[]`) |
| template, _ = sjson.Set(template, "tool_choice", `auto`) |
| toolResults := toolsResult.Array() |
| |
| var names []string |
| for i := 0; i < len(toolResults); i++ { |
| n := toolResults[i].Get("name").String() |
| if n != "" { |
| names = append(names, n) |
| } |
| } |
| shortMap := buildShortNameMap(names) |
| for i := 0; i < len(toolResults); i++ { |
| toolResult := toolResults[i] |
| |
| if toolResult.Get("type").String() == "web_search_20250305" { |
| |
| template, _ = sjson.SetRaw(template, "tools.-1", `{"type":"web_search"}`) |
| continue |
| } |
| tool := toolResult.Raw |
| tool, _ = sjson.Set(tool, "type", "function") |
| |
| if v := toolResult.Get("name"); v.Exists() { |
| name := v.String() |
| if short, ok := shortMap[name]; ok { |
| name = short |
| } else { |
| name = shortenNameIfNeeded(name) |
| } |
| tool, _ = sjson.Set(tool, "name", name) |
| } |
| tool, _ = sjson.SetRaw(tool, "parameters", normalizeToolParameters(toolResult.Get("input_schema").Raw)) |
| tool, _ = sjson.Delete(tool, "input_schema") |
| tool, _ = sjson.Delete(tool, "parameters.$schema") |
| tool, _ = sjson.Set(tool, "strict", false) |
| template, _ = sjson.SetRaw(template, "tools.-1", tool) |
| } |
| } |
|
|
| |
| template, _ = sjson.Set(template, "parallel_tool_calls", true) |
|
|
| |
| reasoningEffort := "medium" |
| if thinkingConfig := rootResult.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() { |
| switch thinkingConfig.Get("type").String() { |
| case "enabled": |
| if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() { |
| budget := int(budgetTokens.Int()) |
| if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" { |
| reasoningEffort = effort |
| } |
| } |
| case "disabled": |
| if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" { |
| reasoningEffort = effort |
| } |
| } |
| } |
| template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort) |
| template, _ = sjson.Set(template, "reasoning.summary", "auto") |
| template, _ = sjson.Set(template, "stream", true) |
| template, _ = sjson.Set(template, "store", false) |
| template, _ = sjson.Set(template, "include", []string{"reasoning.encrypted_content"}) |
|
|
| |
| if misc.GetCodexInstructionsEnabled() { |
| inputResult := gjson.Get(template, "input") |
| if inputResult.Exists() && inputResult.IsArray() { |
| inputResults := inputResult.Array() |
| newInput := "[]" |
| for i := 0; i < len(inputResults); i++ { |
| if i == 0 { |
| firstText := inputResults[i].Get("content.0.text") |
| firstInstructions := "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!" |
| if firstText.Exists() && firstText.String() != firstInstructions { |
| newInput, _ = sjson.SetRaw(newInput, "-1", `{"type":"message","role":"user","content":[{"type":"input_text","text":"EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"}]}`) |
| } |
| } |
| newInput, _ = sjson.SetRaw(newInput, "-1", inputResults[i].Raw) |
| } |
| template, _ = sjson.SetRaw(template, "input", newInput) |
| } |
| } |
|
|
| return []byte(template) |
| } |
|
|
| |
| func shortenNameIfNeeded(name string) string { |
| const limit = 64 |
| if len(name) <= limit { |
| return name |
| } |
| if strings.HasPrefix(name, "mcp__") { |
| idx := strings.LastIndex(name, "__") |
| if idx > 0 { |
| cand := "mcp__" + name[idx+2:] |
| if len(cand) > limit { |
| return cand[:limit] |
| } |
| return cand |
| } |
| } |
| return name[:limit] |
| } |
|
|
| |
| func buildShortNameMap(names []string) map[string]string { |
| const limit = 64 |
| used := map[string]struct{}{} |
| m := map[string]string{} |
|
|
| baseCandidate := func(n string) string { |
| if len(n) <= limit { |
| return n |
| } |
| if strings.HasPrefix(n, "mcp__") { |
| idx := strings.LastIndex(n, "__") |
| if idx > 0 { |
| cand := "mcp__" + n[idx+2:] |
| if len(cand) > limit { |
| cand = cand[:limit] |
| } |
| return cand |
| } |
| } |
| return n[:limit] |
| } |
|
|
| makeUnique := func(cand string) string { |
| if _, ok := used[cand]; !ok { |
| return cand |
| } |
| base := cand |
| for i := 1; ; i++ { |
| suffix := "_" + strconv.Itoa(i) |
| allowed := limit - len(suffix) |
| if allowed < 0 { |
| allowed = 0 |
| } |
| tmp := base |
| if len(tmp) > allowed { |
| tmp = tmp[:allowed] |
| } |
| tmp = tmp + suffix |
| if _, ok := used[tmp]; !ok { |
| return tmp |
| } |
| } |
| } |
|
|
| for _, n := range names { |
| cand := baseCandidate(n) |
| uniq := makeUnique(cand) |
| used[uniq] = struct{}{} |
| m[n] = uniq |
| } |
| return m |
| } |
|
|
| |
| func buildReverseMapFromClaudeOriginalToShort(original []byte) map[string]string { |
| tools := gjson.GetBytes(original, "tools") |
| m := map[string]string{} |
| if !tools.IsArray() { |
| return m |
| } |
| var names []string |
| arr := tools.Array() |
| for i := 0; i < len(arr); i++ { |
| n := arr[i].Get("name").String() |
| if n != "" { |
| names = append(names, n) |
| } |
| } |
| if len(names) > 0 { |
| m = buildShortNameMap(names) |
| } |
| return m |
| } |
|
|
| |
| func normalizeToolParameters(raw string) string { |
| raw = strings.TrimSpace(raw) |
| if raw == "" || raw == "null" || !gjson.Valid(raw) { |
| return `{"type":"object","properties":{}}` |
| } |
| schema := raw |
| result := gjson.Parse(raw) |
| schemaType := result.Get("type").String() |
| if schemaType == "" { |
| schema, _ = sjson.Set(schema, "type", "object") |
| schemaType = "object" |
| } |
| if schemaType == "object" && !result.Get("properties").Exists() { |
| schema, _ = sjson.SetRaw(schema, "properties", `{}`) |
| } |
| return schema |
| } |
|
|