mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-19 05:56:43 +08:00
- 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
114 lines
5.0 KiB
Go
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)
|
|
}
|