Skip to content

Commit 93fb461

Browse files
committed
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.
1 parent 6fd261d commit 93fb461

File tree

3 files changed

+201
-3
lines changed

3 files changed

+201
-3
lines changed

packages/types/src/providers/roo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const rooModels = {
1010
maxTokens: 16_384,
1111
contextWindow: 262_144,
1212
supportsImages: false,
13-
supportsPromptCache: true,
13+
supportsPromptCache: false, // Disabled to prevent context mixing between sessions
1414
inputPrice: 0,
1515
outputPrice: 0,
1616
description:

src/api/providers/__tests__/roo.spec.ts

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ describe("RooHandler", () => {
257257
expect.objectContaining({ role: "user", content: "Second message" }),
258258
]),
259259
}),
260+
expect.any(Object), // Headers object
260261
)
261262
})
262263
})
@@ -331,7 +332,7 @@ describe("RooHandler", () => {
331332
expect(modelInfo.info.maxTokens).toBe(16_384)
332333
expect(modelInfo.info.contextWindow).toBe(262_144)
333334
expect(modelInfo.info.supportsImages).toBe(false)
334-
expect(modelInfo.info.supportsPromptCache).toBe(true)
335+
expect(modelInfo.info.supportsPromptCache).toBe(false) // Should be false now to prevent context mixing
335336
expect(modelInfo.info.inputPrice).toBe(0)
336337
expect(modelInfo.info.outputPrice).toBe(0)
337338
})
@@ -361,6 +362,7 @@ describe("RooHandler", () => {
361362
expect.not.objectContaining({
362363
temperature: expect.anything(),
363364
}),
365+
expect.any(Object), // Headers object
364366
)
365367
})
366368

@@ -378,6 +380,7 @@ describe("RooHandler", () => {
378380
expect.objectContaining({
379381
temperature: 0.9,
380382
}),
383+
expect.any(Object), // Headers object
381384
)
382385
})
383386

@@ -433,4 +436,143 @@ describe("RooHandler", () => {
433436
}).toThrow("Authentication required for Roo Code Cloud")
434437
})
435438
})
439+
440+
describe("session isolation", () => {
441+
beforeEach(() => {
442+
mockHasInstanceFn.mockReturnValue(true)
443+
mockGetSessionTokenFn.mockReturnValue("test-session-token")
444+
mockCreate.mockClear()
445+
})
446+
447+
it("should include session isolation headers in requests", async () => {
448+
handler = new RooHandler(mockOptions)
449+
const stream = handler.createMessage(systemPrompt, messages)
450+
451+
// Consume the stream
452+
for await (const _chunk of stream) {
453+
// Just consume
454+
}
455+
456+
// Verify that create was called with session isolation headers
457+
expect(mockCreate).toHaveBeenCalledWith(
458+
expect.any(Object),
459+
expect.objectContaining({
460+
headers: expect.objectContaining({
461+
"X-Session-Id": expect.any(String),
462+
"X-Request-Id": expect.any(String),
463+
"X-No-Cache": "true",
464+
"Cache-Control": "no-store, no-cache, must-revalidate",
465+
Pragma: "no-cache",
466+
}),
467+
}),
468+
)
469+
})
470+
471+
it("should generate unique session IDs for different handler instances", async () => {
472+
const handler1 = new RooHandler(mockOptions)
473+
const handler2 = new RooHandler(mockOptions)
474+
475+
// Create messages with both handlers
476+
const stream1 = handler1.createMessage(systemPrompt, messages)
477+
for await (const _chunk of stream1) {
478+
// Consume
479+
}
480+
481+
const stream2 = handler2.createMessage(systemPrompt, messages)
482+
for await (const _chunk of stream2) {
483+
// Consume
484+
}
485+
486+
// Get the session IDs from the calls
487+
const call1Headers = mockCreate.mock.calls[0][1].headers
488+
const call2Headers = mockCreate.mock.calls[1][1].headers
489+
490+
// Session IDs should be different for different handler instances
491+
expect(call1Headers["X-Session-Id"]).toBeDefined()
492+
expect(call2Headers["X-Session-Id"]).toBeDefined()
493+
expect(call1Headers["X-Session-Id"]).not.toBe(call2Headers["X-Session-Id"])
494+
})
495+
496+
it("should generate unique request IDs for each request", async () => {
497+
handler = new RooHandler(mockOptions)
498+
499+
// Make two requests with the same handler
500+
const stream1 = handler.createMessage(systemPrompt, messages)
501+
for await (const _chunk of stream1) {
502+
// Consume
503+
}
504+
505+
const stream2 = handler.createMessage(systemPrompt, messages)
506+
for await (const _chunk of stream2) {
507+
// Consume
508+
}
509+
510+
// Get the request IDs from the calls
511+
const call1Headers = mockCreate.mock.calls[0][1].headers
512+
const call2Headers = mockCreate.mock.calls[1][1].headers
513+
514+
// Request IDs should be different for each request
515+
expect(call1Headers["X-Request-Id"]).toBeDefined()
516+
expect(call2Headers["X-Request-Id"]).toBeDefined()
517+
expect(call1Headers["X-Request-Id"]).not.toBe(call2Headers["X-Request-Id"])
518+
519+
// But session IDs should be the same for the same handler
520+
expect(call1Headers["X-Session-Id"]).toBe(call2Headers["X-Session-Id"])
521+
})
522+
523+
it("should include metadata in request params", async () => {
524+
handler = new RooHandler(mockOptions)
525+
const stream = handler.createMessage(systemPrompt, messages)
526+
527+
for await (const _chunk of stream) {
528+
// Consume
529+
}
530+
531+
// Verify metadata is included in the request
532+
expect(mockCreate).toHaveBeenCalledWith(
533+
expect.objectContaining({
534+
metadata: expect.objectContaining({
535+
session_id: expect.any(String),
536+
request_id: expect.any(String),
537+
timestamp: expect.any(String),
538+
}),
539+
}),
540+
expect.any(Object),
541+
)
542+
})
543+
544+
it("should have prompt caching disabled for roo/sonic model", () => {
545+
handler = new RooHandler(mockOptions)
546+
const modelInfo = handler.getModel()
547+
548+
// Verify that prompt caching is disabled
549+
expect(modelInfo.info.supportsPromptCache).toBe(false)
550+
})
551+
552+
it("should maintain session ID consistency across multiple requests", async () => {
553+
handler = new RooHandler(mockOptions)
554+
555+
// Make multiple requests
556+
const requests = []
557+
for (let i = 0; i < 3; i++) {
558+
const stream = handler.createMessage(systemPrompt, messages)
559+
for await (const _chunk of stream) {
560+
// Consume
561+
}
562+
requests.push(i)
563+
}
564+
565+
// All requests should have the same session ID
566+
const sessionIds = mockCreate.mock.calls.map((call) => call[1].headers["X-Session-Id"])
567+
const firstSessionId = sessionIds[0]
568+
569+
expect(sessionIds.every((id) => id === firstSessionId)).toBe(true)
570+
571+
// But all request IDs should be unique
572+
const requestIds = mockCreate.mock.calls.map((call) => call[1].headers["X-Request-Id"])
573+
const uniqueRequestIds = new Set(requestIds)
574+
575+
expect(uniqueRequestIds.size).toBe(requestIds.length)
576+
})
577+
})
436578
})

