diff --git a/src/api/providers/__tests__/openai.test.ts b/src/api/providers/__tests__/openai.test.ts index 52d0c5c2bb..5b5da20f51 100644 --- a/src/api/providers/__tests__/openai.test.ts +++ b/src/api/providers/__tests__/openai.test.ts @@ -1,7 +1,5 @@ import { OpenAiHandler } from "../openai" import { ApiHandlerOptions } from "../../../shared/api" -import { ApiStream } from "../../transform/stream" -import OpenAI from "openai" import { Anthropic } from "@anthropic-ai/sdk" // Mock OpenAI client diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index e65b82ddef..9a14756f5d 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -10,6 +10,8 @@ import { import { ApiHandler, SingleCompletionHandler } from "../index" import { ApiStream } from "../transform/stream" +const ANTHROPIC_DEFAULT_TEMPERATURE = 0 + export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic @@ -44,7 +46,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { { model: modelId, max_tokens: this.getModel().info.maxTokens || 8192, - temperature: 0, + temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], // setting cache breakpoint for system prompt so new tasks can reuse it messages: messages.map((message, index) => { if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { @@ -96,7 +98,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { stream = (await this.client.messages.create({ model: modelId, max_tokens: this.getModel().info.maxTokens || 8192, - temperature: 0, + temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, system: [{ text: systemPrompt, type: "text" }], messages, // tools, @@ -179,7 +181,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { const response = await this.client.messages.create({ model: this.getModel().id, max_tokens: this.getModel().info.maxTokens || 8192, - temperature: 0, + temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, messages: [{ role: "user", content: prompt }], stream: false, }) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 0e90c2bcc4..8f897fda2a 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -11,6 +11,8 @@ import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, be import { ApiStream } from "../transform/stream" import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../transform/bedrock-converse-format" +const BEDROCK_DEFAULT_TEMPERATURE = 0.3 + // Define types for stream events based on AWS SDK export interface StreamEvent { messageStart?: { @@ -104,7 +106,7 @@ export class AwsBedrockHandler implements ApiHandler, SingleCompletionHandler { system: [{ text: systemPrompt }], inferenceConfig: { maxTokens: modelConfig.info.maxTokens || 5000, - temperature: 0.3, + temperature: this.options.modelTemperature ?? BEDROCK_DEFAULT_TEMPERATURE, topP: 0.1, ...(this.options.awsUsePromptCache ? { @@ -262,7 +264,7 @@ export class AwsBedrockHandler implements ApiHandler, SingleCompletionHandler { ]), inferenceConfig: { maxTokens: modelConfig.info.maxTokens || 5000, - temperature: 0.3, + temperature: this.options.modelTemperature ?? BEDROCK_DEFAULT_TEMPERATURE, topP: 0.1, }, } diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 0577a021e6..0d7179320c 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -5,6 +5,8 @@ import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, M import { convertAnthropicMessageToGemini } from "../transform/gemini-format" import { ApiStream } from "../transform/stream" +const GEMINI_DEFAULT_TEMPERATURE = 0 + export class GeminiHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: GoogleGenerativeAI @@ -23,7 +25,7 @@ export class GeminiHandler implements ApiHandler, SingleCompletionHandler { contents: messages.map(convertAnthropicMessageToGemini), generationConfig: { // maxOutputTokens: this.getModel().info.maxTokens, - temperature: 0, + temperature: this.options.modelTemperature ?? GEMINI_DEFAULT_TEMPERATURE, }, }) @@ -60,7 +62,7 @@ export class GeminiHandler implements ApiHandler, SingleCompletionHandler { const result = await model.generateContent({ contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { - temperature: 0, + temperature: this.options.modelTemperature ?? GEMINI_DEFAULT_TEMPERATURE, }, }) diff --git a/src/api/providers/glama.ts b/src/api/providers/glama.ts index 95b806f27c..72b41e5f58 100644 --- a/src/api/providers/glama.ts +++ b/src/api/providers/glama.ts @@ -5,7 +5,8 @@ import { ApiHandler, SingleCompletionHandler } from "../" import { ApiHandlerOptions, ModelInfo, glamaDefaultModelId, glamaDefaultModelInfo } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" -import delay from "delay" + +const GLAMA_DEFAULT_TEMPERATURE = 0 export class GlamaHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions @@ -79,7 +80,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } if (this.supportsTemperature()) { - requestOptions.temperature = 0 + requestOptions.temperature = this.options.modelTemperature ?? GLAMA_DEFAULT_TEMPERATURE } const { data: completion, response } = await this.client.chat.completions @@ -172,7 +173,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } if (this.supportsTemperature()) { - requestOptions.temperature = 0 + requestOptions.temperature = this.options.modelTemperature ?? GLAMA_DEFAULT_TEMPERATURE } if (this.getModel().id.startsWith("anthropic/")) { diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts index 81cec81b4d..7efa037f46 100644 --- a/src/api/providers/lmstudio.ts +++ b/src/api/providers/lmstudio.ts @@ -5,6 +5,8 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../.. import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" +const LMSTUDIO_DEFAULT_TEMPERATURE = 0 + export class LmStudioHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: OpenAI @@ -27,7 +29,7 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler { const stream = await this.client.chat.completions.create({ model: this.getModel().id, messages: openAiMessages, - temperature: 0, + temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE, stream: true, }) for await (const chunk of stream) { @@ -59,7 +61,7 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler { const response = await this.client.chat.completions.create({ model: this.getModel().id, messages: [{ role: "user", content: prompt }], - temperature: 0, + temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE, stream: false, }) return response.choices[0]?.message.content || "" diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index c4377f0003..9ce70a297c 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -14,6 +14,8 @@ import { import { convertToMistralMessages } from "../transform/mistral-format" import { ApiStream } from "../transform/stream" +const MISTRAL_DEFAULT_TEMPERATURE = 0 + export class MistralHandler implements ApiHandler { private options: ApiHandlerOptions private client: Mistral @@ -30,7 +32,7 @@ export class MistralHandler implements ApiHandler { const stream = await this.client.chat.stream({ model: this.getModel().id, // max_completion_tokens: this.getModel().info.maxTokens, - temperature: 0, + temperature: this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE, messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)], stream: true, }) diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts index 4175b78fa5..8c8e9ed8da 100644 --- a/src/api/providers/ollama.ts +++ b/src/api/providers/ollama.ts @@ -5,6 +5,9 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../.. import { convertToOpenAiMessages } from "../transform/openai-format" import { convertToR1Format } from "../transform/r1-format" import { ApiStream } from "../transform/stream" +import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./openai" + +const OLLAMA_DEFAULT_TEMPERATURE = 0 export class OllamaHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions @@ -20,7 +23,7 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler { async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const modelId = this.getModel().id - const useR1Format = modelId.toLowerCase().includes('deepseek-r1') + const useR1Format = modelId.toLowerCase().includes("deepseek-r1") const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "system", content: systemPrompt }, ...(useR1Format ? convertToR1Format(messages) : convertToOpenAiMessages(messages)), @@ -29,7 +32,7 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler { const stream = await this.client.chat.completions.create({ model: this.getModel().id, messages: openAiMessages, - temperature: 0, + temperature: this.options.modelTemperature ?? OLLAMA_DEFAULT_TEMPERATURE, stream: true, }) for await (const chunk of stream) { @@ -53,11 +56,15 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler { async completePrompt(prompt: string): Promise { try { const modelId = this.getModel().id - const useR1Format = modelId.toLowerCase().includes('deepseek-r1') + const useR1Format = modelId.toLowerCase().includes("deepseek-r1") const response = await this.client.chat.completions.create({ model: this.getModel().id, - messages: useR1Format ? convertToR1Format([{ role: "user", content: prompt }]) : [{ role: "user", content: prompt }], - temperature: 0, + messages: useR1Format + ? convertToR1Format([{ role: "user", content: prompt }]) + : [{ role: "user", content: prompt }], + temperature: + this.options.modelTemperature ?? + (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : OLLAMA_DEFAULT_TEMPERATURE), stream: false, }) return response.choices[0]?.message.content || "" diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index e4883b7a98..1a4f9e613a 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -11,6 +11,8 @@ import { import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" +const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0 + export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: OpenAI @@ -88,7 +90,7 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler ): ApiStream { const stream = await this.client.chat.completions.create({ model: modelId, - temperature: 0, + temperature: this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE, messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, @@ -189,7 +191,7 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler return { model: modelId, messages: [{ role: "user", content: prompt }], - temperature: 0, + temperature: this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE, } } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 44421a831d..cea500df26 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -17,6 +17,9 @@ export interface OpenAiHandlerOptions extends ApiHandlerOptions { defaultHeaders?: Record } +export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 +const OPENAI_DEFAULT_TEMPERATURE = 0 + export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { protected options: OpenAiHandlerOptions private client: OpenAI @@ -74,7 +77,9 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, - temperature: 0, + temperature: + this.options.modelTemperature ?? + (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : OPENAI_DEFAULT_TEMPERATURE), messages: convertedMessages, stream: true as const, stream_options: { include_usage: true }, diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 0e23c5d35d..1fcf25260e 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -6,6 +6,9 @@ import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefau import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream" import delay from "delay" +import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./openai" + +const OPENROUTER_DEFAULT_TEMPERATURE = 0 // Add custom interface for OpenRouter params type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { @@ -115,7 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { break } - let temperature = 0 + let defaultTemperature = OPENROUTER_DEFAULT_TEMPERATURE let topP: number | undefined = undefined // Handle models based on deepseek-r1 @@ -124,9 +127,8 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { this.getModel().id === "perplexity/sonar-reasoning" ) { // Recommended temperature for DeepSeek reasoning models - temperature = 0.6 - // DeepSeek highly recommends using user instead of system - // role + defaultTemperature = DEEP_SEEK_DEFAULT_TEMPERATURE + // DeepSeek highly recommends using user instead of system role openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) // Some provider support topP and 0.95 is value that Deepseek used in their benchmarks topP = 0.95 @@ -137,7 +139,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { const stream = await this.client.chat.completions.create({ model: this.getModel().id, max_tokens: maxTokens, - temperature: temperature, + temperature: this.options.modelTemperature ?? defaultTemperature, top_p: topP, messages: openAiMessages, stream: true, @@ -183,31 +185,35 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { // } } - await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready + // retry fetching generation details + let attempt = 0 + while (attempt++ < 10) { + await delay(200) // FIXME: necessary delay to ensure generation endpoint is ready + try { + const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, { + headers: { + Authorization: `Bearer ${this.options.openRouterApiKey}`, + }, + timeout: 5_000, // this request hangs sometimes + }) - try { - const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, { - headers: { - Authorization: `Bearer ${this.options.openRouterApiKey}`, - }, - timeout: 5_000, // this request hangs sometimes - }) - - const generation = response.data?.data - console.log("OpenRouter generation details:", response.data) - yield { - type: "usage", - // cacheWriteTokens: 0, - // cacheReadTokens: 0, - // openrouter generation endpoint fails often - inputTokens: generation?.native_tokens_prompt || 0, - outputTokens: generation?.native_tokens_completion || 0, - totalCost: generation?.total_cost || 0, - fullResponseText, - } as OpenRouterApiStreamUsageChunk - } catch (error) { - // ignore if fails - console.error("Error fetching OpenRouter generation details:", error) + const generation = response.data?.data + console.log("OpenRouter generation details:", response.data) + yield { + type: "usage", + // cacheWriteTokens: 0, + // cacheReadTokens: 0, + // openrouter generation endpoint fails often + inputTokens: generation?.native_tokens_prompt || 0, + outputTokens: generation?.native_tokens_completion || 0, + totalCost: generation?.total_cost || 0, + fullResponseText, + } as OpenRouterApiStreamUsageChunk + return + } catch (error) { + // ignore if fails + console.error("Error fetching OpenRouter generation details:", error) + } } } getModel(): { id: string; info: ModelInfo } { @@ -224,7 +230,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { const response = await this.client.chat.completions.create({ model: this.getModel().id, messages: [{ role: "user", content: prompt }], - temperature: 0, + temperature: this.options.modelTemperature ?? OPENROUTER_DEFAULT_TEMPERATURE, stream: false, }) diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index 46b286f5c5..0599ffa443 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -79,7 +79,7 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler { { model: this.getModel().id.split("/")[1], max_tokens: maxTokens, - temperature: 0, + temperature: this.options.modelTemperature ?? 0, messages: openAiMessages, stream: true, }, @@ -146,7 +146,7 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler { const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: this.getModel().id.split("/")[1], messages: [{ role: "user", content: prompt }], - temperature: 0, + temperature: this.options.modelTemperature ?? 0, } if (this.getModel().id.startsWith("anthropic/")) { diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts index 1ea68eaa4e..0ee22e5893 100644 --- a/src/api/providers/vertex.ts +++ b/src/api/providers/vertex.ts @@ -22,7 +22,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler { const stream = await this.client.messages.create({ model: this.getModel().id, max_tokens: this.getModel().info.maxTokens || 8192, - temperature: 0, + temperature: this.options.modelTemperature ?? 0, system: systemPrompt, messages, stream: true, @@ -89,7 +89,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler { const response = await this.client.messages.create({ model: this.getModel().id, max_tokens: this.getModel().info.maxTokens || 8192, - temperature: 0, + temperature: this.options.modelTemperature ?? 0, messages: [{ role: "user", content: prompt }], stream: false, }) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 83cdabd466..58be39a11b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -143,7 +143,7 @@ export class Cline { this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0 this.providerRef = new WeakRef(provider) this.diffViewProvider = new DiffViewProvider(cwd) - this.checkpointsEnabled = enableCheckpoints ?? false + this.checkpointsEnabled = process.platform !== "win32" && !!enableCheckpoints if (historyItem) { this.taskId = historyItem.id @@ -3240,6 +3240,7 @@ export class Cline { this.checkpointService = await CheckpointService.create({ taskId: this.taskId, baseDir: vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? "", + log: (message) => this.providerRef.deref()?.log(message), }) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 9f18b74480..9b1082664c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -126,6 +126,7 @@ type GlobalStateKey = | "requestyModelId" | "requestyModelInfo" | "unboundModelInfo" + | "modelTemperature" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -1619,6 +1620,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyApiKey, requestyModelId, requestyModelInfo, + modelTemperature, } = apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -1663,6 +1665,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("requestyApiKey", requestyApiKey) await this.updateGlobalState("requestyModelId", requestyModelId) await this.updateGlobalState("requestyModelInfo", requestyModelInfo) + await this.updateGlobalState("modelTemperature", modelTemperature) if (this.cline) { this.cline.api = buildApiHandler(apiConfiguration) } @@ -2513,6 +2516,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyApiKey, requestyModelId, requestyModelInfo, + modelTemperature, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -2592,6 +2596,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("requestyApiKey") as Promise, this.getGlobalState("requestyModelId") as Promise, this.getGlobalState("requestyModelInfo") as Promise, + this.getGlobalState("modelTemperature") as Promise, ]) let apiProvider: ApiProvider @@ -2653,6 +2658,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyApiKey, requestyModelId, requestyModelInfo, + modelTemperature, }, lastShownAnnouncementId, customInstructions, diff --git a/src/services/checkpoints/CheckpointService.ts b/src/services/checkpoints/CheckpointService.ts index cf27b5a33a..672edfd0cc 100644 --- a/src/services/checkpoints/CheckpointService.ts +++ b/src/services/checkpoints/CheckpointService.ts @@ -2,7 +2,6 @@ import fs from "fs/promises" import { existsSync } from "fs" import path from "path" -import debug from "debug" import simpleGit, { SimpleGit, CleanOptions } from "simple-git" export type CheckpointServiceOptions = { @@ -246,15 +245,11 @@ export class CheckpointService { } public static async create({ taskId, git, baseDir, log = console.log }: CheckpointServiceOptions) { - git = - git || - simpleGit({ - baseDir, - binary: "git", - maxConcurrentProcesses: 1, - config: [], - trimmed: true, - }) + if (process.platform === "win32") { + throw new Error("Checkpoints are not supported on Windows.") + } + + git = git || simpleGit({ baseDir }) const version = await git.version() diff --git a/src/services/checkpoints/__tests__/CheckpointService.test.ts b/src/services/checkpoints/__tests__/CheckpointService.test.ts index 9165da332a..caa0952fa1 100644 --- a/src/services/checkpoints/__tests__/CheckpointService.test.ts +++ b/src/services/checkpoints/__tests__/CheckpointService.test.ts @@ -14,6 +14,7 @@ describe("CheckpointService", () => { let git: SimpleGit let testFile: string let service: CheckpointService + let originalPlatform: string const initRepo = async ({ baseDir, @@ -48,6 +49,19 @@ describe("CheckpointService", () => { return { git, testFile } } + beforeAll(() => { + originalPlatform = process.platform + Object.defineProperty(process, "platform", { + value: "darwin", + }) + }) + + afterAll(() => { + Object.defineProperty(process, "platform", { + value: originalPlatform, + }) + }) + beforeEach(async () => { const baseDir = path.join(os.tmpdir(), `checkpoint-service-test-${Date.now()}`) const repo = await initRepo({ baseDir }) diff --git a/src/shared/api.ts b/src/shared/api.ts index 93ebaed9bc..7e926b09cf 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -65,6 +65,7 @@ export interface ApiHandlerOptions { requestyApiKey?: string requestyModelId?: string requestyModelInfo?: ModelInfo + modelTemperature?: number } export type ApiConfiguration = ApiHandlerOptions & { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 2848fb7fd5..47b4e10e21 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from "react" import { useEvent, useInterval } from "react-use" import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { TemperatureControl } from "./TemperatureControl" import * as vscodemodels from "vscode" import { @@ -1390,6 +1391,18 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = )} +
+ { + handleInputChange("modelTemperature")({ + target: { value }, + }) + }} + maxValue={2} + /> +
+ {modelIdErrorMessage && (

{ )} -

-
- ⚠️ - { - setCheckpointsEnabled(e.target.checked) + {process.platform !== "win32" && ( +
+
+ ⚠️ + { + setCheckpointsEnabled(e.target.checked) + }}> + Enable experimental checkpoints + +
+

- Enable experimental checkpoints - + When enabled, Roo will save a checkpoint whenever a file in the workspace is + modified, added or deleted, letting you easily revert to a previous state. +

-

- When enabled, Roo will save a checkpoint whenever a file in the workspace is modified, - added or deleted, letting you easily revert to a previous state. -

-
+ )} {Object.entries(experimentConfigsMap) .filter((config) => config[0] !== "DIFF_STRATEGY") diff --git a/webview-ui/src/components/settings/TemperatureControl.tsx b/webview-ui/src/components/settings/TemperatureControl.tsx new file mode 100644 index 0000000000..422356bf69 --- /dev/null +++ b/webview-ui/src/components/settings/TemperatureControl.tsx @@ -0,0 +1,80 @@ +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { useEffect, useState } from "react" + +interface TemperatureControlProps { + value: number | undefined + onChange: (value: number | undefined) => void + maxValue?: number // Some providers like OpenAI use 0-2 range +} + +export const TemperatureControl = ({ value, onChange, maxValue = 1 }: TemperatureControlProps) => { + const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined) + const [inputValue, setInputValue] = useState(value?.toString() ?? "0") + + // Sync internal state with prop changes when switching profiles + useEffect(() => { + const hasCustomTemperature = value !== undefined + setIsCustomTemperature(hasCustomTemperature) + setInputValue(value?.toString() ?? "0") + }, [value]) + + return ( +
+ { + const isChecked = e.target.checked + setIsCustomTemperature(isChecked) + if (!isChecked) { + onChange(undefined) // Unset the temperature + } else if (value !== undefined) { + onChange(value) // Use the value from apiConfiguration, if set + } + }}> + Use custom temperature + + +

+ Controls randomness in the model's responses. +

+ + {isCustomTemperature && ( +
+
+ setInputValue(e.target.value)} + onBlur={(e) => { + const newValue = parseFloat(e.target.value) + if (!isNaN(newValue) && newValue >= 0 && newValue <= maxValue) { + onChange(newValue) + setInputValue(newValue.toString()) + } else { + setInputValue(value?.toString() ?? "0") // Reset to last valid value + } + }} + style={{ + width: "60px", + padding: "4px 8px", + border: "1px solid var(--vscode-input-border)", + background: "var(--vscode-input-background)", + color: "var(--vscode-input-foreground)", + }} + /> +
+

+ Higher values make output more random, lower values make it more deterministic. +

+
+ )} +
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/TemperatureControl.test.tsx b/webview-ui/src/components/settings/__tests__/TemperatureControl.test.tsx new file mode 100644 index 0000000000..d178cfafbc --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/TemperatureControl.test.tsx @@ -0,0 +1,86 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { TemperatureControl } from "../TemperatureControl" + +describe("TemperatureControl", () => { + it("renders with default temperature disabled", () => { + const onChange = jest.fn() + render() + + const checkbox = screen.getByRole("checkbox") + expect(checkbox).not.toBeChecked() + expect(screen.queryByRole("textbox")).not.toBeInTheDocument() + }) + + it("renders with custom temperature enabled", () => { + const onChange = jest.fn() + render() + + const checkbox = screen.getByRole("checkbox") + expect(checkbox).toBeChecked() + + const input = screen.getByRole("textbox") + expect(input).toBeInTheDocument() + expect(input).toHaveValue("0.7") + }) + + it("updates when checkbox is toggled", () => { + const onChange = jest.fn() + render() + + const checkbox = screen.getByRole("checkbox") + + // Uncheck - should clear temperature + fireEvent.click(checkbox) + expect(onChange).toHaveBeenCalledWith(undefined) + + // Check - should restore previous temperature + fireEvent.click(checkbox) + expect(onChange).toHaveBeenCalledWith(0.7) + }) + + it("updates temperature when input loses focus", () => { + const onChange = jest.fn() + render() + + const input = screen.getByRole("textbox") + fireEvent.change(input, { target: { value: "0.8" } }) + fireEvent.blur(input) + + expect(onChange).toHaveBeenCalledWith(0.8) + }) + + it("respects maxValue prop", () => { + const onChange = jest.fn() + render() + + const input = screen.getByRole("textbox") + + // Valid value within max + fireEvent.change(input, { target: { value: "1.8" } }) + fireEvent.blur(input) + expect(onChange).toHaveBeenCalledWith(1.8) + + // Invalid value above max + fireEvent.change(input, { target: { value: "2.5" } }) + fireEvent.blur(input) + expect(input).toHaveValue("1.5") // Should revert to original value + expect(onChange).toHaveBeenCalledTimes(1) // Should not call onChange for invalid value + }) + + it("syncs checkbox state when value prop changes", () => { + const onChange = jest.fn() + const { rerender } = render() + + // Initially checked + const checkbox = screen.getByRole("checkbox") + expect(checkbox).toBeChecked() + + // Update to undefined + rerender() + expect(checkbox).not.toBeChecked() + + // Update back to a value + rerender() + expect(checkbox).toBeChecked() + }) +}) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index edbaac40a0..19b13e2c6c 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,4 +1,9 @@ -import { ApiConfiguration, glamaDefaultModelId, openRouterDefaultModelId, unboundDefaultModelId } from "../../../src/shared/api" +import { + ApiConfiguration, + glamaDefaultModelId, + openRouterDefaultModelId, + unboundDefaultModelId, +} from "../../../src/shared/api" import { ModelInfo } from "../../../src/shared/api" export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { if (apiConfiguration) {