diff --git a/packages/evals/src/cli/runTaskInCli.ts b/packages/evals/src/cli/runTaskInCli.ts index 031136f8ea..03b3ad4f70 100644 --- a/packages/evals/src/cli/runTaskInCli.ts +++ b/packages/evals/src/cli/runTaskInCli.ts @@ -264,7 +264,7 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R if (rooTaskId && !isClientDisconnected) { logger.info("cancelling task") - client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId }) + client.sendCommand({ commandName: TaskCommandName.CancelTask }) await new Promise((resolve) => setTimeout(resolve, 5_000)) } @@ -289,7 +289,7 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R if (rooTaskId && !isClientDisconnected) { logger.info("closing task") - client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId }) + client.sendCommand({ commandName: TaskCommandName.CloseTask }) await new Promise((resolve) => setTimeout(resolve, 2_000)) } diff --git a/packages/evals/src/cli/runTaskInVscode.ts b/packages/evals/src/cli/runTaskInVscode.ts index 07b7bd7e29..5819f8d405 100644 --- a/packages/evals/src/cli/runTaskInVscode.ts +++ b/packages/evals/src/cli/runTaskInVscode.ts @@ -270,7 +270,7 @@ export const runTaskInVscode = async ({ run, task, publish, logger, jobToken }: if (rooTaskId && !isClientDisconnected) { logger.info("cancelling task") - client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId }) + client.sendCommand({ commandName: TaskCommandName.CancelTask }) await new Promise((resolve) => setTimeout(resolve, 5_000)) // Allow some time for the task to cancel. } @@ -289,7 +289,7 @@ export const runTaskInVscode = async ({ run, task, publish, logger, jobToken }: if (rooTaskId && !isClientDisconnected) { logger.info("closing task") - client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId }) + client.sendCommand({ commandName: TaskCommandName.CloseTask }) await new Promise((resolve) => setTimeout(resolve, 2_000)) // Allow some time for the window to close. } diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index b40e25a259..1a6a79ffe6 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.106.0", + "version": "1.107.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/__tests__/ipc.test.ts b/packages/types/src/__tests__/ipc.test.ts index dd0f7c5cdc..856b3f2cc1 100644 --- a/packages/types/src/__tests__/ipc.test.ts +++ b/packages/types/src/__tests__/ipc.test.ts @@ -27,7 +27,7 @@ describe("IPC Types", () => { const result = taskCommandSchema.safeParse(resumeTaskCommand) expect(result.success).toBe(true) - if (result.success) { + if (result.success && result.data.commandName === TaskCommandName.ResumeTask) { expect(result.data.commandName).toBe("ResumeTask") expect(result.data.data).toBe("non-existent-task-id") } @@ -45,7 +45,7 @@ describe("IPC Types", () => { const result = taskCommandSchema.safeParse(resumeTaskCommand) expect(result.success).toBe(true) - if (result.success) { + if (result.success && result.data.commandName === TaskCommandName.ResumeTask) { expect(result.data.commandName).toBe("ResumeTask") expect(result.data.data).toBe("task-123") } diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts index 5743ac2940..d4a05f8e3e 100644 --- a/packages/types/src/events.ts +++ b/packages/types/src/events.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { clineMessageSchema, tokenUsageSchema } from "./message.js" +import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js" import { toolNamesSchema, toolUsageSchema } from "./tool.js" /** @@ -35,6 +35,7 @@ export enum RooCodeEventName { TaskModeSwitched = "taskModeSwitched", TaskAskResponded = "taskAskResponded", TaskUserMessage = "taskUserMessage", + QueuedMessagesUpdated = "queuedMessagesUpdated", // Task Analytics TaskTokenUsageUpdated = "taskTokenUsageUpdated", @@ -100,6 +101,7 @@ export const rooCodeEventsSchema = z.object({ [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), [RooCodeEventName.TaskUserMessage]: z.tuple([z.string()]), + [RooCodeEventName.QueuedMessagesUpdated]: z.tuple([z.string(), z.array(queuedMessageSchema)]), [RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]), [RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema]), @@ -217,6 +219,11 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [ payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded], taskId: z.number().optional(), }), + z.object({ + eventName: z.literal(RooCodeEventName.QueuedMessagesUpdated), + payload: rooCodeEventsSchema.shape[RooCodeEventName.QueuedMessagesUpdated], + taskId: z.number().optional(), + }), // Task Analytics z.object({ diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 4e1b1ac355..9f6d2de04d 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -64,11 +64,9 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ }), z.object({ commandName: z.literal(TaskCommandName.CancelTask), - data: z.string(), }), z.object({ commandName: z.literal(TaskCommandName.CloseTask), - data: z.string(), }), z.object({ commandName: z.literal(TaskCommandName.ResumeTask), diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index ce764424c2..9332bb17c5 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -157,6 +157,7 @@ export type TaskEvents = { [RooCodeEventName.TaskModeSwitched]: [taskId: string, mode: string] [RooCodeEventName.TaskAskResponded]: [] [RooCodeEventName.TaskUserMessage]: [taskId: string] + [RooCodeEventName.QueuedMessagesUpdated]: [taskId: string, messages: QueuedMessage[]] // Task Analytics [RooCodeEventName.TaskToolFailed]: [taskId: string, tool: ToolName, error: string] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1186d9b1e..d6c895fde3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -762,6 +762,9 @@ importers: '@ai-sdk/groq': specifier: ^3.0.19 version: 3.0.19(zod@3.25.76) + '@ai-sdk/mistral': + specifier: ^3.0.0 + version: 3.0.16(zod@3.25.76) '@anthropic-ai/bedrock-sdk': specifier: ^0.10.2 version: 0.10.4 @@ -1484,6 +1487,12 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/mistral@3.0.16': + resolution: {integrity: sha512-8I/gxXJwghaDLbQQHMBwd61WxYz/PaFUFlG8I38daNYj5qRTMmQ5V10Idi6GJJC0wWEqQkal31lidm9+Y+u6TQ==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/openai-compatible@1.0.31': resolution: {integrity: sha512-znBvaVHM0M6yWNerIEy3hR+O8ZK2sPcE7e2cxfb6kYLEX3k//JH5VDnRnajseVofg7LXtTCFFdjsB7WLf1BdeQ==} engines: {node: '>=18'} @@ -10625,6 +10634,12 @@ snapshots: '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/mistral@3.0.16(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/openai-compatible@1.0.31(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.1 diff --git a/src/api/providers/__tests__/mistral.spec.ts b/src/api/providers/__tests__/mistral.spec.ts index d0bfa9570f..0cac881dff 100644 --- a/src/api/providers/__tests__/mistral.spec.ts +++ b/src/api/providers/__tests__/mistral.spec.ts @@ -1,59 +1,36 @@ -// Mock TelemetryService - must come before other imports -const mockCaptureException = vi.hoisted(() => vi.fn()) -vi.mock("@roo-code/telemetry", () => ({ - TelemetryService: { - instance: { - captureException: mockCaptureException, - }, - }, +// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls +const { mockStreamText, mockGenerateText, mockCreateMistral } = vi.hoisted(() => ({ + mockStreamText: vi.fn(), + mockGenerateText: vi.fn(), + mockCreateMistral: vi.fn(() => { + // Return a function that returns a mock language model + return vi.fn(() => ({ + modelId: "codestral-latest", + provider: "mistral", + })) + }), })) -// Mock Mistral client - must come before other imports -const mockCreate = vi.fn() -const mockComplete = vi.fn() -vi.mock("@mistralai/mistralai", () => { +vi.mock("ai", async (importOriginal) => { + const actual = await importOriginal() return { - Mistral: vi.fn().mockImplementation(() => ({ - chat: { - stream: mockCreate.mockImplementation(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { content: "Test response" }, - index: 0, - }, - ], - }, - } - }, - } - return stream - }), - complete: mockComplete.mockImplementation(async (_options) => { - return { - choices: [ - { - message: { - content: "Test response", - }, - }, - ], - } - }), - }, - })), + ...actual, + streamText: mockStreamText, + generateText: mockGenerateText, } }) +vi.mock("@ai-sdk/mistral", () => ({ + createMistral: mockCreateMistral, +})) + import type { Anthropic } from "@anthropic-ai/sdk" -import type OpenAI from "openai" -import { MistralHandler } from "../mistral" + +import { mistralDefaultModelId, mistralModels, type MistralModelId } from "@roo-code/types" + import type { ApiHandlerOptions } from "../../../shared/api" -import type { ApiHandlerCreateMessageMetadata } from "../../index" -import type { ApiStreamTextChunk, ApiStreamReasoningChunk, ApiStreamToolCallPartialChunk } from "../../transform/stream" + +import { MistralHandler } from "../mistral" describe("MistralHandler", () => { let handler: MistralHandler @@ -61,15 +38,11 @@ describe("MistralHandler", () => { beforeEach(() => { mockOptions = { - apiModelId: "codestral-latest", // Update to match the actual model ID mistralApiKey: "test-api-key", - includeMaxTokens: true, - modelTemperature: 0, + apiModelId: "codestral-latest" as MistralModelId, } handler = new MistralHandler(mockOptions) - mockCreate.mockClear() - mockComplete.mockClear() - mockCaptureException.mockClear() + vi.clearAllMocks() }) describe("constructor", () => { @@ -78,32 +51,53 @@ describe("MistralHandler", () => { expect(handler.getModel().id).toBe(mockOptions.apiModelId) }) - it("should throw error if API key is missing", () => { - expect(() => { - new MistralHandler({ - ...mockOptions, - mistralApiKey: undefined, - }) - }).toThrow("Mistral API key is required") - }) - - it("should use custom base URL if provided", () => { - const customBaseUrl = "https://custom.mistral.ai/v1" - const handlerWithCustomUrl = new MistralHandler({ + it("should use default model ID if not provided", () => { + const handlerWithoutModel = new MistralHandler({ ...mockOptions, - mistralCodestralUrl: customBaseUrl, + apiModelId: undefined, }) - expect(handlerWithCustomUrl).toBeInstanceOf(MistralHandler) + expect(handlerWithoutModel.getModel().id).toBe(mistralDefaultModelId) }) }) describe("getModel", () => { - it("should return correct model info", () => { + it("should return model info for valid model ID", () => { const model = handler.getModel() expect(model.id).toBe(mockOptions.apiModelId) expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8192) + expect(model.info.contextWindow).toBe(256_000) + expect(model.info.supportsImages).toBe(false) expect(model.info.supportsPromptCache).toBe(false) }) + + it("should return provided model ID with default model info if model does not exist", () => { + const handlerWithInvalidModel = new MistralHandler({ + ...mockOptions, + apiModelId: "invalid-model", + }) + const model = handlerWithInvalidModel.getModel() + expect(model.id).toBe("invalid-model") // Returns provided ID + expect(model.info).toBeDefined() + // Should have the same base properties as default model + expect(model.info.contextWindow).toBe(mistralModels[mistralDefaultModelId].contextWindow) + }) + + it("should return default model if no model ID is provided", () => { + const handlerWithoutModel = new MistralHandler({ + ...mockOptions, + apiModelId: undefined, + }) + const model = handlerWithoutModel.getModel() + expect(model.id).toBe(mistralDefaultModelId) + expect(model.info).toBeDefined() + }) + + it("should include model parameters from getModelParams", () => { + const model = handler.getModel() + expect(model).toHaveProperty("temperature") + expect(model).toHaveProperty("maxTokens") + }) }) describe("createMessage", () => { @@ -111,396 +105,446 @@ describe("MistralHandler", () => { const messages: Anthropic.Messages.MessageParam[] = [ { role: "user", - content: [{ type: "text", text: "Hello!" }], + content: [ + { + type: "text" as const, + text: "Hello!", + }, + ], }, ] - it("should create message successfully", async () => { - const iterator = handler.createMessage(systemPrompt, messages) - const result = await iterator.next() + it("should handle streaming responses", async () => { + // Mock the fullStream async generator + async function* mockFullStream() { + yield { type: "text-delta", text: "Test response" } + } + + // Mock usage promise + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + }) + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response") + }) + + it("should include usage information", async () => { + async function* mockFullStream() { + yield { type: "text-delta", text: "Test response" } + } + + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + }) + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + }) - expect(mockCreate).toHaveBeenCalledWith( + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks.length).toBeGreaterThan(0) + expect(usageChunks[0].inputTokens).toBe(10) + expect(usageChunks[0].outputTokens).toBe(5) + }) + + it("should handle reasoning content in streaming responses", async () => { + // Mock the fullStream async generator with reasoning content + async function* mockFullStream() { + yield { type: "reasoning", text: "Let me think about this..." } + yield { type: "reasoning", text: " I'll analyze step by step." } + yield { type: "text-delta", text: "Test response" } + } + + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + details: { + reasoningTokens: 15, + }, + }) + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should have reasoning chunks + const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") + expect(reasoningChunks.length).toBe(2) + expect(reasoningChunks[0].text).toBe("Let me think about this...") + expect(reasoningChunks[1].text).toBe(" I'll analyze step by step.") + + // Should also have text chunks + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks.length).toBe(1) + expect(textChunks[0].text).toBe("Test response") + }) + }) + + describe("completePrompt", () => { + it("should complete a prompt using generateText", async () => { + mockGenerateText.mockResolvedValue({ + text: "Test completion", + }) + + const result = await handler.completePrompt("Test prompt") + + expect(result).toBe("Test completion") + expect(mockGenerateText).toHaveBeenCalledWith( expect.objectContaining({ - model: mockOptions.apiModelId, - messages: expect.any(Array), - maxTokens: expect.any(Number), - temperature: 0, - // Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS) - tools: expect.any(Array), - toolChoice: "any", + prompt: "Test prompt", }), ) - - expect(result.value).toBeDefined() - expect(result.done).toBe(false) }) + }) - it("should handle streaming response correctly", async () => { - const iterator = handler.createMessage(systemPrompt, messages) - const results: ApiStreamTextChunk[] = [] - - for await (const chunk of iterator) { - if ("text" in chunk) { - results.push(chunk as ApiStreamTextChunk) + describe("processUsageMetrics", () => { + it("should correctly process usage metrics", () => { + // We need to access the protected method, so we'll create a test subclass + class TestMistralHandler extends MistralHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) } } - expect(results.length).toBeGreaterThan(0) - expect(results[0].text).toBe("Test response") - }) + const testHandler = new TestMistralHandler(mockOptions) - it("should handle errors gracefully", async () => { - mockCreate.mockRejectedValueOnce(new Error("API Error")) - await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error") + const usage = { + inputTokens: 100, + outputTokens: 50, + details: { + cachedInputTokens: 20, + reasoningTokens: 30, + }, + } + + const result = testHandler.testProcessUsageMetrics(usage) + + expect(result.type).toBe("usage") + expect(result.inputTokens).toBe(100) + expect(result.outputTokens).toBe(50) + expect(result.cacheReadTokens).toBe(20) + expect(result.reasoningTokens).toBe(30) }) - it("should handle thinking content as reasoning chunks", async () => { - // Mock stream with thinking content matching new SDK structure - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - content: [ - { - type: "thinking", - thinking: [{ type: "text", text: "Let me think about this..." }], - }, - { type: "text", text: "Here's the answer" }, - ], - }, - index: 0, - }, - ], - }, - } - }, + it("should handle missing cache metrics gracefully", () => { + class TestMistralHandler extends MistralHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) } - return stream - }) + } + + const testHandler = new TestMistralHandler(mockOptions) + + const usage = { + inputTokens: 100, + outputTokens: 50, + } + + const result = testHandler.testProcessUsageMetrics(usage) - const iterator = handler.createMessage(systemPrompt, messages) - const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] + expect(result.type).toBe("usage") + expect(result.inputTokens).toBe(100) + expect(result.outputTokens).toBe(50) + expect(result.cacheReadTokens).toBeUndefined() + expect(result.reasoningTokens).toBeUndefined() + }) + }) - for await (const chunk of iterator) { - if ("text" in chunk) { - results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk) + describe("getMaxOutputTokens", () => { + it("should return maxTokens from model info", () => { + class TestMistralHandler extends MistralHandler { + public testGetMaxOutputTokens() { + return this.getMaxOutputTokens() } } - expect(results).toHaveLength(2) - expect(results[0]).toEqual({ type: "reasoning", text: "Let me think about this..." }) - expect(results[1]).toEqual({ type: "text", text: "Here's the answer" }) + const testHandler = new TestMistralHandler(mockOptions) + const result = testHandler.testGetMaxOutputTokens() + + // codestral-latest maxTokens is 8192 + expect(result).toBe(8192) }) - it("should handle mixed content arrays correctly", async () => { - // Mock stream with mixed content matching new SDK structure - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - content: [ - { type: "text", text: "First text" }, - { - type: "thinking", - thinking: [{ type: "text", text: "Some reasoning" }], - }, - { type: "text", text: "Second text" }, - ], - }, - index: 0, - }, - ], - }, - } - }, + it("should use modelMaxTokens when provided", () => { + class TestMistralHandler extends MistralHandler { + public testGetMaxOutputTokens() { + return this.getMaxOutputTokens() } - return stream + } + + const customMaxTokens = 5000 + const testHandler = new TestMistralHandler({ + ...mockOptions, + modelMaxTokens: customMaxTokens, }) - const iterator = handler.createMessage(systemPrompt, messages) - const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] + const result = testHandler.testGetMaxOutputTokens() + expect(result).toBe(customMaxTokens) + }) - for await (const chunk of iterator) { - if ("text" in chunk) { - results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk) + it("should fall back to modelInfo.maxTokens when modelMaxTokens is not provided", () => { + class TestMistralHandler extends MistralHandler { + public testGetMaxOutputTokens() { + return this.getMaxOutputTokens() } } - expect(results).toHaveLength(3) - expect(results[0]).toEqual({ type: "text", text: "First text" }) - expect(results[1]).toEqual({ type: "reasoning", text: "Some reasoning" }) - expect(results[2]).toEqual({ type: "text", text: "Second text" }) + const testHandler = new TestMistralHandler(mockOptions) + const result = testHandler.testGetMaxOutputTokens() + + // codestral-latest has maxTokens of 8192 + expect(result).toBe(8192) }) }) - describe("native tool calling", () => { + describe("tool handling", () => { const systemPrompt = "You are a helpful assistant." const messages: Anthropic.Messages.MessageParam[] = [ { role: "user", - content: [{ type: "text", text: "What's the weather?" }], + content: [{ type: "text" as const, text: "Hello!" }], }, ] - const mockTools: OpenAI.Chat.ChatCompletionTool[] = [ - { - type: "function", - function: { - name: "get_weather", - description: "Get the current weather", - parameters: { - type: "object", - properties: { - location: { type: "string" }, + it("should handle tool calls in streaming", async () => { + async function* mockFullStream() { + yield { + type: "tool-input-start", + id: "tool-call-1", + toolName: "read_file", + } + yield { + type: "tool-input-delta", + id: "tool-call-1", + delta: '{"path":"test.ts"}', + } + yield { + type: "tool-input-end", + id: "tool-call-1", + } + } + + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + }) + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + }) + + const stream = handler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { path: { type: "string" } }, + required: ["path"], + }, }, - required: ["location"], }, - }, - }, - ] + ], + }) - it("should include tools in request by default (native is default)", async () => { - const metadata: ApiHandlerCreateMessageMetadata = { - taskId: "test-task", - tools: mockTools, + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) } - const iterator = handler.createMessage(systemPrompt, messages, metadata) - await iterator.next() + const toolCallStartChunks = chunks.filter((c) => c.type === "tool_call_start") + const toolCallDeltaChunks = chunks.filter((c) => c.type === "tool_call_delta") + const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end") - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - tools: expect.arrayContaining([ - expect.objectContaining({ - type: "function", - function: expect.objectContaining({ - name: "get_weather", - description: "Get the current weather", - parameters: expect.any(Object), - }), - }), - ]), - toolChoice: "any", - }), - ) + expect(toolCallStartChunks.length).toBe(1) + expect(toolCallStartChunks[0].id).toBe("tool-call-1") + expect(toolCallStartChunks[0].name).toBe("read_file") + + expect(toolCallDeltaChunks.length).toBe(1) + expect(toolCallDeltaChunks[0].delta).toBe('{"path":"test.ts"}') + + expect(toolCallEndChunks.length).toBe(1) + expect(toolCallEndChunks[0].id).toBe("tool-call-1") }) - it("should always include tools in request (tools are always present after PR #10841)", async () => { - const metadata: ApiHandlerCreateMessageMetadata = { - taskId: "test-task", + it("should ignore tool-call events to prevent duplicate tools in UI", async () => { + // tool-call events are intentionally ignored because tool-input-start/delta/end + // already provide complete tool call information. Emitting tool-call would cause + // duplicate tools in the UI for AI SDK providers. + async function* mockFullStream() { + yield { + type: "tool-call", + toolCallId: "tool-call-1", + toolName: "read_file", + input: { path: "test.ts" }, + } } - const iterator = handler.createMessage(systemPrompt, messages, metadata) - await iterator.next() + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + }) - // Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS) - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - tools: expect.any(Array), - toolChoice: "any", - }), - ) - }) + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + }) - it("should handle tool calls in streaming response", async () => { - // Mock stream with tool calls - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - toolCalls: [ - { - id: "call_123", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"New York"}', - }, - }, - ], - }, - index: 0, - }, - ], + const stream = handler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { path: { type: "string" } }, + required: ["path"], }, - } + }, }, - } - return stream + ], }) - const metadata: ApiHandlerCreateMessageMetadata = { - taskId: "test-task", - tools: mockTools, + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) } - const iterator = handler.createMessage(systemPrompt, messages, metadata) - const results: ApiStreamToolCallPartialChunk[] = [] + // tool-call events are ignored, so no tool_call chunks should be emitted + const toolCallChunks = chunks.filter((c) => c.type === "tool_call") + expect(toolCallChunks.length).toBe(0) + }) + }) - for await (const chunk of iterator) { - if (chunk.type === "tool_call_partial") { - results.push(chunk) + describe("mapToolChoice", () => { + it("should handle string tool choices", () => { + class TestMistralHandler extends MistralHandler { + public testMapToolChoice(toolChoice: any) { + return this.mapToolChoice(toolChoice) } } - expect(results).toHaveLength(1) - expect(results[0]).toEqual({ - type: "tool_call_partial", - index: 0, - id: "call_123", - name: "get_weather", - arguments: '{"location":"New York"}', - }) + const testHandler = new TestMistralHandler(mockOptions) + + expect(testHandler.testMapToolChoice("auto")).toBe("auto") + expect(testHandler.testMapToolChoice("none")).toBe("none") + expect(testHandler.testMapToolChoice("required")).toBe("required") + expect(testHandler.testMapToolChoice("any")).toBe("required") + expect(testHandler.testMapToolChoice("unknown")).toBe("auto") }) - it("should handle multiple tool calls in a single response", async () => { - // Mock stream with multiple tool calls - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - toolCalls: [ - { - id: "call_1", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"NYC"}', - }, - }, - { - id: "call_2", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"LA"}', - }, - }, - ], - }, - index: 0, - }, - ], - }, - } - }, + it("should handle object tool choice with function name", () => { + class TestMistralHandler extends MistralHandler { + public testMapToolChoice(toolChoice: any) { + return this.mapToolChoice(toolChoice) } - return stream - }) - - const metadata: ApiHandlerCreateMessageMetadata = { - taskId: "test-task", - tools: mockTools, } - const iterator = handler.createMessage(systemPrompt, messages, metadata) - const results: ApiStreamToolCallPartialChunk[] = [] + const testHandler = new TestMistralHandler(mockOptions) - for await (const chunk of iterator) { - if (chunk.type === "tool_call_partial") { - results.push(chunk) + const result = testHandler.testMapToolChoice({ + type: "function", + function: { name: "my_tool" }, + }) + + expect(result).toEqual({ type: "tool", toolName: "my_tool" }) + }) + + it("should return undefined for null or undefined", () => { + class TestMistralHandler extends MistralHandler { + public testMapToolChoice(toolChoice: any) { + return this.mapToolChoice(toolChoice) } } - expect(results).toHaveLength(2) - expect(results[0]).toEqual({ - type: "tool_call_partial", - index: 0, - id: "call_1", - name: "get_weather", - arguments: '{"location":"NYC"}', - }) - expect(results[1]).toEqual({ - type: "tool_call_partial", - index: 1, - id: "call_2", - name: "get_weather", - arguments: '{"location":"LA"}', - }) + const testHandler = new TestMistralHandler(mockOptions) + + expect(testHandler.testMapToolChoice(null)).toBeUndefined() + expect(testHandler.testMapToolChoice(undefined)).toBeUndefined() }) + }) - it("should always set toolChoice to 'any' when tools are provided", async () => { - // Even if tool_choice is provided in metadata, we override it to "any" - const metadata: ApiHandlerCreateMessageMetadata = { - taskId: "test-task", - tools: mockTools, - tool_choice: "auto", // This should be ignored - } + describe("Codestral URL handling", () => { + beforeEach(() => { + mockCreateMistral.mockClear() + }) - const iterator = handler.createMessage(systemPrompt, messages, metadata) - await iterator.next() + it("should use default Codestral URL for codestral models", () => { + new MistralHandler({ + ...mockOptions, + apiModelId: "codestral-latest", + }) - expect(mockCreate).toHaveBeenCalledWith( + expect(mockCreateMistral).toHaveBeenCalledWith( expect.objectContaining({ - toolChoice: "any", + baseURL: "https://codestral.mistral.ai/v1", }), ) }) - }) - describe("completePrompt", () => { - it("should complete prompt successfully", async () => { - const prompt = "Test prompt" - const result = await handler.completePrompt(prompt) - - expect(mockComplete).toHaveBeenCalledWith( - { - model: mockOptions.apiModelId, - messages: [{ role: "user", content: prompt }], - temperature: 0, - }, - { - fetchOptions: { - signal: undefined, - }, - }, - ) + it("should use custom Codestral URL when provided", () => { + new MistralHandler({ + ...mockOptions, + apiModelId: "codestral-latest", + mistralCodestralUrl: "https://custom.codestral.url/v1", + }) - expect(result).toBe("Test response") + expect(mockCreateMistral).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://custom.codestral.url/v1", + }), + ) }) - it("should filter out thinking content in completePrompt", async () => { - mockComplete.mockImplementationOnce(async (_options) => { - return { - choices: [ - { - message: { - content: [ - { type: "thinking", text: "Let me think..." }, - { type: "text", text: "Answer part 1" }, - { type: "text", text: "Answer part 2" }, - ], - }, - }, - ], - } + it("should use default Mistral URL for non-codestral models", () => { + new MistralHandler({ + ...mockOptions, + apiModelId: "mistral-large-latest", }) - const prompt = "Test prompt" - const result = await handler.completePrompt(prompt) - - expect(result).toBe("Answer part 1Answer part 2") - }) - - it("should handle errors in completePrompt", async () => { - mockComplete.mockRejectedValueOnce(new Error("API Error")) - await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Mistral completion error: API Error") + expect(mockCreateMistral).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.mistral.ai/v1", + }), + ) }) }) }) diff --git a/src/api/providers/gemini-cli.ts b/src/api/providers/gemini-cli.ts index 88a5c30acc..c2b7f0aece 100644 --- a/src/api/providers/gemini-cli.ts +++ b/src/api/providers/gemini-cli.ts @@ -16,8 +16,14 @@ import { getModelParams } from "../transform/model-params" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { BaseProvider } from "./base-provider" -import { getGeminiCliLiteToolGuide } from "../../core/prompts/tools/lite-descriptions" +import { + getGeminiCliLiteToolGuide, + liteRetryPrompt, + liteToolContractPrompt, + liteToolJudgePrompt, +} from "../../core/prompts/tools/lite-descriptions" import { TagMatcher } from "../../utils/tag-matcher" +import { findLastIndex } from "../../shared/array" // OAuth2 Configuration (from Cline implementation) const OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" @@ -272,7 +278,31 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa } } } + const toolCallTag = "tool_call" // Convert messages to Gemini format + // const lastUserMsg = ((msg) => msg.role === "user") + const lastUserMsgIndex = findLastIndex(messages, (msg) => msg.role === "user") + const lastUserMsg = lastUserMsgIndex > -1 ? messages[lastUserMsgIndex] : undefined + if (lastUserMsg) { + if (Array.isArray(lastUserMsg.content)) { + // const = lastUserMsg.content.find() + const noToolsUsed = lastUserMsg.content.find( + (block) => + block.type === "text" && + block.text.includes("You did not use a tool in your previous response"), + ) + if (noToolsUsed && noToolsUsed.type === "text") { + noToolsUsed.text = `${liteRetryPrompt(toolCallTag)}\n${liteToolJudgePrompt(metadata?.allToolNames)}\n${liteToolContractPrompt(toolCallTag)}` + } + } else { + // if (lastUserMsg.content.includes('You did not use a tool in your previous response')) { + // // messages.splice(lastUserMsgIndex, 1, { + // // }) + // } + // You did not use a tool in your previous response + } + } + const contents = messages.map((message) => convertAnthropicMessageToGemini(message, { includeThoughtSignatures: false, @@ -280,7 +310,6 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa isGeminiCli: true, }), ) - // Prepare request body for Code Assist API - matching Cline's structure const requestBody: any = { model: model, @@ -289,13 +318,28 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa contents: [ { role: "user", - parts: [{ text: systemInstruction + "\n\n" + getGeminiCliLiteToolGuide() }], + parts: [{ text: systemInstruction }], + }, + { + role: "user", + parts: [ + { + text: + liteToolJudgePrompt(metadata?.allToolNames) + + "\n" + + liteToolContractPrompt(toolCallTag), + }, + ], }, ...contents, - // { - // role: "user", - // parts: [{ text: getGeminiCliLiteToolGuide() }], - // }, + { + role: "user", + parts: [ + { + text: getGeminiCliLiteToolGuide(metadata?.allToolNames), + }, + ], + }, ], generationConfig: { temperature: this.options.modelTemperature ?? 0.7, @@ -312,7 +356,7 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa try { // Call Code Assist streaming endpoint using OAuth2Client const response = await this.authClient.request({ - url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:streamGenerateContent`, + url: `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent`, method: "POST", params: { alt: "sse" }, headers: { @@ -325,7 +369,7 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa // Process the SSE stream let lastUsageMetadata: any = undefined const toolCallMatcher = new TagMatcher( - "tool_call", + toolCallTag, (chunk) => { return { type: chunk.matched ? "fake_tool_call" : "text", diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index 0b0248686d..ea37b8b457 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -1,229 +1,199 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { Mistral } from "@mistralai/mistralai" -import OpenAI from "openai" +import { createMistral } from "@ai-sdk/mistral" +import { streamText, generateText, ToolSet, LanguageModel } from "ai" import { - type MistralModelId, - mistralDefaultModelId, mistralModels, + mistralDefaultModelId, + type MistralModelId, + type ModelInfo, MISTRAL_DEFAULT_TEMPERATURE, - ApiProviderError, } from "@roo-code/types" -import { TelemetryService } from "@roo-code/telemetry" -import { ApiHandlerOptions } from "../../shared/api" - -import { convertToMistralMessages } from "../transform/mistral-format" -import { ApiStream } from "../transform/stream" -import { handleProviderError } from "./utils/error-handler" +import type { ApiHandlerOptions } from "../../shared/api" +import { + convertToAiSdkMessages, + convertToolsForAiSdk, + processAiSdkStreamPart, + handleAiSdkError, +} from "../transform/ai-sdk" +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { getModelParams } from "../transform/model-params" + +import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -// Type helper to handle thinking chunks from Mistral API -// The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union -type ContentChunkWithThinking = { - type: string - text?: string - thinking?: Array<{ type: string; text?: string }> -} - -// Type for Mistral tool calls in stream delta -type MistralToolCall = { - id?: string - type?: string - function?: { - name?: string - arguments?: string - } -} - -// Type for Mistral tool definition - matches Mistral SDK Tool type -type MistralTool = { - type: "function" - function: { - name: string - description?: string - parameters: Record - } -} - +/** + * Mistral provider using the dedicated @ai-sdk/mistral package. + * Provides access to Mistral AI models including Codestral, Mistral Large, and more. + */ export class MistralHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions - private client: Mistral - private readonly providerName = "Mistral" + protected provider: ReturnType constructor(options: ApiHandlerOptions) { super() + this.options = options - if (!options.mistralApiKey) { - throw new Error("Mistral API key is required") - } + const modelId = options.apiModelId ?? mistralDefaultModelId - // Set default model ID if not provided. - const apiModelId = options.apiModelId || mistralDefaultModelId - this.options = { ...options, apiModelId } + // Determine the base URL based on the model (Codestral uses a different endpoint) + const baseURL = modelId.startsWith("codestral-") + ? options.mistralCodestralUrl || "https://codestral.mistral.ai/v1" + : "https://api.mistral.ai/v1" - this.client = new Mistral({ - serverURL: apiModelId.startsWith("codestral-") - ? this.options.mistralCodestralUrl || "https://codestral.mistral.ai" - : "https://api.mistral.ai", - apiKey: this.options.mistralApiKey, + // Create the Mistral provider using AI SDK + this.provider = createMistral({ + apiKey: options.mistralApiKey ?? "not-provided", + baseURL, + headers: DEFAULT_HEADERS, }) } - override async *createMessage( - systemPrompt: string, - messages: Anthropic.Messages.MessageParam[], - metadata?: ApiHandlerCreateMessageMetadata, - ): ApiStream { - const { id: model, info, maxTokens, temperature } = this.getModel() - - // Build request options - const requestOptions: { - model: string - messages: ReturnType - maxTokens: number - temperature: number - tools?: MistralTool[] - toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } } - } = { - model, - messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)], - maxTokens: maxTokens ?? info.maxTokens, - temperature, - } - - requestOptions.tools = this.convertToolsForMistral(metadata?.tools ?? []) - // Always use "any" to require tool use - requestOptions.toolChoice = "any" + override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } { + const id = (this.options.apiModelId ?? mistralDefaultModelId) as MistralModelId + const info = mistralModels[id as keyof typeof mistralModels] || mistralModels[mistralDefaultModelId] + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + return { id, info, ...params } + } - // Temporary debug log for QA - // console.log("[MISTRAL DEBUG] Raw API request body:", requestOptions) + /** + * Get the language model for the configured model ID. + */ + protected getLanguageModel(): LanguageModel { + const { id } = this.getModel() + // Type assertion needed due to version mismatch between @ai-sdk/mistral and ai packages + return this.provider(id) as unknown as LanguageModel + } - let response - try { - response = await this.client.chat.stream(requestOptions) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - const apiError = new ApiProviderError(errorMessage, this.providerName, model, "createMessage") - TelemetryService.instance.captureException(apiError) - throw new Error(`Mistral completion error: ${errorMessage}`) + /** + * Process usage metrics from the AI SDK response. + */ + protected processUsageMetrics(usage: { + inputTokens?: number + outputTokens?: number + details?: { + cachedInputTokens?: number + reasoningTokens?: number } + }): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage.inputTokens || 0, + outputTokens: usage.outputTokens || 0, + cacheReadTokens: usage.details?.cachedInputTokens, + reasoningTokens: usage.details?.reasoningTokens, + } + } - for await (const event of response) { - const delta = event.data.choices[0]?.delta - - if (delta?.content) { - if (typeof delta.content === "string") { - // Handle string content as text - yield { type: "text", text: delta.content } - } else if (Array.isArray(delta.content)) { - // Handle array of content chunks - // The SDK v1.9.18 supports ThinkChunk with type "thinking" - for (const chunk of delta.content as ContentChunkWithThinking[]) { - if (chunk.type === "thinking" && chunk.thinking) { - // Handle thinking content as reasoning chunks - // ThinkChunk has a 'thinking' property that contains an array of text/reference chunks - for (const thinkingPart of chunk.thinking) { - if (thinkingPart.type === "text" && thinkingPart.text) { - yield { type: "reasoning", text: thinkingPart.text } - } - } - } else if (chunk.type === "text" && chunk.text) { - // Handle text content normally - yield { type: "text", text: chunk.text } - } - } - } - } + /** + * Map OpenAI tool_choice to AI SDK toolChoice format. + */ + protected mapToolChoice( + toolChoice: any, + ): "auto" | "none" | "required" | { type: "tool"; toolName: string } | undefined { + if (!toolChoice) { + return undefined + } - // Handle tool calls in stream - // Mistral SDK provides tool_calls in delta similar to OpenAI format - const toolCalls = (delta as { toolCalls?: MistralToolCall[] })?.toolCalls - if (toolCalls) { - for (let i = 0; i < toolCalls.length; i++) { - const toolCall = toolCalls[i] - yield { - type: "tool_call_partial", - index: i, - id: toolCall.id, - name: toolCall.function?.name, - arguments: toolCall.function?.arguments, - } - } + // Handle string values + if (typeof toolChoice === "string") { + switch (toolChoice) { + case "auto": + return "auto" + case "none": + return "none" + case "required": + case "any": + return "required" + default: + return "auto" } + } - if (event.data.usage) { - yield { - type: "usage", - inputTokens: event.data.usage.promptTokens || 0, - outputTokens: event.data.usage.completionTokens || 0, - } + // Handle object values (OpenAI ChatCompletionNamedToolChoice format) + if (typeof toolChoice === "object" && "type" in toolChoice) { + if (toolChoice.type === "function" && "function" in toolChoice && toolChoice.function?.name) { + return { type: "tool", toolName: toolChoice.function.name } } } + + return undefined } /** - * Convert OpenAI tool definitions to Mistral format. - * Mistral uses the same format as OpenAI for function tools. + * Get the max tokens parameter to include in the request. */ - private convertToolsForMistral(tools: OpenAI.Chat.ChatCompletionTool[]): MistralTool[] { - return tools - .filter((tool) => tool.type === "function") - .map((tool) => ({ - type: "function" as const, - function: { - name: tool.function.name, - description: tool.function.description, - // Mistral SDK requires parameters to be defined, use empty object as fallback - parameters: (tool.function.parameters as Record) || {}, - }, - })) + protected getMaxOutputTokens(): number | undefined { + const { info } = this.getModel() + return this.options.modelMaxTokens || info.maxTokens || undefined } - override getModel() { - const id = this.options.apiModelId ?? mistralDefaultModelId - const info = mistralModels[id as MistralModelId] ?? mistralModels[mistralDefaultModelId] - - // @TODO: Move this to the `getModelParams` function. - const maxTokens = this.options.includeMaxTokens ? info.maxTokens : undefined - const temperature = this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE - - return { id, info, maxTokens, temperature } - } + /** + * Create a message stream using the AI SDK. + */ + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const languageModel = this.getLanguageModel() + + // Convert messages to AI SDK format + const aiSdkMessages = convertToAiSdkMessages(messages) + + // Convert tools to OpenAI format first, then to AI SDK format + const openAiTools = this.convertToolsForOpenAI(metadata?.tools) + const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined + + // Build the request options + // Use MISTRAL_DEFAULT_TEMPERATURE (1) as fallback to match original behavior + const requestOptions: Parameters[0] = { + model: languageModel, + system: systemPrompt, + messages: aiSdkMessages, + temperature: this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE, + maxOutputTokens: this.getMaxOutputTokens(), + tools: aiSdkTools, + toolChoice: this.mapToolChoice(metadata?.tool_choice), + } - async completePrompt(prompt: string, systemPrompt?: string, metadata?: any): Promise { - const { id: model, temperature } = this.getModel() + // Use streamText for streaming responses + const result = streamText(requestOptions) try { - const response = await this.client.chat.complete( - { - model, - messages: [{ role: "user", content: prompt }], - temperature, - }, - { - fetchOptions: { signal: metadata?.signal }, - }, - ) - - const content = response.choices?.[0]?.message.content - - if (Array.isArray(content)) { - // Only return text content, filter out thinking content for non-streaming - return (content as ContentChunkWithThinking[]) - .filter((c) => c.type === "text" && c.text) - .map((c) => c.text || "") - .join("") + // Process the full stream to get all events including reasoning + for await (const part of result.fullStream) { + for (const chunk of processAiSdkStreamPart(part)) { + yield chunk + } } - return content || "" + // Yield usage metrics at the end + const usage = await result.usage + if (usage) { + yield this.processUsageMetrics(usage) + } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - const apiError = new ApiProviderError(errorMessage, this.providerName, model, "completePrompt") - TelemetryService.instance.captureException(apiError) - throw new Error(`Mistral completion error: ${errorMessage}`) + // Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.) + throw handleAiSdkError(error, "Mistral") } } + + async completePrompt(prompt: string, systemPrompt?: string, metadata?: any): Promise { + const languageModel = this.getLanguageModel() + + // Use MISTRAL_DEFAULT_TEMPERATURE (1) as fallback to match original behavior + const { text } = await generateText({ + model: languageModel, + prompt, + maxOutputTokens: this.getMaxOutputTokens(), + temperature: this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE, + abortSignal: metadata?.signal, + }) + + return text + } } diff --git a/src/api/providers/zgsm.ts b/src/api/providers/zgsm.ts index 54ece3d58b..9cb3941b22 100644 --- a/src/api/providers/zgsm.ts +++ b/src/api/providers/zgsm.ts @@ -38,7 +38,7 @@ import { getEditorType } from "../../utils/getEditorType" import { ChatCompletionChunk } from "openai/resources/index.mjs" import { convertToZAiFormat } from "../transform/zai-format" import { isDebug } from "../../utils/getDebugState" -import { xmlLiteToolGuide } from "../../core/prompts/tools/lite-descriptions" +import { liteToolContractPrompt } from "../../core/prompts/tools/lite-descriptions" const autoModeModelId = "Auto" const isDev = process.env.NODE_ENV === "development" @@ -391,9 +391,9 @@ export class ZgsmAiHandler extends BaseProvider implements SingleCompletionHandl } else { if (_mid?.includes("qwen")) { if (Array.isArray(systemMessage.content)) { - systemMessage.content[0].text = systemMessage.content[0].text + "\n" + xmlLiteToolGuide + systemMessage.content[0].text = systemMessage.content[0].text + "\n" + liteToolContractPrompt() } else { - systemMessage.content = systemMessage.content + "\n" + xmlLiteToolGuide + systemMessage.content = systemMessage.content + "\n" + liteToolContractPrompt() } } convertedMessages = [ diff --git a/src/core/prompts/tools/lite-descriptions.ts b/src/core/prompts/tools/lite-descriptions.ts index 55411e7a3d..11fef6029c 100644 --- a/src/core/prompts/tools/lite-descriptions.ts +++ b/src/core/prompts/tools/lite-descriptions.ts @@ -4,7 +4,7 @@ Read file contents with line numbers. Supports text extraction from PDF and DOCX Two modes: - slice (default): Read lines sequentially with offset/limit - indentation: Extract semantic code blocks based on anchor_line -Params: +Params fields: - path (REQUIRED): File path relative to workspace - mode (optional): Reading mode - 'slice' or 'indentation' (default: 'slice') - offset (optional): 1-based start line for slice mode (default: 1) @@ -20,35 +20,35 @@ getLiteReadFileDescription.toolname = "read_file" export function getLiteWriteToFileDescription(): string { return `## write_to_file Create/overwrite file with content. -Params: path, content(REQUIRED)` +Params fields: path, content(REQUIRED)` } getLiteWriteToFileDescription.toolname = "write_to_file" export function getLiteSearchFilesDescription(): string { return `## search_files Regex search in directory. -Params: path (REQUIRED), regex (REQUIRED), file_pattern (REQUIRED)` +Params fields: path (REQUIRED), regex (REQUIRED), file_pattern (REQUIRED)` } getLiteSearchFilesDescription.toolname = "search_files" export function getLiteListFilesDescription(): string { return `## list_files List directory contents. -Params: path (REQUIRED), recursive (REQUIRED)` +Params fields: path (REQUIRED), recursive (REQUIRED)` } getLiteListFilesDescription.toolname = "list_files" export function getLiteExecuteCommandDescription(): string { return `## execute_command Execute CLI command. -Params: command (REQUIRED), cwd (REQUIRED)` +Params fields: command (REQUIRED), cwd (REQUIRED)` } getLiteExecuteCommandDescription.toolname = "execute_command" export function getLiteAskFollowupQuestionDescription(): string { return `## ask_followup_question Ask user for clarification. -Params: question (REQUIRED), follow_up (REQUIRED) +Params fields: question (REQUIRED), follow_up (REQUIRED) - question: Clear, specific question addressing the information needed - follow_up: Array of 2-4 suggested responses - text (REQUIRED): Suggested answer the user can pick @@ -59,35 +59,35 @@ getLiteAskFollowupQuestionDescription.toolname = "ask_followup_question" export function getLiteAttemptCompletionDescription(): string { return `## attempt_completion Present final result after task completion. -Params: result (REQUIRED)` +Params fields: result (REQUIRED)` } getLiteAttemptCompletionDescription.toolname = "attempt_completion" export function getLiteBrowserActionDescription(): string { return `## browser_action Browser interaction: screenshot, click, type, scroll. -Params: action (REQUIRED), url/coordinate/size/text/path based on action` +Params fields: action (REQUIRED), url/coordinate/size/text/path based on action` } getLiteBrowserActionDescription.toolname = "browser_action" export function getLiteSwitchModeDescription(): string { return `## switch_mode Switch to different mode. -Params: mode_slug (REQUIRED), reason (REQUIRED)` +Params fields: mode_slug (REQUIRED), reason (REQUIRED)` } getLiteSwitchModeDescription.toolname = "switch_mode" export function getLiteNewTaskDescription(): string { return `## new_task Create new task in specified mode. -Params: mode (REQUIRED), message (REQUIRED), todos (REQUIRED)` +Params fields: mode (REQUIRED), message (REQUIRED), todos (REQUIRED)` } getLiteNewTaskDescription.toolname = "new_task" export function getLiteUpdateTodoListDescription(): string { return `## update_todo_list Update TODO checklist. -Params: todos (REQUIRED) +Params fields: todos (REQUIRED) - todos: Full markdown checklist in execution order Format: [ ] pending, [x] completed, [-] in progress` } @@ -96,7 +96,7 @@ getLiteUpdateTodoListDescription.toolname = "update_todo_list" export function getLiteSkillDescription(): string { return `## skill Load and execute a skill by name. Skills provide specialized instructions for common tasks like creating MCP servers or custom modes. -Params: +Params fields: - skill (REQUIRED): Name of the skill to load (e.g., create-mcp-server, create-mode) - args (optional): Context or arguments to pass to the skill` } @@ -105,35 +105,35 @@ getLiteSkillDescription.toolname = "skill" export function getLiteCodebaseSearchDescription(): string { return `## codebase_search Semantic search for relevant code. -Params: query (REQUIRED), path (REQUIRED)` +Params fields: query (REQUIRED), path (REQUIRED)` } getLiteCodebaseSearchDescription.toolname = "codebase_search" export function getLiteAccessMcpResourceDescription(): string { return `## access_mcp_resource Access MCP server resource. -Params: server_name (REQUIRED), uri (REQUIRED)` +Params fields: server_name (REQUIRED), uri (REQUIRED)` } getLiteAccessMcpResourceDescription.toolname = "access_mcp_resource" export function getLiteGenerateImageDescription(): string { return `## generate_image Generate image using AI. -Params: prompt (REQUIRED), path (REQUIRED), image (REQUIRED)` +Params fields: prompt (REQUIRED), path (REQUIRED), image (REQUIRED)` } getLiteGenerateImageDescription.toolname = "generate_image" export function getLiteRunSlashCommandDescription(): string { return `## run_slash_command Run a VS Code slash command. -Params: command (REQUIRED), args (REQUIRED)` +Params fields: command (REQUIRED), args (REQUIRED)` } getLiteRunSlashCommandDescription.toolname = "run_slash_command" export function getLiteReadCommandOutputDescription(): string { return `## read_command_output Retrieve the full output from a command that was truncated in execute_command. -Params: artifact_id (REQUIRED), search (optional), offset (optional), limit (optional) +Params fields: artifact_id (REQUIRED), search (optional), offset (optional), limit (optional) - artifact_id: The artifact filename from truncated output (e.g., "cmd-1706119234567.txt") - search: Optional pattern to filter lines (regex or literal, case-insensitive) - offset: Byte offset to start reading from (default: 0) @@ -145,7 +145,7 @@ getLiteReadCommandOutputDescription.toolname = "read_command_output" export function getLiteApplyDiffDescription(): string { return `## apply_diff Apply precise, targeted modifications to an existing file using one or more search/replace blocks. -Params: path (REQUIRED), diff (REQUIRED) +Params fields: path (REQUIRED), diff (REQUIRED) - path: File path relative to workspace - diff: String containing search/replace blocks` } @@ -154,21 +154,21 @@ getLiteApplyDiffDescription.toolname = "apply_diff" export function getLiteApplyPatchDescription(): string { return `## apply_patch Apply a patch to a file. supports creating new files, deleting files, and updating existing files with precise changes. -Params: patch (REQUIRED)` +Params fields: patch (REQUIRED)` } getLiteApplyPatchDescription.toolname = "apply_patch" export function getLiteEditFileDescription(): string { return `## edit_file Replace text in an existing file, or create a new file. -Params: file_path (REQUIRED), old_string (REQUIRED), new_string (REQUIRED), expected_replacements (optional)` +Params fields: file_path (REQUIRED), old_string (REQUIRED), new_string (REQUIRED), expected_replacements (optional)` } getLiteEditFileDescription.toolname = "edit_file" export function getLiteAskMultipleChoiceDescription(): string { return `## ask_multiple_choice Ask the user to select one or more options from a list of choices. -Params: title (REQUIRED), questions (REQUIRED) +Params fields: title (REQUIRED), questions (REQUIRED) - questions: Array of question objects (at least 1) - id (REQUIRED): Unique identifier for the question - prompt (REQUIRED): Question text to display @@ -183,7 +183,7 @@ getLiteAskMultipleChoiceDescription.toolname = "ask_multiple_choice" export function getLiteSearchAndReplaceDescription(): string { return `## search_and_replace Apply precise, targeted modifications using search and replace operations. -Params: path (REQUIRED), operations (REQUIRED) +Params fields: path (REQUIRED), operations (REQUIRED) - path: File path relative to workspace - operations: Array of search/replace operations - search (REQUIRED): Exact text to find @@ -194,95 +194,149 @@ getLiteSearchAndReplaceDescription.toolname = "search_and_replace" export function getLiteSearchReplaceDescription(): string { return `## search_replace Replace ONE occurrence of old_string with new_string in a file. -Params: file_path (REQUIRED), old_string (REQUIRED), new_string (REQUIRED) +Params fields: file_path (REQUIRED), old_string (REQUIRED), new_string (REQUIRED) - file_path: Path to the file (relative or absolute) - old_string: Text to replace (must be unique, include 3-5 lines of context) - new_string: Edited text to replace with` } getLiteSearchReplaceDescription.toolname = "search_replace" -export const xmlLiteToolGuide = ` -# Response Format (CRITICAL): -When calling a tool, you MUST wrap the tool call parameters in a XML tag containing a valid JSON object. +getLiteReadFileDescription.toolname = "read_file" -Required format: -\`\`\`xml - -{ - "name": "tool_name_here", - "arguments": { - "param1": "value1", - "param2": "value2" - } -} - -\`\`\` +getLiteWriteToFileDescription.toolname = "write_to_file" -Requirements: - - The content inside tags MUST be valid JSON - - "name" field: string, the exact name of the tool to call - - "arguments" field: object, containing all required parameters for the tool - - Do NOT include comments in the JSON - - Ensure proper JSON syntax (double quotes, no trailing commas) -` +getLiteSearchFilesDescription.toolname = "search_files" -export const getGeminiCliLiteToolGuide = () => { - return `---- CRITICAL TOOL RULES (MUST FOLLOW) ---- +getLiteListFilesDescription.toolname = "list_files" -# User Local Available Tools +getLiteExecuteCommandDescription.toolname = "execute_command" -──────────────────────────────────── +getLiteAskFollowupQuestionDescription.toolname = "ask_followup_question" -${getLiteReadFileDescription()} +getLiteAttemptCompletionDescription.toolname = "attempt_completion" -${getLiteWriteToFileDescription()} +getLiteBrowserActionDescription.toolname = "browser_action" -${getLiteSearchFilesDescription()} +getLiteSwitchModeDescription.toolname = "switch_mode" -${getLiteListFilesDescription()} +getLiteNewTaskDescription.toolname = "new_task" -${getLiteExecuteCommandDescription()} +getLiteUpdateTodoListDescription.toolname = "update_todo_list" -${getLiteReadCommandOutputDescription()} +getLiteSkillDescription.toolname = "skill" -${getLiteAskFollowupQuestionDescription()} +getLiteCodebaseSearchDescription.toolname = "codebase_search" -${getLiteAttemptCompletionDescription()} +getLiteAccessMcpResourceDescription.toolname = "access_mcp_resource" -${getLiteBrowserActionDescription()} +getLiteGenerateImageDescription.toolname = "generate_image" -${getLiteSwitchModeDescription()} +getLiteRunSlashCommandDescription.toolname = "run_slash_command" -${getLiteNewTaskDescription()} +getLiteReadCommandOutputDescription.toolname = "read_command_output" -${getLiteUpdateTodoListDescription()} +getLiteApplyDiffDescription.toolname = "apply_diff" -${getLiteSkillDescription()} +getLiteApplyPatchDescription.toolname = "apply_patch" -${getLiteCodebaseSearchDescription()} +getLiteEditFileDescription.toolname = "edit_file" -${getLiteAccessMcpResourceDescription()} +getLiteAskMultipleChoiceDescription.toolname = "ask_multiple_choice" -${getLiteGenerateImageDescription()} +getLiteSearchAndReplaceDescription.toolname = "search_and_replace" -${getLiteRunSlashCommandDescription()} +getLiteSearchReplaceDescription.toolname = "search_replace" -${getLiteApplyPatchDescription()} +const liteTools = [ + getLiteReadFileDescription, + getLiteWriteToFileDescription, + getLiteSearchFilesDescription, + getLiteListFilesDescription, + getLiteExecuteCommandDescription, + getLiteReadCommandOutputDescription, + getLiteAskFollowupQuestionDescription, + getLiteBrowserActionDescription, + getLiteSwitchModeDescription, + getLiteNewTaskDescription, + getLiteUpdateTodoListDescription, + getLiteSkillDescription, + getLiteCodebaseSearchDescription, + getLiteAccessMcpResourceDescription, + getLiteGenerateImageDescription, + getLiteRunSlashCommandDescription, + getLiteApplyPatchDescription, + getLiteEditFileDescription, + getLiteAskMultipleChoiceDescription, + getLiteApplyDiffDescription, + getLiteSearchAndReplaceDescription, + getLiteSearchReplaceDescription, + getLiteAttemptCompletionDescription, +] + +export const liteRetryPrompt = (tag = "tool_call") => ` +# Your previous response did not follow the required format. + +You MUST respond with ONLY the <${tag}> XML. +Do not include any explanation or extra text. + +Retry now. +` +export const liteToolContractPrompt = (tag = "tool_call") => ` +# RESPONSE OUTPUT FORMAT CONTRACT (STRICT) + +When calling a tool, you MUST wrap the tool call parameters in a <${tag}> tag containing a valid JSON object. + +Requirements: +- Output ONLY the <${tag}> XML block +- "name" field: string, the exact name of the tool to call +- "arguments" field: object, containing all required parameters for the tool +- No text before or after +- No markdown +- No explanation +- No comments +- The content inside <${tag}> tags MUST be valid JSON +- Do NOT include comments in the JSON +- Ensure proper JSON syntax (double quotes, no trailing commas) + +Any deviation will cause automatic failure. + +Valid Response example: + +<${tag}> +{ + "name": "read_file", + "arguments": { + "path": "src/index.ts", + "offset": 1, + "limit": 200 + } +} + -${getLiteEditFileDescription()} +Your response is parsed by a strict machine parser. +` -${getLiteAskMultipleChoiceDescription()} +export const liteToolJudgePrompt = (allowedToolNames?: string[]) => ` +You can ONLY call the following built-in tools by name: -${getLiteApplyDiffDescription()} +${liteTools.map((t) => (allowedToolNames?.includes(t.toolname) || !allowedToolNames ? t.toolname : "")).join("\n")} -${getLiteSearchAndReplaceDescription()} +Select exactly ONE tool if a tool is required. +Do not explain your decision. -${getLiteSearchReplaceDescription()} +` -──────────────────────────────────── +export const getGeminiCliLiteToolGuide = (allowedToolNames?: string[]) => { + return ` +# User Local Available Built-in Tools -${xmlLiteToolGuide} +${liteTools + .map((t) => { + if (allowedToolNames?.includes(t.toolname) || !allowedToolNames) return t() + else return "" + }) + .filter((tn) => !!tn) + .join("\n\n")} ------------------------------------------ ` } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 4c41b451be..548ef4d6b1 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -58,7 +58,7 @@ import { } from "@roo-code/types" // import { CloudService, BridgeOrchestrator } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" -import { customToolRegistry } from "@roo-code/core" +// import { customToolRegistry } from "@roo-code/core" // api import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api" @@ -74,7 +74,7 @@ import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../ import { ClineAskResponse } from "../../shared/WebviewMessage" import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes" import { DiffStrategy, type ToolUse } from "../../shared/tools" -import { EXPERIMENT_IDS, experiments, parallelToolCallsEnabled } from "../../shared/experiments" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { getModelMaxOutputTokens } from "../../shared/api" // services @@ -86,7 +86,7 @@ import { RepoPerTaskCheckpointService } from "../../services/checkpoints" // integrations import { DiffViewProvider } from "../../integrations/editor/DiffViewProvider" -import { findToolName } from "../../integrations/misc/export-markdown" +// import { findToolName } from "../../integrations/misc/export-markdown" import { RooTerminalProcess } from "../../integrations/terminal/types" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor" @@ -139,12 +139,11 @@ import { MessageQueueService } from "../message-queue/MessageQueueService" import { ErrorCodeManager } from "../costrict/error-code" import { ZgsmAuthService } from "../costrict/auth" import { AutoApprovalHandler, checkAutoApproval } from "../auto-approval" -import psTree from "ps-tree" import { MessageManager } from "../message-manager" import { validateAndFixToolResultIds } from "./validateToolResultIds" -import { fixNativeToolname } from "../../utils/fixNativeToolname" import { getModelsFromCache } from "../../api/providers/fetchers/modelCache" import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages" +import { resolveToolAlias } from "../prompts/tools/filter-tools-for-mode" const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds @@ -735,6 +734,7 @@ export class Task extends EventEmitter implements TaskLike { this.messageQueueStateChangedHandler = () => { this.emit(RooCodeEventName.TaskUserMessage, this.taskId) + this.emit(RooCodeEventName.QueuedMessagesUpdated, this.taskId, this.messageQueueService.messages) this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory() } @@ -4545,6 +4545,10 @@ export class Task extends EventEmitter implements TaskLike { const metadata: ApiHandlerCreateMessageMetadata = { mode: mode, + allToolNames: + apiConfiguration?.apiProvider === "gemini-cli" + ? allTools.map((tool: any) => resolveToolAlias(tool?.function?.name)).filter((name) => !!name) + : undefined, zgsmCodeMode, provider: this.apiConfiguration.apiProvider, zgsmWorkflowMode: this.zgsmWorkflowMode, diff --git a/src/extension/api.ts b/src/extension/api.ts index d69619b98b..3a8f856997 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -29,7 +29,6 @@ export class API extends EventEmitter implements RooCodeAPI { private readonly sidebarProvider: ClineProvider private readonly context: vscode.ExtensionContext private readonly ipc?: IpcServer - private readonly taskMap = new Map() private readonly log: (...args: unknown[]) => void private logfile?: string @@ -64,35 +63,37 @@ export class API extends EventEmitter implements RooCodeAPI { ipc.listen() this.log(`[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`) - ipc.on(IpcMessageType.TaskCommand, async (_clientId, { commandName, data }) => { - switch (commandName) { + ipc.on(IpcMessageType.TaskCommand, async (_clientId, command) => { + switch (command.commandName) { case TaskCommandName.StartNewTask: - this.log(`[API] StartNewTask -> ${data.text}, ${JSON.stringify(data.configuration)}`) - await this.startNewTask(data) + this.log( + `[API] StartNewTask -> ${command.data.text}, ${JSON.stringify(command.data.configuration)}`, + ) + await this.startNewTask(command.data) break case TaskCommandName.CancelTask: - this.log(`[API] CancelTask -> ${data}`) - await this.cancelTask(data) + this.log(`[API] CancelTask`) + await this.cancelCurrentTask() break case TaskCommandName.CloseTask: - this.log(`[API] CloseTask -> ${data}`) + this.log(`[API] CloseTask`) await vscode.commands.executeCommand("workbench.action.files.saveFiles") await vscode.commands.executeCommand("workbench.action.closeWindow") break case TaskCommandName.ResumeTask: - this.log(`[API] ResumeTask -> ${data}`) + this.log(`[API] ResumeTask -> ${command.data}`) try { - await this.resumeTask(data) + await this.resumeTask(command.data) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.log(`[API] ResumeTask failed for taskId ${data}: ${errorMessage}`) + this.log(`[API] ResumeTask failed for taskId ${command.data}: ${errorMessage}`) // Don't rethrow - we want to prevent IPC server crashes // The error is logged for debugging purposes } break case TaskCommandName.SendMessage: - this.log(`[API] SendMessage -> ${data.text}`) - await this.sendMessage(data.text, data.images) + this.log(`[API] SendMessage -> ${command.data.text}`) + await this.sendMessage(command.data.text, command.data.images) break } }) @@ -180,15 +181,6 @@ export class API extends EventEmitter implements RooCodeAPI { await this.sidebarProvider.cancelTask() } - public async cancelTask(taskId: string) { - const provider = this.taskMap.get(taskId) - - if (provider) { - await provider.cancelTask() - this.taskMap.delete(taskId) - } - } - public async sendMessage(text?: string, images?: string[]) { await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images }) } @@ -211,7 +203,6 @@ export class API extends EventEmitter implements RooCodeAPI { task.on(RooCodeEventName.TaskStarted, async () => { this.emit(RooCodeEventName.TaskStarted, task.taskId) - this.taskMap.set(task.taskId, provider) await this.fileLog(`[${new Date().toISOString()}] taskStarted -> ${task.taskId}\n`) }) @@ -220,8 +211,6 @@ export class API extends EventEmitter implements RooCodeAPI { isSubtask: !!task.parentTaskId, }) - this.taskMap.delete(task.taskId) - await this.fileLog( `[${new Date().toISOString()}] taskCompleted -> ${task.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`, ) @@ -229,7 +218,6 @@ export class API extends EventEmitter implements RooCodeAPI { task.on(RooCodeEventName.TaskAborted, () => { this.emit(RooCodeEventName.TaskAborted, task.taskId) - this.taskMap.delete(task.taskId) }) task.on(RooCodeEventName.TaskFocused, () => { @@ -300,6 +288,10 @@ export class API extends EventEmitter implements RooCodeAPI { this.emit(RooCodeEventName.TaskAskResponded, task.taskId) }) + task.on(RooCodeEventName.QueuedMessagesUpdated, (taskId, messages) => { + this.emit(RooCodeEventName.QueuedMessagesUpdated, taskId, messages) + }) + // Task Analytics task.on(RooCodeEventName.TaskToolFailed, (taskId, tool, error) => { diff --git a/src/package.json b/src/package.json index 20c7ba72b4..d152b97c69 100644 --- a/src/package.json +++ b/src/package.json @@ -841,6 +841,7 @@ "@ai-sdk/deepseek": "^2.0.14", "@ai-sdk/fireworks": "^2.0.26", "@ai-sdk/groq": "^3.0.19", + "@ai-sdk/mistral": "^3.0.0", "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.7.0",