Fixed: #1741
fix(translator): handle tool name mappings and improve tool call handling in OpenAI and Claude integrations
This commit is contained in:
@@ -85,6 +85,11 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
|
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
|
if toolUseID := contentResult.Get("id").String(); toolUseID != "" {
|
||||||
|
if derived := toolNameFromClaudeToolUseID(toolUseID); derived != "" {
|
||||||
|
functionName = derived
|
||||||
|
}
|
||||||
|
}
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
argsResult := gjson.Parse(functionArgs)
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
@@ -100,10 +105,9 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if toolCallID == "" {
|
if toolCallID == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
funcName := toolCallID
|
funcName := toolNameFromClaudeToolUseID(toolCallID)
|
||||||
toolCallIDs := strings.Split(toolCallID, "-")
|
if funcName == "" {
|
||||||
if len(toolCallIDs) > 1 {
|
funcName = toolCallID
|
||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
||||||
@@ -230,3 +234,11 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toolNameFromClaudeToolUseID(toolUseID string) string {
|
||||||
|
parts := strings.Split(toolUseID, "-")
|
||||||
|
if len(parts) <= 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(parts[0:len(parts)-1], "-")
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -25,6 +25,8 @@ type Params struct {
|
|||||||
ResponseType int
|
ResponseType int
|
||||||
ResponseIndex int
|
ResponseIndex int
|
||||||
HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
|
HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
|
||||||
|
ToolNameMap map[string]string
|
||||||
|
SawToolCall bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolUseIDCounter provides a process-wide unique counter for tool use identifiers.
|
// toolUseIDCounter provides a process-wide unique counter for tool use identifiers.
|
||||||
@@ -53,6 +55,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
HasFirstResponse: false,
|
HasFirstResponse: false,
|
||||||
ResponseType: 0,
|
ResponseType: 0,
|
||||||
ResponseIndex: 0,
|
ResponseIndex: 0,
|
||||||
|
ToolNameMap: util.ToolNameMapFromClaudeRequest(originalRequestRawJSON),
|
||||||
|
SawToolCall: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +70,6 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track whether tools are being used in this response chunk
|
|
||||||
usedTool := false
|
|
||||||
output := ""
|
output := ""
|
||||||
|
|
||||||
// Initialize the streaming session with a message_start event
|
// Initialize the streaming session with a message_start event
|
||||||
@@ -175,12 +177,13 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
} else if functionCallResult.Exists() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function/tool calls from the AI model
|
// Handle function/tool calls from the AI model
|
||||||
// This processes tool usage requests and formats them for Claude API compatibility
|
// This processes tool usage requests and formats them for Claude API compatibility
|
||||||
usedTool = true
|
(*param).(*Params).SawToolCall = true
|
||||||
fcName := functionCallResult.Get("name").String()
|
upstreamToolName := functionCallResult.Get("name").String()
|
||||||
|
clientToolName := util.MapToolName((*param).(*Params).ToolNameMap, upstreamToolName)
|
||||||
|
|
||||||
// FIX: Handle streaming split/delta where name might be empty in subsequent chunks.
|
// FIX: Handle streaming split/delta where name might be empty in subsequent chunks.
|
||||||
// If we are already in tool use mode and name is empty, treat as continuation (delta).
|
// If we are already in tool use mode and name is empty, treat as continuation (delta).
|
||||||
if (*param).(*Params).ResponseType == 3 && fcName == "" {
|
if (*param).(*Params).ResponseType == 3 && upstreamToolName == "" {
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
output = output + "event: content_block_delta\n"
|
output = output + "event: content_block_delta\n"
|
||||||
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw)
|
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw)
|
||||||
@@ -221,8 +224,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// Create the tool use block with unique ID and function details
|
// Create the tool use block with unique ID and function details
|
||||||
data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex)
|
data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex)
|
||||||
data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)))
|
data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d", upstreamToolName, atomic.AddUint64(&toolUseIDCounter, 1)))
|
||||||
data, _ = sjson.Set(data, "content_block.name", fcName)
|
data, _ = sjson.Set(data, "content_block.name", clientToolName)
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
||||||
|
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
@@ -249,7 +252,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
output = output + `data: `
|
output = output + `data: `
|
||||||
|
|
||||||
template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
if usedTool {
|
if (*param).(*Params).SawToolCall {
|
||||||
template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
} else if finish := gjson.GetBytes(rawJSON, "candidates.0.finishReason"); finish.Exists() && finish.String() == "MAX_TOKENS" {
|
} else if finish := gjson.GetBytes(rawJSON, "candidates.0.finishReason"); finish.Exists() && finish.String() == "MAX_TOKENS" {
|
||||||
template = `{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
template = `{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
@@ -278,10 +281,10 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude-compatible JSON response.
|
// - string: A Claude-compatible JSON response.
|
||||||
func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||||
_ = originalRequestRawJSON
|
|
||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
||||||
|
|
||||||
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
out, _ = sjson.Set(out, "id", root.Get("responseId").String())
|
out, _ = sjson.Set(out, "id", root.Get("responseId").String())
|
||||||
@@ -336,11 +339,12 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
flushText()
|
flushText()
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
|
|
||||||
name := functionCall.Get("name").String()
|
upstreamToolName := functionCall.Get("name").String()
|
||||||
|
clientToolName := util.MapToolName(toolNameMap, upstreamToolName)
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("%s-%d", upstreamToolName, toolIDCounter))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.Set(toolBlock, "name", clientToolName)
|
||||||
inputRaw := "{}"
|
inputRaw := "{}"
|
||||||
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
inputRaw = args.Raw
|
inputRaw = args.Raw
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ var (
|
|||||||
|
|
||||||
// ConvertOpenAIResponseToAnthropicParams holds parameters for response conversion
|
// ConvertOpenAIResponseToAnthropicParams holds parameters for response conversion
|
||||||
type ConvertOpenAIResponseToAnthropicParams struct {
|
type ConvertOpenAIResponseToAnthropicParams struct {
|
||||||
MessageID string
|
MessageID string
|
||||||
Model string
|
Model string
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
|
ToolNameMap map[string]string
|
||||||
|
SawToolCall bool
|
||||||
// Content accumulator for streaming
|
// Content accumulator for streaming
|
||||||
ContentAccumulator strings.Builder
|
ContentAccumulator strings.Builder
|
||||||
// Tool calls accumulator for streaming
|
// Tool calls accumulator for streaming
|
||||||
@@ -78,6 +80,8 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
MessageID: "",
|
MessageID: "",
|
||||||
Model: "",
|
Model: "",
|
||||||
CreatedAt: 0,
|
CreatedAt: 0,
|
||||||
|
ToolNameMap: nil,
|
||||||
|
SawToolCall: false,
|
||||||
ContentAccumulator: strings.Builder{},
|
ContentAccumulator: strings.Builder{},
|
||||||
ToolCallsAccumulator: nil,
|
ToolCallsAccumulator: nil,
|
||||||
TextContentBlockStarted: false,
|
TextContentBlockStarted: false,
|
||||||
@@ -97,6 +101,10 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
|
if (*param).(*ConvertOpenAIResponseToAnthropicParams).ToolNameMap == nil {
|
||||||
|
(*param).(*ConvertOpenAIResponseToAnthropicParams).ToolNameMap = util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is the [DONE] marker
|
// Check if this is the [DONE] marker
|
||||||
rawStr := strings.TrimSpace(string(rawJSON))
|
rawStr := strings.TrimSpace(string(rawJSON))
|
||||||
if rawStr == "[DONE]" {
|
if rawStr == "[DONE]" {
|
||||||
@@ -111,6 +119,16 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func effectiveOpenAIFinishReason(param *ConvertOpenAIResponseToAnthropicParams) string {
|
||||||
|
if param == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if param.SawToolCall {
|
||||||
|
return "tool_calls"
|
||||||
|
}
|
||||||
|
return param.FinishReason
|
||||||
|
}
|
||||||
|
|
||||||
// convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events
|
// convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events
|
||||||
func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string {
|
func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string {
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
@@ -197,6 +215,7 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
|
param.SawToolCall = true
|
||||||
index := int(toolCall.Get("index").Int())
|
index := int(toolCall.Get("index").Int())
|
||||||
blockIndex := param.toolContentBlockIndex(index)
|
blockIndex := param.toolContentBlockIndex(index)
|
||||||
|
|
||||||
@@ -215,7 +234,7 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
// Handle function name
|
// Handle function name
|
||||||
if function := toolCall.Get("function"); function.Exists() {
|
if function := toolCall.Get("function"); function.Exists() {
|
||||||
if name := function.Get("name"); name.Exists() {
|
if name := function.Get("name"); name.Exists() {
|
||||||
accumulator.Name = name.String()
|
accumulator.Name = util.MapToolName(param.ToolNameMap, name.String())
|
||||||
|
|
||||||
stopThinkingContentBlock(param, &results)
|
stopThinkingContentBlock(param, &results)
|
||||||
|
|
||||||
@@ -246,7 +265,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
// Handle finish_reason (but don't send message_delta/message_stop yet)
|
// Handle finish_reason (but don't send message_delta/message_stop yet)
|
||||||
if finishReason := root.Get("choices.0.finish_reason"); finishReason.Exists() && finishReason.String() != "" {
|
if finishReason := root.Get("choices.0.finish_reason"); finishReason.Exists() && finishReason.String() != "" {
|
||||||
reason := finishReason.String()
|
reason := finishReason.String()
|
||||||
param.FinishReason = reason
|
if param.SawToolCall {
|
||||||
|
param.FinishReason = "tool_calls"
|
||||||
|
} else {
|
||||||
|
param.FinishReason = reason
|
||||||
|
}
|
||||||
|
|
||||||
// Send content_block_stop for thinking content if needed
|
// Send content_block_stop for thinking content if needed
|
||||||
if param.ThinkingContentBlockStarted {
|
if param.ThinkingContentBlockStarted {
|
||||||
@@ -294,7 +317,7 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage)
|
inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage)
|
||||||
// Send message_delta with usage
|
// Send message_delta with usage
|
||||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
if cachedTokens > 0 {
|
||||||
@@ -348,7 +371,7 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
// If we haven't sent message_delta yet (no usage info was received), send it now
|
// If we haven't sent message_delta yet (no usage info was received), send it now
|
||||||
if param.FinishReason != "" && !param.MessageDeltaSent {
|
if param.FinishReason != "" && !param.MessageDeltaSent {
|
||||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||||
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
||||||
param.MessageDeltaSent = true
|
param.MessageDeltaSent = true
|
||||||
}
|
}
|
||||||
@@ -531,10 +554,10 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - string: An Anthropic-compatible JSON response.
|
// - string: An Anthropic-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||||
_ = originalRequestRawJSON
|
|
||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
||||||
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||||
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
||||||
@@ -590,7 +613,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
|
toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String())
|
toolUse, _ = sjson.Set(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String()))
|
||||||
|
|
||||||
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
@@ -647,7 +670,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String()))
|
||||||
|
|
||||||
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -219,3 +220,54 @@ func FixJSON(input string) string {
|
|||||||
|
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CanonicalToolName(name string) string {
|
||||||
|
canonical := strings.TrimSpace(name)
|
||||||
|
canonical = strings.TrimLeft(canonical, "_")
|
||||||
|
return strings.ToLower(canonical)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolNameMapFromClaudeRequest returns a canonical-name -> original-name map extracted from a Claude request.
|
||||||
|
// It is used to restore exact tool name casing for clients that require strict tool name matching (e.g. Claude Code).
|
||||||
|
func ToolNameMapFromClaudeRequest(rawJSON []byte) map[string]string {
|
||||||
|
if len(rawJSON) == 0 || !gjson.ValidBytes(rawJSON) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := gjson.GetBytes(rawJSON, "tools")
|
||||||
|
if !tools.Exists() || !tools.IsArray() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
toolResults := tools.Array()
|
||||||
|
out := make(map[string]string, len(toolResults))
|
||||||
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
|
name := strings.TrimSpace(tool.Get("name").String())
|
||||||
|
if name == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
key := CanonicalToolName(name)
|
||||||
|
if key == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, exists := out[key]; !exists {
|
||||||
|
out[key] = name
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapToolName(toolNameMap map[string]string, name string) string {
|
||||||
|
if name == "" || toolNameMap == nil {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if mapped, ok := toolNameMap[CanonicalToolName(name)]; ok && mapped != "" {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user