Files
CLIProxyAPI/internal/runtime/executor/gemini_vertex_executor_test.go
Luis Pater 3ebee06516 fix(interactions): bridge apply_patch tool calls in responses translation
- Handle apply_patch custom tool call streaming deltas and snapshots for Responses API translation
- Track and resolve step indexes, function call identities, and error states across stream events
- Propagate apply_patch tool input errors and upstream failure states in stream and non-stream responses
2026-10-02 03:49:51 +08:00

114 lines
5.0 KiB
Go

package executor
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
"github.com/tidwall/gjson"
)
const executorPatchRequest = `{"tools":[{"type":"namespace","name":"functions","tools":[{"type":"custom","name":"apply_patch","format":{"type":"grammar","definition":"start: patch"}}]}],"input":"patch a file"}`
const executorPatchResponse = `{"responseId":"patch","candidates":[{"content":{"parts":[{"functionCall":{"name":"functions__apply_patch","args":{"input":" *** Begin Patch\n*** End Patch\n "}}}]},"finishReason":"STOP"}]}`
const executorPatchInput = " *** Begin Patch\n*** End Patch\n "
func assertExecutorPatchDeclaration(t *testing.T, payload []byte) {
t.Helper()
declaration := gjson.GetBytes(payload, "tools.0.functionDeclarations.0")
if declaration.Get("name").String() != "functions__apply_patch" || !strings.Contains(declaration.Get("description").String(), "*** Begin Patch") || !strings.Contains(declaration.Get("description").String(), "start: patch") || declaration.Get("parametersJsonSchema.properties.input.type").String() != "string" || !declaration.Get("parametersJsonSchema.additionalProperties").Exists() || declaration.Get("parametersJsonSchema.additionalProperties").Bool() {
t.Fatalf("missing standard patch declaration: %s", payload)
}
}
func assertExecutorPatchOutput(t *testing.T, payload []byte) {
t.Helper()
item := gjson.GetBytes(payload, `output.#(type=="custom_tool_call")`)
if item.Get("type").String() != "custom_tool_call" || item.Get("input").String() != executorPatchInput || item.Get("name").String() != "apply_patch" || item.Get("namespace").String() != "functions" {
t.Fatalf("wrong patch output: %s", payload)
}
}
func assertExecutorPatchStream(t *testing.T, chunks <-chan cliproxyexecutor.StreamChunk, acknowledgeSnapshot ...func()) {
t.Helper()
acknowledged := len(acknowledgeSnapshot) == 0
var delta strings.Builder
var done, item, final gjson.Result
for chunk := range chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
for _, line := range strings.Split(string(chunk.Payload), "\n") {
if !strings.HasPrefix(line, "data:") {
continue
}
ev := gjson.Parse(strings.TrimSpace(strings.TrimPrefix(line, "data:")))
switch ev.Get("type").String() {
case "response.output_text.delta":
if !acknowledged {
acknowledged = true
acknowledgeSnapshot[0]()
}
case "response.custom_tool_call_input.delta":
if !acknowledged {
t.Fatal("fabricated patch progress before upstream snapshot")
}
delta.WriteString(ev.Get("delta").String())
case "response.custom_tool_call_input.done":
done = ev
case "response.output_item.done":
if ev.Get("item.type").String() == "custom_tool_call" {
item = ev.Get("item")
}
case "response.completed":
final = ev.Get("response")
}
}
}
if !acknowledged || delta.String() != executorPatchInput || done.Get("input").String() != executorPatchInput || item.Get("input").String() != executorPatchInput || done.Get("item_id").String() != item.Get("id").String() || done.Get("call_id").String() != item.Get("call_id").String() {
t.Fatalf("inconsistent stream delta=%q done=%s item=%s", delta.String(), done.Raw, item.Raw)
}
assertExecutorPatchOutput(t, []byte(final.Raw))
}
func TestGeminiVertexApplyPatchExecutorReuse(t *testing.T) {
requests := make(chan []byte, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, errReadAll := io.ReadAll(r.Body)
if errReadAll != nil {
t.Error(errReadAll)
w.WriteHeader(500)
return
}
requests <- body
if strings.Contains(r.URL.Path, "streamGenerateContent") {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "data: "+executorPatchResponse+"\n\n")
} else {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, executorPatchResponse)
}
}))
defer server.Close()
exec := NewGeminiVertexExecutor(&config.Config{})
auth := &cliproxyauth.Auth{ID: "vertex-patch", Provider: "vertex", Attributes: map[string]string{"api_key": "test-key", "base_url": server.URL}}
req := cliproxyexecutor.Request{Model: "gemini-3.1-pro-preview", Payload: []byte(executorPatchRequest)}
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: req.Payload}
response, errExecute := exec.Execute(context.Background(), auth, req, opts)
if errExecute != nil {
t.Fatal(errExecute)
}
assertExecutorPatchDeclaration(t, <-requests)
assertExecutorPatchOutput(t, response.Payload)
stream, errExecuteStream := exec.ExecuteStream(context.Background(), auth, req, opts)
if errExecuteStream != nil {
t.Fatal(errExecuteStream)
}
assertExecutorPatchDeclaration(t, <-requests)
assertExecutorPatchStream(t, stream.Chunks)
}