Assistant message "refusal": null in outbound chat-completion request breaks strict OpenAI-compatible providers (e.g. Gemini)
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 55/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- node.js, typescript
- Área
- api
Línea de trabajo
Comienza con la serialización de mensajes salientes de Copilot SDK/CLI y reproduce el problema usando la sesión TypeScript mínima contra el endpoint compatible con Gemini. Inspecciona la solicitud capturada en ~/.copilot/logs/process-*.log y verifica después que el assistant message ya no contenga un null refusal field y que la segunda solicitud se complete correctamente después de un tool call.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
TL;DR
@github/[email protected] (bundled @github/[email protected]) sends an assistant message containing a literal JSON "refusal": null field in the outbound messages[] array when using a BYOK OpenAI-compatible provider. OpenAI's own API accepts null here, so nobody notices. But Google Gemini's OpenAI-compatible endpoint (generativelanguage.googleapis.com/v1beta/openai) strictly rejects it with 400 Bad Request ("Value is not a string: null"), and this error body is again swallowed by the CLI's retry logic, surfacing only CAPIError: 400 400 400 Bad Request with no indication of the real cause.
This is the same failure class as #1129 (copilot_mcp_server_name leaking into tools[]), but a different field/message, and #1129's fix does not cover this case — confirmed still failing on the latest published versions (SDK 1.0.8, CLI 1.0.73) as of 2026-07-21.
Environment
@github/copilot-sdk:1.0.8(latest stable on npm at time of testing)@github/copilot(bundled darwin-arm64 binary):1.0.73(latest stable)- Node.js:
v24.16.0 - OS: macOS 15.7.7 (Darwin 24G720)
- Provider: BYOK,
type: "openai",baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/" - Model:
gemini-2.5-flash - MCP server: local stdio MCP server (Atlassian Jira MCP), at least one tool call in the conversation
Reproduction
Minimal SDK-driven session with any BYOK OpenAI-compatible provider that does strict schema validation, once the conversation reaches a second turn after a tool call has been executed (i.e. once an assistant message with tool_calls needs to be replayed back to the model):
import { CopilotClient, approveAll } from "@github/copilot-sdk";
const client = new CopilotClient();
const session = await client.createSession({
onPermissionRequest: approveAll,
model: "gemini-2.5-flash",
provider: {
type: "openai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
apiKey: process.env.GEMINI_API_KEY!,
}
});
await session.sendAndWait({ prompt: "Please call a tool, then respond." });
Expected
A normal multi-turn chat: tool call executes, result is sent back, model produces a final answer.
Actual
The first request (before any tool call) succeeds. The second request (after the tool result is appended to history) fails:
CAPIError: 400 400 400 Bad Request
Root cause
Captured the outbound request body via debug logging (~/.copilot/logs/process-*.log, logLevel: 'debug'). The second request's messages[] array contains an assistant message shaped like:
{
"role": "assistant",
"content": "...",
"refusal": null, // ← non-standard-for-Gemini null value
"tool_calls": [ ... ]
}
Replaying the exact captured request body directly against Gemini via curl reproduces the failure:
{
"error": {
"code": 400,
"message": "Value is not a string: null",
"status": "INVALID_ARGUMENT"
}
}
Removing only the "refusal": null key from that same assistant message and re-sending the identical request returns 200 OK with a valid completion. So the non-standard null value for refusal is the sole cause.
Verified by diffing:
- Captured request as-is (with
"refusal": null) →400 - Same request with
refusalkey removed →200
Related — second, distinct bug with gemini-3.1-pro-preview
Using model gemini-3.1-pro-preview instead fails on the same kind of second-turn request with a different Gemini-side error:
{
"error": {
"code": 400,
"message": "Function call is missing a thought_signature in functionCall parts. This is required for tools to work correctly...",
"status": "INVALID_ARGUMENT"
}
}
Gemini 3.x requires the thought_signature value (returned by Gemini in extra_content.google.thought_signature on the assistant's tool_calls[].function) to be echoed back verbatim on the next request. The CLI does appear to attempt round-tripping this value (it is present in the outbound extra_content.google.thought_signature of the replayed tool_calls entry), but Gemini still rejects it — suggests either a serialization mismatch (e.g. wrapping/escaping) or the signature isn't being preserved correctly end-to-end. Not fully root-caused to the same file/line level as the refusal: null issue above, but flagging it here since it's the same "strict-provider vs. Copilot's OpenAI-compat serializer" problem class, on the same code path.
Workaround
None found yet on the client side
Related issues
- #1129 — same failure class (unknown/non-conformant field breaking strict OpenAI-compatible providers), different field, confirmed fixed for that specific field but not for this one.
- Lenguaje dominante
- TypeScript
- Estrellas
- 10.5k
- Forks
- 1.5k
- Merge medio
- 1 d 11 h
- PR fusionados (30 d)
- 81
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de github/copilot-sdk
-
Clarify SDK architecture and in-process runtime transportPosiblemente ocupada @KalebCole la tomó hace 3 días. Abiertodocumentation
Dificultad 1/5 Menos de una hora Aptitud para principiantes 92/100
github/copilot-sdk#2804 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Python ModelLimits drops max_output_tokens from model metadataPosiblemente ocupada @HDMowri la tomó hace 5 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
github/copilot-sdk#2798 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
github/copilot-sdk#2793 ·
Los mantenedores suelen responder en 1 día
-
agentic-workflows
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
github/copilot-sdk#2782 ·
Los mantenedores suelen responder en 1 día
-
Rust: subagent lifecycle hooks are logged as unknownPosiblemente ocupada @hackberry-lab la tomó hace 7 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
github/copilot-sdk#2781 ·
Los mantenedores suelen responder en 1 día
Todos los issues de github/copilot-sdk
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
OHDSI/Data2Evidence#3496 ·
Los mantenedores suelen responder en 2 días
-
TaskSecret.vue: replace explicit `any` with real typesPosiblemente ocupada @prayas-bit la tomó hoy. Abiertoarea/frontend good first issue kind/cooldown
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
kestra-io/kestra#20352 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Bouncer: long messages are re-split at 350 bytes, breaks echo-message reconciliationPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día