Skip to content

Commit 74fd8b4

Browse files
authored
Merge branch 'RooCodeInc:main' into main
2 parents ab1f9fc + 93f88b4 commit 74fd8b4

40 files changed

+999
-21
lines changed

apps/web-roo-code/src/app/evals/evals.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function Evals({
6161
<div className="flex flex-col gap-4">
6262
<div>
6363
Roo Code tests each frontier model against{" "}
64-
<a href="https://github.com/cte/evals/" className="underline">
64+
<a href="https://github.com/RooCodeInc/Roo-Code-Evals" className="underline">
6565
a suite of hundreds of exercises
6666
</a>{" "}
6767
across 5 programming languages with varying difficulty. These results can help you find the right

packages/types/src/provider-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,18 @@ export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
5353
* ProviderSettings
5454
*/
5555

56+
/**
57+
* Default value for consecutive mistake limit
58+
*/
59+
export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
60+
5661
const baseProviderSettingsSchema = z.object({
5762
includeMaxTokens: z.boolean().optional(),
5863
diffEnabled: z.boolean().optional(),
5964
fuzzyMatchThreshold: z.number().optional(),
6065
modelTemperature: z.number().nullish(),
6166
rateLimitSeconds: z.number().optional(),
67+
consecutiveMistakeLimit: z.number().min(0).optional(),
6268

6369
// Model reasoning.
6470
enableReasoningEffort: z.boolean().optional(),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, test, expect } from "vitest"
2+
import { convertModelNameForVertex, getClaudeCodeModelId } from "../claude-code.js"
3+
4+
describe("convertModelNameForVertex", () => {
5+
test("should convert hyphen-date format to @date format", () => {
6+
expect(convertModelNameForVertex("claude-sonnet-4-20250514")).toBe("claude-sonnet-4@20250514")
7+
expect(convertModelNameForVertex("claude-opus-4-20250514")).toBe("claude-opus-4@20250514")
8+
expect(convertModelNameForVertex("claude-3-7-sonnet-20250219")).toBe("claude-3-7-sonnet@20250219")
9+
expect(convertModelNameForVertex("claude-3-5-sonnet-20241022")).toBe("claude-3-5-sonnet@20241022")
10+
expect(convertModelNameForVertex("claude-3-5-haiku-20241022")).toBe("claude-3-5-haiku@20241022")
11+
})
12+
13+
test("should not modify models without date pattern", () => {
14+
expect(convertModelNameForVertex("some-other-model")).toBe("some-other-model")
15+
expect(convertModelNameForVertex("claude-model")).toBe("claude-model")
16+
expect(convertModelNameForVertex("model-with-short-date-123")).toBe("model-with-short-date-123")
17+
})
18+
19+
test("should only convert 8-digit date patterns at the end", () => {
20+
expect(convertModelNameForVertex("claude-20250514-sonnet")).toBe("claude-20250514-sonnet")
21+
expect(convertModelNameForVertex("model-20250514-with-more")).toBe("model-20250514-with-more")
22+
})
23+
})
24+
25+
describe("getClaudeCodeModelId", () => {
26+
test("should return original model when useVertex is false", () => {
27+
expect(getClaudeCodeModelId("claude-sonnet-4-20250514", false)).toBe("claude-sonnet-4-20250514")
28+
expect(getClaudeCodeModelId("claude-opus-4-20250514", false)).toBe("claude-opus-4-20250514")
29+
expect(getClaudeCodeModelId("claude-3-7-sonnet-20250219", false)).toBe("claude-3-7-sonnet-20250219")
30+
})
31+
32+
test("should return converted model when useVertex is true", () => {
33+
expect(getClaudeCodeModelId("claude-sonnet-4-20250514", true)).toBe("claude-sonnet-4@20250514")
34+
expect(getClaudeCodeModelId("claude-opus-4-20250514", true)).toBe("claude-opus-4@20250514")
35+
expect(getClaudeCodeModelId("claude-3-7-sonnet-20250219", true)).toBe("claude-3-7-sonnet@20250219")
36+
})
37+
38+
test("should default to useVertex false when parameter not provided", () => {
39+
expect(getClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-20250514")
40+
})
41+
})

packages/types/src/providers/claude-code.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
11
import type { ModelInfo } from "../model.js"
22
import { anthropicModels } from "./anthropic.js"
33

4+
// Regex pattern to match 8-digit date at the end of model names
5+
const VERTEX_DATE_PATTERN = /-(\d{8})$/
6+
7+
/**
8+
* Converts Claude model names from hyphen-date format to Vertex AI's @-date format.
9+
*
10+
* @param modelName - The original model name (e.g., "claude-sonnet-4-20250514")
11+
* @returns The converted model name for Vertex AI (e.g., "claude-sonnet-4@20250514")
12+
*
13+
* @example
14+
* convertModelNameForVertex("claude-sonnet-4-20250514") // returns "claude-sonnet-4@20250514"
15+
* convertModelNameForVertex("claude-model") // returns "claude-model" (no change)
16+
*/
17+
export function convertModelNameForVertex(modelName: string): string {
18+
// Convert hyphen-date format to @date format for Vertex AI
19+
return modelName.replace(VERTEX_DATE_PATTERN, "@$1")
20+
}
21+
422
// Claude Code
523
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
624
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
725
export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 8000
26+
27+
/**
28+
* Gets the appropriate model ID based on whether Vertex AI is being used.
29+
*
30+
* @param baseModelId - The base Claude Code model ID
31+
* @param useVertex - Whether to format the model ID for Vertex AI (default: false)
32+
* @returns The model ID, potentially formatted for Vertex AI
33+
*
34+
* @example
35+
* getClaudeCodeModelId("claude-sonnet-4-20250514", true) // returns "claude-sonnet-4@20250514"
36+
* getClaudeCodeModelId("claude-sonnet-4-20250514", false) // returns "claude-sonnet-4-20250514"
37+
*/
38+
export function getClaudeCodeModelId(baseModelId: ClaudeCodeModelId, useVertex = false): string {
39+
return useVertex ? convertModelNameForVertex(baseModelId) : baseModelId
40+
}
41+
842
export const claudeCodeModels = {
943
"claude-sonnet-4-20250514": {
1044
...anthropicModels["claude-sonnet-4-20250514"],

packages/types/src/telemetry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ export const taskPropertiesSchema = z.object({
8989
modelId: z.string().optional(),
9090
diffStrategy: z.string().optional(),
9191
isSubtask: z.boolean().optional(),
92+
todos: z
93+
.object({
94+
total: z.number(),
95+
completed: z.number(),
96+
inProgress: z.number(),
97+
pending: z.number(),
98+
})
99+
.optional(),
92100
})
93101

94102
export const gitPropertiesSchema = z.object({

src/api/providers/claude-code.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { Anthropic } from "@anthropic-ai/sdk"
2-
import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels, type ModelInfo } from "@roo-code/types"
2+
import {
3+
claudeCodeDefaultModelId,
4+
type ClaudeCodeModelId,
5+
claudeCodeModels,
6+
type ModelInfo,
7+
getClaudeCodeModelId,
8+
} from "@roo-code/types"
39
import { type ApiHandler } from ".."
410
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
511
import { runClaudeCode } from "../../integrations/claude-code/run"
@@ -20,11 +26,17 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
2026
// Filter out image blocks since Claude Code doesn't support them
2127
const filteredMessages = filterMessagesForClaudeCode(messages)
2228

29+
const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1"
30+
const model = this.getModel()
31+
32+
// Validate that the model ID is a valid ClaudeCodeModelId
33+
const modelId = model.id in claudeCodeModels ? (model.id as ClaudeCodeModelId) : claudeCodeDefaultModelId
34+
2335
const claudeProcess = runClaudeCode({
2436
systemPrompt,
2537
messages: filteredMessages,
2638
path: this.options.claudeCodePath,
27-
modelId: this.getModel().id,
39+
modelId: getClaudeCodeModelId(modelId, useVertex),
2840
maxOutputTokens: this.options.claudeCodeMaxOutputTokens,
2941
})
3042

src/api/providers/fetchers/__tests__/litellm.spec.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,132 @@ describe("getLiteLLMModels", () => {
3939
})
4040
})
4141

42+
it("handles base URLs with a path correctly", async () => {
43+
const mockResponse = {
44+
data: {
45+
data: [],
46+
},
47+
}
48+
49+
mockedAxios.get.mockResolvedValue(mockResponse)
50+
51+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm")
52+
53+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", {
54+
headers: {
55+
Authorization: "Bearer test-api-key",
56+
"Content-Type": "application/json",
57+
...DEFAULT_HEADERS,
58+
},
59+
timeout: 5000,
60+
})
61+
})
62+
63+
it("handles base URLs with a path and trailing slash correctly", async () => {
64+
const mockResponse = {
65+
data: {
66+
data: [],
67+
},
68+
}
69+
70+
mockedAxios.get.mockResolvedValue(mockResponse)
71+
72+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm/")
73+
74+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", {
75+
headers: {
76+
Authorization: "Bearer test-api-key",
77+
"Content-Type": "application/json",
78+
...DEFAULT_HEADERS,
79+
},
80+
timeout: 5000,
81+
})
82+
})
83+
84+
it("handles base URLs with double slashes correctly", async () => {
85+
const mockResponse = {
86+
data: {
87+
data: [],
88+
},
89+
}
90+
91+
mockedAxios.get.mockResolvedValue(mockResponse)
92+
93+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm//")
94+
95+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", {
96+
headers: {
97+
Authorization: "Bearer test-api-key",
98+
"Content-Type": "application/json",
99+
...DEFAULT_HEADERS,
100+
},
101+
timeout: 5000,
102+
})
103+
})
104+
105+
it("handles base URLs with query parameters correctly", async () => {
106+
const mockResponse = {
107+
data: {
108+
data: [],
109+
},
110+
}
111+
112+
mockedAxios.get.mockResolvedValue(mockResponse)
113+
114+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm?key=value")
115+
116+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info?key=value", {
117+
headers: {
118+
Authorization: "Bearer test-api-key",
119+
"Content-Type": "application/json",
120+
...DEFAULT_HEADERS,
121+
},
122+
timeout: 5000,
123+
})
124+
})
125+
126+
it("handles base URLs with fragments correctly", async () => {
127+
const mockResponse = {
128+
data: {
129+
data: [],
130+
},
131+
}
132+
133+
mockedAxios.get.mockResolvedValue(mockResponse)
134+
135+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm#section")
136+
137+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info#section", {
138+
headers: {
139+
Authorization: "Bearer test-api-key",
140+
"Content-Type": "application/json",
141+
...DEFAULT_HEADERS,
142+
},
143+
timeout: 5000,
144+
})
145+
})
146+
147+
it("handles base URLs with port and no path correctly", async () => {
148+
const mockResponse = {
149+
data: {
150+
data: [],
151+
},
152+
}
153+
154+
mockedAxios.get.mockResolvedValue(mockResponse)
155+
156+
await getLiteLLMModels("test-api-key", "http://localhost:4000")
157+
158+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", {
159+
headers: {
160+
Authorization: "Bearer test-api-key",
161+
"Content-Type": "application/json",
162+
...DEFAULT_HEADERS,
163+
},
164+
timeout: 5000,
165+
})
166+
})
167+
42168
it("successfully fetches and formats LiteLLM models", async () => {
43169
const mockResponse = {
44170
data: {

src/api/providers/fetchers/litellm.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
2424
headers["Authorization"] = `Bearer ${apiKey}`
2525
}
2626
// Use URL constructor to properly join base URL and path
27-
const url = new URL("/v1/model/info", baseUrl).href
27+
// This approach handles all edge cases including paths, query params, and fragments
28+
const urlObj = new URL(baseUrl)
29+
// Normalize the pathname by removing trailing slashes and multiple slashes
30+
urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/model/info"
31+
const url = urlObj.href
2832
// Added timeout to prevent indefinite hanging
2933
const response = await axios.get(url, { headers, timeout: 5000 })
3034
const models: ModelRecord = {}

src/core/config/ProviderSettingsManager.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type ProviderSettingsEntry,
66
providerSettingsSchema,
77
providerSettingsSchemaDiscriminated,
8+
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
89
} from "@roo-code/types"
910
import { TelemetryService } from "@roo-code/telemetry"
1011

@@ -26,6 +27,7 @@ export const providerProfilesSchema = z.object({
2627
rateLimitSecondsMigrated: z.boolean().optional(),
2728
diffSettingsMigrated: z.boolean().optional(),
2829
openAiHeadersMigrated: z.boolean().optional(),
30+
consecutiveMistakeLimitMigrated: z.boolean().optional(),
2931
})
3032
.optional(),
3133
})
@@ -48,6 +50,7 @@ export class ProviderSettingsManager {
4850
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
4951
diffSettingsMigrated: true, // Mark as migrated on fresh installs
5052
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
53+
consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs
5154
},
5255
}
5356

