mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
fix: Normalize Bash tool args and add signature caching support
Normalize Bash tool arguments by converting a "command" key into "cmd" using JSON-aware parsing, avoiding brittle string replacements that could corrupt values. Apply this conversion in both streaming and non-streaming response paths so bash-style tool calls are emitted with the expected "cmd" field. Add support for accumulating thinking text and carrying session identifiers to enable signature caching/restore for unsigned thinking blocks, improving handling of thinking-state continuity across requests/responses. Also perform small cleanups: import logging, tidy comments and test descriptions. These changes make tool-argument handling more robust and enable reliable signature restoration for thinking blocks.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -124,7 +125,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
signature = signatureResult.String()
|
signature = signatureResult.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// P3: Try to restore signature from cache for unsigned thinking blocks
|
// Try to restore signature from cache for unsigned thinking blocks
|
||||||
if !cache.HasValidSignature(signature) && sessionID != "" && thinkingText != "" {
|
if !cache.HasValidSignature(signature) && sessionID != "" && thinkingText != "" {
|
||||||
if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" {
|
if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" {
|
||||||
signature = cachedSig
|
signature = cachedSig
|
||||||
@@ -132,7 +133,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// P2-A: Skip trailing unsigned thinking blocks on last assistant message
|
// Skip trailing unsigned thinking blocks on last assistant message
|
||||||
isLastMessage := (i == numMessages-1)
|
isLastMessage := (i == numMessages-1)
|
||||||
isLastContent := (j == numContents-1)
|
isLastContent := (j == numContents-1)
|
||||||
isAssistant := (originalRole == "assistant")
|
isAssistant := (originalRole == "assistant")
|
||||||
@@ -278,7 +279,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
out := `{"model":"","request":{"contents":[]}}`
|
out := `{"model":"","request":{"contents":[]}}`
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.Set(out, "model", modelName)
|
||||||
|
|
||||||
// P2-B: Inject interleaved thinking hint when both tools and thinking are active
|
// Inject interleaved thinking hint when both tools and thinking are active
|
||||||
hasTools := toolDeclCount > 0
|
hasTools := toolDeclCount > 0
|
||||||
thinkingResult := gjson.GetBytes(rawJSON, "thinking")
|
thinkingResult := gjson.GetBytes(rawJSON, "thinking")
|
||||||
hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled"
|
hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled"
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ func TestConvertClaudeRequestToAntigravity_GenerationConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// P2-A: Trailing Unsigned Thinking Block Removal
|
// Trailing Unsigned Thinking Block Removal
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestConvertClaudeRequestToAntigravity_TrailingUnsignedThinking_Removed(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_TrailingUnsignedThinking_Removed(t *testing.T) {
|
||||||
@@ -435,7 +435,7 @@ func TestConvertClaudeRequestToAntigravity_MiddleUnsignedThinking_SentinelApplie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// P2-B: Tool + Thinking System Hint Injection
|
// Tool + Thinking System Hint Injection
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestConvertClaudeRequestToAntigravity_ToolAndThinking_HintInjected(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_ToolAndThinking_HintInjected(t *testing.T) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Params struct {
|
|||||||
HasToolUse bool // Indicates if tool use was observed in the stream
|
HasToolUse bool // Indicates if tool use was observed in the stream
|
||||||
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
|
||||||
|
|
||||||
// P3: Signature caching support
|
// Signature caching support
|
||||||
SessionID string // Session ID derived from request for signature caching
|
SessionID string // Session ID derived from request for signature caching
|
||||||
CurrentThinkingText strings.Builder // Accumulates thinking text for signature caching
|
CurrentThinkingText strings.Builder // Accumulates thinking text for signature caching
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
||||||
params.ResponseType = 2 // Set state to thinking
|
params.ResponseType = 2 // Set state to thinking
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
// P3: Start accumulating thinking text for signature caching
|
// Start accumulating thinking text for signature caching
|
||||||
params.CurrentThinkingText.Reset()
|
params.CurrentThinkingText.Reset()
|
||||||
params.CurrentThinkingText.WriteString(partTextResult.String())
|
params.CurrentThinkingText.WriteString(partTextResult.String())
|
||||||
}
|
}
|
||||||
@@ -276,8 +276,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
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() {
|
||||||
|
argsRaw := fcArgsResult.Raw
|
||||||
|
// Convert command → cmd for Bash tools using proper JSON parsing
|
||||||
|
if fcName == "Bash" || fcName == "bash" || fcName == "bash_20241022" {
|
||||||
|
argsRaw = convertBashCommandToCmdField(argsRaw)
|
||||||
|
}
|
||||||
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":""}}`, 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":""}}`, params.ResponseIndex), "delta.partial_json", argsRaw)
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
||||||
}
|
}
|
||||||
params.ResponseType = 3
|
params.ResponseType = 3
|
||||||
@@ -365,6 +370,36 @@ func resolveStopReason(params *Params) string {
|
|||||||
return "end_turn"
|
return "end_turn"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertBashCommandToCmdField converts "command" field to "cmd" field for Bash tools.
|
||||||
|
// Amp expects "cmd" but Gemini sends "command". This uses proper JSON parsing
|
||||||
|
// to avoid accidentally replacing "command" that appears in values.
|
||||||
|
func convertBashCommandToCmdField(argsRaw string) string {
|
||||||
|
// Only process valid JSON
|
||||||
|
if !gjson.Valid(argsRaw) {
|
||||||
|
return argsRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if "command" key exists and "cmd" doesn't
|
||||||
|
commandVal := gjson.Get(argsRaw, "command")
|
||||||
|
cmdVal := gjson.Get(argsRaw, "cmd")
|
||||||
|
|
||||||
|
if commandVal.Exists() && !cmdVal.Exists() {
|
||||||
|
// Set "cmd" to the value of "command", preserve the raw value type
|
||||||
|
result, err := sjson.SetRaw(argsRaw, "cmd", commandVal.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return argsRaw
|
||||||
|
}
|
||||||
|
// Delete "command" key
|
||||||
|
result, err = sjson.Delete(result, "command")
|
||||||
|
if err != nil {
|
||||||
|
return argsRaw
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return argsRaw
|
||||||
|
}
|
||||||
|
|
||||||
// ConvertAntigravityResponseToClaudeNonStream converts a non-streaming Gemini CLI response to a non-streaming Claude response.
|
// ConvertAntigravityResponseToClaudeNonStream converts a non-streaming Gemini CLI response to a non-streaming Claude response.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -476,7 +511,12 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
||||||
|
|
||||||
if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) {
|
if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) {
|
||||||
toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
|
argsRaw := args.Raw
|
||||||
|
// Convert command → cmd for Bash tools
|
||||||
|
if name == "Bash" || name == "bash" || name == "bash_20241022" {
|
||||||
|
argsRaw = convertBashCommandToCmdField(argsRaw)
|
||||||
|
}
|
||||||
|
toolBlock, _ = sjson.SetRaw(toolBlock, "input", argsRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureContentArray()
|
ensureContentArray()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func TestConvertBashCommandToCmdField(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// P3: Signature Caching Tests
|
// Signature Caching Tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) {
|
func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) {
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ func compareJSON(t *testing.T, expectedJSON, actualJSON string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// P0-1: Empty Schema Placeholder Tests
|
// Empty Schema Placeholder Tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestCleanJSONSchemaForAntigravity_EmptySchemaPlaceholder(t *testing.T) {
|
func TestCleanJSONSchemaForAntigravity_EmptySchemaPlaceholder(t *testing.T) {
|
||||||
@@ -732,7 +732,7 @@ func TestCleanJSONSchemaForAntigravity_EmptySchemaWithDescription(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// P0-2: Format field handling (ad-hoc patch removal)
|
// Format field handling (ad-hoc patch removal)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestCleanJSONSchemaForAntigravity_FormatFieldRemoval(t *testing.T) {
|
func TestCleanJSONSchemaForAntigravity_FormatFieldRemoval(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user