From 8d2a100a6367c313ab01711aa9056e3d58cc9917 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 22 Aug 2025 15:57:46 +0000 Subject: [PATCH] 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 --- src/api/providers/__tests__/chutes.spec.ts | 108 +++++++++++++++++++-- src/api/providers/chutes.ts | 40 +++++++- 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/api/providers/__tests__/chutes.spec.ts b/src/api/providers/__tests__/chutes.spec.ts index 22ac6d67e9..5ea545453e 100644 --- a/src/api/providers/__tests__/chutes.spec.ts +++ b/src/api/providers/__tests__/chutes.spec.ts @@ -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, + }) + }) }) diff --git a/src/api/providers/chutes.ts b/src/api/providers/chutes.ts index 62121bd19d..ae44c75bcc 100644 --- a/src/api/providers/chutes.ts +++ b/src/api/providers/chutes.ts @@ -46,6 +46,7 @@ export class ChutesHandler extends BaseOpenAiCompatibleProvider { 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 { 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 { // 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.`, + ) + } } }