@@ -113,6 +116,7 @@ export class ProviderSettingsManager {
113116
rateLimitSecondsMigrated: false,
114117
diffSettingsMigrated: false,
115118
openAiHeadersMigrated: false,
119+
consecutiveMistakeLimitMigrated: false,
116120
} // Initialize with default values
117121
isDirty = true
118122
}
@@ -135,6 +139,12 @@ export class ProviderSettingsManager {
135139
isDirty = true
136140
}
137141

142+
if (!providerProfiles.migrations.consecutiveMistakeLimitMigrated) {
143+
await this.migrateConsecutiveMistakeLimit(providerProfiles)
144+
providerProfiles.migrations.consecutiveMistakeLimitMigrated = true
145+
isDirty = true
146+
}
147+
138148
if (isDirty) {
139149
await this.store(providerProfiles)
140150
}
@@ -228,6 +238,18 @@ export class ProviderSettingsManager {
228238
}
229239
}
230240

241+
private async migrateConsecutiveMistakeLimit(providerProfiles: ProviderProfiles) {
242+
try {
243+
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
244+
if (apiConfig.consecutiveMistakeLimit == null) {
245+
apiConfig.consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
246+
}
247+
}
248+
} catch (error) {
249+
console.error(`[MigrateConsecutiveMistakeLimit] Failed to migrate consecutive mistake limit:`, error)
250+
}
251+
}
252+
231253
/**
232254
* List all available configs with metadata.
233255
*/

0 commit comments

Comments
 (0)