Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/fix-multiline-quoted-command-parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ Quote masking is comment-aware: a quote character inside a `#` comment is not pa

**Pattern selector (UI)**: the command pattern breakdown shown after execution now uses the same heredoc- and quote-aware parser (`parseCommand`) before extracting patterns, so an unterminated or terminated heredoc no longer produces spurious tokens like `EOF`, body-line words, or `<<` fragments in the allow/deny selector.

Note: this change only prevents *auto-approval* of fragments from a malformed command; it does not reject malformed commands before execution, which will be addressed in a separate PR to keep the scope focused here.
Note: this change only prevents _auto-approval_ of fragments from a malformed command; it does not reject malformed commands before execution, which will be addressed in a separate PR to keep the scope focused here.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ for this exact support, so if you are having problems or if you have question, j
- [简体中文](locales/zh-CN/README.md)
- [繁體中文](locales/zh-TW/README.md)
- ...
</details>
</details>

---

Expand Down
20 changes: 20 additions & 0 deletions src/api/providers/__tests__/unbound.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,24 @@ describe("UnboundHandler", () => {
}),
)
})

it("completePrompt returns the response text", async () => {
const mockCreate = (OpenAI as unknown as any)().chat.completions.create
mockCreate.mockResolvedValue({
choices: [{ message: { content: "completed text" } }],
})

const handler = new UnboundHandler({
unboundApiKey: "test-key",
unboundModelId: "openai/gpt-4o",
})

const result = await handler.completePrompt("Write a haiku")
expect(result).toBe("completed text")
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
messages: [{ role: "system", content: "Write a haiku" }],
}),
)
})
})
6 changes: 3 additions & 3 deletions src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
let { id, info, temperature, maxTokens, reasoning: thinking, betas } = this.getModel()
const { id, info, temperature, maxTokens, reasoning: thinking, betas } = this.getModel()

const { supportsPromptCache } = info

Expand Down Expand Up @@ -210,7 +210,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple

