fix: handle GitHub Copilot gpt-4.1 model ID response bug

- Added debug logging to track VS Code LM API responses for Copilot models
- Implemented detection for when model ID is returned instead of actual content
- Skip model ID chunks and continue processing valid content
- Throw descriptive error when only model ID is returned with no valid content
- Added comprehensive tests for the workaround

Fixes #6105
This commit is contained in:
Roo Code 2025-07-23 11:51:45 +00:00
parent 9956cc1f48
commit 9c8b9e4221
2 changed files with 184 additions and 0 deletions

View file

@ -300,4 +300,152 @@ describe("VsCodeLmHandler", () => {
await expect(promise).rejects.toThrow("VSCode LM completion error: Completion failed")
})
})
describe("GitHub Copilot gpt-4.1 bug handling", () => {
it("should skip chunks that contain only the model ID", async () => {
const mockModel = {
...mockLanguageModelChat,
id: "copilot/gpt-4.1",
vendor: "copilot",
family: "gpt-4.1",
}
;(vscode.lm.selectChatModels as Mock).mockResolvedValueOnce([mockModel])
const systemPrompt = "You are a helpful assistant"
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user" as const,
content: "Hello",
},
]
// Mock a response that includes the model ID as a chunk followed by actual content
mockModel.sendRequest.mockResolvedValueOnce({
stream: (async function* () {
yield new vscode.LanguageModelTextPart("copilot/gpt-4.1") // Bug: model ID as response
yield new vscode.LanguageModelTextPart("Hello! How can I help you?") // Actual content
return
})(),
text: (async function* () {
yield "copilot/gpt-4.1"
yield "Hello! How can I help you?"
return
})(),
})
// Override the default client with our test client
handler["client"] = mockModel
const stream = handler.createMessage(systemPrompt, messages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Should only have the actual content and usage, not the model ID
expect(chunks).toHaveLength(2)
expect(chunks[0]).toEqual({
type: "text",
text: "Hello! How can I help you?",
})
expect(chunks[1]).toMatchObject({
type: "usage",
inputTokens: expect.any(Number),
outputTokens: expect.any(Number),
})
})
it("should throw error when only model ID is returned", async () => {
const mockModel = {
...mockLanguageModelChat,
id: "copilot/gpt-4.1",
vendor: "copilot",
family: "gpt-4.1",
}
;(vscode.lm.selectChatModels as Mock).mockResolvedValueOnce([mockModel])
const systemPrompt = "You are a helpful assistant"
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user" as const,
content: "Hello",
},
]
// Mock a response that only contains the model ID
mockModel.sendRequest.mockResolvedValueOnce({
stream: (async function* () {
yield new vscode.LanguageModelTextPart("copilot/gpt-4.1")
return
})(),
text: (async function* () {
yield "copilot/gpt-4.1"
return
})(),
})
// Override the default client with our test client
handler["client"] = mockModel
const stream = handler.createMessage(systemPrompt, messages)
// Collect all chunks and expect an error
await expect(async () => {
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
}).rejects.toThrow(
'The VS Code Language Model API returned only the model ID "copilot/gpt-4.1" instead of generating a response',
)
})
it("should handle variations of model ID patterns", async () => {
const mockModel = {
...mockLanguageModelChat,
id: "github/gpt-4.1",
vendor: "github",
family: "gpt-4.1",
}
;(vscode.lm.selectChatModels as Mock).mockResolvedValueOnce([mockModel])
const systemPrompt = "You are a helpful assistant"
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user" as const,
content: "Hello",
},
]
// Mock a response with different model ID variations
mockModel.sendRequest.mockResolvedValueOnce({
stream: (async function* () {
yield new vscode.LanguageModelTextPart(" GitHub/gpt-4.1 ") // With spaces and different case
yield new vscode.LanguageModelTextPart("Actual response content")
return
})(),
text: (async function* () {
yield " GitHub/gpt-4.1 "
yield "Actual response content"
return
})(),
})
// Override the default client with our test client
handler["client"] = mockModel
const stream = handler.createMessage(systemPrompt, messages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Should skip the model ID and only return actual content
expect(chunks).toHaveLength(2)
expect(chunks[0]).toEqual({
type: "text",
text: "Actual response content",
})
})
})
})

View file

@ -363,6 +363,8 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
let accumulatedText: string = ""
let hasValidContent = false
let detectedModelIdAsResponse = false
try {
// Create the response stream with minimal required options
@ -382,12 +384,39 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
// Consume the stream and handle both text and tool call chunks
for await (const chunk of response.stream) {
if (chunk instanceof vscode.LanguageModelTextPart) {
// Debug logging for GitHub Copilot gpt-4.1 issue
if (client.id && (client.id.includes("gpt-4.1") || client.id.includes("copilot"))) {
console.debug("Roo Code <Language Model API>: Processing chunk for model:", client.id, {
chunkType: typeof chunk,
chunkValue: chunk.value,
chunkValueType: typeof chunk.value,
chunkValueLength: chunk.value?.length,
isModelId: chunk.value === client.id || chunk.value === `${client.vendor}/${client.family}`,
})
}
// Validate text part value
if (typeof chunk.value !== "string") {
console.warn("Roo Code <Language Model API>: Invalid text part value received:", chunk.value)
continue
}
// Check if the chunk value is just the model ID (GitHub Copilot gpt-4.1 bug)
const modelIdPattern = /^(copilot|github)\/gpt-4\.1$/i
const trimmedValue = chunk.value.trim()
if (modelIdPattern.test(trimmedValue) || trimmedValue === client.id) {
console.warn(
"Roo Code <Language Model API>: Detected model ID as response content, this appears to be a VS Code LM API bug:",
chunk.value,
)
detectedModelIdAsResponse = true
// Skip this chunk as it's not actual content
continue
}
// If we get here, we have valid content
hasValidContent = true
accumulatedText += chunk.value
yield {
type: "text",
@ -444,6 +473,13 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
}
}
// If we only received the model ID and no valid content, throw an error
if (detectedModelIdAsResponse && !hasValidContent) {
throw new Error(
`Roo Code <Language Model API>: The VS Code Language Model API returned only the model ID "${client.id}" instead of generating a response. This is a known issue with GitHub Copilot's gpt-4.1 model. Please try using a different model or wait for a fix from the VS Code/GitHub Copilot team.`,
)
}
// Count tokens in the accumulated text after stream completion
const totalOutputTokens: number = await this.internalCountTokens(accumulatedText)