mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-24 05:21:11 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024bc25b2c | ||
|
|
ffdfad8482 | ||
|
|
b91ee8d008 | ||
|
|
6586f08584 | ||
|
|
f49e887fe6 | ||
|
|
b602eae215 |
@@ -84,13 +84,18 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
contentResult := contentResults[j]
|
contentResult := contentResults[j]
|
||||||
contentTypeResult := contentResult.Get("type")
|
contentTypeResult := contentResult.Get("type")
|
||||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" {
|
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" {
|
||||||
prompt := contentResult.Get("thinking").String()
|
// Claude "thinking" blocks are internal-only. They also require a valid provider signature
|
||||||
|
// when replayed as conversation history. Since we cannot mint signatures, only forward
|
||||||
|
// thinking blocks when the client provides a non-empty signature; otherwise, drop them.
|
||||||
signatureResult := contentResult.Get("signature")
|
signatureResult := contentResult.Get("signature")
|
||||||
signature := geminiCLIClaudeThoughtSignature
|
if signatureResult.Type == gjson.String && signatureResult.String() != "" {
|
||||||
if signatureResult.Exists() {
|
prompt := contentResult.Get("thinking").String()
|
||||||
signature = signatureResult.String()
|
clientContent.Parts = append(clientContent.Parts, client.Part{
|
||||||
|
Text: prompt,
|
||||||
|
Thought: true,
|
||||||
|
ThoughtSignature: signatureResult.String(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt, Thought: true, ThoughtSignature: signature})
|
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||||
prompt := contentResult.Get("text").String()
|
prompt := contentResult.Get("text").String()
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
|
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
|
||||||
@@ -117,9 +122,17 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
funcName := toolCallID
|
funcName := toolCallID
|
||||||
toolCallIDs := strings.Split(toolCallID, "-")
|
toolCallIDs := strings.Split(toolCallID, "-")
|
||||||
if len(toolCallIDs) > 1 {
|
if len(toolCallIDs) > 1 {
|
||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-2], "-")
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
functionResponseResult := contentResult.Get("content")
|
||||||
|
|
||||||
|
responseData := ""
|
||||||
|
if functionResponseResult.Type == gjson.String {
|
||||||
|
responseData = functionResponseResult.String()
|
||||||
|
} else {
|
||||||
|
responseData = contentResult.Get("content").Raw
|
||||||
|
}
|
||||||
|
|
||||||
functionResponse := client.FunctionResponse{ID: toolCallID, Name: funcName, Response: map[string]interface{}{"result": responseData}}
|
functionResponse := client.FunctionResponse{ID: toolCallID, Name: funcName, Response: map[string]interface{}{"result": responseData}}
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
|
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
|
||||||
}
|
}
|
||||||
@@ -134,7 +147,9 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contents = append(contents, clientContent)
|
if len(clientContent.Parts) > 0 {
|
||||||
|
contents = append(contents, clientContent)
|
||||||
|
}
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
prompt := contentsResult.String()
|
prompt := contentsResult.String()
|
||||||
contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
|
contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
|
||||||
|
|||||||
@@ -114,44 +114,54 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
// Extract the different types of content from each part
|
// Extract the different types of content from each part
|
||||||
partTextResult := partResult.Get("text")
|
partTextResult := partResult.Get("text")
|
||||||
functionCallResult := partResult.Get("functionCall")
|
functionCallResult := partResult.Get("functionCall")
|
||||||
|
thoughtSignatureResult := partResult.Get("thoughtSignature")
|
||||||
|
if !thoughtSignatureResult.Exists() {
|
||||||
|
thoughtSignatureResult = partResult.Get("thought_signature")
|
||||||
|
}
|
||||||
|
hasThoughtSignature := thoughtSignatureResult.Exists() && thoughtSignatureResult.String() != ""
|
||||||
|
isThought := partResult.Get("thought").Bool()
|
||||||
|
|
||||||
|
// Some Antigravity/Vertex Claude streams emit the thought signature as a standalone part
|
||||||
|
// (no text payload). Claude requires this signature to be replayed verbatim on subsequent turns.
|
||||||
|
if isThought && hasThoughtSignature && !partTextResult.Exists() && !functionCallResult.Exists() {
|
||||||
|
if params.ResponseType == 2 {
|
||||||
|
output = output + "event: content_block_delta\n"
|
||||||
|
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignatureResult.String())
|
||||||
|
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
||||||
|
params.HasContent = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Handle text content (both regular content and thinking)
|
// Handle text content (both regular content and thinking)
|
||||||
if partTextResult.Exists() {
|
if partTextResult.Exists() {
|
||||||
// Process thinking content (internal reasoning)
|
// Process thinking content (internal reasoning)
|
||||||
if partResult.Get("thought").Bool() {
|
if isThought {
|
||||||
if thoughtSignature := partResult.Get("thoughtSignature"); thoughtSignature.Exists() && thoughtSignature.String() != "" {
|
// Ensure we have an open thinking block to attach thinking/signature deltas to.
|
||||||
output = output + "event: content_block_delta\n"
|
if params.ResponseType != 2 {
|
||||||
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignature.String())
|
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
params.HasContent = true
|
|
||||||
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
|
|
||||||
output = output + "event: content_block_delta\n"
|
|
||||||
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String())
|
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
params.HasContent = true
|
|
||||||
} else {
|
|
||||||
// Transition from another state to thinking
|
|
||||||
// First, close any existing content block
|
|
||||||
if params.ResponseType != 0 {
|
if params.ResponseType != 0 {
|
||||||
if params.ResponseType == 2 {
|
|
||||||
// output = output + "event: content_block_delta\n"
|
|
||||||
// output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex)
|
|
||||||
// output = output + "\n\n\n"
|
|
||||||
}
|
|
||||||
output = output + "event: content_block_stop\n"
|
output = output + "event: content_block_stop\n"
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
||||||
output = output + "\n\n\n"
|
output = output + "\n\n\n"
|
||||||
params.ResponseIndex++
|
params.ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new thinking content block
|
|
||||||
output = output + "event: content_block_start\n"
|
output = output + "event: content_block_start\n"
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex)
|
output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex)
|
||||||
output = output + "\n\n\n"
|
output = output + "\n\n\n"
|
||||||
|
params.ResponseType = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if partTextResult.String() != "" {
|
||||||
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":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String())
|
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String())
|
||||||
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.HasContent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasThoughtSignature {
|
||||||
|
output = output + "event: content_block_delta\n"
|
||||||
|
data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignatureResult.String())
|
||||||
|
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -368,6 +378,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
var contentBlocks []interface{}
|
var contentBlocks []interface{}
|
||||||
textBuilder := strings.Builder{}
|
textBuilder := strings.Builder{}
|
||||||
thinkingBuilder := strings.Builder{}
|
thinkingBuilder := strings.Builder{}
|
||||||
|
thinkingSignature := ""
|
||||||
toolIDCounter := 0
|
toolIDCounter := 0
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
|
|
||||||
@@ -386,19 +397,37 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := map[string]interface{}{
|
||||||
"type": "thinking",
|
"type": "thinking",
|
||||||
"thinking": thinkingBuilder.String(),
|
"thinking": thinkingBuilder.String(),
|
||||||
})
|
}
|
||||||
|
if thinkingSignature != "" {
|
||||||
|
block["signature"] = thinkingSignature
|
||||||
|
}
|
||||||
|
contentBlocks = append(contentBlocks, block)
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
|
thinkingSignature = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if parts.IsArray() {
|
if parts.IsArray() {
|
||||||
for _, part := range parts.Array() {
|
for _, part := range parts.Array() {
|
||||||
|
thoughtSignatureResult := part.Get("thoughtSignature")
|
||||||
|
if !thoughtSignatureResult.Exists() {
|
||||||
|
thoughtSignatureResult = part.Get("thought_signature")
|
||||||
|
}
|
||||||
|
if part.Get("thought").Bool() && thoughtSignatureResult.Exists() && thoughtSignatureResult.String() != "" && (!part.Get("text").Exists() || part.Get("text").String() == "") {
|
||||||
|
// Signature-only thought part (no text payload).
|
||||||
|
thinkingSignature = thoughtSignatureResult.String()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if text := part.Get("text"); text.Exists() && text.String() != "" {
|
if text := part.Get("text"); text.Exists() && text.String() != "" {
|
||||||
if part.Get("thought").Bool() {
|
if part.Get("thought").Bool() {
|
||||||
flushText()
|
flushText()
|
||||||
thinkingBuilder.WriteString(text.String())
|
thinkingBuilder.WriteString(text.String())
|
||||||
|
if thoughtSignatureResult.Exists() && thoughtSignatureResult.String() != "" {
|
||||||
|
thinkingSignature = thoughtSignatureResult.String()
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
flushThinking()
|
flushThinking()
|
||||||
|
|||||||
@@ -222,62 +222,61 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
|
p := 0
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
p++
|
||||||
// Tool calls -> single model content with functionCall parts
|
}
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").String()
|
|
||||||
fargs := tc.Get("function.arguments").String()
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"user","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
if tc.Get("type").String() != "function" {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
continue
|
||||||
resp := toolResponses[fid]
|
}
|
||||||
if resp == "" {
|
fid := tc.Get("id").String()
|
||||||
resp = "{}"
|
fname := tc.Get("function.name").String()
|
||||||
}
|
fargs := tc.Get("function.arguments").String()
|
||||||
// Handle non-JSON output gracefully (matches dev branch approach)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
|
||||||
if resp != "null" {
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
parsed := gjson.Parse(resp)
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
if parsed.Type == gjson.JSON {
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
||||||
toolNode, _ = sjson.SetRawBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(parsed.Raw))
|
p++
|
||||||
} else {
|
if fid != "" {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", resp)
|
fIDs = append(fIDs, fid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pp++
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"user","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
// Handle non-JSON output gracefully (matches dev branch approach)
|
||||||
|
if resp != "null" {
|
||||||
|
parsed := gjson.Parse(resp)
|
||||||
|
if parsed.Type == gjson.JSON {
|
||||||
|
toolNode, _ = sjson.SetRawBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(parsed.Raw))
|
||||||
|
} else {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,18 +360,3 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -205,52 +205,52 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
p := 0
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
// Assistant text -> single model content
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
p++
|
||||||
// Tool calls -> single model content with functionCall parts
|
}
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").String()
|
|
||||||
fargs := tc.Get("function.arguments").String()
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
if tc.Get("type").String() != "function" {
|
||||||
resp := toolResponses[fid]
|
continue
|
||||||
if resp == "" {
|
}
|
||||||
resp = "{}"
|
fid := tc.Get("id").String()
|
||||||
}
|
fname := tc.Get("function.name").String()
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
fargs := tc.Get("function.arguments").String()
|
||||||
pp++
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
||||||
|
p++
|
||||||
|
if fid != "" {
|
||||||
|
fIDs = append(fIDs, fid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,18 +334,3 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -207,15 +207,16 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
|
p := 0
|
||||||
|
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
// Assistant text -> single model content
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
|
p++
|
||||||
} else if content.IsArray() {
|
} else if content.IsArray() {
|
||||||
// Assistant multimodal content (e.g. text + image) -> single model content with parts
|
// Assistant multimodal content (e.g. text + image) -> single model content with parts
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
for _, item := range content.Array() {
|
for _, item := range content.Array() {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -237,47 +238,45 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
}
|
||||||
// Tool calls -> single model content with functionCall parts
|
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").String()
|
|
||||||
fargs := tc.Get("function.arguments").String()
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
if tc.Get("type").String() != "function" {
|
||||||
resp := toolResponses[fid]
|
continue
|
||||||
if resp == "" {
|
}
|
||||||
resp = "{}"
|
fid := tc.Get("id").String()
|
||||||
}
|
fname := tc.Get("function.name").String()
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
fargs := tc.Get("function.arguments").String()
|
||||||
pp++
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
|
||||||
|
p++
|
||||||
|
if fid != "" {
|
||||||
|
fIDs = append(fIDs, fid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,18 +362,3 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
109
test/antigravity_claude_signature_test.go
Normal file
109
test/antigravity_claude_signature_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
agclaude "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAntigravityClaudeRequest_DropsUnsignedThinkingBlocks(t *testing.T) {
|
||||||
|
model := "gemini-claude-sonnet-4-5-thinking"
|
||||||
|
input := []byte(`{
|
||||||
|
"model":"` + model + `",
|
||||||
|
"messages":[
|
||||||
|
{"role":"assistant","content":[{"type":"thinking","thinking":"secret without signature"}]},
|
||||||
|
{"role":"user","content":[{"type":"text","text":"hi"}]}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
out := agclaude.ConvertClaudeRequestToAntigravity(model, input, false)
|
||||||
|
contents := gjson.GetBytes(out, "request.contents")
|
||||||
|
if !contents.Exists() || !contents.IsArray() {
|
||||||
|
t.Fatalf("expected request.contents array, got: %s", string(out))
|
||||||
|
}
|
||||||
|
if got := len(contents.Array()); got != 1 {
|
||||||
|
t.Fatalf("expected 1 content message after dropping unsigned thinking-only assistant message, got %d: %s", got, contents.Raw)
|
||||||
|
}
|
||||||
|
if role := contents.Array()[0].Get("role").String(); role != "user" {
|
||||||
|
t.Fatalf("expected remaining message role=user, got %q", role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntigravityClaudeStreamResponse_EmitsSignatureDeltaForStandaloneSignaturePart(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"response":{
|
||||||
|
"responseId":"resp_1",
|
||||||
|
"modelVersion":"claude-sonnet-4-5-thinking",
|
||||||
|
"candidates":[{
|
||||||
|
"content":{"parts":[
|
||||||
|
{"text":"THOUGHT","thought":true},
|
||||||
|
{"thought":true,"thoughtSignature":"sig123"},
|
||||||
|
{"text":"ANSWER","thought":false}
|
||||||
|
]},
|
||||||
|
"finishReason":"STOP"
|
||||||
|
}],
|
||||||
|
"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"thoughtsTokenCount":1,"totalTokenCount":3}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var param any
|
||||||
|
chunks := agclaude.ConvertAntigravityResponseToClaude(context.Background(), "", nil, nil, raw, ¶m)
|
||||||
|
joined := strings.Join(chunks, "")
|
||||||
|
if !strings.Contains(joined, `"type":"signature_delta"`) {
|
||||||
|
t.Fatalf("expected signature_delta in stream output, got: %s", joined)
|
||||||
|
}
|
||||||
|
if !strings.Contains(joined, `"signature":"sig123"`) {
|
||||||
|
t.Fatalf("expected signature sig123 in stream output, got: %s", joined)
|
||||||
|
}
|
||||||
|
// Signature delta must be attached to the thinking content block (index 0 in this minimal stream).
|
||||||
|
if !strings.Contains(joined, `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig123"}}`) {
|
||||||
|
t.Fatalf("expected signature_delta to target thinking block index 0, got: %s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntigravityClaudeNonStreamResponse_IncludesThinkingSignature(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"response":{
|
||||||
|
"responseId":"resp_1",
|
||||||
|
"modelVersion":"claude-sonnet-4-5-thinking",
|
||||||
|
"candidates":[{
|
||||||
|
"content":{"parts":[
|
||||||
|
{"text":"THOUGHT","thought":true},
|
||||||
|
{"thought":true,"thoughtSignature":"sig123"},
|
||||||
|
{"text":"ANSWER","thought":false}
|
||||||
|
]},
|
||||||
|
"finishReason":"STOP"
|
||||||
|
}],
|
||||||
|
"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"thoughtsTokenCount":1,"totalTokenCount":3}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
out := agclaude.ConvertAntigravityResponseToClaudeNonStream(context.Background(), "", nil, nil, raw, nil)
|
||||||
|
if !gjson.Valid(out) {
|
||||||
|
t.Fatalf("expected valid JSON output, got: %s", out)
|
||||||
|
}
|
||||||
|
content := gjson.Get(out, "content")
|
||||||
|
if !content.Exists() || !content.IsArray() {
|
||||||
|
t.Fatalf("expected content array in output, got: %s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, block := range content.Array() {
|
||||||
|
if block.Get("type").String() != "thinking" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
if got := block.Get("signature").String(); got != "sig123" {
|
||||||
|
t.Fatalf("expected thinking.signature=sig123, got %q (block=%s)", got, block.Raw)
|
||||||
|
}
|
||||||
|
if got := block.Get("thinking").String(); got != "THOUGHT" {
|
||||||
|
t.Fatalf("expected thinking.thinking=THOUGHT, got %q (block=%s)", got, block.Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected a thinking block in output, got: %s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user