package interactions import ( "bytes" "context" "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" ) func TestConvertInteractionsRequestToAntigravityWithToolMessagesDirect(t *testing.T) { out := ConvertInteractionsRequestToAntigravity("antigravity-test", []byte(`{"model":"antigravity-test","system_instruction":"be brief","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]},{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_1","result":{"ok":true}}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}]}`), false) if got := gjson.GetBytes(out, "request.systemInstruction.parts.0.text").String(); got != "be brief" { t.Fatalf("request.systemInstruction.parts.0.text = %q, want be brief. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.contents.0.parts.0.text").String(); got != "hi" { t.Fatalf("request.contents.0.parts.0.text = %q, want hi. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.contents.1.parts.0.functionCall.name").String(); got != "lookup" { t.Fatalf("functionCall.name = %q, want lookup. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.contents.2.parts.0.functionResponse.name").String(); got != "lookup" { t.Fatalf("functionResponse.name = %q, want lookup. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.name").String(); got != "lookup" { t.Fatalf("request.tools.0.functionDeclarations.0.name = %q, want lookup. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.parametersJsonSchema.properties.q.type").String(); got != "string" { t.Fatalf("tool parameters schema was not preserved. Output: %s", string(out)) } } func TestConvertInteractionsRequestToAntigravityPreservesGenerationConfig(t *testing.T) { out := ConvertInteractionsRequestToAntigravity("antigravity-test", []byte(`{"model":"antigravity-test","input":"hi","generation_config":{"max_output_tokens":16,"top_p":0.8,"tool_choice":"auto","thinking_level":"high","thinking_summaries":"auto"},"reasoning":{"summary":"auto"},"stream":true}`), true) if gjson.GetBytes(out, "input").Exists() { t.Fatalf("raw interactions input exists in translated request. Output: %s", string(out)) } for _, path := range []string{ "request.generationConfig.toolChoice", "request.generationConfig.thinkingLevel", "request.generationConfig.thinkingSummaries", } { if gjson.GetBytes(out, path).Exists() { t.Fatalf("%s exists, want omitted. Output: %s", path, string(out)) } } if got := gjson.GetBytes(out, "request.stream").Bool(); !got { t.Fatalf("request.stream = false, want true. Output: %s", string(out)) } if got := gjson.GetBytes(out, "request.contents.0.parts.0.text").String(); got != "hi" { t.Fatalf("request.contents.0.parts.0.text = %q, want hi. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.generationConfig.maxOutputTokens").Int(); got != 16 { t.Fatalf("request.generationConfig.maxOutputTokens = %d, want 16. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.generationConfig.topP").Float(); got != 0.8 { t.Fatalf("request.generationConfig.topP = %v, want 0.8. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel").String(); got != "high" { t.Fatalf("request.generationConfig.thinkingConfig.thinkingLevel = %q, want high. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts").Bool(); !got { t.Fatalf("request.generationConfig.thinkingConfig.includeThoughts = false, want true. Output: %s", string(out)) } if got := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.mode").String(); got != "AUTO" { t.Fatalf("request.toolConfig.functionCallingConfig.mode = %q, want AUTO. Output: %s", got, string(out)) } } func TestConvertInteractionsReasoningToAntigravityKeepsSummaryIndependent(t *testing.T) { tests := []struct { name string reasoning string want bool wantExists bool }{ {name: "effort only leaves summaries unspecified", reasoning: `{"effort":"high"}`}, {name: "explicit auto enables summaries", reasoning: `{"effort":"high","summary":"auto"}`, want: true, wantExists: true}, {name: "explicit none disables summaries", reasoning: `{"effort":"high","summary":"none"}`, wantExists: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := []byte(`{"model":"antigravity-test","input":"hi","reasoning":` + test.reasoning + `}`) out := ConvertInteractionsRequestToAntigravity("antigravity-test", body, false) if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel").String(); got != "high" { t.Fatalf("thinkingLevel = %q, want high. Output: %s", got, out) } includeThoughts := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts") if includeThoughts.Exists() != test.wantExists { t.Fatalf("includeThoughts exists = %v, want %v. Output: %s", includeThoughts.Exists(), test.wantExists, out) } if test.wantExists && includeThoughts.Bool() != test.want { t.Fatalf("includeThoughts = %v, want %v. Output: %s", includeThoughts.Bool(), test.want, out) } }) } } func TestConvertAntigravityResponseToInteractionsNonStream(t *testing.T) { raw := []byte(`{"response":{"responseId":"resp_1","candidates":[{"content":{"role":"model","parts":[{"text":"ok"},{"functionCall":{"name":"lookup","id":"call_1","args":{"q":"x"}}}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":2,"totalTokenCount":5}}}`) out := ConvertAntigravityResponseToInteractionsNonStream(context.Background(), "antigravity-test", nil, nil, raw, nil) if got := gjson.GetBytes(out, "steps.0.content.0.text").String(); got != "ok" { t.Fatalf("steps.0.content.0.text = %q, want ok. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "steps.1.type").String(); got != "function_call" { t.Fatalf("steps.1.type = %q, want function_call. Output: %s", got, string(out)) } if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 5 { t.Fatalf("usage.total_tokens = %d, want 5. Output: %s", got, string(out)) } } func TestConvertAntigravityResponseToInteractionsStream(t *testing.T) { ctx := context.WithValue(context.Background(), "alt", "") var param any events := ConvertAntigravityResponseToInteractions(ctx, "antigravity-test", nil, nil, []byte(`data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]}}]}}`), ¶m) payload := findAntigravityInteractionsEventPayload(events, "step.delta") if len(payload) == 0 { t.Fatalf("step.delta event not found: %q", events) } if got := gjson.GetBytes(payload, "delta.text").String(); got != "ok" { t.Fatalf("delta.text = %q, want ok. Payload: %s", got, string(payload)) } } func TestConvertAntigravityResponseToInteractionsStreamFunctionCallStartHasCallID(t *testing.T) { var param any events := ConvertAntigravityResponseToInteractions(context.Background(), "antigravity-test", nil, nil, []byte(`data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"lookup","id":"call_1","args":{"q":"x"}}}]}}]}}`), ¶m) payload := findAntigravityInteractionsEventPayload(events, "step.start") if got := gjson.GetBytes(payload, "step.call_id").String(); got != "call_1" { t.Fatalf("step.call_id = %q, want call_1. Payload: %s", got, string(payload)) } } func TestConvertInteractionsRequestToAntigravityDeduplicatesAndDisambiguatesTools(t *testing.T) { first := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build" second := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build_logs" inputJSON := []byte(`{ "input":[ {"type":"function_call","name":"` + second + `","call_id":"call_1","arguments":{}}, {"type":"function_result","name":"` + second + `","call_id":"call_1","result":{}} ], "tools":[ {"functionDeclarations":[{"name":"lookup"},{"name":"` + first + `"}]}, {"function_declarations":[{"name":"lookup"},{"name":"` + second + `"}]} ], "tool_choice":{"type":"function","function":{"name":"` + second + `"}} }`) out := ConvertInteractionsRequestToAntigravity("antigravity-test", inputJSON, false) declarations := gjson.GetBytes(out, "request.tools.0.functionDeclarations").Array() if len(declarations) != 3 { t.Fatalf("declaration count = %d, want 3. Output: %s", len(declarations), out) } firstMapped := declarations[1].Get("name").String() secondMapped := declarations[2].Get("name").String() if firstMapped == secondMapped || len(secondMapped) > 64 { t.Fatalf("collision names = %q and %q, want distinct names <= 64 chars", firstMapped, secondMapped) } if got := gjson.GetBytes(out, "request.contents.0.parts.0.functionCall.name").String(); got != secondMapped { t.Fatalf("functionCall.name = %q, want %q. Output: %s", got, secondMapped, out) } if got := gjson.GetBytes(out, "request.contents.1.parts.0.functionResponse.name").String(); got != secondMapped { t.Fatalf("functionResponse.name = %q, want %q. Output: %s", got, secondMapped, out) } if got := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames.0").String(); got != secondMapped { t.Fatalf("allowedFunctionNames.0 = %q, want %q. Output: %s", got, secondMapped, out) } } func TestConvertInteractionsRequestToAntigravityPreservesNameMappingWhitespace(t *testing.T) { inputJSON := []byte(`{ "input":[{"type":"function_call","name":" read/file ","arguments":{}}], "tools":[{"type":"function","name":" read/file ","parameters":{"type":"object"}}], "tool_choice":{"type":"function","function":{"name":" read/file "}} }`) out := ConvertInteractionsRequestToAntigravity("antigravity-test", inputJSON, false) declarationName := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.name").String() callName := gjson.GetBytes(out, "request.contents.0.parts.0.functionCall.name").String() allowedName := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames.0").String() if declarationName == "" || callName != declarationName || allowedName != declarationName { t.Fatalf("mapped names declaration=%q call=%q allowed=%q. Output: %s", declarationName, callName, allowedName, out) } } func TestConvertAntigravityResponseToInteractionsRestoresDisambiguatedName(t *testing.T) { first := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build" second := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build_logs" original := []byte(`{"tools":[{"name":"` + first + `"},{"name":"` + second + `"}]}`) mapped := util.SanitizedFunctionNameMap(original)[second] raw := []byte(`{"response":{"candidates":[{"content":{"parts":[{"functionCall":{"name":"` + mapped + `","args":{}}}]}}]}}`) out := ConvertAntigravityResponseToInteractionsNonStream(context.Background(), "antigravity-test", original, nil, raw, nil) if got := gjson.GetBytes(out, "steps.0.name").String(); got != second { t.Fatalf("function call name = %q, want %q. Output: %s", got, second, out) } } func findAntigravityInteractionsEventPayload(events [][]byte, eventType string) []byte { prefix := []byte("data:") for _, event := range events { for _, line := range bytes.Split(event, []byte("\n")) { line = bytes.TrimSpace(line) if !bytes.HasPrefix(line, prefix) { continue } payload := bytes.TrimSpace(line[len(prefix):]) if gjson.GetBytes(payload, "type").String() == eventType || gjson.GetBytes(payload, "event_type").String() == eventType { return payload } } } return nil }