| | |
| | |
| | |
| | |
| | |
| | package claude |
| |
|
| | import ( |
| | "bytes" |
| | "fmt" |
| | "strconv" |
| | "strings" |
| |
|
| | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| | "github.com/tidwall/gjson" |
| | "github.com/tidwall/sjson" |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte { |
| | rawJSON := bytes.Clone(inputRawJSON) |
| |
|
| | template := `{"model":"","instructions":"","input":[]}` |
| |
|
| | _, instructions := misc.CodexInstructionsForModel(modelName, "") |
| | 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":"user","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 thinking := rootResult.Get("thinking"); thinking.Exists() && thinking.IsObject() { |
| | switch thinking.Get("type").String() { |
| | case "enabled": |
| | if util.ModelUsesThinkingLevels(modelName) { |
| | if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { |
| | budget := int(budgetTokens.Int()) |
| | if effort, ok := util.ThinkingBudgetToEffort(modelName, budget); ok && effort != "" { |
| | reasoningEffort = effort |
| | } |
| | } |
| | } |
| | case "disabled": |
| | if effort, ok := util.ThinkingBudgetToEffort(modelName, 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"}) |
| |
|
| | |
| | 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 |
| | } |
| |
|