github / github/copilot-cli

BYOK Responses streaming drops apply_patch input before execution

Đang mở
#4,327 1 bình luận 2 reaction 0 người được giao Xem trên GitHub
area:models area:tools
Ngôn ngữ chính
Shell
Star
11.2k
Fork
1.9k
Merge trung bình
14 giờ 16 phút
Pull request đã merge (30 ngày)
6

Mô tả

## 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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Chạy trình tái hiện Go độc lập và kiểm tra quá trình chuyển tiếp sự kiện từ assistant.tool_call_delta đến tool.execution_start cho đường dẫn streaming của Responses. Truy vết bộ điều hợp Responses và bộ điều phối công cụ của runtime để tìm nơi dữ liệu đầu vào apply_patch đã được tích lũy bị mất; được xem là hoàn tất khi các đối số thực thi chứa bản vá, công cụ thực thi thành công và target.txt chứa PATCHED_STREAMING_PROBE.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
go
Lĩnh vực
api, cli
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
55/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.