Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

289 changes: 289 additions & 0 deletions src/api/providers/__tests__/mimo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,115 @@ describe("MimoHandler", () => {
expect(params.messages[0].content).toBe("You are a helpful assistant")
expect(params.messages[1].role).toBe("user")
})

it("should emit tool_call_end when stream ends without finish_reason (MiMo premature termination)", async () => {
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Write a large file" }] },
]

mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
function: { name: "write_to_file", arguments: "" },
},
],
},
},
],
}
yield {
choices: [
{
delta: {
tool_calls: [
{ index: 0, function: { arguments: '{"path":"test.txt","content":"' } },
],
},
},
],
}
// MiMo premature termination: choices: [] without finish_reason
yield {
choices: [],
usage: {
prompt_tokens: 527,
completion_tokens: 100,
total_tokens: 627,
completion_tokens_details: { reasoning_tokens: 45 },
},
}
},
}))

const chunks: any[] = []
const stream = handler.createMessage("System", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

// Post-loop cleanup in MimoHandler.createMessage should emit tool_call_end
const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end")
expect(toolCallEndChunks).toHaveLength(1)
expect(toolCallEndChunks[0].id).toBe("call_1")
})

it("should emit tool_call_end when finish_reason is 'length' with active tool calls", async () => {
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Write a large file" }] },
]

mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_length",
function: { name: "write_to_file", arguments: "" },
},
],
},
},
],
}
yield {
choices: [
{
delta: {
tool_calls: [{ index: 0, function: { arguments: '{"path":"test.txt"}' } }],
},
},
],
}
// Standard OpenAI token limit behavior
yield {
choices: [{ delta: {}, finish_reason: "length" }],
usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
}
},
}))

const chunks: any[] = []
const stream = handler.createMessage("System", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

// processToolCalls should emit tool_call_end for finish_reason: "length"
const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end")
expect(toolCallEndChunks).toHaveLength(1)
expect(toolCallEndChunks[0].id).toBe("call_length")
})
})

describe("completePrompt", () => {
Expand Down Expand Up @@ -950,4 +1059,184 @@ describe("MimoHandler", () => {
expect(params.model).toBe("mimo-v2.5")
})
})

describe("advanced streaming scenarios", () => {
it("should handle stream with multiple text chunks concatenated", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Hello" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: { content: " world" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: { content: "!" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
usage: { prompt_tokens: 10, completion_tokens: 3, total_tokens: 13 },
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Hi" }] },
]

const chunks: any[] = []
const stream = handler.createMessage("System prompt", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

const textChunks = chunks.filter((c) => c.type === "text")
expect(textChunks).toHaveLength(3)
expect(textChunks.map((c: any) => c.text).join("")).toBe("Hello world!")
})

it("should handle stream with both reasoning and tool calls", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { reasoning_content: "Let me think" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: { reasoning_content: " about this" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: { content: "I'll read it" }, index: 0 }],
usage: null,
}
yield {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_read",
function: { name: "read_file", arguments: '{"path":"test.ts"}' },
},
],
},
index: 0,
},
],
usage: null,
}
yield {
choices: [{ delta: {}, index: 0, finish_reason: "tool_calls" }],
usage: { prompt_tokens: 20, completion_tokens: 15, total_tokens: 35 },
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Read test.ts" }] },
]

const chunks: any[] = []
const stream = handler.createMessage("System prompt", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
expect(reasoningChunks).toHaveLength(2)
expect(reasoningChunks.map((c: any) => c.text).join("")).toBe("Let me think about this")

const textChunks = chunks.filter((c) => c.type === "text")
expect(textChunks).toHaveLength(1)
expect(textChunks[0].text).toBe("I'll read it")

const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")
expect(toolChunks).toHaveLength(1)
expect(toolChunks[0].id).toBe("call_read")
expect(toolChunks[0].name).toBe("read_file")

// finish_reason "tool_calls" flushes the active tool call as a tool_call_end event.
const endChunks = chunks.filter((c) => c.type === "tool_call_end")
expect(endChunks).toHaveLength(1)
expect(endChunks[0].id).toBe("call_read")
})

it("should handle stream with no usage in final chunk", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Done" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
usage: null,
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Hello" }] },
]

const chunks: any[] = []
const stream = handler.createMessage("System prompt", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(0)

const textChunks = chunks.filter((c) => c.type === "text")
expect(textChunks).toHaveLength(1)
expect(textChunks[0].text).toBe("Done")
})

it("should handle stream with zero cache tokens in usage", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Hi" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
usage: {
prompt_tokens: 50,
completion_tokens: 10,
total_tokens: 60,
prompt_tokens_details: {
cache_write_tokens: 0,
cached_tokens: 0,
},
},
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Hello" }] },
]

const chunks: any[] = []
const stream = handler.createMessage("System prompt", messages)
for await (const chunk of stream) {
chunks.push(chunk)
}

const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].inputTokens).toBe(50)
expect(usageChunks[0].outputTokens).toBe(10)
// Handler uses `|| undefined` so zero-valued cache tokens are omitted
expect(usageChunks[0].cacheWriteTokens).toBeUndefined()
expect(usageChunks[0].cacheReadTokens).toBeUndefined()
})
})
})
Loading
Loading