getModel() {
const modelId = this.options.apiModelId
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
const id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
let info: ModelInfo = vertexModels[id]

// Check if 1M context beta should be enabled for supported models
Expand Down Expand Up @@ -270,7 +270,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple

async completePrompt(prompt: string) {
try {
let {
const {
id,
info: { supportsPromptCache },
temperature,
Expand Down
6 changes: 3 additions & 3 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
): ApiStream {
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let {
const {
id: modelId,
betas = ["fine-grained-tool-streaming-2025-05-14"],
maxTokens,
Expand Down Expand Up @@ -352,7 +352,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa

getModel() {
const modelId = this.options.apiModelId
let id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId
const id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId
let info: ModelInfo = anthropicModels[id]

// If 1M context beta is enabled for supported models, update the model info
Expand Down Expand Up @@ -398,7 +398,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
}

async completePrompt(prompt: string) {
let { id: model, temperature } = this.getModel()
const { id: model, temperature } = this.getModel()

let message
try {
Expand Down
15 changes: 9 additions & 6 deletions src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
constructor(options: ProviderSettings) {
super()
this.options = options
let region = this.options.awsRegion
const region = this.options.awsRegion

// process the various user input options, be opinionated about the intent of the options
// and determine the model to use during inference and for cost calculations
Expand Down Expand Up @@ -591,8 +591,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
//so that pricing, context window, caching etc have values that can be used
//However, we want to keep the id of the model to be the ID for the router for
//subsequent requests so they are sent back through the router
let invokedArnInfo = this.parseArn(streamEvent.trace.promptRouter.invokedModelId)
let invokedModel = this.getModelById(invokedArnInfo.modelId as string, invokedArnInfo.modelType)
const invokedArnInfo = this.parseArn(streamEvent.trace.promptRouter.invokedModelId)
const invokedModel = this.getModelById(
invokedArnInfo.modelId as string,
invokedArnInfo.modelType,
)
if (invokedModel) {
invokedModel.id = modelConfig.id
this.costModelConfig = invokedModel
Expand Down Expand Up @@ -934,7 +937,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}

// Get cache point placements
let strategy = new MultiPointStrategy(config)
const strategy = new MultiPointStrategy(config)
const cacheResult = strategy.determineOptimalCachePoints()

// Store cache point placements for future use if conversation ID is provided
Expand Down Expand Up @@ -998,7 +1001,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
*/

const arnRegex = /^arn:[^:]+:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^\/]+)\/([\w\.\-:]+)|([^\/]+))$/
let match = arn.match(arnRegex)
const match = arn.match(arnRegex)

if (match && match[1] && match[3] && match[4]) {
// Create the result object
Expand All @@ -1025,7 +1028,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// Check if the original model ID had a region prefix
if (originalModelId && result.modelId !== originalModelId) {
// If the model ID changed after parsing, it had a region prefix
let prefix = originalModelId.replace(result.modelId, "")
const prefix = originalModelId.replace(result.modelId, "")
result.crossRegionInference = AwsBedrockHandler.isSystemInferenceProfile(prefix)
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/fake-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface FakeAI {
*
* We use the ID to lookup the original FakeAI object in the mapping.
*/
let fakeAiMap: Map<string, FakeAI> = new Map()
const fakeAiMap: Map<string, FakeAI> = new Map()

export class FakeAIHandler implements ApiHandler, SingleCompletionHandler {
private ai: FakeAI
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/fetchers/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function getOllamaModels(

const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { headers })
const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
let modelInfoPromises = []
const modelInfoPromises = []

if (parsedResponse.success) {
for (const ollamaModel of parsedResponse.data.models) {
Expand Down
4 changes: 3 additions & 1 deletion src/api/providers/fetchers/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export async function getOpencodeGoModels(apiKey?: string): Promise<Record<strin
const data = Array.isArray(rawData) ? rawData : []

if (!result.success) {
console.warn(`Opencode Go models response did not match expected schema; falling back to per-item parsing: ${JSON.stringify(result.error.format())}`)
console.warn(
`Opencode Go models response did not match expected schema; falling back to per-item parsing: ${JSON.stringify(result.error.format())}`,
)
}

for (const rawModel of data) {
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// Bill both completion and reasoning ("thoughts") tokens as output.
const billedOutputTokens = outputTokens + reasoningTokens

let cacheReadCost = cacheReadTokens > 0 ? cacheReadsPrice * (cacheReadTokens / 1_000_000) : 0
const cacheReadCost = cacheReadTokens > 0 ? cacheReadsPrice * (cacheReadTokens / 1_000_000) : 0

const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
const outputTokensCost = outputPrice * (billedOutputTokens / 1_000_000)
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/lite-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
}

// Required by some providers; others default to max tokens allowed
let maxTokens: number | undefined = info.maxTokens ?? undefined
const maxTokens: number | undefined = info.maxTokens ?? undefined

// Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens
const isGPT5Model = this.isGpt5(modelId)
Expand Down
3 changes: 1 addition & 2 deletions src/api/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
const { id: modelId, info, maxTokens, temperature } = this.getModel()

Expand Down Expand Up @@ -113,7 +112,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
tool_choice: convertOpenAIToolChoice(metadata?.tool_choice),
}

stream = await this.client.messages.create(requestParams)
const stream = await this.client.messages.create(requestParams)

let inputTokens = 0
let outputTokens = 0
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
override getModel() {
const modelId = this.options.apiModelId

let id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId

const info: ModelInfo = openAiCodexModels[id]

Expand Down
6 changes: 3 additions & 3 deletions src/api/providers/openai-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,8 +674,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
const decoder = new TextDecoder()
let buffer = ""
let hasContent = false
let totalInputTokens = 0
let totalOutputTokens = 0
const totalInputTokens = 0
const totalOutputTokens = 0

try {
while (true) {
Expand Down Expand Up @@ -1435,7 +1435,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
override getModel() {
const modelId = this.options.apiModelId

let id =
const id =
modelId && modelId in openAiNativeModels ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId

const info: ModelInfo = openAiNativeModels[id]
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
}

async completePrompt(prompt: string) {
let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
const { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()

const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/requesty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
async completePrompt(prompt: string): Promise<string> {
const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()

let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]

const completionParams: RequestyChatCompletionParams = {
model,
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/unbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class UnboundHandler extends BaseProvider implements SingleCompletionHand
async completePrompt(prompt: string): Promise<string> {
const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()

let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]

const completionParams: UnboundChatCompletionParams = {
model,
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand

override getModel() {
const modelId = this.options.apiModelId
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
const id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
let info: ModelInfo = vertexModels[id]
const params = getModelParams({
format: "gemini",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,8 @@ describe("Cache Strategy", () => {
convertToBedrockConverseMessagesMock.lastConfig = config

// Create a strategy based on the config
let strategy
// Use MultiPointStrategy for all cases
strategy = new MultiPointStrategy(config as any)
const strategy = new MultiPointStrategy(config as any)

// Store the result
const result = strategy.determineOptimalCachePoints()
Expand Down Expand Up @@ -484,9 +483,8 @@ describe("Cache Strategy", () => {
convertToBedrockConverseMessagesMock.lastConfig = config

// Create a strategy based on the config
let strategy
// Use MultiPointStrategy for all cases
strategy = new MultiPointStrategy(config as any)
const strategy = new MultiPointStrategy(config as any)

// Store the result
const result = strategy.determineOptimalCachePoints()
Expand Down
2 changes: 1 addition & 1 deletion src/api/transform/cache-strategy/multi-point-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class MultiPointStrategy extends CacheStrategy {

const placements = this.determineMessageCachePoints(minTokensPerPoint, remainingCachePoints)
const messages = this.messagesToContentBlocks(this.config.messages)
let cacheResult = this.formatResult(systemBlocks, this.applyCachePoints(messages, placements))
const cacheResult = this.formatResult(systemBlocks, this.applyCachePoints(messages, placements))

// Store the placements for future use (to maintain consistency across consecutive messages)
// This needs to be handled by the caller by passing these placements back in the next call
Expand Down
2 changes: 1 addition & 1 deletion src/api/transform/caching/vercel-ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function addCacheBreakpoints(systemPrompt: string, messages: OpenAI.Chat.

if (Array.isArray(msg.content)) {
// Find the last text part in the message content
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()

if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
// @ts-ignore-next-line
Expand Down
2 changes: 1 addition & 1 deletion src/api/transform/model-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function getModelParams({
let temperature = customTemperature ?? model.defaultTemperature ?? defaultTemperature
let reasoningBudget: ModelParams["reasoningBudget"] = undefined
let reasoningEffort: ModelParams["reasoningEffort"] = undefined
let verbosity: VerbosityLevel | undefined = customVerbosity
const verbosity: VerbosityLevel | undefined = customVerbosity

if (shouldUseReasoningBudget({ model, settings })) {
// Check if this is a Gemini 2.5 Pro model
Expand Down
4 changes: 2 additions & 2 deletions src/api/transform/openai-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export function convertToOpenAiMessages(
)

// Process tool result messages FIRST since they must follow the tool use messages
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
const toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
Expand Down Expand Up @@ -462,7 +462,7 @@ export function convertToOpenAiMessages(
}

// Process tool use messages
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: normalizeId(toolMessage.id),
type: "function",
function: {
Expand Down
1 change: 0 additions & 1 deletion src/core/auto-approval/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { parseCommand } from "../../shared/parse-command"


/**
* Detect dangerous parameter substitutions that could lead to command execution.
* These patterns are never auto-approved and always require explicit user approval.
Expand Down
2 changes: 1 addition & 1 deletion src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ export class CustomModesManager {
? path.join(baseDir, `rules-${slug}`)
: path.join(baseDir, ".roo", `rules-${slug}`)

let rulesFiles: RuleFile[] = []
const rulesFiles: RuleFile[] = []
try {
const stats = await fs.stat(modeRulesDir)
if (stats.isDirectory()) {
Expand Down
4 changes: 2 additions & 2 deletions src/core/config/ProviderSettingsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ export class ProviderSettingsManager {
existingNames.delete(existingName)

// Handle name conflict
let finalName = cloudName
const finalName = cloudName
if (existingNames.has(cloudName)) {
// There's a conflict - rename the existing non-cloud profile
const conflictingProfile = providerProfiles.apiConfigs[cloudName]
Expand Down Expand Up @@ -847,7 +847,7 @@ export class ProviderSettingsManager {
// If name is the same and profile hasn't changed, do nothing
} else {
// Step 4: Add new cloud profile
let finalName = cloudName
const finalName = cloudName

// Handle name conflict with existing non-cloud profile
if (existingNames.has(cloudName)) {
Expand Down
Loading
Loading