Skip to content

Commit d69fcec

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/add-todo-list-settings-checkbox
2 parents 9399da4 + 7e34fbc commit d69fcec

File tree

5 files changed

+173
-38
lines changed

5 files changed

+173
-38
lines changed

packages/types/src/__tests__/provider-settings.test.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ describe("getApiProtocol", () => {
1212
expect(getApiProtocol("claude-code")).toBe("anthropic")
1313
expect(getApiProtocol("claude-code", "some-model")).toBe("anthropic")
1414
})
15+
16+
it("should return 'anthropic' for bedrock provider", () => {
17+
expect(getApiProtocol("bedrock")).toBe("anthropic")
18+
expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic")
19+
expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic")
20+
})
1521
})
1622

1723
describe("Vertex provider with Claude models", () => {
@@ -27,25 +33,14 @@ describe("getApiProtocol", () => {
2733
expect(getApiProtocol("vertex", "gemini-pro")).toBe("openai")
2834
expect(getApiProtocol("vertex", "llama-2")).toBe("openai")
2935
})
30-
})
31-
32-
describe("Bedrock provider with Claude models", () => {
33-
it("should return 'anthropic' for bedrock provider with claude models", () => {
34-
expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic")
35-
expect(getApiProtocol("bedrock", "Claude-3-Sonnet")).toBe("anthropic")
36-
expect(getApiProtocol("bedrock", "CLAUDE-instant")).toBe("anthropic")
37-
expect(getApiProtocol("bedrock", "anthropic.claude-v2")).toBe("anthropic")
38-
})
3936

40-
it("should return 'openai' for bedrock provider with non-claude models", () => {
41-
expect(getApiProtocol("bedrock", "gpt-4")).toBe("openai")
42-
expect(getApiProtocol("bedrock", "titan-text")).toBe("openai")
43-
expect(getApiProtocol("bedrock", "llama-2")).toBe("openai")
37+
it("should return 'openai' for vertex provider without model", () => {
38+
expect(getApiProtocol("vertex")).toBe("openai")
4439
})
4540
})
4641

47-
describe("Other providers with Claude models", () => {
48-
it("should return 'openai' for non-vertex/bedrock providers with claude models", () => {
42+
describe("Other providers", () => {
43+
it("should return 'openai' for non-anthropic providers regardless of model", () => {
4944
expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai")
5045
expect(getApiProtocol("openai", "claude-3-sonnet")).toBe("openai")
5146
expect(getApiProtocol("litellm", "claude-instant")).toBe("openai")
@@ -59,20 +54,13 @@ describe("getApiProtocol", () => {
5954
expect(getApiProtocol(undefined, "claude-3-opus")).toBe("openai")
6055
})
6156

62-
it("should return 'openai' when model is undefined", () => {
63-
expect(getApiProtocol("openai")).toBe("openai")
64-
expect(getApiProtocol("vertex")).toBe("openai")
65-
expect(getApiProtocol("bedrock")).toBe("openai")
66-
})
67-
6857
it("should handle empty strings", () => {
6958
expect(getApiProtocol("vertex", "")).toBe("openai")
70-
expect(getApiProtocol("bedrock", "")).toBe("openai")
7159
})
7260

7361
it("should be case-insensitive for claude detection", () => {
7462
expect(getApiProtocol("vertex", "CLAUDE-3-OPUS")).toBe("anthropic")
75-
expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic")
63+
expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic")
7664
expect(getApiProtocol("vertex", "ClAuDe-InStAnT")).toBe("anthropic")
7765
})
7866
})

packages/types/src/provider-settings.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ export const getModelId = (settings: ProviderSettings): string | undefined => {
302302
}
303303

304304
// Providers that use Anthropic-style API protocol
305-
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code"]
305+
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"]
306306

307307
// Helper function to determine API protocol for a provider and model
308308
export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
@@ -311,13 +311,8 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
311311
return "anthropic"
312312
}
313313

314-
// For vertex and bedrock providers, check if the model ID contains "claude" (case-insensitive)
315-
if (
316-
provider &&
317-
(provider === "vertex" || provider === "bedrock") &&
318-
modelId &&
319-
modelId.toLowerCase().includes("claude")
320-
) {
314+
// For vertex provider, check if the model ID contains "claude" (case-insensitive)
315+
if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) {
321316
return "anthropic"
322317
}
323318

src/core/prompts/sections/__tests__/custom-instructions.spec.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,157 @@ describe("Rules directory reading", () => {
10331033
expect(result).toContain("content of file3")
10341034
})
10351035

1036+
it("should return files in alphabetical order by filename", async () => {
1037+
// Simulate .roo/rules directory exists
1038+
statMock.mockResolvedValueOnce({
1039+
isDirectory: vi.fn().mockReturnValue(true),
1040+
} as any)
1041+
1042+
// Simulate listing files in non-alphabetical order to test sorting
1043+
readdirMock.mockResolvedValueOnce([
1044+
{ name: "zebra.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" },
1045+
{ name: "alpha.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" },
1046+
{ name: "Beta.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" }, // Test case-insensitive sorting
1047+
] as any)
1048+
1049+
statMock.mockImplementation((path) => {
1050+
return Promise.resolve({
1051+
isFile: vi.fn().mockReturnValue(true),
1052+
}) as any
1053+
})
1054+
1055+
readFileMock.mockImplementation((filePath: PathLike) => {
1056+
const pathStr = filePath.toString()
1057+
const normalizedPath = pathStr.replace(/\\/g, "/")
1058+
if (normalizedPath === "/fake/path/.roo/rules/zebra.txt") {
1059+
return Promise.resolve("zebra content")
1060+
}
1061+
if (normalizedPath === "/fake/path/.roo/rules/alpha.txt") {
1062+
return Promise.resolve("alpha content")
1063+
}
1064+
if (normalizedPath === "/fake/path/.roo/rules/Beta.txt") {
1065+
return Promise.resolve("beta content")
1066+
}
1067+
return Promise.reject({ code: "ENOENT" })
1068+
})
1069+
1070+
const result = await loadRuleFiles("/fake/path")
1071+
1072+
// Files should appear in alphabetical order: alpha.txt, Beta.txt, zebra.txt
1073+
const alphaIndex = result.indexOf("alpha content")
1074+
const betaIndex = result.indexOf("beta content")
1075+
const zebraIndex = result.indexOf("zebra content")
1076+
1077+
expect(alphaIndex).toBeLessThan(betaIndex)
1078+
expect(betaIndex).toBeLessThan(zebraIndex)
1079+
1080+
// Verify the expected file paths are in the result
1081+
const expectedAlphaPath =
1082+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\alpha.txt" : "/fake/path/.roo/rules/alpha.txt"
1083+
const expectedBetaPath =
1084+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\Beta.txt" : "/fake/path/.roo/rules/Beta.txt"
1085+
const expectedZebraPath =
1086+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\zebra.txt" : "/fake/path/.roo/rules/zebra.txt"
1087+
1088+
expect(result).toContain(`# Rules from ${expectedAlphaPath}:`)
1089+
expect(result).toContain(`# Rules from ${expectedBetaPath}:`)
1090+
expect(result).toContain(`# Rules from ${expectedZebraPath}:`)
1091+
})
1092+
1093+
it("should sort symlinks by their symlink names, not target names", async () => {
1094+
// Reset mocks
1095+
statMock.mockReset()
1096+
readdirMock.mockReset()
1097+
readlinkMock.mockReset()
1098+
readFileMock.mockReset()
1099+
1100+
// First call: check if .roo/rules directory exists
1101+
statMock.mockResolvedValueOnce({
1102+
isDirectory: vi.fn().mockReturnValue(true),
1103+
} as any)
1104+
1105+
// Simulate listing files with symlinks that point to files with different names
1106+
readdirMock.mockResolvedValueOnce([
1107+
{
1108+
name: "01-first.link",
1109+
isFile: () => false,
1110+
isSymbolicLink: () => true,
1111+
parentPath: "/fake/path/.roo/rules",
1112+
},
1113+
{
1114+
name: "02-second.link",
1115+
isFile: () => false,
1116+
isSymbolicLink: () => true,
1117+
parentPath: "/fake/path/.roo/rules",
1118+
},
1119+
{
1120+
name: "03-third.link",
1121+
isFile: () => false,
1122+
isSymbolicLink: () => true,
1123+
parentPath: "/fake/path/.roo/rules",
1124+
},
1125+
] as any)
1126+
1127+
// Mock readlink to return target paths that would sort differently than symlink names
1128+
readlinkMock
1129+
.mockResolvedValueOnce("../../targets/zzz-last.txt") // 01-first.link -> zzz-last.txt
1130+
.mockResolvedValueOnce("../../targets/aaa-first.txt") // 02-second.link -> aaa-first.txt
1131+
.mockResolvedValueOnce("../../targets/mmm-middle.txt") // 03-third.link -> mmm-middle.txt
1132+
1133+
// Set up stat mock for the remaining calls
1134+
statMock.mockImplementation((path) => {
1135+
const normalizedPath = path.toString().replace(/\\/g, "/")
1136+
// Target files exist and are files
1137+
if (normalizedPath.endsWith(".txt")) {
1138+
return Promise.resolve({
1139+
isFile: vi.fn().mockReturnValue(true),
1140+
isDirectory: vi.fn().mockReturnValue(false),
1141+
} as any)
1142+
}
1143+
return Promise.resolve({
1144+
isFile: vi.fn().mockReturnValue(false),
1145+
isDirectory: vi.fn().mockReturnValue(false),
1146+
} as any)
1147+
})
1148+
1149+
readFileMock.mockImplementation((filePath: PathLike) => {
1150+
const pathStr = filePath.toString()
1151+
const normalizedPath = pathStr.replace(/\\/g, "/")
1152+
if (normalizedPath.endsWith("zzz-last.txt")) {
1153+
return Promise.resolve("content from zzz-last.txt")
1154+
}
1155+
if (normalizedPath.endsWith("aaa-first.txt")) {
1156+
return Promise.resolve("content from aaa-first.txt")
1157+
}
1158+
if (normalizedPath.endsWith("mmm-middle.txt")) {
1159+
return Promise.resolve("content from mmm-middle.txt")
1160+
}
1161+
return Promise.reject({ code: "ENOENT" })
1162+
})
1163+
1164+
const result = await loadRuleFiles("/fake/path")
1165+
1166+
// Content should appear in order of symlink names (01-first, 02-second, 03-third)
1167+
// NOT in order of target names (aaa-first, mmm-middle, zzz-last)
1168+
const firstIndex = result.indexOf("content from zzz-last.txt") // from 01-first.link
1169+
const secondIndex = result.indexOf("content from aaa-first.txt") // from 02-second.link
1170+
const thirdIndex = result.indexOf("content from mmm-middle.txt") // from 03-third.link
1171+
1172+
// All content should be found
1173+
expect(firstIndex).toBeGreaterThan(-1)
1174+
expect(secondIndex).toBeGreaterThan(-1)
1175+
expect(thirdIndex).toBeGreaterThan(-1)
1176+
1177+
// And they should be in the order of symlink names, not target names
1178+
expect(firstIndex).toBeLessThan(secondIndex)
1179+
expect(secondIndex).toBeLessThan(thirdIndex)
1180+
1181+
// Verify the target paths are shown (not symlink paths)
1182+
expect(result).toContain("zzz-last.txt")
1183+
expect(result).toContain("aaa-first.txt")
1184+
expect(result).toContain("mmm-middle.txt")
1185+
})
1186+
10361187
it("should handle empty file list gracefully", async () => {
10371188
// Simulate .roo/rules directory exists
10381189
statMock.mockResolvedValueOnce({

src/core/sliding-window/__tests__/sliding-window.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,9 +1103,9 @@ describe("Sliding Window", () => {
11031103
expect(result2.prevContextTokens).toBe(50001)
11041104
})
11051105

1106-
it("should use 20% of context window as buffer when maxTokens is undefined", async () => {
1106+
it("should use ANTHROPIC_DEFAULT_MAX_TOKENS as buffer when maxTokens is undefined", async () => {
11071107
const modelInfo = createModelInfo(100000, undefined)
1108-
// Max tokens = 100000 - (100000 * 0.2) = 80000
1108+
// Max tokens = 100000 - ANTHROPIC_DEFAULT_MAX_TOKENS = 100000 - 8192 = 91808
11091109

11101110
// Create messages with very small content in the last one to avoid token overflow
11111111
const messagesWithSmallContent = [
@@ -1117,7 +1117,7 @@ describe("Sliding Window", () => {
11171117
// Below max tokens and buffer - no truncation
11181118
const result1 = await truncateConversationIfNeeded({
11191119
messages: messagesWithSmallContent,
1120-
totalTokens: 69999, // Well below threshold + dynamic buffer
1120+
totalTokens: 81807, // Well below threshold + dynamic buffer (91808 - 10000 = 81808)
11211121
contextWindow: modelInfo.contextWindow,
11221122
maxTokens: modelInfo.maxTokens,
11231123
apiHandler: mockApiHandler,
@@ -1132,13 +1132,13 @@ describe("Sliding Window", () => {
11321132
messages: messagesWithSmallContent,
11331133
summary: "",
11341134
cost: 0,
1135-
prevContextTokens: 69999,
1135+
prevContextTokens: 81807,
11361136
})
11371137

11381138
// Above max tokens - truncate
11391139
const result2 = await truncateConversationIfNeeded({
11401140
messages: messagesWithSmallContent,
1141-
totalTokens: 80001, // Above threshold
1141+
totalTokens: 81809, // Above threshold (81808)
11421142
contextWindow: modelInfo.contextWindow,
11431143
maxTokens: modelInfo.maxTokens,
11441144
apiHandler: mockApiHandler,
@@ -1153,7 +1153,7 @@ describe("Sliding Window", () => {
11531153
expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
11541154
expect(result2.summary).toBe("")
11551155
expect(result2.cost).toBe(0)
1156-
expect(result2.prevContextTokens).toBe(80001)
1156+
expect(result2.prevContextTokens).toBe(81809)
11571157
})
11581158

11591159
it("should handle small context windows appropriately", async () => {

src/core/sliding-window/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TelemetryService } from "@roo-code/telemetry"
55
import { ApiHandler } from "../../api"
66
import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
77
import { ApiMessage } from "../task-persistence/apiMessages"
8+
import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
89

910
/**
1011
* Default percentage of the context window to use as a buffer when deciding when to truncate
@@ -105,7 +106,7 @@ export async function truncateConversationIfNeeded({
105106
let error: string | undefined
106107
let cost = 0
107108
// Calculate the maximum tokens reserved for response
108-
const reservedTokens = maxTokens || contextWindow * 0.2
109+
const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS
109110

110111
// Estimate tokens for the last message (which is always a user message)
111112
const lastMessage = messages[messages.length - 1]

0 commit comments

Comments
 (0)