mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-23 21:01:01 +08:00
fix: shorten claude codex tool call ids
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -173,7 +175,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
case "tool_use":
|
||||
flushMessage()
|
||||
functionCallMessage := []byte(`{"type":"function_call"}`)
|
||||
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "call_id", messageContentResult.Get("id").String())
|
||||
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "call_id", shortenCodexCallIDIfNeeded(messageContentResult.Get("id").String()))
|
||||
{
|
||||
name := messageContentResult.Get("name").String()
|
||||
if short, ok := toolNameMap[name]; ok {
|
||||
@@ -188,7 +190,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
case "tool_result":
|
||||
flushMessage()
|
||||
functionCallOutputMessage := []byte(`{"type":"function_call_output"}`)
|
||||
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String())
|
||||
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "call_id", shortenCodexCallIDIfNeeded(messageContentResult.Get("tool_use_id").String()))
|
||||
|
||||
contentResult := messageContentResult.Get("content")
|
||||
if contentResult.IsArray() {
|
||||
@@ -362,6 +364,23 @@ func isFernetLikeReasoningSignature(signature string) bool {
|
||||
return ciphertextLen > 0 && ciphertextLen%aesBlockSize == 0
|
||||
}
|
||||
|
||||
// shortenCodexCallIDIfNeeded keeps Claude tool IDs within the OpenAI Responses
|
||||
// API call_id limit while preserving a stable, low-collision mapping.
|
||||
func shortenCodexCallIDIfNeeded(id string) string {
|
||||
const limit = 64
|
||||
if len(id) <= limit {
|
||||
return id
|
||||
}
|
||||
|
||||
sum := sha256.Sum256([]byte(id))
|
||||
suffix := "_" + hex.EncodeToString(sum[:8])
|
||||
prefixLen := limit - len(suffix)
|
||||
if prefixLen <= 0 {
|
||||
return suffix[len(suffix)-limit:]
|
||||
}
|
||||
return id[:prefixLen] + suffix
|
||||
}
|
||||
|
||||
func isClaudeWebSearchToolType(toolType string) bool {
|
||||
return toolType == "web_search_20250305" || toolType == "web_search_20260209"
|
||||
}
|
||||
|
||||
@@ -136,6 +136,56 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_ShortenLongToolUseIDs(t *testing.T) {
|
||||
longID := "toolu_" + strings.Repeat("a", 62)
|
||||
if len(longID) <= 64 {
|
||||
t.Fatalf("test setup error: longID length = %d, want > 64", len(longID))
|
||||
}
|
||||
|
||||
inputJSON := `{
|
||||
"model": "claude-3-opus",
|
||||
"messages": [
|
||||
{"role": "user", "content": [{"type":"text","text":"run pwd"}]},
|
||||
{"role": "assistant", "content": [
|
||||
{"type":"tool_use","id":"` + longID + `","name":"Bash","input":{"cmd":"pwd"}}
|
||||
]},
|
||||
{"role": "user", "content": [
|
||||
{"type":"tool_result","tool_use_id":"` + longID + `","content":"ok"}
|
||||
]}
|
||||
]
|
||||
}`
|
||||
|
||||
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
||||
inputs := gjson.GetBytes(result, "input").Array()
|
||||
|
||||
var callID string
|
||||
var outputCallID string
|
||||
for _, item := range inputs {
|
||||
switch item.Get("type").String() {
|
||||
case "function_call":
|
||||
callID = item.Get("call_id").String()
|
||||
case "function_call_output":
|
||||
outputCallID = item.Get("call_id").String()
|
||||
}
|
||||
}
|
||||
|
||||
if callID == "" {
|
||||
t.Fatalf("missing function_call item. Output: %s", string(result))
|
||||
}
|
||||
if outputCallID == "" {
|
||||
t.Fatalf("missing function_call_output item. Output: %s", string(result))
|
||||
}
|
||||
if callID != outputCallID {
|
||||
t.Fatalf("call_id mismatch: function_call=%q function_call_output=%q. Output: %s", callID, outputCallID, string(result))
|
||||
}
|
||||
if len(callID) > 64 {
|
||||
t.Fatalf("call_id length = %d, want <= 64: %q", len(callID), callID)
|
||||
}
|
||||
if callID == longID {
|
||||
t.Fatalf("long call_id was not shortened: %q", callID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_ToolChoiceModeMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -140,7 +140,7 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
params.HasReceivedArgumentsDelta = false
|
||||
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`)
|
||||
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
||||
template, _ = sjson.SetBytes(template, "content_block.id", util.SanitizeClaudeToolID(itemResult.Get("call_id").String()))
|
||||
template, _ = sjson.SetBytes(template, "content_block.id", shortenCodexCallIDIfNeeded(util.SanitizeClaudeToolID(itemResult.Get("call_id").String())))
|
||||
{
|
||||
name := itemResult.Get("name").String()
|
||||
rev := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
||||
@@ -350,7 +350,7 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
||||
}
|
||||
|
||||
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
toolBlock, _ = sjson.SetBytes(toolBlock, "id", util.SanitizeClaudeToolID(item.Get("call_id").String()))
|
||||
toolBlock, _ = sjson.SetBytes(toolBlock, "id", shortenCodexCallIDIfNeeded(util.SanitizeClaudeToolID(item.Get("call_id").String())))
|
||||
toolBlock, _ = sjson.SetBytes(toolBlock, "name", name)
|
||||
inputRaw := "{}"
|
||||
if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
|
||||
|
||||
@@ -459,6 +459,70 @@ func TestConvertCodexResponseToClaude_StreamEmptyOutputUsesOutputItemDoneMessage
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_ShortensLongToolUseIDs(t *testing.T) {
|
||||
longCallID := "call_" + strings.Repeat("a", 62)
|
||||
if len(longCallID) <= 64 {
|
||||
t.Fatalf("test setup error: longCallID length = %d, want > 64", len(longCallID))
|
||||
}
|
||||
|
||||
t.Run("stream", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","input_schema":{"type":"object","properties":{}}}]}`)
|
||||
var param any
|
||||
|
||||
outputs := ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, []byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"`+longCallID+`","name":"lookup"}}`), ¶m)
|
||||
|
||||
toolID := ""
|
||||
for _, out := range outputs {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
|
||||
if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "tool_use" {
|
||||
toolID = data.Get("content_block.id").String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toolID == "" {
|
||||
t.Fatalf("missing stream tool_use block. Outputs=%q", outputs)
|
||||
}
|
||||
if len(toolID) > 64 {
|
||||
t.Fatalf("stream tool_use id length = %d, want <= 64: %q", len(toolID), toolID)
|
||||
}
|
||||
if toolID == longCallID {
|
||||
t.Fatalf("stream tool_use id was not shortened: %q", toolID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonstream", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","input_schema":{"type":"object","properties":{}}}]}`)
|
||||
response := []byte(`{
|
||||
"type":"response.completed",
|
||||
"response":{
|
||||
"id":"resp_1",
|
||||
"model":"gpt-5",
|
||||
"usage":{"input_tokens":1,"output_tokens":1},
|
||||
"output":[{"type":"function_call","call_id":"` + longCallID + `","name":"lookup","arguments":"{}"}]
|
||||
}
|
||||
}`)
|
||||
|
||||
out := ConvertCodexResponseToClaudeNonStream(ctx, "", originalRequest, nil, response, nil)
|
||||
toolID := gjson.GetBytes(out, "content.0.id").String()
|
||||
if toolID == "" {
|
||||
t.Fatalf("missing nonstream tool_use id. Output: %s", string(out))
|
||||
}
|
||||
if len(toolID) > 64 {
|
||||
t.Fatalf("nonstream tool_use id length = %d, want <= 64: %q", len(toolID), toolID)
|
||||
}
|
||||
if toolID == longCallID {
|
||||
t.Fatalf("nonstream tool_use id was not shortened: %q", toolID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamStopReasonMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user