BYOK Responses streaming drops apply_patch input before execution
Dieses Issue hat noch niemand übernommen.
- Vorherrschende Sprache
- Shell
- Sterne
- 11.2k
- Forks
- 1.9k
- Ø Merge
- 14 Std. 16 Min.
- Gemergte PRs (30 T.)
- 6
Beschreibung
Describe the bug
When Copilot CLI runs a streamed BYOK session using an OpenAI-compatible provider with wireApi: "responses", the model can emit a complete raw input for the built-in apply_patch tool, but the CLI invokes apply_patch with an empty argument string.
The SDK event stream contains all assistant.tool_call_delta fragments needed to reconstruct a valid patch. The corresponding tool.execution_start event then reports arguments: "", and execution fails with:
apply_patch requires a non-empty string input (the patch content).
This was reproduced with both Copilot CLI 1.0.74-0 and 1.0.78-2. The standalone reproducer below uses only the official Go SDK and the Copilot CLI; it does not depend on an application framework.
Affected version
GitHub Copilot CLI 1.0.78-2
github.com/github/copilot-sdk/go v1.0.8
go version go1.26.5 windows/amd64
Windows 11 Pro 10.0.26200, amd64
Also reproduced on Copilot CLI 1.0.74-0.
Steps to reproduce the behavior
-
Install and authenticate Copilot CLI.
-
Create an empty directory and initialize the reproducer:
go mod init example.com/copilot-apply-patch-repro go get github.com/github/copilot-sdk/go@v1.0.8 -
Save the following as
main.go:Standalone Go reproducer
package main import ( "context" "encoding/json" "fmt" "log" "os" "path/filepath" "strings" "time" copilot "github.com/github/copilot-sdk/go" ) type evidence struct { deltaCount int delta strings.Builder arguments any callID string success bool toolError string } func main() { baseURL := os.Getenv("COPILOT_REPRO_BASE_URL") apiKey := os.Getenv("COPILOT_REPRO_API_KEY") model := os.Getenv("COPILOT_REPRO_MODEL") if baseURL == "" || apiKey == "" || model == "" { log.Fatal("COPILOT_REPRO_BASE_URL, COPILOT_REPRO_API_KEY, and COPILOT_REPRO_MODEL are required") } wd, err := os.Getwd() if err != nil { log.Fatal(err) } target := filepath.Join(wd, "target.txt") if err := os.WriteFile(target, []byte("ORIGINAL_STREAMING_PROBE\n"), 0o600); err != nil { log.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() client := copilot.NewClient(nil) if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: model, ReasoningEffort: "low", Streaming: copilot.Bool(true), WorkingDirectory: wd, AvailableTools: []string{"apply_patch"}, OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "openai", WireAPI: "responses", BaseURL: baseURL, APIKey: apiKey, }, SkipCustomInstructions: copilot.Bool(true), EnableConfigDiscovery: copilot.Bool(false), EnableSkills: copilot.Bool(false), }) if err != nil { log.Fatal(err) } defer session.Disconnect() var got evidence session.On(func(event copilot.SessionEvent) { switch data := event.Data.(type) { case *copilot.AssistantToolCallDeltaData: if data.ToolName != nil && *data.ToolName == "apply_patch" { got.deltaCount++ got.delta.WriteString(data.InputDelta) } case *copilot.ToolExecutionStartData: if data.ToolName == "apply_patch" { got.callID = data.ToolCallID got.arguments = data.Arguments } case *copilot.ToolExecutionCompleteData: if data.ToolCallID == got.callID { got.success = data.Success if data.Error != nil { got.toolError = data.Error.Message } } } }) prompt := fmt.Sprintf(`Call the built-in apply_patch tool exactly once. Do not call any other tool. Replace the complete line ORIGINAL_STREAMING_PROBE with PATCHED_STREAMING_PROBE in this existing file: %s Do not retry if apply_patch fails.`, filepath.ToSlash(target)) if _, err := session.SendPromptAndWait(ctx, prompt); err != nil { log.Print(err) } args, _ := json.Marshal(got.arguments) content, _ := os.ReadFile(target) fmt.Printf("delta_count=%d\ndelta=%q\narguments=%s\nsuccess=%t\nerror=%q\ntarget=%q\n", got.deltaCount, got.delta.String(), args, got.success, got.toolError, content) } -
Configure any OpenAI-compatible Responses provider and run:
$env:COPILOT_REPRO_BASE_URL = "https://your-openai-compatible-endpoint" $env:COPILOT_REPRO_API_KEY = "..." $env:COPILOT_REPRO_MODEL = "your-model" go run . -
Observe that the reconstructed delta is a valid patch while execution arguments are empty.
Actual sanitized output from 1.0.78-2:
delta_count=55
delta_length=174
delta_sha256=93917537028d251c59aedc8c2791697bf689214973906e641dfec977e3f1a15e
delta_input_json="*** Begin Patch\n*** Update File: D:\\project\\r42\\.r42\\diagnostics\\copilot-sdk-apply-patch-repro\\target.txt\n@@\n-ORIGINAL_STREAMING_PROBE\n+PATCHED_STREAMING_PROBE\n*** End Patch\n"
execution_arguments_json=""
tool_success=false
tool_error="apply_patch requires a non-empty string input (the patch content)."
target_content="ORIGINAL_STREAMING_PROBE\n"
The relevant event transition is:
assistant.tool_call_delta: 55 fragments -> complete 174-byte patch
tool.execution_start: {"toolName":"apply_patch","arguments":""}
tool.execution_complete: success=false, non-empty-string error
Expected behavior
The accumulated raw custom-tool input should be passed to the built-in apply_patch invocation. tool.execution_start.arguments should contain the patch, execution should succeed, and target.txt should contain PATCHED_STREAMING_PROBE.
Additional context
Control result
On the same machine and the same CLI version, a logged-in GitHub-hosted gpt-5.4 session succeeds with the same prompt, streaming enabled, and only apply_patch available:
copilot --model gpt-5.4 --stream on --available-tools=apply_patch --allow-all `
--no-custom-instructions --no-experimental -p "<same prompt>"
The command exits 0, reports one line added and one removed, and the file contains PATCHED_STREAMING_PROBE.
Analysis
The provider/model output is not empty: the CLI has already decoded and emitted the complete raw custom-tool input through assistant.tool_call_delta. The file path and write permission are also valid, as shown by the GitHub-hosted control run. The value becomes empty between the emitted deltas and the built-in tool execution event.
This suggests a fidelity issue in the BYOK OpenAI Responses streaming path when converting a streamed custom tool call into the built-in apply_patch invocation. I cannot determine from the public artifacts whether the fix belongs in the Responses adapter or the runtime tool dispatcher, but the event boundary above should provide a narrow regression test.
I am filing a corresponding SDK issue because this is directly observable through the public Go SDK event contract. I will cross-link it here once created.
Beitragsleitfaden
Erste Schritte
- Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
- Forke das Repository und arbeite in einem Branch.
- Öffne einen Pull Request, der die Issue-Nummer nennt.
Rechercherichtung
Führe den eigenständigen Go-Reproducer aus und untersuche den Ereignisübergang von assistant.tool_call_delta bis tool.execution_start für den Responses-Streaming-Pfad. Verfolge den Responses-Adapter und den Runtime-Tool-Dispatcher, um herauszufinden, wo die angesammelte apply_patch-Eingabe verloren geht; abgeschlossen ist die Aufgabe, wenn die Ausführungsargumente den Patch enthalten, das Tool erfolgreich ist und target.txt PATCHED_STREAMING_PROBE enthält.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- go
- Bereich
- api, cli
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Ruhig
- Klarheit
- Größtenteils klar
- Anfängerfreundlichkeit
- 55/100