github / github/copilot-sdk

Claude models fail with missing finish_reason after custom tool call completion

Ouverte
#1,081 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
Java
Étoiles
10.5k
Forks
1.5k
Merge moyen
1 j 11 h
PR mergées (30 j)
128

Description

## Summary

Claude models (e.g. `claude-opus-4.6`) fail with "Failed to get response from the AI model; retried 5 times" after a custom tool call completes successfully. GPT models work fine with the same code. The issue is 100% reproducible.

## Root cause from server logs

The CLI subprocess server log (`~/.copilot/logs/process-*.log`) shows:

```
StreamingErrorRetryProcessor: Non-API error detected (retry 1): missing finish_reason for choice 0
...
StreamingErrorRetryProcessor: Non-API error detected (retry 6): missing finish_reason for choice 0
Ignoring event of kind: turn_failed
```

The server log also shows:
```
"WEBSOCKET_RESPONSES": "false",
"copilot_cli_websocket_responses": "null",
```

This suggests the SDK CLI subprocess is being routed through the **Chat Completions API** path (not the Responses API). The Copilot proxy's Claude → Chat Completions format adapter appears to not set `finish_reason` in the streaming response after tool results are submitted, causing `StreamingErrorRetryProcessor` to reject it as invalid.

This does not affect VS Code Copilot Chat or `gh copilot` CLI, which use the Responses API path where `finish_reason` is not applicable.

## Minimal repro

```typescript
import { CopilotClient, defineTool } from "@github/copilot-sdk";
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";

async function runTest(model: string) {
const configDir = path.join(os.tmpdir(), `sdk-repro-${model}-${Date.now()}`);
await fsp.mkdir(configDir, { recursive: true });

// Resolve native binary path
const cliPath = [
path.join(__dirname, "..", "node_modules", "@github", "copilot-darwin-arm64", "copilot"),
path.join(__dirname, "..", "node_modules", "@github", "copilot-linux-x64", "copilot"),
].find(c => fs.existsSync(c));
if (!cliPath) throw new Error("No native copilot binary found");

const client = new CopilotClient({ cliPath });
await client.start();

const writeTool = defineTool("write_result", {
description: "Write a result string.",
parameters: {
type: "object" as const,
properties: { answer: { type: "string" as const, description: "The answer" } },
required: ["answer"],
},
skipPermission: true,
handler: async (args: { answer: string }) => {
console.log(` [handler] Tool called with: ${args.answer}`);
return `OK: received "${args.answer}"`;
},
});

const session = await client.createSession({
model,
configDir,
enableConfigDiscovery: false,
streaming: true,
systemMessage: {
mode: "replace" as const,
content: "You are a test assistant. Call the write_result tool with your answer.",
},
tools: [writeTool],
onPermissionRequest: () => ({ kind: "approved" as const }),
infiniteSessions: { enabled: false },
} as any);

try {
await session.sendAndWait(
{ prompt: "What is 2 + 2? Call write_result with your answer." },
60_000
);
console.log(` [${model}] SUCCESS`);
} catch (err) {
console.log(` [${model}] FAILED: ${err instanceof Error ? err.message : err}`);
}

await session.disconnect().catch(() => {});
await client.deleteSession(session.sessionId).catch(() => {});
await client.stop().catch(() => {});
}

async function main() {
for (const model of ["claude-opus-4.6", "gpt-5.4"]) {
console.log(`--- ${model} ---`);
await runTest(model);
}
}
main();
```

## Output

```
--- claude-opus-4.6 ---
[handler] Tool called with: 4
[claude-opus-4.6] FAILED: Execution failed: Error: Failed to get response from the AI model;
retried 5 times (total retry wait time: 5.36 seconds) Last error: Unknown error

--- gpt-5.4 ---
[handler] Tool called with: 4
[gpt-5.4] SUCCESS
```

Note: The tool handler runs successfully and returns a result for both models. The failure happens when the SDK sends the tool result back and waits for the model's follow-up response.

## Environment

- `@github/copilot-sdk`: 0.2.2
- `@github/copilot` (CLI binary): 1.0.27 (also reproduced on 1.0.25)
- Platform: macOS arm64
- Node.js: v22.x
- Tested models: `claude-opus-4.6`, `claude-sonnet-4`, `claude-haiku-4.5` (all fail); `gpt-5.4`, `gpt-4.1` (all succeed)

## Workaround

Catch the error after `sendAndWait` and check if the tool already wrote its output. If it did, treat the turn as successful and continue. This works because the tool call itself always completes — only the post-tool-call model response fails.

## Likely fix

Either:
1. Enable the Responses API path for SDK CLI subprocess sessions (currently feature-flagged off)
2. Fix the Copilot proxy's Chat Completions adapter to set `finish_reason` correctly for Claude tool call responses

Guide de contribution

Ouvrir le guide de contribution

Piste de recherche

Start with the TypeScript minimal repro using CopilotClient, defineTool, and session.sendAndWait, then compare Claude and GPT runs. Inspect ~/.copilot/logs/process-*.log for the missing finish_reason and the WEBSOCKET_RESPONSES setting. Done means Claude tool calls complete and the follow-up response succeeds without retries or turn_failed.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
node.js, typescript
Domaine
api, cli
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Calme
Clarté
Plutôt claire
Accessibilité débutants
45/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.