Skip to content

Commit e6f0e79

Browse files
daniel-lxsclaude
andauthored
feat: implement ModelMessage storage layer with AI SDK response messages (#11409)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dcb33c4 commit e6f0e79

103 files changed

Lines changed: 2648 additions & 3206 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/api/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import OpenAI from "openai"
33

44
import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types"
55

6+
import type { RooMessage } from "../core/task-persistence/rooMessage"
7+
68
import { ApiStream } from "./transform/stream"
79

810
import {
@@ -89,11 +91,7 @@ export interface ApiHandlerCreateMessageMetadata {
8991
}
9092

9193
export interface ApiHandler {
92-
createMessage(
93-
systemPrompt: string,
94-
messages: Anthropic.Messages.MessageParam[],
95-
metadata?: ApiHandlerCreateMessageMetadata,
96-
): ApiStream
94+
createMessage(systemPrompt: string, messages: RooMessage[], metadata?: ApiHandlerCreateMessageMetadata): ApiStream
9795

9896
getModel(): { id: string; info: ModelInfo }
9997

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 10 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
12
// npx vitest run src/api/providers/__tests__/anthropic-vertex.spec.ts
23

34
import { AnthropicVertexHandler } from "../anthropic-vertex"
@@ -54,6 +55,7 @@ vitest.mock("../../transform/ai-sdk", () => ({
5455
}),
5556
mapToolChoice: vitest.fn().mockReturnValue(undefined),
5657
handleAiSdkError: vitest.fn().mockImplementation((error: any) => error),
58+
yieldResponseMessage: vitest.fn().mockImplementation(function* () {}),
5759
}))
5860

5961
// Import mocked modules
@@ -184,7 +186,7 @@ describe("AnthropicVertexHandler", () => {
184186
})
185187

186188
describe("createMessage", () => {
187-
const mockMessages: Anthropic.Messages.MessageParam[] = [
189+
const mockMessages: RooMessage[] = [
188190
{
189191
role: "user",
190192
content: "Hello",
@@ -244,15 +246,20 @@ describe("AnthropicVertexHandler", () => {
244246
)
245247
})
246248

247-
it("should call convertToAiSdkMessages with the messages", async () => {
249+
it("should pass messages directly to streamText as ModelMessage[]", async () => {
248250
mockStreamText.mockReturnValue(createMockStreamResult([]))
249251

250252
const stream = handler.createMessage(systemPrompt, mockMessages)
251253
for await (const _chunk of stream) {
252254
// consume
253255
}
254256

255-
expect(convertToAiSdkMessages).toHaveBeenCalledWith(mockMessages)
257+
// Messages are now already in ModelMessage format, passed directly to streamText
258+
expect(mockStreamText).toHaveBeenCalledWith(
259+
expect.objectContaining({
260+
messages: mockMessages,
261+
}),
262+
)
256263
})
257264

258265
it("should pass tools through AI SDK conversion pipeline", async () => {
@@ -363,55 +370,6 @@ describe("AnthropicVertexHandler", () => {
363370
expect(textChunks[0].text).toBe("Here's my answer:")
364371
})
365372

366-
it("should capture thought signature from stream events", async () => {
367-
const streamParts = [
368-
{
369-
type: "reasoning-delta",
370-
text: "thinking...",
371-
providerMetadata: {
372-
anthropic: { signature: "test-signature-abc123" },
373-
},
374-
},
375-
{ type: "text-delta", text: "answer" },
376-
]
377-
378-
mockStreamText.mockReturnValue(createMockStreamResult(streamParts))
379-
380-
const stream = handler.createMessage(systemPrompt, mockMessages)
381-
for await (const _chunk of stream) {
382-
// consume
383-
}
384-
385-
expect(handler.getThoughtSignature()).toBe("test-signature-abc123")
386-
})
387-
388-
it("should capture redacted thinking blocks from stream events", async () => {
389-
const streamParts = [
390-
{
391-
type: "reasoning-delta",
392-
text: "",
393-
providerMetadata: {
394-
anthropic: { redactedData: "encrypted-redacted-data" },
395-
},
396-
},
397-
{ type: "text-delta", text: "answer" },
398-
]
399-
400-
mockStreamText.mockReturnValue(createMockStreamResult(streamParts))
401-
402-
const stream = handler.createMessage(systemPrompt, mockMessages)
403-
for await (const _chunk of stream) {
404-
// consume
405-
}
406-
407-
const redactedBlocks = handler.getRedactedThinkingBlocks()
408-
expect(redactedBlocks).toHaveLength(1)
409-
expect(redactedBlocks![0]).toEqual({
410-
type: "redacted_thinking",
411-
data: "encrypted-redacted-data",
412-
})
413-
})
414-
415373
it("should configure thinking providerOptions for thinking models", async () => {
416374
const thinkingHandler = new AnthropicVertexHandler({
417375
apiModelId: "claude-3-7-sonnet@20250219:thinking",
@@ -674,50 +632,4 @@ describe("AnthropicVertexHandler", () => {
674632
expect(handler.isAiSdkProvider()).toBe(true)
675633
})
676634
})
677-
678-
describe("thought signature and redacted thinking", () => {
679-
beforeEach(() => {
680-
handler = new AnthropicVertexHandler({
681-
apiModelId: "claude-3-5-sonnet-v2@20241022",
682-
vertexProjectId: "test-project",
683-
vertexRegion: "us-central1",
684-
})
685-
})
686-
687-
it("should return undefined for thought signature before any request", () => {
688-
expect(handler.getThoughtSignature()).toBeUndefined()
689-
})
690-
691-
it("should return undefined for redacted thinking blocks before any request", () => {
692-
expect(handler.getRedactedThinkingBlocks()).toBeUndefined()
693-
})
694-
695-
it("should reset thought signature on each createMessage call", async () => {
696-
// First call with signature
697-
mockStreamText.mockReturnValue(
698-
createMockStreamResult([
699-
{
700-
type: "reasoning-delta",
701-
text: "thinking",
702-
providerMetadata: { anthropic: { signature: "sig-1" } },
703-
},
704-
]),
705-
)
706-
707-
const stream1 = handler.createMessage("test", [{ role: "user", content: "Hello" }])
708-
for await (const _chunk of stream1) {
709-
// consume
710-
}
711-
expect(handler.getThoughtSignature()).toBe("sig-1")
712-
713-
// Second call without signature
714-
mockStreamText.mockReturnValue(createMockStreamResult([{ type: "text-delta", text: "just text" }]))
715-
716-
const stream2 = handler.createMessage("test", [{ role: "user", content: "Hello again" }])
717-
for await (const _chunk of stream2) {
718-
// consume
719-
}
720-
expect(handler.getThoughtSignature()).toBeUndefined()
721-
})
722-
})
723635
})

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

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ vitest.mock("../../transform/ai-sdk", () => ({
5050
}),
5151
mapToolChoice: vitest.fn().mockReturnValue(undefined),
5252
handleAiSdkError: vitest.fn().mockImplementation((error: any) => error),
53+
yieldResponseMessage: vitest.fn().mockImplementation(function* () {}),
5354
}))
5455

5556
// Import mocked modules
@@ -398,85 +399,6 @@ describe("AnthropicHandler", () => {
398399
expect(endChunk).toBeDefined()
399400
})
400401

401-
it("should capture thinking signature from stream events", async () => {
402-
const testSignature = "test-thinking-signature"
403-
setupStreamTextMock([
404-
{
405-
type: "reasoning-delta",
406-
text: "thinking...",
407-
providerMetadata: { anthropic: { signature: testSignature } },
408-
},
409-
{ type: "text-delta", text: "Answer" },
410-
])
411-
412-
const stream = handler.createMessage(systemPrompt, [
413-
{ role: "user", content: [{ type: "text" as const, text: "test" }] },
414-
])
415-
416-
for await (const _chunk of stream) {
417-
// Consume stream
418-
}
419-
420-
expect(handler.getThoughtSignature()).toBe(testSignature)
421-
})
422-
423-
it("should capture redacted thinking blocks from stream events", async () => {
424-
setupStreamTextMock([
425-
{
426-
type: "reasoning-delta",
427-
text: "",
428-
providerMetadata: { anthropic: { redactedData: "redacted-data-base64" } },
429-
},
430-
{ type: "text-delta", text: "Answer" },
431-
])
432-
433-
const stream = handler.createMessage(systemPrompt, [
434-
{ role: "user", content: [{ type: "text" as const, text: "test" }] },
435-
])
436-
437-
for await (const _chunk of stream) {
438-
// Consume stream
439-
}
440-
441-
const redactedBlocks = handler.getRedactedThinkingBlocks()
442-
expect(redactedBlocks).toBeDefined()
443-
expect(redactedBlocks).toHaveLength(1)
444-
expect(redactedBlocks![0]).toEqual({
445-
type: "redacted_thinking",
446-
data: "redacted-data-base64",
447-
})
448-
})
449-
450-
it("should reset thinking state between requests", async () => {
451-
// First request with signature
452-
setupStreamTextMock([
453-
{
454-
type: "reasoning-delta",
455-
text: "thinking...",
456-
providerMetadata: { anthropic: { signature: "sig-1" } },
457-
},
458-
])
459-
460-
const stream1 = handler.createMessage(systemPrompt, [
461-
{ role: "user", content: [{ type: "text" as const, text: "test 1" }] },
462-
])
463-
for await (const _chunk of stream1) {
464-
// Consume
465-
}
466-
expect(handler.getThoughtSignature()).toBe("sig-1")
467-
468-
// Second request without signature
469-
setupStreamTextMock([{ type: "text-delta", text: "plain answer" }])
470-
471-
const stream2 = handler.createMessage(systemPrompt, [
472-
{ role: "user", content: [{ type: "text" as const, text: "test 2" }] },
473-
])
474-
for await (const _chunk of stream2) {
475-
// Consume
476-
}
477-
expect(handler.getThoughtSignature()).toBeUndefined()
478-
})
479-
480402
it("should pass system prompt via system param with systemProviderOptions for cache control", async () => {
481403
setupStreamTextMock([{ type: "text-delta", text: "test" }])
482404

@@ -610,14 +532,4 @@ describe("AnthropicHandler", () => {
610532
expect(handler.isAiSdkProvider()).toBe(true)
611533
})
612534
})
613-
614-
describe("thinking signature", () => {
615-
it("should return undefined when no signature captured", () => {
616-
expect(handler.getThoughtSignature()).toBeUndefined()
617-
})
618-
619-
it("should return undefined for redacted blocks when none captured", () => {
620-
expect(handler.getRedactedThinkingBlocks()).toBeUndefined()
621-
})
622-
})
623535
})

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
12
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
23
const { mockStreamText, mockGenerateText, mockCreateAzure } = vi.hoisted(() => ({
34
mockStreamText: vi.fn(),
@@ -132,7 +133,7 @@ describe("AzureHandler", () => {
132133

133134
describe("createMessage", () => {
134135
const systemPrompt = "You are a helpful assistant."
135-
const messages: Anthropic.Messages.MessageParam[] = [
136+
const messages: RooMessage[] = [
136137
{
137138
role: "user",
138139
content: [
@@ -376,7 +377,7 @@ describe("AzureHandler", () => {
376377

377378
describe("tools", () => {
378379
const systemPrompt = "You are a helpful assistant."
379-
const messages: Anthropic.Messages.MessageParam[] = [
380+
const messages: RooMessage[] = [
380381
{
381382
role: "user",
382383
content: [{ type: "text" as const, text: "Use a tool" }],

src/api/providers/__tests__/base-provider.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
12
import { Anthropic } from "@anthropic-ai/sdk"
23

34
import type { ModelInfo } from "@roo-code/types"
@@ -7,7 +8,7 @@ import type { ApiStream } from "../../transform/stream"
78

89
// Create a concrete implementation for testing
910
class TestProvider extends BaseProvider {
10-
createMessage(_systemPrompt: string, _messages: Anthropic.Messages.MessageParam[]): ApiStream {
11+
createMessage(_systemPrompt: string, _messages: RooMessage[]): ApiStream {
1112
throw new Error("Not implemented")
1213
}
1314

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
12
// npx vitest run src/api/providers/__tests__/baseten.spec.ts
23

34
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
@@ -101,7 +102,7 @@ describe("BasetenHandler", () => {
101102

102103
describe("createMessage", () => {
103104
const systemPrompt = "You are a helpful assistant."
104-
const messages: Anthropic.Messages.MessageParam[] = [
105+
const messages: RooMessage[] = [
105106
{
106107
role: "user",
107108
content: [
@@ -281,7 +282,7 @@ describe("BasetenHandler", () => {
281282

282283
describe("tool handling", () => {
283284
const systemPrompt = "You are a helpful assistant."
284-
const messages: Anthropic.Messages.MessageParam[] = [
285+
const messages: RooMessage[] = [
285286
{
286287
role: "user",
287288
content: [{ type: "text" as const, text: "Hello!" }],
@@ -389,7 +390,7 @@ describe("BasetenHandler", () => {
389390

390391
describe("error handling", () => {
391392
const systemPrompt = "You are a helpful assistant."
392-
const messages: Anthropic.Messages.MessageParam[] = [
393+
const messages: RooMessage[] = [
393394
{
394395
role: "user",
395396
content: [{ type: "text" as const, text: "Hello!" }],

0 commit comments

Comments
 (0)