From 93fb46121f9c0c34496ac645e743c23fbf04e861 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 21 Aug 2025 17:54:13 +0000 Subject: [PATCH] fix: prevent context mixing in Roo/Sonic model (fixes #7292) - Disabled prompt caching for roo/sonic model to prevent cross-session contamination - Added unique session IDs to each RooHandler instance for request isolation - Added request-specific headers (X-Session-Id, X-Request-Id, X-No-Cache) to prevent caching - Added comprehensive tests to verify session isolation is working correctly - Updated model configuration to set supportsPromptCache to false This fix addresses the issue where the Roo/Sonic model was giving completely unrelated responses, suggesting context was being mixed between different users or sessions. --- packages/types/src/providers/roo.ts | 2 +- src/api/providers/__tests__/roo.spec.ts | 144 +++++++++++++++++++++++- src/api/providers/roo.ts | 58 +++++++++- 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/packages/types/src/providers/roo.ts b/packages/types/src/providers/roo.ts index c95e500f54..1300b1e192 100644 --- a/packages/types/src/providers/roo.ts +++ b/packages/types/src/providers/roo.ts @@ -10,7 +10,7 @@ export const rooModels = { maxTokens: 16_384, contextWindow: 262_144, supportsImages: false, - supportsPromptCache: true, + supportsPromptCache: false, // Disabled to prevent context mixing between sessions inputPrice: 0, outputPrice: 0, description: diff --git a/src/api/providers/__tests__/roo.spec.ts b/src/api/providers/__tests__/roo.spec.ts index 093137e1b2..80c7a1bb62 100644 --- a/src/api/providers/__tests__/roo.spec.ts +++ b/src/api/providers/__tests__/roo.spec.ts @@ -257,6 +257,7 @@ describe("RooHandler", () => { expect.objectContaining({ role: "user", content: "Second message" }), ]), }), + expect.any(Object), // Headers object ) }) }) @@ -331,7 +332,7 @@ describe("RooHandler", () => { expect(modelInfo.info.maxTokens).toBe(16_384) expect(modelInfo.info.contextWindow).toBe(262_144) expect(modelInfo.info.supportsImages).toBe(false) - expect(modelInfo.info.supportsPromptCache).toBe(true) + expect(modelInfo.info.supportsPromptCache).toBe(false) // Should be false now to prevent context mixing expect(modelInfo.info.inputPrice).toBe(0) expect(modelInfo.info.outputPrice).toBe(0) }) @@ -361,6 +362,7 @@ describe("RooHandler", () => { expect.not.objectContaining({ temperature: expect.anything(), }), + expect.any(Object), // Headers object ) }) @@ -378,6 +380,7 @@ describe("RooHandler", () => { expect.objectContaining({ temperature: 0.9, }), + expect.any(Object), // Headers object ) }) @@ -433,4 +436,143 @@ describe("RooHandler", () => { }).toThrow("Authentication required for Roo Code Cloud") }) }) + + describe("session isolation", () => { + beforeEach(() => { + mockHasInstanceFn.mockReturnValue(true) + mockGetSessionTokenFn.mockReturnValue("test-session-token") + mockCreate.mockClear() + }) + + it("should include session isolation headers in requests", async () => { + handler = new RooHandler(mockOptions) + const stream = handler.createMessage(systemPrompt, messages) + + // Consume the stream + for await (const _chunk of stream) { + // Just consume + } + + // Verify that create was called with session isolation headers + expect(mockCreate).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + headers: expect.objectContaining({ + "X-Session-Id": expect.any(String), + "X-Request-Id": expect.any(String), + "X-No-Cache": "true", + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + }), + }), + ) + }) + + it("should generate unique session IDs for different handler instances", async () => { + const handler1 = new RooHandler(mockOptions) + const handler2 = new RooHandler(mockOptions) + + // Create messages with both handlers + const stream1 = handler1.createMessage(systemPrompt, messages) + for await (const _chunk of stream1) { + // Consume + } + + const stream2 = handler2.createMessage(systemPrompt, messages) + for await (const _chunk of stream2) { + // Consume + } + + // Get the session IDs from the calls + const call1Headers = mockCreate.mock.calls[0][1].headers + const call2Headers = mockCreate.mock.calls[1][1].headers + + // Session IDs should be different for different handler instances + expect(call1Headers["X-Session-Id"]).toBeDefined() + expect(call2Headers["X-Session-Id"]).toBeDefined() + expect(call1Headers["X-Session-Id"]).not.toBe(call2Headers["X-Session-Id"]) + }) + + it("should generate unique request IDs for each request", async () => { + handler = new RooHandler(mockOptions) + + // Make two requests with the same handler + const stream1 = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream1) { + // Consume + } + + const stream2 = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream2) { + // Consume + } + + // Get the request IDs from the calls + const call1Headers = mockCreate.mock.calls[0][1].headers + const call2Headers = mockCreate.mock.calls[1][1].headers + + // Request IDs should be different for each request + expect(call1Headers["X-Request-Id"]).toBeDefined() + expect(call2Headers["X-Request-Id"]).toBeDefined() + expect(call1Headers["X-Request-Id"]).not.toBe(call2Headers["X-Request-Id"]) + + // But session IDs should be the same for the same handler + expect(call1Headers["X-Session-Id"]).toBe(call2Headers["X-Session-Id"]) + }) + + it("should include metadata in request params", async () => { + handler = new RooHandler(mockOptions) + const stream = handler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume + } + + // Verify metadata is included in the request + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + session_id: expect.any(String), + request_id: expect.any(String), + timestamp: expect.any(String), + }), + }), + expect.any(Object), + ) + }) + + it("should have prompt caching disabled for roo/sonic model", () => { + handler = new RooHandler(mockOptions) + const modelInfo = handler.getModel() + + // Verify that prompt caching is disabled + expect(modelInfo.info.supportsPromptCache).toBe(false) + }) + + it("should maintain session ID consistency across multiple requests", async () => { + handler = new RooHandler(mockOptions) + + // Make multiple requests + const requests = [] + for (let i = 0; i < 3; i++) { + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // Consume + } + requests.push(i) + } + + // All requests should have the same session ID + const sessionIds = mockCreate.mock.calls.map((call) => call[1].headers["X-Session-Id"]) + const firstSessionId = sessionIds[0] + + expect(sessionIds.every((id) => id === firstSessionId)).toBe(true) + + // But all request IDs should be unique + const requestIds = mockCreate.mock.calls.map((call) => call[1].headers["X-Request-Id"]) + const uniqueRequestIds = new Set(requestIds) + + expect(uniqueRequestIds.size).toBe(requestIds.length) + }) + }) }) diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index d986d6cd10..28bb15903c 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -1,15 +1,20 @@ import { Anthropic } from "@anthropic-ai/sdk" import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" +import { randomUUID } from "crypto" +import OpenAI from "openai" import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" import { t } from "../../i18n" +import { convertToOpenAiMessages } from "../transform/openai-format" import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" export class RooHandler extends BaseOpenAiCompatibleProvider { + private sessionId: string + constructor(options: ApiHandlerOptions) { // Check if CloudService is available and get the session token. if (!CloudService.hasInstance()) { @@ -22,6 +27,9 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { throw new Error(t("common:errors.roo.authenticationRequired")) } + // Generate a unique session ID for this handler instance to ensure request isolation + const sessionId = randomUUID() + super({ ...options, providerName: "Roo Code Cloud", @@ -31,6 +39,53 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { providerModels: rooModels, defaultTemperature: 0.7, }) + + this.sessionId = sessionId + } + + protected override createStream( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ) { + const { + id: model, + info: { maxTokens: max_tokens }, + } = this.getModel() + + // Generate unique request ID for this specific request + const requestId = randomUUID() + + // Create the request with session isolation metadata + const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model, + max_tokens, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + // Add session isolation metadata to prevent context mixing + metadata: { + session_id: this.sessionId, + request_id: requestId, + timestamp: new Date().toISOString(), + } as any, + } + + // Only include temperature if explicitly set + if (this.options.modelTemperature !== undefined) { + params.temperature = this.options.modelTemperature + } + + // Create the stream with additional headers for session isolation + return this.client.chat.completions.create(params, { + headers: { + "X-Session-Id": this.sessionId, + "X-Request-Id": requestId, + "X-No-Cache": "true", // Prevent any server-side caching + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + }, + }) } override async *createMessage( @@ -78,13 +133,14 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { } // Return the requested model ID even if not found, with fallback info. + // Note: supportsPromptCache is now false to prevent context mixing return { id: modelId as RooModelId, info: { maxTokens: 16_384, contextWindow: 262_144, supportsImages: false, - supportsPromptCache: true, + supportsPromptCache: false, // Disabled to prevent context mixing inputPrice: 0, outputPrice: 0, },