Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ import {
} from "./providers"
import { NativeOllamaHandler } from "./providers/native-ollama"

/**
* Options for completePrompt — unified with ApiHandlerCreateMessageMetadata.
* Uses abortSignal (not signal) to match the metadata pattern used in stream path.
*/
export interface CompletePromptOptions extends Pick<ApiHandlerCreateMessageMetadata, "abortSignal"> {
/** Optional timeout override (ms) — falls back to provider default if omitted */
timeoutMs?: number
}

export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
completePrompt(prompt: string, metadata?: CompletePromptOptions): Promise<string>
}

export interface ApiHandlerCreateMessageMetadata {
Expand Down
79 changes: 79 additions & 0 deletions src/api/providers/__tests__/fake-ai.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Anthropic } from "@anthropic-ai/sdk"

import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../../index"
import { FakeAIHandler } from "../fake-ai"

const modelInfo = {
contextWindow: 8192,
maxTokens: 4096,
supportsImages: false,
supportsPromptCache: false,
}

describe("FakeAIHandler", () => {
it("should delegate completePrompt with options to the cached FakeAI instance", async () => {
const completePrompt = vitest.fn().mockResolvedValue("delegated response")
const fakeAi: {
id: string
createMessage: () => AsyncGenerator<never, void, unknown>
getModel: () => { id: string; info: typeof modelInfo }
countTokens: ReturnType<typeof vitest.fn>
completePrompt: typeof completePrompt
removeFromCache?: () => void
} = {
id: "fake-ai-completePrompt-delegation",
createMessage: async function* () {},
getModel: () => ({ id: "fake-model", info: modelInfo }),
countTokens: vitest.fn().mockResolvedValue(0),
completePrompt,
}
const controller = new AbortController()
const options: CompletePromptOptions = { abortSignal: controller.signal, timeoutMs: 1234 }

const handler = new FakeAIHandler({ fakeAi })
const result = await handler.completePrompt("Test prompt", options)

expect(result).toBe("delegated response")
expect(completePrompt).toHaveBeenCalledWith("Test prompt", options)
fakeAi.removeFromCache?.()
})

it("should delegate createMessage, getModel, and countTokens to FakeAI", async () => {
const metadata = { taskId: "task-1" } as ApiHandlerCreateMessageMetadata
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }]
const createMessage = vitest.fn(async function* () {
yield { type: "text" as const, text: "Hello" }
})
const getModel = vitest.fn(() => ({ id: "fake-model", info: modelInfo }))
const countTokens = vitest.fn().mockResolvedValue(7)
const fakeAi: {
id: string
createMessage: typeof createMessage
getModel: typeof getModel
countTokens: typeof countTokens
completePrompt: ReturnType<typeof vitest.fn>
removeFromCache?: () => void
} = {
id: "fake-ai-handler-delegation",
createMessage,
getModel,
countTokens,
completePrompt: vitest.fn().mockResolvedValue("complete"),
}

const handler = new FakeAIHandler({ fakeAi })
const chunks = []
for await (const chunk of handler.createMessage("System", messages, metadata)) {
chunks.push(chunk)
}

expect(chunks).toEqual([{ type: "text", text: "Hello" }])
expect(createMessage).toHaveBeenCalledWith("System", messages, metadata)
expect(handler.getModel()).toEqual({ id: "fake-model", info: modelInfo })
expect(getModel).toHaveBeenCalledTimes(1)
await expect(handler.countTokens(content)).resolves.toBe(7)
expect(countTokens).toHaveBeenCalledWith(content)
fakeAi.removeFromCache?.()
})
})
4 changes: 2 additions & 2 deletions src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {

import { BaseProvider } from "./base-provider"
import { parseVertexJsonCredentials } from "./utils/vertex-credentials"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
Expand Down Expand Up @@ -270,7 +270,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
}
}

