fix: handle empty responses from Chutes AI API

- Add content tracking in ChutesHandler.createMessage() for both DeepSeek and non-DeepSeek models
- Throw descriptive error when API returns no content
- Add test coverage for empty response scenarios
- Fixes #7322
This commit is contained in:
Roo Code 2025-08-22 15:57:46 +00:00
parent f14e6acaf0
commit 8d2a100a63
2 changed files with 141 additions and 7 deletions

View file

@ -341,7 +341,10 @@ describe("ChutesHandler", () => {
mockCreate.mockClear()
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
// Empty stream for this test
// Yield minimal content to avoid triggering the empty response error
yield {
choices: [{ delta: { content: "test" } }],
}
},
}))
@ -376,11 +379,22 @@ describe("ChutesHandler", () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
[Symbol.asyncIterator]: () => {
let called = false
return {
async next() {
if (!called) {
called = true
// Return minimal content to avoid triggering the empty response error
return {
done: false,
value: { choices: [{ delta: { content: "test" } }] },
}
}
return { done: true }
},
}
},
}
})
@ -421,4 +435,86 @@ describe("ChutesHandler", () => {
const model = handlerWithModel.getModel()
expect(model.info.temperature).toBe(0.5)
})
it("should throw an error when API returns no content", async () => {
// Mock a stream that returns no content chunks
const mockStream = {
async *[Symbol.asyncIterator]() {
// Only yield usage data, no content
yield {
choices: [{ delta: {} }],
usage: {
prompt_tokens: 100,
completion_tokens: 0,
},
}
},
}
mockCreate.mockResolvedValueOnce(mockStream)
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
const generator = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
await expect(async () => {
for await (const chunk of generator) {
chunks.push(chunk)
}
}).rejects.toThrow("Chutes API did not return any content")
// Should have yielded usage before throwing
expect(chunks).toHaveLength(1)
expect(chunks[0]).toEqual({
type: "usage",
inputTokens: 100,
outputTokens: 0,
})
})
it("should throw an error for DeepSeek R1 models when API returns no content", async () => {
const modelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
const handlerWithModel = new ChutesHandler({
apiModelId: modelId,
chutesApiKey: "test-chutes-api-key",
})
// Mock a stream that returns no content chunks
const mockStream = {
async *[Symbol.asyncIterator]() {
// Only yield usage data, no content
yield {
choices: [{ delta: {} }],
usage: {
prompt_tokens: 100,
completion_tokens: 0,
},
}
},
}
mockCreate.mockResolvedValueOnce(mockStream)
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
const generator = handlerWithModel.createMessage(systemPrompt, messages)
const chunks: any[] = []
await expect(async () => {
for await (const chunk of generator) {
chunks.push(chunk)
}
}).rejects.toThrow("Chutes API did not return any content")
// Should have yielded usage before throwing
expect(chunks).toHaveLength(1)
expect(chunks[0]).toEqual({
type: "usage",
inputTokens: 100,
outputTokens: 0,
})
})
})

View file

@ -46,6 +46,7 @@ export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let hasContent = false
if (model.id.includes("DeepSeek-R1")) {
const stream = await this.client.chat.completions.create({
@ -66,6 +67,7 @@ export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
hasContent = true
for (const processedChunk of matcher.update(delta.content)) {
yield processedChunk
}
@ -82,10 +84,46 @@ export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
// Process any remaining content
for (const processedChunk of matcher.final()) {
hasContent = true
yield processedChunk
}
// If no content was received, throw an error
if (!hasContent) {
throw new Error(
`${this.providerName} API did not return any content. This may indicate an issue with the API, model configuration, or request parameters.`,
)
}
} else {
yield* super.createMessage(systemPrompt, messages)
// For non-DeepSeek models, track content and handle empty responses
const stream = await this.createStream(systemPrompt, messages)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
hasContent = true
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
// If no content was received, throw an error
if (!hasContent) {
throw new Error(
`${this.providerName} API did not return any content. This may indicate an issue with the API, model configuration, or request parameters.`,
)
}
}
}