BYOK Responses streaming drops apply_patch input before execution
- Langage dominant
- Shell
- Étoiles
- 11.2k
- Forks
- 1.9k
- Merge moyen
- 14 h 16 min
- PR mergées (30 j)
- 6
Description
## 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:
```text
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
```text
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
1. Install and authenticate Copilot CLI.
2. Create an empty directory and initialize the reproducer:
```powershell
go mod init example.com/copilot-apply-patch-repro
go get github.com/github/copilot-sdk/go@v1.0.8
```
3. Save the following as `main.go`:
Standalone Go reproducer
```go
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)
}
```
4. Configure any OpenAI-compatible Responses provider and run:
```powershell
$env:COPILOT_REPRO_BASE_URL = "https://your-openai-compatible-endpoint"
$env:COPILOT_REPRO_API_KEY = "..."
$env:COPILOT_REPRO_MODEL = "your-model"
go run .
```
5. Observe that the reconstructed delta is a valid patch while execution arguments are empty.
Actual sanitized output from `1.0.78-2`:
```text
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:
```text
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:
```powershell
copilot --model gpt-5.4 --stream on --available-tools=apply_patch --allow-all `
--no-custom-instructions --no-experimental -p ""
```
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.
Guide de contribution
Ouvrir le guide de contribution
Piste de recherche
Exécutez le reproducteur autonome en Go et inspectez la transition d’événements de assistant.tool_call_delta à tool.execution_start pour le chemin de streaming de Responses. Suivez l’adaptateur Responses et le dispatcher d’outils du runtime pour trouver où l’entrée apply_patch accumulée est perdue ; la tâche est terminée lorsque les arguments d’exécution contiennent le patch, que l’outil réussit et que target.txt contient PATCHED_STREAMING_PROBE.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- go
- Domaine
- api, cli
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Calme
- Clarté
- Plutôt claire
- Accessibilité débutants
- 55/100