mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
fix: resolve GitHub Copilot 400 error for Claude models in Cursor IDE (#220)
- github.js: add sanitizeMessages() to convert unsupported content types (tool_use, tool_result, thinking) to plain text before sending to GitHub /chat/completions endpoint - openai-responses.js: skip pushing message blocks with empty content (e.g. assistant messages that only contain tool_calls) - providerModels.js: revert targetFormat changes (not needed with sanitize fix) Fixes: https://github.com/decolua/9router/issues/219
This commit is contained in:
@@ -34,15 +34,59 @@ export class GithubExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
// Sanitize messages for GitHub Copilot /chat/completions endpoint.
|
||||
// The endpoint only accepts 'text' and 'image_url' content part types.
|
||||
// Tool-related content (tool_use, tool_result, thinking) must be serialized as text.
|
||||
sanitizeMessagesForChatCompletions(body) {
|
||||
if (!body?.messages) return body;
|
||||
|
||||
const sanitized = { ...body };
|
||||
sanitized.messages = body.messages.map(msg => {
|
||||
// assistant messages with only tool_calls have content: null — leave as-is
|
||||
if (!msg.content) return msg;
|
||||
|
||||
// String content is always fine
|
||||
if (typeof msg.content === "string") return msg;
|
||||
|
||||
// Array content: filter/convert unsupported part types
|
||||
if (Array.isArray(msg.content)) {
|
||||
const cleanContent = msg.content
|
||||
.map(part => {
|
||||
if (part.type === "text") return part;
|
||||
if (part.type === "image_url") return part;
|
||||
// Serialize tool_use, tool_result, thinking, etc. as text
|
||||
const text = part.text || part.content || JSON.stringify(part);
|
||||
return { type: "text", text: typeof text === "string" ? text : JSON.stringify(text) };
|
||||
})
|
||||
.filter(part => part.text !== ""); // remove empty text parts
|
||||
|
||||
// If all content was stripped (e.g. only tool_result with no text), drop content
|
||||
return { ...msg, content: cleanContent.length > 0 ? cleanContent : null };
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
async execute(options) {
|
||||
const { model, log } = options;
|
||||
|
||||
// Only use /responses for models that are explicitly known to need it (e.g. gpt codex models)
|
||||
if (this.knownCodexModels.has(model)) {
|
||||
log?.debug("GITHUB", `Using cached /responses route for ${model}`);
|
||||
return this.executeWithResponsesEndpoint(options);
|
||||
}
|
||||
|
||||
const result = await super.execute(options);
|
||||
// Sanitize messages before sending to /chat/completions
|
||||
// This handles Claude models on GitHub Copilot which reject non-text/image_url content types
|
||||
const sanitizedOptions = {
|
||||
...options,
|
||||
body: this.sanitizeMessagesForChatCompletions(options.body)
|
||||
};
|
||||
|
||||
const result = await super.execute(sanitizedOptions);
|
||||
|
||||
if (result.response.status === HTTP_STATUS.BAD_REQUEST) {
|
||||
const errorBody = await result.response.clone().text();
|
||||
|
||||
@@ -175,16 +175,23 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map(c => {
|
||||
if (c.type === "text") return { type: contentType, text: c.text };
|
||||
if (c.type === "image_url") return { type: contentType, text: "[Image content]" };
|
||||
return c;
|
||||
if (c.type === "image_url") return { type: "image_url", image_url: c.image_url };
|
||||
// Serialize any unknown type (tool_use, tool_result, thinking, etc.) as text
|
||||
const text = c.text || c.content || JSON.stringify(c);
|
||||
return { type: contentType, text: typeof text === "string" ? text : JSON.stringify(text) };
|
||||
})
|
||||
: [];
|
||||
|
||||
result.input.push({
|
||||
type: "message",
|
||||
role: msg.role,
|
||||
content
|
||||
});
|
||||
// Only push a message block if content is non-empty.
|
||||
// Assistant messages with only tool_calls have content: null — skip the
|
||||
// message block in that case; the tool_calls are pushed separately below.
|
||||
if (content.length > 0) {
|
||||
result.input.push({
|
||||
type: "message",
|
||||
role: msg.role,
|
||||
content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tool calls
|
||||
|
||||
Reference in New Issue
Block a user