src/api/providers/roo.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
33
import { CloudService } from "@roo-code/cloud"
4+
import { randomUUID } from "crypto"
5+
import OpenAI from "openai"
46

57
import type { ApiHandlerOptions } from "../../shared/api"
68
import { ApiStream } from "../transform/stream"
79
import { t } from "../../i18n"
10+
import { convertToOpenAiMessages } from "../transform/openai-format"
811

912
import type { ApiHandlerCreateMessageMetadata } from "../index"
1013
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
1114

1215
export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
16+
private sessionId: string
17+
1318
constructor(options: ApiHandlerOptions) {
1419
// Check if CloudService is available and get the session token.
1520
if (!CloudService.hasInstance()) {
@@ -22,6 +27,9 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
2227
throw new Error(t("common:errors.roo.authenticationRequired"))
2328
}
2429

30+
// Generate a unique session ID for this handler instance to ensure request isolation
31+
const sessionId = randomUUID()
32+
2533
super({
2634
...options,
2735
providerName: "Roo Code Cloud",
@@ -31,6 +39,53 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
3139
providerModels: rooModels,
3240
defaultTemperature: 0.7,
3341
})
42+
43+
this.sessionId = sessionId
44+
}
45+
46+
protected override createStream(
47+
systemPrompt: string,
48+
messages: Anthropic.Messages.MessageParam[],
49+
metadata?: ApiHandlerCreateMessageMetadata,
50+
) {
51+
const {
52+
id: model,
53+
info: { maxTokens: max_tokens },
54+
} = this.getModel()
55+
56+
// Generate unique request ID for this specific request
57+
const requestId = randomUUID()
58+
59+
// Create the request with session isolation metadata
60+
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
61+
model,
62+
max_tokens,
63+
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
64+
stream: true,
65+
stream_options: { include_usage: true },
66+
// Add session isolation metadata to prevent context mixing
67+
metadata: {
68+
session_id: this.sessionId,
69+
request_id: requestId,
70+
timestamp: new Date().toISOString(),
71+
} as any,
72+
}
73+
74+
// Only include temperature if explicitly set
75+
if (this.options.modelTemperature !== undefined) {
76+
params.temperature = this.options.modelTemperature
77+
}
78+
79+
// Create the stream with additional headers for session isolation
80+
return this.client.chat.completions.create(params, {
81+
headers: {
82+
"X-Session-Id": this.sessionId,
83+
"X-Request-Id": requestId,
84+
"X-No-Cache": "true", // Prevent any server-side caching
85+
"Cache-Control": "no-store, no-cache, must-revalidate",
86+
Pragma: "no-cache",
87+
},
88+
})
3489
}
3590

3691
override async *createMessage(
@@ -78,13 +133,14 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
78133
}
79134

80135
// Return the requested model ID even if not found, with fallback info.
136+
// Note: supportsPromptCache is now false to prevent context mixing
81137
return {
82138
id: modelId as RooModelId,
83139
info: {
84140
maxTokens: 16_384,
85141
contextWindow: 262_144,
86142
supportsImages: false,
87-
supportsPromptCache: true,
143+
supportsPromptCache: false, // Disabled to prevent context mixing
88144
inputPrice: 0,
89145
outputPrice: 0,
90146
},

0 commit comments

Comments
 (0)