github / github/copilot-cli

BYOK Responses streaming drops apply_patch input before execution

未關閉
#4,327 1 則留言 2 個 reaction 已指派 0 人 在 GitHub 檢視
area:models area:tools
主要語言
Shell
星號
11.2k
分支
1.9k
平均合併
14 小時 16 分鐘
30 天內合併 PR
6

描述

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

貢獻指南

開啟貢獻指南

研究方向

執行獨立的 Go 重現程式,並檢查 Responses 串流路徑中從 assistant.tool_call_delta 到 tool.execution_start 的事件轉換。追蹤 Responses 配接器與執行階段工具分派器,找出累積的 apply_patch 輸入在哪裡遺失;當執行引數包含該修補程式、工具成功執行,且 target.txt 包含 PATCHED_STREAMING_PROBE 時,即表示完成。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
go
領域
api, cli
Issue 類型
缺陷
難度
4/5
預估耗時
3-5 天
活躍度
冷清
描述清晰度
基本清楚
新手友好度
55/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。