async completePrompt(prompt: string) {
async completePrompt(prompt: string, options?: CompletePromptOptions) {
try {
const {
id,
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { getAnthropicProviderReasoning } from "../transform/reasoning"
import { handleProviderError } from "./utils/error-handler"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { calculateApiCostAnthropic } from "../../shared/cost"
import {
convertOpenAIToolsToAnthropic,
Expand Down Expand Up @@ -429,7 +429,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
}
}

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

let message
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/base-openai-compatible-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { TagMatcher } from "../../utils/tag-matcher"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"

import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import { handleOpenAIError } from "./utils/error-handler"
Expand Down Expand Up @@ -212,7 +212,7 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
const { id: modelId, info: modelInfo } = this.getModel()

const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor
import { getModelParams } from "../transform/model-params"
import { shouldUseReasoningBudget } from "../../shared/api"
import { normalizeToolSchema } from "../../utils/json-schema"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

/************************************************************************************
*
Expand Down Expand Up @@ -798,7 +798,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
try {
const modelConfig = this.getModel()

Expand Down
13 changes: 9 additions & 4 deletions src/api/providers/fake-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"

import type { ModelInfo } from "@roo-code/types"

import type { ApiHandler, SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type {
ApiHandler,
SingleCompletionHandler,
ApiHandlerCreateMessageMetadata,
CompletePromptOptions,
} from "../index"
import type { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"

Expand All @@ -28,7 +33,7 @@ interface FakeAI {
): ApiStream
getModel(): { id: string; info: ModelInfo }
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number>
completePrompt(prompt: string): Promise<string>
completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string>
}

/**
Expand Down Expand Up @@ -75,7 +80,7 @@ export class FakeAIHandler implements ApiHandler, SingleCompletionHandler {
return this.ai.countTokens(content)
}

completePrompt(prompt: string): Promise<string> {
return this.ai.completePrompt(prompt)
completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
return this.ai.completePrompt(prompt, options)
}
}
4 changes: 2 additions & 2 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { t } from "i18next"
import type { ApiStream, GroundingSource } from "../transform/stream"
import { getModelParams } from "../transform/model-params"

import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { BaseProvider } from "./base-provider"
import { parseVertexJsonCredentials } from "./utils/vertex-credentials"

Expand Down Expand Up @@ -576,7 +576,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
return citationLinks.join(", ")
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
const { id: model, info } = this.getModel()

try {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/kenari.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"

import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { RouterProvider } from "./router-provider"
import { extractReasoningFromDelta } from "./utils/extract-reasoning"

Expand Down Expand Up @@ -123,7 +123,7 @@ export class KenariHandler extends RouterProvider implements SingleCompletionHan
* @returns The model's reply text, or an empty string if no content is returned.
* @throws Error with a Kenari-specific prefix if the request fails.
*/
async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
const { id: modelId, info } = await this.fetchModel()

try {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/lite-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { GEMINI_THOUGHT_SIGNATURE_BYPASS } from "../transform/gemini-format"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"

import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { RouterProvider } from "./router-provider"
import { extractReasoningFromDelta } from "./utils/extract-reasoning"

Expand Down Expand Up @@ -311,7 +311,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
const { id: modelId, info } = await this.fetchModel()

// Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/lm-studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { getModelsFromCache } from "./fetchers/modelCache"
import { handleOpenAIError } from "./utils/error-handler"

Expand Down Expand Up @@ -187,7 +187,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
try {
// Create params object with optional draft model
const params: any = {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getModelParams } from "../transform/model-params"
import { mergeEnvironmentDetailsForMiniMax } from "../transform/minimax-format"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { calculateApiCostAnthropic } from "../../shared/cost"
import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"

Expand Down Expand Up @@ -289,7 +289,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
}
}

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

const message = await this.client.messages.create({
Expand Down
5 changes: 2 additions & 3 deletions src/api/providers/mistral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ApiStream } from "../transform/stream"
import { handleProviderError } from "./utils/error-handler"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

// Type helper to handle thinking chunks from Mistral API
// The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union
Expand Down Expand Up @@ -192,8 +192,7 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand

return { id, info, maxTokens, temperature }
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
const { id: model, temperature } = this.getModel()

try {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/native-ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { BaseProvider } from "./base-provider"
import type { ApiHandlerOptions } from "../../shared/api"
import { getOllamaModels } from "./fetchers/ollama"
import { TagMatcher } from "../../utils/tag-matcher"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

interface OllamaChatOptions {
temperature: number
Expand Down Expand Up @@ -550,7 +550,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
try {
const client = this.ensureClient()
const { id: modelId } = await this.fetchModel()
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { isMcpTool } from "../../utils/mcp-name"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth"
Expand Down Expand Up @@ -1154,7 +1154,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
return this.lastResponseId
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
this.abortController = new AbortController()

try {
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/openai-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"

import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

/**
* Configuration options for creating an OpenAI-compatible provider.
Expand Down Expand Up @@ -197,7 +197,7 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si
/**
* Complete a prompt using the AI SDK generateText.
*/
async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
const languageModel = this.getLanguageModel()

const { text } = await generateText({
Expand Down
12 changes: 5 additions & 7 deletions src/api/providers/openai-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"

import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
import { isMcpTool } from "../../utils/mcp-name"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"

Expand Down Expand Up @@ -1484,14 +1484,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
getResponseId(): string | undefined {
return this.lastResponseId
}

async completePrompt(prompt: string): Promise<string> {
// Create AbortController for cancellation
this.abortController = new AbortController()

async completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
try {
this.abortController = new AbortController()

const model = this.getModel()
const { verbosity, reasoning } = model
const { verbosity } = model

// Resolve reasoning effort for models that support it
const reasoningEffort = this.getReasoningEffort(model)
Expand Down
Loading
Loading