fix: prevent crash when streaming chunks have null choices array (#9105)

This commit is contained in:
Daniel 2025-11-07 13:18:38 -05:00 committed by GitHub
parent ca64953061
commit fc2147ab4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 7 deletions

View file

@ -279,6 +279,34 @@ describe("MiniMaxHandler", () => {
undefined,
)
})
it("should handle streaming chunks with null choices array", async () => {
const testContent = "Content after null choices"
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vitest
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: null },
})
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: { content: testContent } }] },
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
})
})
describe("Model Configuration", () => {

View file

@ -116,7 +116,15 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Check for provider-specific error responses (e.g., MiniMax base_resp)
const chunkAny = chunk as any
if (chunkAny.base_resp?.status_code && chunkAny.base_resp.status_code !== 0) {
throw new Error(
`${this.providerName} API Error (${chunkAny.base_resp.status_code}): ${chunkAny.base_resp.status_msg || "Unknown error"}`,
)
}
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
@ -155,7 +163,15 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
messages: [{ role: "user", content: prompt }],
})
return response.choices[0]?.message.content || ""
// Check for provider-specific error responses (e.g., MiniMax base_resp)
const responseAny = response as any
if (responseAny.base_resp?.status_code && responseAny.base_resp.status_code !== 0) {
throw new Error(
`${this.providerName} API Error (${responseAny.base_resp.status_code}): ${responseAny.base_resp.status_msg || "Unknown error"}`,
)
}
return response.choices?.[0]?.message.content || ""
} catch (error) {
throw handleOpenAIError(error, this.providerName)
}

View file

@ -191,7 +191,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
let lastUsage
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta ?? {}
const delta = chunk.choices?.[0]?.delta ?? {}
if (delta.content) {
for (const chunk of matcher.update(delta.content)) {
@ -242,7 +242,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
yield {
type: "text",
text: response.choices[0]?.message.content || "",
text: response.choices?.[0]?.message.content || "",
}
yield this.processUsageMetrics(response.usage, modelInfo)
@ -290,7 +290,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
throw handleOpenAIError(error, this.providerName)
}
return response.choices[0]?.message.content || ""
return response.choices?.[0]?.message.content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`${this.providerName} completion error: ${error.message}`)
@ -373,7 +373,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
yield {
type: "text",
text: response.choices[0]?.message.content || "",
text: response.choices?.[0]?.message.content || "",
}
yield this.processUsageMetrics(response.usage)
}
@ -381,7 +381,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",