From e6421037f2b3295f35d823c205951d4b35c6bc70 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Wed, 3 Dec 2025 16:13:28 -0600 Subject: [PATCH 01/14] Made changes for adding responses suppport --- proto/cline/models.proto | 4 + src/core/api/providers/oca.ts | 214 +++++++++++++++++- .../controller/models/refreshOcaModels.ts | 11 +- src/services/auth/oca/utils/constants.ts | 3 + src/shared/api.ts | 2 + .../models/api-configuration-conversion.ts | 4 + .../models/typeConversion.ts | 2 + 7 files changed, 237 insertions(+), 3 deletions(-) diff --git a/proto/cline/models.proto b/proto/cline/models.proto index 159afb81f86..f005c93e99e 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -380,6 +380,10 @@ message OcaModelInfo { string model_name = 17; // The API format used by this model optional ApiFormat api_format = 18; + // Supports responses api? + optional bool supports_responses_api = 19; + // Supports chat completions api? + optional bool supports_chat_api = 20; } // Aggregated OCA model catalog keyed by model identifier diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 7b02d7ee88c..0c229210351 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -1,4 +1,4 @@ -import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api" +import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api" import OpenAI, { APIError, OpenAIError } from "openai" import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" @@ -9,18 +9,20 @@ import { } from "@/services/auth/oca/utils/constants" import { createOcaHeaders } from "@/services/auth/oca/utils/utils" import { Logger } from "@/services/logging/Logger" +import { OcaModelInfo } from "@/shared/api" import { ClineStorageMessage } from "@/shared/messages/content" import { fetch } from "@/shared/net" import { ApiHandler, type CommonApiHandlerOptions } from ".." import { withRetry } from "../retry" import { convertToOpenAiMessages } from "../transform/openai-format" +import { convertToOpenAIResponsesInput } from "../transform/openai-response-format" import { ApiStream } from "../transform/stream" import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor" export interface OcaHandlerOptions extends CommonApiHandlerOptions { ocaBaseUrl?: string ocaModelId?: string - ocaModelInfo?: LiteLLMModelInfo + ocaModelInfo?: OcaModelInfo thinkingBudgetTokens?: number ocaUsePromptCache?: boolean taskId?: string @@ -140,6 +142,18 @@ export class OcaHandler implements ApiHandler { @withRetry() async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + // const USE_RESPONSES_API = true; + if (this.options.ocaModelInfo?.supportsResponsesApi) { + // if (this.options.ocaModelInfo?.supportsChatApi) { + // if (USE_RESPONSES_API) { + yield* this.createMessageResponsesApi(systemPrompt, messages, tools) + } else { + yield* this.createMessageChatApi(systemPrompt, messages, tools) + } + } + + async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + console.log("Using Chat API") const client = this.ensureClient() const formattedMessages = convertToOpenAiMessages(messages) const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { @@ -270,6 +284,202 @@ export class OcaHandler implements ApiHandler { } } + async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + console.log("Using Responses API") + const client = this.ensureClient() + + // Convert messages to Responses API input format + const input = convertToOpenAIResponsesInput(messages) + + // Convert ChatCompletion tools to Responses API format if provided + const responseTools = tools + ?.filter((tool) => tool.type === "function") + .map((tool: any) => ({ + type: "function" as const, + name: tool.function.name, + description: tool.function.description, + parameters: tool.function.parameters, + strict: tool.function.strict ?? true, // Responses API defaults to strict mode + reasoning: { effort: "medium", summary: "auto" }, + })) + + Logger.debug("OCA Responses Input: " + JSON.stringify(input)) + + // Create the response using Responses API + const stream = await client.responses.create({ + model: this.options.ocaModelId || liteLlmDefaultModelId, + instructions: systemPrompt, + input, + stream: true, + tools: responseTools, + }) + + // Process the response stream + for await (const chunk of stream) { + Logger.debug("OCA Responses Chunk: " + JSON.stringify(chunk)) + + console.log("Chunk: ", chunk) + + // Handle different event types from Responses API + if (chunk.type === "response.output_item.added") { + const item = chunk.item + if (item.type === "function_call" && item.id) { + yield { + type: "tool_calls", + id: item.id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, + }, + }, + } + } + if (item.type === "reasoning" && item.encrypted_content && item.id) { + yield { + type: "reasoning", + id: item.id, + reasoning: "", + redacted_data: item.encrypted_content, + } + } + } + if (chunk.type === "response.output_item.done") { + const item = chunk.item + if (item.type === "function_call") { + yield { + type: "tool_calls", + id: item.id || item.call_id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, + }, + }, + } + } + if (item.type === "reasoning") { + yield { + type: "reasoning", + id: item.id, + details: item.summary, + reasoning: "", + } + } + } + if (chunk.type === "response.reasoning_summary_part.added") { + yield { + type: "reasoning", + id: chunk.item_id, + reasoning: chunk.part.text, + } + } + if (chunk.type === "response.reasoning_summary_text.delta") { + yield { + type: "reasoning", + id: chunk.item_id, + reasoning: chunk.delta, + } + } + if (chunk.type === "response.reasoning_summary_part.done") { + yield { + type: "reasoning", + id: chunk.item_id, + details: chunk.part, + reasoning: "", + } + } + if (chunk.type === "response.output_text.delta") { + // Handle text content deltas + if (chunk.delta) { + yield { + id: chunk.item_id, + type: "text", + text: chunk.delta, + } + } + } + if (chunk.type === "response.reasoning_text.delta") { + // Handle reasoning content deltas + if (chunk.delta) { + yield { + id: chunk.item_id, + type: "reasoning", + reasoning: chunk.delta, + } + } + } + if (chunk.type === "response.function_call_arguments.delta") { + yield { + type: "tool_calls", + tool_call: { + function: { + id: chunk.item_id, + name: chunk.item_id, + arguments: chunk.delta, + }, + }, + } + } + if (chunk.type === "response.function_call_arguments.done") { + // Handle completed function call + if (chunk.item_id && chunk.name && chunk.arguments) { + yield { + type: "tool_calls", + tool_call: { + function: { + id: chunk.item_id, + name: chunk.name, + arguments: chunk.arguments, + }, + }, + } + } + } + + if ( + chunk.type === "response.incomplete" && + chunk.response?.status === "incomplete" && + chunk.response?.incomplete_details?.reason === "max_output_tokens" + ) { + console.log("Ran out of tokens") + if (chunk.response?.output_text?.length > 0) { + console.log("Partial output:", chunk.response.output_text) + } else { + console.log("Ran out of tokens during reasoning") + } + } + + if (chunk.type === "response.completed" && chunk.response?.usage) { + // Handle usage information when response is complete + const usage = chunk.response.usage + const inputTokens = usage.input_tokens || 0 + const outputTokens = usage.output_tokens || 0 + const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 + const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 + const totalTokens = usage.total_tokens || 0 + Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) + const inputCost = (await this.calculateCost(1e6, 0)) || 0 + const outputCost = (await this.calculateCost(0, 1e6)) || 0 + const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 + const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) + yield { + type: "usage", + inputTokens: nonCachedInputTokens, + outputTokens: outputTokens, + cacheWriteTokens: cacheWriteTokens, + cacheReadTokens: cacheReadTokens, + totalCost: totalCost, + id: chunk.response.id, + } + } + } + } + getModel() { return { id: this.options.ocaModelId || liteLlmDefaultModelId, diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index 1dc4c12b1d8..561cb87755d 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -3,7 +3,12 @@ import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models import axios from "axios" import { HostProvider } from "@/hosts/host-provider" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" -import { DEFAULT_EXTERNAL_OCA_BASE_URL, DEFAULT_INTERNAL_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" +import { + CHAT_COMPLETIONS_API, + DEFAULT_EXTERNAL_OCA_BASE_URL, + DEFAULT_INTERNAL_OCA_BASE_URL, + RESPONSES_API, +} from "@/services/auth/oca/utils/constants" import { createOcaHeaders } from "@/services/auth/oca/utils/utils" import { Logger } from "@/services/logging/Logger" import { getAxiosSettings } from "@/shared/net" @@ -50,6 +55,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe } for (const model of response.data.data) { const modelId = model.litellm_params?.model + console.log("Model: ", modelId, "Supported list: ", model.model_info.supported_api_list) if (typeof modelId !== "string" || !modelId) { continue } @@ -57,6 +63,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe defaultModelId = modelId } const modelInfo = model.model_info + const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API] models[modelId] = OcaModelInfo.create({ maxTokens: model.litellm_params?.max_tokens || -1, contextWindow: modelInfo.context_window, @@ -73,6 +80,8 @@ export async function refreshOcaModels(controller: Controller, request: StringRe temperature: modelInfo.temperature || 0, banner: modelInfo.banner, modelName: modelId, + supportsChatApi: supportedApiList.includes(CHAT_COMPLETIONS_API), + supportsResponsesApi: supportedApiList.includes(RESPONSES_API), }) } console.log("OCA models fetched", models) diff --git a/src/services/auth/oca/utils/constants.ts b/src/services/auth/oca/utils/constants.ts index 4a5d6539b52..50079bc5976 100644 --- a/src/services/auth/oca/utils/constants.ts +++ b/src/services/auth/oca/utils/constants.ts @@ -15,3 +15,6 @@ export const DEFAULT_EXTERNAL_IDSC_SCOPES = "openid offline_access" export const DEFAULT_EXTERNAL_OCA_BASE_URL = "https://code.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm" export const OCI_HEADER_OPC_REQUEST_ID = "opc-request-id" + +export const CHAT_COMPLETIONS_API = "CHAT_COMPLETIONS" +export const RESPONSES_API = "RESPONSES" diff --git a/src/shared/api.ts b/src/shared/api.ts index 06efe27524a..34026fc884e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -276,6 +276,8 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo { surveyId?: string banner?: string surveyContent?: string + supportsChatApi?: boolean + supportsResponsesApi?: boolean } export const CLAUDE_SONNET_1M_SUFFIX = ":1m" diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index a3731eb5bca..3980eababd1 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -108,6 +108,8 @@ function convertOcaModelInfoToProtoOcaModelInfo(info: OcaModelInfo | undefined): surveyId: info.surveyId, banner: info.banner, modelName: info.modelName, + supportsChatApi: info.supportsChatApi, + supportsResponsesApi: info.supportsResponsesApi, } } @@ -131,6 +133,8 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi surveyId: info.surveyId, banner: info.banner, modelName: info.modelName, + supportsChatApi: info.supportsChatApi, + supportsResponsesApi: info.supportsResponsesApi, } } diff --git a/src/shared/proto-conversions/models/typeConversion.ts b/src/shared/proto-conversions/models/typeConversion.ts index 31cedcbd64e..9aeaa0c12b6 100644 --- a/src/shared/proto-conversions/models/typeConversion.ts +++ b/src/shared/proto-conversions/models/typeConversion.ts @@ -144,6 +144,8 @@ export function fromProtobufOcaModelInfo(protoInfo: ProtoOcaModelInfo): OcaModel surveyId: protoInfo.surveyId, banner: protoInfo.banner, surveyContent: protoInfo.surveyContent, + supportsChatApi: protoInfo.supportsChatApi, + supportsResponsesApi: protoInfo.supportsResponsesApi, } } From 1071f0cb8993c88e4b5fc7af1db00f81f72692a9 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Wed, 3 Dec 2025 16:27:34 -0600 Subject: [PATCH 02/14] removed some logs --- src/core/api/providers/oca.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 0c229210351..0ae0d9150a4 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -318,8 +318,6 @@ export class OcaHandler implements ApiHandler { for await (const chunk of stream) { Logger.debug("OCA Responses Chunk: " + JSON.stringify(chunk)) - console.log("Chunk: ", chunk) - // Handle different event types from Responses API if (chunk.type === "response.output_item.added") { const item = chunk.item From daac2eefcd2aee3510099004bbbb432486f2c922 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Wed, 10 Dec 2025 16:49:06 -0600 Subject: [PATCH 03/14] Made change to disallow format --- src/core/api/transform/openai-response-format.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/api/transform/openai-response-format.ts b/src/core/api/transform/openai-response-format.ts index 6c8e6872642..ffa7edba60b 100644 --- a/src/core/api/transform/openai-response-format.ts +++ b/src/core/api/transform/openai-response-format.ts @@ -215,7 +215,7 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): // Flush any remaining user message content if (messageContent.length > 0) { - allItems.push({ role: m.role, content: [...messageContent] }) + allItems.push({ role: m.role, content: [messageContent[0]] }) } } } From e873cdbf7d6405e0deaac7fd84f12e15fdc537fb Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Mon, 15 Dec 2025 10:08:24 -0600 Subject: [PATCH 04/14] Added logging for cline --- src/core/api/providers/oca.ts | 31 +++++++++++-- src/core/task/focus-chain/prompts.ts | 16 ++++--- src/shared/net.ts | 67 +++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 0ae0d9150a4..53b454b1a31 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -207,7 +207,7 @@ export class OcaHandler implements ApiHandler { const toolCallProcessor = new ToolCallProcessor() - const stream = await client.chat.completions.create({ + const chatCompletionParams = { model: this.options.ocaModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, @@ -220,7 +220,31 @@ export class OcaHandler implements ApiHandler { litellm_session_id: `cline-${this.options.taskId}`, ...getOpenAIToolParams(tools), }), // Add session ID for LiteLLM tracking - }) + } + + // Log the exact API request payload for debugging + console.log( + "[OCA Chat API Request]:", + JSON.stringify( + { + ...chatCompletionParams, + messages: chatCompletionParams.messages.map((msg) => ({ + role: msg.role, + content: + typeof msg.content === "string" + ? msg.content.substring(0, 200) + (msg.content.length > 200 ? "..." : "") + : msg.content, + // Don't log full content to avoid cluttering logs + })), + }, + null, + 2, + ), + ) + + const stream = (await client.chat.completions.create( + chatCompletionParams, + )) as AsyncIterable const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 @@ -303,7 +327,7 @@ export class OcaHandler implements ApiHandler { reasoning: { effort: "medium", summary: "auto" }, })) - Logger.debug("OCA Responses Input: " + JSON.stringify(input)) + // Logger.debug("OCA Responses Input: " + JSON.stringify(input)) // Create the response using Responses API const stream = await client.responses.create({ @@ -312,6 +336,7 @@ export class OcaHandler implements ApiHandler { input, stream: true, tools: responseTools, + reasoning: { effort: "medium", summary: "auto" }, }) // Process the response stream diff --git a/src/core/task/focus-chain/prompts.ts b/src/core/task/focus-chain/prompts.ts index dffbec1d979..432bbee5f1a 100644 --- a/src/core/task/focus-chain/prompts.ts +++ b/src/core/task/focus-chain/prompts.ts @@ -86,13 +86,19 @@ Reminder on how to use the task_progress parameter: ${reminder}` -const recommended = ` -# task_progress RECOMMENDED +// const recommended = ` +// # task_progress RECOMMENDED -When starting a new task, it is recommended to include a todo list using the task_progress parameter. +// When starting a new task, it is recommended to include a todo list using the task_progress parameter. -${listInstructionsRecommended} -` +// const recommended = ` +// # task_progress RECOMMENDED + +// For new tasks, consider including a todo list via the task_progress parameter in tool calls.\n\nKey elements:\n1. Develop a comprehensive checklist outlining all required steps.\n2. Format using Markdown: - [ ] for incomplete items, - [x] for completed ones.\n\n**Advantages of a task_progress list:**\n\t- Provides a clear implementation roadmap.\n\t- Enables ongoing progress tracking.\n\t- Prevents overlooking any steps.\n\t- Allows users to review, monitor, and adjust the plan as needed.\n\n**Sample checklist format:**\n\`\`\`\n- [ ] Analyze requirements\n- [ ] Set up necessary files\n- [ ] Implement main functionality\n- [ ] Handle edge cases\n- [ ] Test the implementation\n- [ ] Verify results\n\`\`\`\n\nA well-maintained task_progress list supports effective progress tracking and comprehensive task coverage.\n + +// ` + +const recommended = "" const apiRequestCount = ` # task_progress diff --git a/src/shared/net.ts b/src/shared/net.ts index d1a6ed32bdb..b6c9306d47d 100644 --- a/src/shared/net.ts +++ b/src/shared/net.ts @@ -94,6 +94,7 @@ */ import { EnvHttpProxyAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici" +import { Logger } from "@/services/logging/Logger" let mockFetch: typeof globalThis.fetch | undefined @@ -121,7 +122,71 @@ export const fetch: typeof globalThis.fetch = (() => { baseFetch = undiciFetch as any as typeof globalThis.fetch } - return (input: string | URL | Request, init?: RequestInit): Promise => (mockFetch || baseFetch)(input, init) + return async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + const method = init?.method || (input instanceof Request ? input.method : "GET") + + // Log request details + console.log(`[Cline Network Request] ${method} ${url}`) + console.log(init?.body) + if (init?.headers) { + console.log( + "[Cline Network Request Headers]:", + JSON.stringify(Object.fromEntries(Object.entries(init.headers).map(([k, v]) => [k, String(v)])), null, 2), + ) + } + if (init?.body && typeof init.body === "string") { + try { + // Try to parse as JSON for pretty printing + console.log(init.body) + Logger.debug("body: " + init?.body) + const parsed = JSON.parse(init.body) + console.log("[Cline Network Request Body]:", JSON.stringify(parsed, null, 2)) + } catch { + // If not JSON, log as string + console.log("[Cline Network Request Body]:", init.body) + } + } else if (init?.body) { + console.log("[Cline Network Request Body]:", "[Binary data or FormData]") + } + + const startTime = Date.now() + const response = await (mockFetch || baseFetch)(input, init) + const duration = Date.now() - startTime + + // Clone the response to read its body without consuming it + const clonedResponse = response.clone() + + // Log response details + console.log(`[Cline Network Response] ${response.status} ${response.statusText} (${duration}ms)`) + + // Log response headers + const responseHeaders: Record = {} + clonedResponse.headers.forEach((value, key) => { + responseHeaders[key] = value + }) + console.log("[Cline Network Response Headers]:", JSON.stringify(responseHeaders, null, 2)) + + // Try to log response body for JSON responses + try { + const contentType = clonedResponse.headers.get("content-type") + if (contentType?.includes("application/json")) { + const responseText = await clonedResponse.text() + try { + const parsed = JSON.parse(responseText) + console.log("[Cline Network Response Body]:", JSON.stringify(parsed, null, 2)) + } catch { + console.log("[Cline Network Response Body]:", responseText) + } + } else { + console.log("[Cline Network Response Body]:", "[Non-JSON content or streaming response]") + } + } catch (error) { + console.log("[Cline Network Response Body]:", "[Error reading response body]", error) + } + + return response + } })() /** From 83a8a80ef8b4f2ae7c559d095ae75af8eb45c218 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Mon, 15 Dec 2025 12:41:46 -0600 Subject: [PATCH 05/14] Fixed codex prompts --- src/utils/model-utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/model-utils.ts b/src/utils/model-utils.ts index 99307aeb8ed..f0876c48931 100644 --- a/src/utils/model-utils.ts +++ b/src/utils/model-utils.ts @@ -16,6 +16,7 @@ export function isNextGenModelProvider(providerInfo: ApiProviderInfo): boolean { "openai-native", "baseten", "vercel-ai-gateway", + "oca", ].some((id) => providerId === id) } From f9315125eb06049c24a4b230a2eb805f89279c99 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 16 Dec 2025 17:01:09 -0600 Subject: [PATCH 06/14] Made changes to make cline work --- proto/cline/models.proto | 28 +- proto/cline/state.proto | 1 + src/core/api/index.ts | 1 + src/core/api/providers/oca.ts | 276 +++++++++--------- src/core/api/providers/openai-native.ts | 2 +- .../api/transform/openai-response-format.ts | 9 +- src/core/controller/index.ts | 2 + .../controller/models/refreshOcaModels.ts | 1 + src/core/controller/state/updateSettings.ts | 4 + src/core/storage/StateManager.ts | 9 + src/core/storage/utils/state-helpers.ts | 6 + src/core/task/index.ts | 4 + src/shared/ExtensionMessage.ts | 1 + src/shared/api.ts | 4 + .../models/api-configuration-conversion.ts | 7 + .../models/typeConversion.ts | 1 + src/shared/storage/state-keys.ts | 3 + .../settings/providers/OcaModelPicker.tsx | 87 +++++- 18 files changed, 299 insertions(+), 147 deletions(-) diff --git a/proto/cline/models.proto b/proto/cline/models.proto index f005c93e99e..23b7328e1a6 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -384,6 +384,8 @@ message OcaModelInfo { optional bool supports_responses_api = 19; // Supports chat completions api? optional bool supports_chat_api = 20; + // Supports reasoning + optional bool supports_reasoning = 21; } // Aggregated OCA model catalog keyed by model identifier @@ -606,12 +608,13 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130; optional string plan_mode_oca_model_id = 131; optional OcaModelInfo plan_mode_oca_model_info = 132; - optional string plan_mode_hicap_model_id = 133; - optional OpenRouterModelInfo plan_mode_hicap_model_info = 134; - optional string plan_mode_aihubmix_model_id = 135; - optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136; - optional string plan_mode_nous_research_model_id = 137; - optional string gemini_plan_mode_thinking_level = 138; + optional string plan_mode_oca_reasoning_effort = 133; + optional string plan_mode_hicap_model_id = 134; + optional OpenRouterModelInfo plan_mode_hicap_model_info = 135; + optional string plan_mode_aihubmix_model_id = 136; + optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137; + optional string plan_mode_nous_research_model_id = 138; + optional string gemini_plan_mode_thinking_level = 139; // Act mode configurations optional ApiProvider act_mode_api_provider = 200; @@ -647,10 +650,11 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230; optional string act_mode_oca_model_id = 231; optional OcaModelInfo act_mode_oca_model_info = 232; - optional string act_mode_hicap_model_id = 233; - optional OpenRouterModelInfo act_mode_hicap_model_info = 234; - optional string act_mode_aihubmix_model_id = 235; - optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236; - optional string act_mode_nous_research_model_id = 237; - optional string gemini_act_mode_thinking_level = 238; + optional string act_mode_oca_reasoning_effort = 233; + optional string act_mode_hicap_model_id = 234; + optional OpenRouterModelInfo act_mode_hicap_model_info = 235; + optional string act_mode_aihubmix_model_id = 236; + optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237; + optional string act_mode_nous_research_model_id = 238; + optional string gemini_act_mode_thinking_level = 239; } diff --git a/proto/cline/state.proto b/proto/cline/state.proto index 8c0e8b1a487..de59965f3a2 100644 --- a/proto/cline/state.proto +++ b/proto/cline/state.proto @@ -369,6 +369,7 @@ message UpdateSettingsRequest { optional OnboardingModelGroup onboarding_models = 33; optional bool cline_web_tools_enabled = 34; optional bool enable_parallel_tool_calling = 35; + optional string oca_reasoning_effort = 36; } message UpdateTerminalConnectionTimeoutRequest { diff --git a/src/core/api/index.ts b/src/core/api/index.ts index af4879d22a8..60a54349de5 100644 --- a/src/core/api/index.ts +++ b/src/core/api/index.ts @@ -393,6 +393,7 @@ function createHandlerForProvider( ocaBaseUrl: options.ocaBaseUrl, ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId, ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo, + ocaReasoningEffort: mode === "plan" ? options.planModeOcaReasoningEffort : options.actModeOcaReasoningEffort, thinkingBudgetTokens: mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens, ocaUsePromptCache: diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 53b454b1a31..04ae2cee70b 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -23,6 +23,7 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions { ocaBaseUrl?: string ocaModelId?: string ocaModelInfo?: OcaModelInfo + ocaReasoningEffort?: string thinkingBudgetTokens?: number ocaUsePromptCache?: boolean taskId?: string @@ -313,7 +314,7 @@ export class OcaHandler implements ApiHandler { const client = this.ensureClient() // Convert messages to Responses API input format - const input = convertToOpenAIResponsesInput(messages) + const input = convertToOpenAIResponsesInput(systemPrompt, messages) // Convert ChatCompletion tools to Responses API format if provided const responseTools = tools @@ -329,177 +330,190 @@ export class OcaHandler implements ApiHandler { // Logger.debug("OCA Responses Input: " + JSON.stringify(input)) - // Create the response using Responses API - const stream = await client.responses.create({ + const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = { model: this.options.ocaModelId || liteLlmDefaultModelId, - instructions: systemPrompt, input, stream: true, tools: responseTools, - reasoning: { effort: "medium", summary: "auto" }, - }) + } - // Process the response stream - for await (const chunk of stream) { - Logger.debug("OCA Responses Chunk: " + JSON.stringify(chunk)) + if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) { + responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" } + } - // Handle different event types from Responses API - if (chunk.type === "response.output_item.added") { - const item = chunk.item - if (item.type === "function_call" && item.id) { - yield { - type: "tool_calls", - id: item.id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, + // Create the response using Responses API + const stream = await client.responses.create(responsesParams) + + try { + // Process the response stream + for await (const chunk of stream) { + Logger.debug("OCA Responses Chunk: " + JSON.stringify(chunk)) + + // Skip undefined chunks + if (!chunk) { + continue + } + + // Handle different event types from Responses API + if (chunk.type === "response.output_item.added") { + const item = chunk.item + if (item.type === "function_call" && item.id) { + yield { + type: "tool_calls", + id: item.id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, + }, }, - }, + } } - } - if (item.type === "reasoning" && item.encrypted_content && item.id) { - yield { - type: "reasoning", - id: item.id, - reasoning: "", - redacted_data: item.encrypted_content, + if (item.type === "reasoning" && item.encrypted_content && item.id) { + yield { + type: "reasoning", + id: item.id, + reasoning: "", + redacted_data: item.encrypted_content, + } } } - } - if (chunk.type === "response.output_item.done") { - const item = chunk.item - if (item.type === "function_call") { - yield { - type: "tool_calls", - id: item.id || item.call_id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, + if (chunk.type === "response.output_item.done") { + const item = chunk.item + if (item.type === "function_call") { + yield { + type: "tool_calls", + id: item.id || item.call_id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, + }, }, - }, + } + } + if (item.type === "reasoning") { + yield { + type: "reasoning", + id: item.id, + details: item.summary, + reasoning: "", + } } } - if (item.type === "reasoning") { + if (chunk.type === "response.reasoning_summary_part.added") { yield { type: "reasoning", - id: item.id, - details: item.summary, - reasoning: "", + id: chunk.item_id, + reasoning: chunk.part.text, } } - } - if (chunk.type === "response.reasoning_summary_part.added") { - yield { - type: "reasoning", - id: chunk.item_id, - reasoning: chunk.part.text, - } - } - if (chunk.type === "response.reasoning_summary_text.delta") { - yield { - type: "reasoning", - id: chunk.item_id, - reasoning: chunk.delta, - } - } - if (chunk.type === "response.reasoning_summary_part.done") { - yield { - type: "reasoning", - id: chunk.item_id, - details: chunk.part, - reasoning: "", - } - } - if (chunk.type === "response.output_text.delta") { - // Handle text content deltas - if (chunk.delta) { + if (chunk.type === "response.reasoning_summary_text.delta") { yield { + type: "reasoning", id: chunk.item_id, - type: "text", - text: chunk.delta, + reasoning: chunk.delta, } } - } - if (chunk.type === "response.reasoning_text.delta") { - // Handle reasoning content deltas - if (chunk.delta) { + if (chunk.type === "response.reasoning_summary_part.done") { yield { - id: chunk.item_id, type: "reasoning", - reasoning: chunk.delta, + id: chunk.item_id, + details: chunk.part, + reasoning: "", } } - } - if (chunk.type === "response.function_call_arguments.delta") { - yield { - type: "tool_calls", - tool_call: { - function: { + if (chunk.type === "response.output_text.delta") { + // Handle text content deltas + if (chunk.delta) { + yield { id: chunk.item_id, - name: chunk.item_id, - arguments: chunk.delta, - }, - }, + type: "text", + text: chunk.delta, + } + } } - } - if (chunk.type === "response.function_call_arguments.done") { - // Handle completed function call - if (chunk.item_id && chunk.name && chunk.arguments) { + if (chunk.type === "response.reasoning_text.delta") { + // Handle reasoning content deltas + if (chunk.delta) { + yield { + id: chunk.item_id, + type: "reasoning", + reasoning: chunk.delta, + } + } + } + if (chunk.type === "response.function_call_arguments.delta") { yield { type: "tool_calls", tool_call: { function: { id: chunk.item_id, - name: chunk.name, - arguments: chunk.arguments, + name: chunk.item_id, + arguments: chunk.delta, }, }, } } - } + if (chunk.type === "response.function_call_arguments.done") { + // Handle completed function call + if (chunk.item_id && chunk.name && chunk.arguments) { + yield { + type: "tool_calls", + tool_call: { + function: { + id: chunk.item_id, + name: chunk.name, + arguments: chunk.arguments, + }, + }, + } + } + } - if ( - chunk.type === "response.incomplete" && - chunk.response?.status === "incomplete" && - chunk.response?.incomplete_details?.reason === "max_output_tokens" - ) { - console.log("Ran out of tokens") - if (chunk.response?.output_text?.length > 0) { - console.log("Partial output:", chunk.response.output_text) - } else { - console.log("Ran out of tokens during reasoning") + if ( + chunk.type === "response.incomplete" && + chunk.response?.status === "incomplete" && + chunk.response?.incomplete_details?.reason === "max_output_tokens" + ) { + console.log("Ran out of tokens") + if (chunk.response?.output_text?.length > 0) { + console.log("Partial output:", chunk.response.output_text) + } else { + console.log("Ran out of tokens during reasoning") + } } - } - if (chunk.type === "response.completed" && chunk.response?.usage) { - // Handle usage information when response is complete - const usage = chunk.response.usage - const inputTokens = usage.input_tokens || 0 - const outputTokens = usage.output_tokens || 0 - const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 - const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 - const totalTokens = usage.total_tokens || 0 - Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) - const inputCost = (await this.calculateCost(1e6, 0)) || 0 - const outputCost = (await this.calculateCost(0, 1e6)) || 0 - const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 - const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) - yield { - type: "usage", - inputTokens: nonCachedInputTokens, - outputTokens: outputTokens, - cacheWriteTokens: cacheWriteTokens, - cacheReadTokens: cacheReadTokens, - totalCost: totalCost, - id: chunk.response.id, + if (chunk.type === "response.completed" && chunk.response?.usage) { + // Handle usage information when response is complete + const usage = chunk.response.usage + const inputTokens = usage.input_tokens || 0 + const outputTokens = usage.output_tokens || 0 + const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 + const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 + const totalTokens = usage.total_tokens || 0 + Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) + const inputCost = (await this.calculateCost(1e6, 0)) || 0 + const outputCost = (await this.calculateCost(0, 1e6)) || 0 + const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 + const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) + yield { + type: "usage", + inputTokens: nonCachedInputTokens, + outputTokens: outputTokens, + cacheWriteTokens: cacheWriteTokens, + cacheReadTokens: cacheReadTokens, + totalCost: totalCost, + id: chunk.response.id, + } } } + } catch (err) { + console.log(err) } } diff --git a/src/core/api/providers/openai-native.ts b/src/core/api/providers/openai-native.ts index debbe4e8e0e..852f2796b49 100644 --- a/src/core/api/providers/openai-native.ts +++ b/src/core/api/providers/openai-native.ts @@ -155,7 +155,7 @@ export class OpenAiNativeHandler implements ApiHandler { const model = this.getModel() // Convert messages to Responses API input format - const input = convertToOpenAIResponsesInput(messages) + const input = convertToOpenAIResponsesInput("", messages) // Convert ChatCompletion tools to Responses API format if provided const responseTools = tools diff --git a/src/core/api/transform/openai-response-format.ts b/src/core/api/transform/openai-response-format.ts index ffa7edba60b..5721d716476 100644 --- a/src/core/api/transform/openai-response-format.ts +++ b/src/core/api/transform/openai-response-format.ts @@ -71,10 +71,15 @@ import { ClineStorageMessage } from "@/shared/messages/content" * @param messages - Array of ClineStorageMessage objects to be converted * @returns ResponseInput array containing the transformed messages with proper reasoning pairing */ -export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): ResponseInput { +export function convertToOpenAIResponsesInput(systemPrompt: string, messages: ClineStorageMessage[]): ResponseInput { const allItems: any[] = [] const toolUseIdToCallId = new Map() + allItems.push({ + role: "system", + content: systemPrompt, + }) + for (const m of messages) { if (typeof m.content === "string") { allItems.push({ role: m.role, content: [{ type: "input_text", text: m.content }] }) @@ -215,7 +220,7 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): // Flush any remaining user message content if (messageContent.length > 0) { - allItems.push({ role: m.role, content: [messageContent[0]] }) + allItems.push({ role: m.role, content: messageContent }) } } } diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index e2d76d77e1d..4e497874fc0 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -838,6 +838,7 @@ export class Controller { const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode") const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile") const isNewUser = this.stateManager.getGlobalStateKey("isNewUser") + const ocaReasoningEffort = this.stateManager.getGlobalSettingsKey("ocaReasoningEffort") // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts const welcomeViewCompleted = !!this.stateManager.getGlobalStateKey("welcomeViewCompleted") @@ -894,6 +895,7 @@ export class Controller { dictationSettings: updatedDictationSettings, preferredLanguage, openaiReasoningEffort, + ocaReasoningEffort, mode, strictPlanModeEnabled, yoloModeToggled, diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index 561cb87755d..d8b48e26bd8 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -82,6 +82,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe modelName: modelId, supportsChatApi: supportedApiList.includes(CHAT_COMPLETIONS_API), supportsResponsesApi: supportedApiList.includes(RESPONSES_API), + supportsReasoning: modelInfo.is_reasoning_model || false, }) } console.log("OCA models fetched", models) diff --git a/src/core/controller/state/updateSettings.ts b/src/core/controller/state/updateSettings.ts index 6bc3b384bc7..5054cf65c34 100644 --- a/src/core/controller/state/updateSettings.ts +++ b/src/core/controller/state/updateSettings.ts @@ -126,6 +126,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett controller.stateManager.setGlobalState("openaiReasoningEffort", reasoningEffort) } + if (request.ocaReasoningEffort) { + controller.stateManager.setGlobalState("ocaReasoningEffort", request.ocaReasoningEffort) + } + if (request.preferredLanguage !== undefined) { controller.stateManager.setGlobalState("preferredLanguage", request.preferredLanguage) } diff --git a/src/core/storage/StateManager.ts b/src/core/storage/StateManager.ts index 787e3d8677c..22798bf381d 100644 --- a/src/core/storage/StateManager.ts +++ b/src/core/storage/StateManager.ts @@ -560,6 +560,7 @@ export class StateManager { requestTimeoutMs, ocaBaseUrl, ocaMode, + ocaReasoningEffort, hicapApiKey, hicapModelId, aihubmixApiKey, @@ -597,6 +598,7 @@ export class StateManager { planModeHuaweiCloudMaasModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -635,6 +637,7 @@ export class StateManager { actModeHuaweiCloudMaasModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, @@ -677,6 +680,7 @@ export class StateManager { planModeHuaweiCloudMaasModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -716,6 +720,7 @@ export class StateManager { actModeHuaweiCloudMaasModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, @@ -762,6 +767,7 @@ export class StateManager { difyBaseUrl, qwenCodeOauthPath, ocaBaseUrl, + ocaReasoningEffort, minimaxApiLine, ocaMode, hicapModelId, @@ -1198,6 +1204,7 @@ export class StateManager { qwenCodeOauthPath: this.taskStateCache["qwenCodeOauthPath"] || this.globalStateCache["qwenCodeOauthPath"], difyBaseUrl: this.taskStateCache["difyBaseUrl"] || this.globalStateCache["difyBaseUrl"], ocaBaseUrl: this.globalStateCache["ocaBaseUrl"], + ocaReasoningEffort: this.globalStateCache["ocaReasoningEffort"], minimaxApiLine: this.taskStateCache["minimaxApiLine"] || this.globalStateCache["minimaxApiLine"], ocaMode: this.globalStateCache["ocaMode"], hicapModelId: this.globalStateCache["hicapModelId"], @@ -1265,6 +1272,7 @@ export class StateManager { this.globalStateCache["planModeHuaweiCloudMaasModelInfo"], planModeOcaModelId: this.globalStateCache["planModeOcaModelId"], planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"], + planModeOcaReasoningEffort: this.globalStateCache["planModeOcaReasoningEffort"], planModeHicapModelId: this.taskStateCache["planModeHicapModelId"] || this.globalStateCache["planModeHicapModelId"], planModeHicapModelInfo: this.taskStateCache["planModeHicapModelInfo"] || this.globalStateCache["planModeHicapModelInfo"], @@ -1336,6 +1344,7 @@ export class StateManager { this.globalStateCache["actModeHuaweiCloudMaasModelInfo"], actModeOcaModelId: this.globalStateCache["actModeOcaModelId"], actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"], + actModeOcaReasoningEffort: this.globalStateCache["actModeOcaReasoningEffort"], actModeHicapModelId: this.globalStateCache["actModeHicapModelId"], actModeHicapModelInfo: this.globalStateCache["actModeHicapModelInfo"], actModeAihubmixModelId: diff --git a/src/core/storage/utils/state-helpers.ts b/src/core/storage/utils/state-helpers.ts index 00030882d29..0798ae2179b 100644 --- a/src/core/storage/utils/state-helpers.ts +++ b/src/core/storage/utils/state-helpers.ts @@ -265,6 +265,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis const ocaMode = context.globalState.get("ocaMode") as string | undefined const openaiReasoningEffort = context.globalState.get("openaiReasoningEffort") + const ocaReasoningEffort = context.globalState.get("ocaReasoningEffort") const preferredLanguage = context.globalState.get("preferredLanguage") const focusChainSettings = context.globalState.get("focusChainSettings") const dictationSettings = context.globalState.get("dictationSettings") as @@ -389,6 +390,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis context.globalState.get("planModeBasetenModelInfo") const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined + const planModeOcaReasoningEffort = context.globalState.get("planModeOcaReasoningEffort") as string | undefined const planModeHicapModelId = context.globalState.get("planModeHicapModelId") const planModeHicapModelInfo = @@ -462,6 +464,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis context.globalState.get("actModeBasetenModelInfo") const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined + const actModeOcaReasoningEffort = context.globalState.get("actModeOcaReasoningEffort") as string | undefined const actModeNousResearchModelId = context.globalState.get("actModeNousResearchModelId") const sapAiCoreUseOrchestrationMode = @@ -602,6 +605,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis planModeBasetenModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -640,6 +644,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis actModeBasetenModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, @@ -663,6 +668,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled) preferredLanguage: preferredLanguage || "English", openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium", + ocaReasoningEffort: ocaReasoningEffort as string | undefined, mode: mode || "act", userInfo, mcpMarketplaceEnabled: mcpMarketplaceEnabledRaw ?? true, diff --git a/src/core/task/index.ts b/src/core/task/index.ts index b868a25d973..aec501728ad 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -2522,6 +2522,10 @@ export class Task { try { for await (const chunk of stream) { + console.log(chunk) + if (!chunk) { + continue + } switch (chunk.type) { case "usage": this.streamHandler.setRequestId(chunk.id) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f939ccba88d..4c4fadcee81 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -47,6 +47,7 @@ export interface ExtensionState { remoteBrowserHost?: string preferredLanguage?: string openaiReasoningEffort?: OpenaiReasoningEffort + ocaReasoningEffort?: string mode: Mode checkpointManagerErrorMessage?: string clineMessages: ClineMessage[] diff --git a/src/shared/api.ts b/src/shared/api.ts index 34026fc884e..d9a40e1a66b 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -135,6 +135,7 @@ export interface ApiHandlerOptions { ocaBaseUrl?: string minimaxApiLine?: string ocaMode?: string + ocaReasoningEffort?: string aihubmixBaseUrl?: string aihubmixAppCode?: string @@ -171,6 +172,7 @@ export interface ApiHandlerOptions { planModeHuaweiCloudMaasModelInfo?: ModelInfo planModeOcaModelId?: string planModeOcaModelInfo?: OcaModelInfo + planModeOcaReasoningEffort?: string planModeAihubmixModelId?: string planModeAihubmixModelInfo?: OpenAiCompatibleModelInfo planModeHicapModelId?: string @@ -211,6 +213,7 @@ export interface ApiHandlerOptions { actModeHuaweiCloudMaasModelInfo?: ModelInfo actModeOcaModelId?: string actModeOcaModelInfo?: OcaModelInfo + actModeOcaReasoningEffort?: string actModeAihubmixModelId?: string actModeAihubmixModelInfo?: OpenAiCompatibleModelInfo actModeHicapModelId?: string @@ -278,6 +281,7 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo { surveyContent?: string supportsChatApi?: boolean supportsResponsesApi?: boolean + supportsReasoning?: boolean } export const CLAUDE_SONNET_1M_SUFFIX = ":1m" diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index 3980eababd1..43c9192f648 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -110,6 +110,7 @@ function convertOcaModelInfoToProtoOcaModelInfo(info: OcaModelInfo | undefined): modelName: info.modelName, supportsChatApi: info.supportsChatApi, supportsResponsesApi: info.supportsResponsesApi, + supportsReasoning: info.supportsReasoning, } } @@ -135,6 +136,7 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi modelName: info.modelName, supportsChatApi: info.supportsChatApi, supportsResponsesApi: info.supportsResponsesApi, + supportsReasoning: info.supportsReasoning, } } @@ -414,6 +416,7 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid // Converts application ApiConfiguration to proto ApiConfiguration export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration { + config.ocaReasoningEffort return { // Global configuration fields apiKey: config.apiKey, @@ -532,6 +535,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA planModeSapAiCoreDeploymentId: config.planModeSapAiCoreDeploymentId, planModeOcaModelId: config.planModeOcaModelId, planModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.planModeOcaModelInfo), + planModeOcaReasoningEffort: config.planModeOcaReasoningEffort, planModeAihubmixModelId: config.planModeAihubmixModelId, planModeAihubmixModelInfo: convertOpenAiCompatibleModelInfoToProto(config.planModeAihubmixModelInfo), planModeHicapModelId: config.planModeHicapModelId, @@ -571,6 +575,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA actModeSapAiCoreDeploymentId: config.actModeSapAiCoreDeploymentId, actModeOcaModelId: config.actModeOcaModelId, actModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.actModeOcaModelInfo), + actModeOcaReasoningEffort: config.actModeOcaReasoningEffort, actModeAihubmixModelId: config.actModeAihubmixModelId, actModeAihubmixModelInfo: convertOpenAiCompatibleModelInfoToProto(config.actModeAihubmixModelInfo), actModeHicapModelId: config.actModeHicapModelId, @@ -702,6 +707,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio planModeSapAiCoreDeploymentId: protoConfig.planModeSapAiCoreDeploymentId, planModeOcaModelId: protoConfig.planModeOcaModelId, planModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.planModeOcaModelInfo), + planModeOcaReasoningEffort: protoConfig.planModeOcaReasoningEffort, planModeAihubmixModelId: protoConfig.planModeAihubmixModelId, planModeAihubmixModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.planModeAihubmixModelInfo), planModeHicapModelId: protoConfig.planModeHicapModelId, @@ -742,6 +748,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio actModeSapAiCoreDeploymentId: protoConfig.actModeSapAiCoreDeploymentId, actModeOcaModelId: protoConfig.actModeOcaModelId, actModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.actModeOcaModelInfo), + actModeOcaReasoningEffort: protoConfig.actModeOcaReasoningEffort, actModeAihubmixModelId: protoConfig.actModeAihubmixModelId, actModeAihubmixModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.actModeAihubmixModelInfo), actModeHicapModelId: protoConfig.actModeHicapModelId, diff --git a/src/shared/proto-conversions/models/typeConversion.ts b/src/shared/proto-conversions/models/typeConversion.ts index 9aeaa0c12b6..5367017e6c3 100644 --- a/src/shared/proto-conversions/models/typeConversion.ts +++ b/src/shared/proto-conversions/models/typeConversion.ts @@ -146,6 +146,7 @@ export function fromProtobufOcaModelInfo(protoInfo: ProtoOcaModelInfo): OcaModel surveyContent: protoInfo.surveyContent, supportsChatApi: protoInfo.supportsChatApi, supportsResponsesApi: protoInfo.supportsResponsesApi, + supportsReasoning: protoInfo.supportsReasoning, } } diff --git a/src/shared/storage/state-keys.ts b/src/shared/storage/state-keys.ts index db3735a62c5..3805d304bc6 100644 --- a/src/shared/storage/state-keys.ts +++ b/src/shared/storage/state-keys.ts @@ -116,6 +116,7 @@ export interface Settings { clineWebToolsEnabled: boolean preferredLanguage: string openaiReasoningEffort: OpenaiReasoningEffort + ocaReasoningEffort: string | undefined mode: Mode dictationSettings: DictationSettings focusChainSettings: FocusChainSettings @@ -165,6 +166,7 @@ export interface Settings { planModeHuaweiCloudMaasModelInfo: ModelInfo | undefined planModeOcaModelId: string | undefined planModeOcaModelInfo: OcaModelInfo | undefined + planModeOcaReasoningEffort: string | undefined planModeHicapModelId: string | undefined planModeHicapModelInfo: ModelInfo | undefined planModeAihubmixModelId: string | undefined @@ -203,6 +205,7 @@ export interface Settings { actModeHuaweiCloudMaasModelInfo: ModelInfo | undefined actModeOcaModelId: string | undefined actModeOcaModelInfo: OcaModelInfo | undefined + actModeOcaReasoningEffort: string | undefined actModeHicapModelId: string | undefined actModeHicapModelInfo: ModelInfo | undefined actModeAihubmixModelId: string | undefined diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index b0c19e8d62f..bc6d55421b0 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -1,5 +1,5 @@ import type { ApiConfiguration, OcaModelInfo } from "@shared/api" -import { Mode } from "@shared/storage/types" +import { Mode, OpenaiReasoningEffort } from "@shared/storage/types" import { VSCodeButton, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" import React, { useMemo } from "react" import { VSC_BUTTON_BACKGROUND, VSC_BUTTON_FOREGROUND, VSC_DESCRIPTION_FOREGROUND, VSC_FOREGROUND } from "@/utils/vscStyles" @@ -55,6 +55,18 @@ const OcaModelPicker: React.FC = ({ } } + const handleReasoningEffortChange = async (newValue: string) => { + await handleModeFieldsChange( + { + ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, + }, + { + ocaReasoningEffort: newValue, + }, + currentMode, + ) + } + const onAcknowledge = async () => { if (pendingModelId && ocaModels) { await handleModeFieldsChange( @@ -80,6 +92,15 @@ const OcaModelPicker: React.FC = ({ const { selectedModelId, selectedModelInfo } = useMemo(() => { return normalizeApiConfiguration(apiConfiguration, currentMode) }, [apiConfiguration, currentMode]) + console.log("Selected Model Info: ", selectedModelInfo) + + const selectedReasoningEffort = useMemo(() => { + if (currentMode == "plan") { + return apiConfiguration?.planModeOcaReasoningEffort + } else { + return apiConfiguration?.actModeOcaReasoningEffort + } + }, [apiConfiguration, currentMode]) const modelIds = useMemo(() => { return Object.keys(ocaModels || []).sort((a, b) => a.localeCompare(b)) @@ -108,6 +129,10 @@ const OcaModelPicker: React.FC = ({ max-height: 100px; overflow: auto; } + #reasoning-effort-dropdown::part(listbox){ + max-height: 100px; + overflow: auto; + } `}
@@ -155,6 +180,66 @@ const OcaModelPicker: React.FC = ({ Last refreshed at {lastRefreshedText}
) : null} + {selectedModelInfo.supportsReasoning && ( + + +
+ { + const newValue = e.target.currentValue as OpenaiReasoningEffort + handleReasoningEffortChange(newValue) + }}> + + Minimal + + + Low + + + Medium + + + High + + +
+
+ )} {selectedModelInfo && ( <> {showBudgetSlider && } From f446f7930d086e81d536aba3a536363446ef5775 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Wed, 17 Dec 2025 10:30:29 -0600 Subject: [PATCH 07/14] Removed extra changes --- proto/cline/state.proto | 1 - src/core/api/providers/oca.ts | 297 ++++++++---------- src/core/api/providers/openai-native.ts | 2 +- .../api/transform/openai-response-format.ts | 9 +- src/core/controller/index.ts | 2 - .../controller/models/refreshOcaModels.ts | 1 - src/core/controller/state/updateSettings.ts | 4 - src/core/storage/StateManager.ts | 3 - src/core/storage/utils/state-helpers.ts | 2 - src/core/task/focus-chain/prompts.ts | 16 +- src/core/task/index.ts | 4 - src/shared/ExtensionMessage.ts | 1 - src/shared/api.ts | 1 - src/shared/net.ts | 67 +--- .../models/api-configuration-conversion.ts | 1 - src/shared/storage/state-keys.ts | 1 - .../settings/providers/OcaModelPicker.tsx | 5 +- 17 files changed, 141 insertions(+), 276 deletions(-) diff --git a/proto/cline/state.proto b/proto/cline/state.proto index de59965f3a2..8c0e8b1a487 100644 --- a/proto/cline/state.proto +++ b/proto/cline/state.proto @@ -369,7 +369,6 @@ message UpdateSettingsRequest { optional OnboardingModelGroup onboarding_models = 33; optional bool cline_web_tools_enabled = 34; optional bool enable_parallel_tool_calling = 35; - optional string oca_reasoning_effort = 36; } message UpdateTerminalConnectionTimeoutRequest { diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 04ae2cee70b..44e3dca12b2 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -143,10 +143,7 @@ export class OcaHandler implements ApiHandler { @withRetry() async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { - // const USE_RESPONSES_API = true; if (this.options.ocaModelInfo?.supportsResponsesApi) { - // if (this.options.ocaModelInfo?.supportsChatApi) { - // if (USE_RESPONSES_API) { yield* this.createMessageResponsesApi(systemPrompt, messages, tools) } else { yield* this.createMessageChatApi(systemPrompt, messages, tools) @@ -208,7 +205,7 @@ export class OcaHandler implements ApiHandler { const toolCallProcessor = new ToolCallProcessor() - const chatCompletionParams = { + const stream = await client.chat.completions.create({ model: this.options.ocaModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, @@ -221,31 +218,7 @@ export class OcaHandler implements ApiHandler { litellm_session_id: `cline-${this.options.taskId}`, ...getOpenAIToolParams(tools), }), // Add session ID for LiteLLM tracking - } - - // Log the exact API request payload for debugging - console.log( - "[OCA Chat API Request]:", - JSON.stringify( - { - ...chatCompletionParams, - messages: chatCompletionParams.messages.map((msg) => ({ - role: msg.role, - content: - typeof msg.content === "string" - ? msg.content.substring(0, 200) + (msg.content.length > 200 ? "..." : "") - : msg.content, - // Don't log full content to avoid cluttering logs - })), - }, - null, - 2, - ), - ) - - const stream = (await client.chat.completions.create( - chatCompletionParams, - )) as AsyncIterable + }) const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 @@ -314,7 +287,10 @@ export class OcaHandler implements ApiHandler { const client = this.ensureClient() // Convert messages to Responses API input format - const input = convertToOpenAIResponsesInput(systemPrompt, messages) + const input: OpenAI.Responses.ResponseInputItem[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAIResponsesInput(messages), + ] // Convert ChatCompletion tools to Responses API format if provided const responseTools = tools @@ -328,8 +304,6 @@ export class OcaHandler implements ApiHandler { reasoning: { effort: "medium", summary: "auto" }, })) - // Logger.debug("OCA Responses Input: " + JSON.stringify(input)) - const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = { model: this.options.ocaModelId || liteLlmDefaultModelId, input, @@ -344,176 +318,165 @@ export class OcaHandler implements ApiHandler { // Create the response using Responses API const stream = await client.responses.create(responsesParams) - try { - // Process the response stream - for await (const chunk of stream) { - Logger.debug("OCA Responses Chunk: " + JSON.stringify(chunk)) - - // Skip undefined chunks - if (!chunk) { - continue - } - - // Handle different event types from Responses API - if (chunk.type === "response.output_item.added") { - const item = chunk.item - if (item.type === "function_call" && item.id) { - yield { - type: "tool_calls", - id: item.id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, - }, + // Process the response stream + for await (const chunk of stream) { + // Handle different event types from Responses API + if (chunk.type === "response.output_item.added") { + const item = chunk.item + if (item.type === "function_call" && item.id) { + yield { + type: "tool_calls", + id: item.id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, }, - } + }, } - if (item.type === "reasoning" && item.encrypted_content && item.id) { - yield { - type: "reasoning", - id: item.id, - reasoning: "", - redacted_data: item.encrypted_content, - } + } + if (item.type === "reasoning" && item.encrypted_content && item.id) { + yield { + type: "reasoning", + id: item.id, + reasoning: "", + redacted_data: item.encrypted_content, } } - if (chunk.type === "response.output_item.done") { - const item = chunk.item - if (item.type === "function_call") { - yield { - type: "tool_calls", - id: item.id || item.call_id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, - }, + } + if (chunk.type === "response.output_item.done") { + const item = chunk.item + if (item.type === "function_call") { + yield { + type: "tool_calls", + id: item.id || item.call_id, + tool_call: { + call_id: item.call_id, + function: { + id: item.id, + name: item.name, + arguments: item.arguments, }, - } - } - if (item.type === "reasoning") { - yield { - type: "reasoning", - id: item.id, - details: item.summary, - reasoning: "", - } + }, } } - if (chunk.type === "response.reasoning_summary_part.added") { + if (item.type === "reasoning") { yield { type: "reasoning", - id: chunk.item_id, - reasoning: chunk.part.text, + id: item.id, + details: item.summary, + reasoning: "", } } - if (chunk.type === "response.reasoning_summary_text.delta") { + } + if (chunk.type === "response.reasoning_summary_part.added") { + yield { + type: "reasoning", + id: chunk.item_id, + reasoning: chunk.part.text, + } + } + if (chunk.type === "response.reasoning_summary_text.delta") { + yield { + type: "reasoning", + id: chunk.item_id, + reasoning: chunk.delta, + } + } + if (chunk.type === "response.reasoning_summary_part.done") { + yield { + type: "reasoning", + id: chunk.item_id, + details: chunk.part, + reasoning: "", + } + } + if (chunk.type === "response.output_text.delta") { + // Handle text content deltas + if (chunk.delta) { yield { - type: "reasoning", id: chunk.item_id, - reasoning: chunk.delta, + type: "text", + text: chunk.delta, } } - if (chunk.type === "response.reasoning_summary_part.done") { + } + if (chunk.type === "response.reasoning_text.delta") { + // Handle reasoning content deltas + if (chunk.delta) { yield { - type: "reasoning", id: chunk.item_id, - details: chunk.part, - reasoning: "", - } - } - if (chunk.type === "response.output_text.delta") { - // Handle text content deltas - if (chunk.delta) { - yield { - id: chunk.item_id, - type: "text", - text: chunk.delta, - } + type: "reasoning", + reasoning: chunk.delta, } } - if (chunk.type === "response.reasoning_text.delta") { - // Handle reasoning content deltas - if (chunk.delta) { - yield { + } + if (chunk.type === "response.function_call_arguments.delta") { + yield { + type: "tool_calls", + tool_call: { + function: { id: chunk.item_id, - type: "reasoning", - reasoning: chunk.delta, - } - } + name: chunk.item_id, + arguments: chunk.delta, + }, + }, } - if (chunk.type === "response.function_call_arguments.delta") { + } + if (chunk.type === "response.function_call_arguments.done") { + // Handle completed function call + if (chunk.item_id && chunk.name && chunk.arguments) { yield { type: "tool_calls", tool_call: { function: { id: chunk.item_id, - name: chunk.item_id, - arguments: chunk.delta, + name: chunk.name, + arguments: chunk.arguments, }, }, } } - if (chunk.type === "response.function_call_arguments.done") { - // Handle completed function call - if (chunk.item_id && chunk.name && chunk.arguments) { - yield { - type: "tool_calls", - tool_call: { - function: { - id: chunk.item_id, - name: chunk.name, - arguments: chunk.arguments, - }, - }, - } - } - } + } - if ( - chunk.type === "response.incomplete" && - chunk.response?.status === "incomplete" && - chunk.response?.incomplete_details?.reason === "max_output_tokens" - ) { - console.log("Ran out of tokens") - if (chunk.response?.output_text?.length > 0) { - console.log("Partial output:", chunk.response.output_text) - } else { - console.log("Ran out of tokens during reasoning") - } + if ( + chunk.type === "response.incomplete" && + chunk.response?.status === "incomplete" && + chunk.response?.incomplete_details?.reason === "max_output_tokens" + ) { + console.log("Ran out of tokens") + if (chunk.response?.output_text?.length > 0) { + console.log("Partial output:", chunk.response.output_text) + } else { + console.log("Ran out of tokens during reasoning") } + } - if (chunk.type === "response.completed" && chunk.response?.usage) { - // Handle usage information when response is complete - const usage = chunk.response.usage - const inputTokens = usage.input_tokens || 0 - const outputTokens = usage.output_tokens || 0 - const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 - const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 - const totalTokens = usage.total_tokens || 0 - Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) - const inputCost = (await this.calculateCost(1e6, 0)) || 0 - const outputCost = (await this.calculateCost(0, 1e6)) || 0 - const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 - const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) - yield { - type: "usage", - inputTokens: nonCachedInputTokens, - outputTokens: outputTokens, - cacheWriteTokens: cacheWriteTokens, - cacheReadTokens: cacheReadTokens, - totalCost: totalCost, - id: chunk.response.id, - } + if (chunk.type === "response.completed" && chunk.response?.usage) { + // Handle usage information when response is complete + const usage = chunk.response.usage + const inputTokens = usage.input_tokens || 0 + const outputTokens = usage.output_tokens || 0 + const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 + const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 + const totalTokens = usage.total_tokens || 0 + Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) + const inputCost = (await this.calculateCost(1e6, 0)) || 0 + const outputCost = (await this.calculateCost(0, 1e6)) || 0 + const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 + const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) + yield { + type: "usage", + inputTokens: nonCachedInputTokens, + outputTokens: outputTokens, + cacheWriteTokens: cacheWriteTokens, + cacheReadTokens: cacheReadTokens, + totalCost: totalCost, + id: chunk.response.id, } } - } catch (err) { - console.log(err) } } diff --git a/src/core/api/providers/openai-native.ts b/src/core/api/providers/openai-native.ts index 852f2796b49..debbe4e8e0e 100644 --- a/src/core/api/providers/openai-native.ts +++ b/src/core/api/providers/openai-native.ts @@ -155,7 +155,7 @@ export class OpenAiNativeHandler implements ApiHandler { const model = this.getModel() // Convert messages to Responses API input format - const input = convertToOpenAIResponsesInput("", messages) + const input = convertToOpenAIResponsesInput(messages) // Convert ChatCompletion tools to Responses API format if provided const responseTools = tools diff --git a/src/core/api/transform/openai-response-format.ts b/src/core/api/transform/openai-response-format.ts index 5721d716476..6c8e6872642 100644 --- a/src/core/api/transform/openai-response-format.ts +++ b/src/core/api/transform/openai-response-format.ts @@ -71,15 +71,10 @@ import { ClineStorageMessage } from "@/shared/messages/content" * @param messages - Array of ClineStorageMessage objects to be converted * @returns ResponseInput array containing the transformed messages with proper reasoning pairing */ -export function convertToOpenAIResponsesInput(systemPrompt: string, messages: ClineStorageMessage[]): ResponseInput { +export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): ResponseInput { const allItems: any[] = [] const toolUseIdToCallId = new Map() - allItems.push({ - role: "system", - content: systemPrompt, - }) - for (const m of messages) { if (typeof m.content === "string") { allItems.push({ role: m.role, content: [{ type: "input_text", text: m.content }] }) @@ -220,7 +215,7 @@ export function convertToOpenAIResponsesInput(systemPrompt: string, messages: Cl // Flush any remaining user message content if (messageContent.length > 0) { - allItems.push({ role: m.role, content: messageContent }) + allItems.push({ role: m.role, content: [...messageContent] }) } } } diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index 4e497874fc0..e2d76d77e1d 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -838,7 +838,6 @@ export class Controller { const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode") const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile") const isNewUser = this.stateManager.getGlobalStateKey("isNewUser") - const ocaReasoningEffort = this.stateManager.getGlobalSettingsKey("ocaReasoningEffort") // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts const welcomeViewCompleted = !!this.stateManager.getGlobalStateKey("welcomeViewCompleted") @@ -895,7 +894,6 @@ export class Controller { dictationSettings: updatedDictationSettings, preferredLanguage, openaiReasoningEffort, - ocaReasoningEffort, mode, strictPlanModeEnabled, yoloModeToggled, diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index d8b48e26bd8..fe1e8710d4c 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -55,7 +55,6 @@ export async function refreshOcaModels(controller: Controller, request: StringRe } for (const model of response.data.data) { const modelId = model.litellm_params?.model - console.log("Model: ", modelId, "Supported list: ", model.model_info.supported_api_list) if (typeof modelId !== "string" || !modelId) { continue } diff --git a/src/core/controller/state/updateSettings.ts b/src/core/controller/state/updateSettings.ts index 5054cf65c34..6bc3b384bc7 100644 --- a/src/core/controller/state/updateSettings.ts +++ b/src/core/controller/state/updateSettings.ts @@ -126,10 +126,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett controller.stateManager.setGlobalState("openaiReasoningEffort", reasoningEffort) } - if (request.ocaReasoningEffort) { - controller.stateManager.setGlobalState("ocaReasoningEffort", request.ocaReasoningEffort) - } - if (request.preferredLanguage !== undefined) { controller.stateManager.setGlobalState("preferredLanguage", request.preferredLanguage) } diff --git a/src/core/storage/StateManager.ts b/src/core/storage/StateManager.ts index 22798bf381d..38f821a07d1 100644 --- a/src/core/storage/StateManager.ts +++ b/src/core/storage/StateManager.ts @@ -560,7 +560,6 @@ export class StateManager { requestTimeoutMs, ocaBaseUrl, ocaMode, - ocaReasoningEffort, hicapApiKey, hicapModelId, aihubmixApiKey, @@ -767,7 +766,6 @@ export class StateManager { difyBaseUrl, qwenCodeOauthPath, ocaBaseUrl, - ocaReasoningEffort, minimaxApiLine, ocaMode, hicapModelId, @@ -1204,7 +1202,6 @@ export class StateManager { qwenCodeOauthPath: this.taskStateCache["qwenCodeOauthPath"] || this.globalStateCache["qwenCodeOauthPath"], difyBaseUrl: this.taskStateCache["difyBaseUrl"] || this.globalStateCache["difyBaseUrl"], ocaBaseUrl: this.globalStateCache["ocaBaseUrl"], - ocaReasoningEffort: this.globalStateCache["ocaReasoningEffort"], minimaxApiLine: this.taskStateCache["minimaxApiLine"] || this.globalStateCache["minimaxApiLine"], ocaMode: this.globalStateCache["ocaMode"], hicapModelId: this.globalStateCache["hicapModelId"], diff --git a/src/core/storage/utils/state-helpers.ts b/src/core/storage/utils/state-helpers.ts index 0798ae2179b..3e2f3b791fa 100644 --- a/src/core/storage/utils/state-helpers.ts +++ b/src/core/storage/utils/state-helpers.ts @@ -265,7 +265,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis const ocaMode = context.globalState.get("ocaMode") as string | undefined const openaiReasoningEffort = context.globalState.get("openaiReasoningEffort") - const ocaReasoningEffort = context.globalState.get("ocaReasoningEffort") const preferredLanguage = context.globalState.get("preferredLanguage") const focusChainSettings = context.globalState.get("focusChainSettings") const dictationSettings = context.globalState.get("dictationSettings") as @@ -668,7 +667,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled) preferredLanguage: preferredLanguage || "English", openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium", - ocaReasoningEffort: ocaReasoningEffort as string | undefined, mode: mode || "act", userInfo, mcpMarketplaceEnabled: mcpMarketplaceEnabledRaw ?? true, diff --git a/src/core/task/focus-chain/prompts.ts b/src/core/task/focus-chain/prompts.ts index 432bbee5f1a..dffbec1d979 100644 --- a/src/core/task/focus-chain/prompts.ts +++ b/src/core/task/focus-chain/prompts.ts @@ -86,19 +86,13 @@ Reminder on how to use the task_progress parameter: ${reminder}` -// const recommended = ` -// # task_progress RECOMMENDED +const recommended = ` +# task_progress RECOMMENDED -// When starting a new task, it is recommended to include a todo list using the task_progress parameter. +When starting a new task, it is recommended to include a todo list using the task_progress parameter. -// const recommended = ` -// # task_progress RECOMMENDED - -// For new tasks, consider including a todo list via the task_progress parameter in tool calls.\n\nKey elements:\n1. Develop a comprehensive checklist outlining all required steps.\n2. Format using Markdown: - [ ] for incomplete items, - [x] for completed ones.\n\n**Advantages of a task_progress list:**\n\t- Provides a clear implementation roadmap.\n\t- Enables ongoing progress tracking.\n\t- Prevents overlooking any steps.\n\t- Allows users to review, monitor, and adjust the plan as needed.\n\n**Sample checklist format:**\n\`\`\`\n- [ ] Analyze requirements\n- [ ] Set up necessary files\n- [ ] Implement main functionality\n- [ ] Handle edge cases\n- [ ] Test the implementation\n- [ ] Verify results\n\`\`\`\n\nA well-maintained task_progress list supports effective progress tracking and comprehensive task coverage.\n - -// ` - -const recommended = "" +${listInstructionsRecommended} +` const apiRequestCount = ` # task_progress diff --git a/src/core/task/index.ts b/src/core/task/index.ts index aec501728ad..b868a25d973 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -2522,10 +2522,6 @@ export class Task { try { for await (const chunk of stream) { - console.log(chunk) - if (!chunk) { - continue - } switch (chunk.type) { case "usage": this.streamHandler.setRequestId(chunk.id) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4c4fadcee81..f939ccba88d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -47,7 +47,6 @@ export interface ExtensionState { remoteBrowserHost?: string preferredLanguage?: string openaiReasoningEffort?: OpenaiReasoningEffort - ocaReasoningEffort?: string mode: Mode checkpointManagerErrorMessage?: string clineMessages: ClineMessage[] diff --git a/src/shared/api.ts b/src/shared/api.ts index d9a40e1a66b..61bb512635d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -135,7 +135,6 @@ export interface ApiHandlerOptions { ocaBaseUrl?: string minimaxApiLine?: string ocaMode?: string - ocaReasoningEffort?: string aihubmixBaseUrl?: string aihubmixAppCode?: string diff --git a/src/shared/net.ts b/src/shared/net.ts index b6c9306d47d..d1a6ed32bdb 100644 --- a/src/shared/net.ts +++ b/src/shared/net.ts @@ -94,7 +94,6 @@ */ import { EnvHttpProxyAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici" -import { Logger } from "@/services/logging/Logger" let mockFetch: typeof globalThis.fetch | undefined @@ -122,71 +121,7 @@ export const fetch: typeof globalThis.fetch = (() => { baseFetch = undiciFetch as any as typeof globalThis.fetch } - return async (input: string | URL | Request, init?: RequestInit): Promise => { - const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url - const method = init?.method || (input instanceof Request ? input.method : "GET") - - // Log request details - console.log(`[Cline Network Request] ${method} ${url}`) - console.log(init?.body) - if (init?.headers) { - console.log( - "[Cline Network Request Headers]:", - JSON.stringify(Object.fromEntries(Object.entries(init.headers).map(([k, v]) => [k, String(v)])), null, 2), - ) - } - if (init?.body && typeof init.body === "string") { - try { - // Try to parse as JSON for pretty printing - console.log(init.body) - Logger.debug("body: " + init?.body) - const parsed = JSON.parse(init.body) - console.log("[Cline Network Request Body]:", JSON.stringify(parsed, null, 2)) - } catch { - // If not JSON, log as string - console.log("[Cline Network Request Body]:", init.body) - } - } else if (init?.body) { - console.log("[Cline Network Request Body]:", "[Binary data or FormData]") - } - - const startTime = Date.now() - const response = await (mockFetch || baseFetch)(input, init) - const duration = Date.now() - startTime - - // Clone the response to read its body without consuming it - const clonedResponse = response.clone() - - // Log response details - console.log(`[Cline Network Response] ${response.status} ${response.statusText} (${duration}ms)`) - - // Log response headers - const responseHeaders: Record = {} - clonedResponse.headers.forEach((value, key) => { - responseHeaders[key] = value - }) - console.log("[Cline Network Response Headers]:", JSON.stringify(responseHeaders, null, 2)) - - // Try to log response body for JSON responses - try { - const contentType = clonedResponse.headers.get("content-type") - if (contentType?.includes("application/json")) { - const responseText = await clonedResponse.text() - try { - const parsed = JSON.parse(responseText) - console.log("[Cline Network Response Body]:", JSON.stringify(parsed, null, 2)) - } catch { - console.log("[Cline Network Response Body]:", responseText) - } - } else { - console.log("[Cline Network Response Body]:", "[Non-JSON content or streaming response]") - } - } catch (error) { - console.log("[Cline Network Response Body]:", "[Error reading response body]", error) - } - - return response - } + return (input: string | URL | Request, init?: RequestInit): Promise => (mockFetch || baseFetch)(input, init) })() /** diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index 43c9192f648..f217e1246b7 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -416,7 +416,6 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid // Converts application ApiConfiguration to proto ApiConfiguration export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration { - config.ocaReasoningEffort return { // Global configuration fields apiKey: config.apiKey, diff --git a/src/shared/storage/state-keys.ts b/src/shared/storage/state-keys.ts index 3805d304bc6..17efdabbf05 100644 --- a/src/shared/storage/state-keys.ts +++ b/src/shared/storage/state-keys.ts @@ -116,7 +116,6 @@ export interface Settings { clineWebToolsEnabled: boolean preferredLanguage: string openaiReasoningEffort: OpenaiReasoningEffort - ocaReasoningEffort: string | undefined mode: Mode dictationSettings: DictationSettings focusChainSettings: FocusChainSettings diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index bc6d55421b0..bcd68a8cc2d 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -1,5 +1,5 @@ import type { ApiConfiguration, OcaModelInfo } from "@shared/api" -import { Mode, OpenaiReasoningEffort } from "@shared/storage/types" +import { Mode } from "@shared/storage/types" import { VSCodeButton, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" import React, { useMemo } from "react" import { VSC_BUTTON_BACKGROUND, VSC_BUTTON_FOREGROUND, VSC_DESCRIPTION_FOREGROUND, VSC_FOREGROUND } from "@/utils/vscStyles" @@ -92,7 +92,6 @@ const OcaModelPicker: React.FC = ({ const { selectedModelId, selectedModelInfo } = useMemo(() => { return normalizeApiConfiguration(apiConfiguration, currentMode) }, [apiConfiguration, currentMode]) - console.log("Selected Model Info: ", selectedModelInfo) const selectedReasoningEffort = useMemo(() => { if (currentMode == "plan") { @@ -189,7 +188,7 @@ const OcaModelPicker: React.FC = ({ currentValue={selectedReasoningEffort} id="reasoning-effort-dropdown" onChange={(e: any) => { - const newValue = e.target.currentValue as OpenaiReasoningEffort + const newValue = e.target.currentValue handleReasoningEffortChange(newValue) }}> Date: Wed, 17 Dec 2025 10:38:45 -0600 Subject: [PATCH 08/14] Added reasoning effort also to chat completions --- src/core/api/providers/oca.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 44e3dca12b2..6bc1b51237d 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -205,7 +205,7 @@ export class OcaHandler implements ApiHandler { const toolCallProcessor = new ToolCallProcessor() - const stream = await client.chat.completions.create({ + const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: this.options.ocaModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, @@ -218,7 +218,13 @@ export class OcaHandler implements ApiHandler { litellm_session_id: `cline-${this.options.taskId}`, ...getOpenAIToolParams(tools), }), // Add session ID for LiteLLM tracking - }) + } + + if (this.options.ocaModelInfo?.supportsReasoningEffort) { + chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any) + } + + const stream = await client.chat.completions.create(chatCompletionsParams) const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 From f7c69b5eafed6b89ee33ad8d62fb5f5d97fd6ed2 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Thu, 18 Dec 2025 16:01:21 -0600 Subject: [PATCH 09/14] Made changes to fix issues with cline based on bugbash --- proto/cline/models.proto | 2 + .../controller/models/refreshOcaModels.ts | 1 + src/shared/api.ts | 1 + .../models/api-configuration-conversion.ts | 2 + .../models/typeConversion.ts | 1 + .../settings/providers/OcaModelPicker.tsx | 153 ++++++++---------- 6 files changed, 78 insertions(+), 82 deletions(-) diff --git a/proto/cline/models.proto b/proto/cline/models.proto index 23b7328e1a6..bde344d558d 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -386,6 +386,8 @@ message OcaModelInfo { optional bool supports_chat_api = 20; // Supports reasoning optional bool supports_reasoning = 21; + // reasoning effort options + repeated string reasoning_effort_options = 22; } // Aggregated OCA model catalog keyed by model identifier diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index fe1e8710d4c..3850510fafe 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -82,6 +82,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe supportsChatApi: supportedApiList.includes(CHAT_COMPLETIONS_API), supportsResponsesApi: supportedApiList.includes(RESPONSES_API), supportsReasoning: modelInfo.is_reasoning_model || false, + reasoningEffortOptions: modelInfo.reasoning_effort_options || [], }) } console.log("OCA models fetched", models) diff --git a/src/shared/api.ts b/src/shared/api.ts index 61bb512635d..1e735045470 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -281,6 +281,7 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo { supportsChatApi?: boolean supportsResponsesApi?: boolean supportsReasoning?: boolean + reasoningEffortOptions: string[] } export const CLAUDE_SONNET_1M_SUFFIX = ":1m" diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index f217e1246b7..6ba92683c11 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -111,6 +111,7 @@ function convertOcaModelInfoToProtoOcaModelInfo(info: OcaModelInfo | undefined): supportsChatApi: info.supportsChatApi, supportsResponsesApi: info.supportsResponsesApi, supportsReasoning: info.supportsReasoning, + reasoningEffortOptions: info.reasoningEffortOptions, } } @@ -137,6 +138,7 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi supportsChatApi: info.supportsChatApi, supportsResponsesApi: info.supportsResponsesApi, supportsReasoning: info.supportsReasoning, + reasoningEffortOptions: info.reasoningEffortOptions, } } diff --git a/src/shared/proto-conversions/models/typeConversion.ts b/src/shared/proto-conversions/models/typeConversion.ts index 5367017e6c3..5d84f699223 100644 --- a/src/shared/proto-conversions/models/typeConversion.ts +++ b/src/shared/proto-conversions/models/typeConversion.ts @@ -147,6 +147,7 @@ export function fromProtobufOcaModelInfo(protoInfo: ProtoOcaModelInfo): OcaModel supportsChatApi: protoInfo.supportsChatApi, supportsResponsesApi: protoInfo.supportsResponsesApi, supportsReasoning: protoInfo.supportsReasoning, + reasoningEffortOptions: protoInfo.reasoningEffortOptions, } } diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index bcd68a8cc2d..92c0cb6d73f 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -40,17 +40,22 @@ const OcaModelPicker: React.FC = ({ setPendingModelId(newModelId) setShowRestrictedPopup(true) } else { - await handleModeFieldsChange( - { - ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, - ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, - }, - { - ocaModelId: newModelId, - ocaModelInfo: ocaModels[newModelId], - }, - currentMode, - ) + const fieldChangeKeys: any = { + ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, + ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + } + const fieldChangeValues: any = { + ocaModelId: newModelId, + ocaModelInfo: ocaModels[newModelId], + } + if (ocaModels[newModelId].supportsReasoning && ocaModels[newModelId].reasoningEffortOptions.length > 0) { + fieldChangeKeys["ocaReasoningEffort"] = { + plan: "planModeOcaReasoningEffort", + act: "actModeOcaReasoningEffort", + } + fieldChangeValues["ocaReasoningEffort"] = ocaModels[newModelId].reasoningEffortOptions[0] + } + await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) } } } @@ -69,17 +74,20 @@ const OcaModelPicker: React.FC = ({ const onAcknowledge = async () => { if (pendingModelId && ocaModels) { - await handleModeFieldsChange( - { - ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, - ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, - }, - { - ocaModelId: pendingModelId, - ocaModelInfo: ocaModels[pendingModelId], - }, - currentMode, - ) + console.log(pendingModelId) + const fieldChangeKeys: any = { + ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, + ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + } + const fieldChangeValues: any = { + ocaModelId: pendingModelId, + ocaModelInfo: ocaModels[pendingModelId], + } + if (ocaModels[pendingModelId].supportsReasoning && ocaModels[pendingModelId].reasoningEffortOptions.length > 0) { + fieldChangeKeys["ocaReasoningEffort"] = { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" } + fieldChangeValues["ocaReasoningEffort"] = ocaModels[pendingModelId].reasoningEffortOptions[0] + } + await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) setPendingModelId(null) setShowRestrictedPopup(false) } @@ -101,6 +109,15 @@ const OcaModelPicker: React.FC = ({ } }, [apiConfiguration, currentMode]) + const selectedOcaModelInfo = useMemo(() => { + if (currentMode == "plan") { + return apiConfiguration?.planModeOcaModelInfo + } else { + return apiConfiguration?.actModeOcaModelInfo + } + }, [apiConfiguration, currentMode]) + console.log(selectedOcaModelInfo) + const modelIds = useMemo(() => { return Object.keys(ocaModels || []).sort((a, b) => a.localeCompare(b)) }, [ocaModels]) @@ -179,66 +196,38 @@ const OcaModelPicker: React.FC = ({ Last refreshed at {lastRefreshedText} ) : null} - {selectedModelInfo.supportsReasoning && ( - - -
- { - const newValue = e.target.currentValue - handleReasoningEffortChange(newValue) - }}> - - Minimal - - - Low - - - Medium - - - High - - -
-
- )} + {selectedOcaModelInfo && + selectedOcaModelInfo.supportsReasoning && + selectedOcaModelInfo.reasoningEffortOptions.length >= 0 && ( + + +
+ { + const newValue = e.target.currentValue + handleReasoningEffortChange(newValue) + }}> + {selectedOcaModelInfo?.reasoningEffortOptions.map((reasoningEffort) => ( + + {reasoningEffort} + + ))} + +
+
+ )} {selectedModelInfo && ( <> {showBudgetSlider && } From 0a56a219778f7263aa8f6c8db02bc05c642cbdca Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Thu, 18 Dec 2025 16:01:44 -0600 Subject: [PATCH 10/14] removed extra console.log statements --- webview-ui/src/components/settings/providers/OcaModelPicker.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index 92c0cb6d73f..5fb5c6fc77a 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -74,7 +74,6 @@ const OcaModelPicker: React.FC = ({ const onAcknowledge = async () => { if (pendingModelId && ocaModels) { - console.log(pendingModelId) const fieldChangeKeys: any = { ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, @@ -116,7 +115,6 @@ const OcaModelPicker: React.FC = ({ return apiConfiguration?.actModeOcaModelInfo } }, [apiConfiguration, currentMode]) - console.log(selectedOcaModelInfo) const modelIds = useMemo(() => { return Object.keys(ocaModels || []).sort((a, b) => a.localeCompare(b)) From 2cb64ac9611fd781a1c6a46b78d40d4af359cc5f Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Fri, 19 Dec 2025 10:52:43 -0600 Subject: [PATCH 11/14] Added extra changes to make reasoningEffortOptions working properly(outputs undefined) --- .../controller/models/refreshOcaModels.ts | 23 ++++++++++++++++++ .../settings/providers/OcaModelPicker.tsx | 24 +++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index 3850510fafe..f134568508f 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -101,6 +101,25 @@ export async function refreshOcaModels(controller: Controller, request: StringRe ? apiConfiguration.actModeOcaModelId : defaultModelId! + let planModeOcaReasoningEffort + let actModeOcaReasoningEffort + if ( + models[planModeSelectedModelId].supportsReasoning && + models[planModeSelectedModelId].reasoningEffortOptions.length > 0 + ) { + planModeOcaReasoningEffort = apiConfiguration.planModeOcaReasoningEffort + ? apiConfiguration.planModeOcaReasoningEffort + : models[planModeSelectedModelId].reasoningEffortOptions[0] + } + if ( + models[actModeSelectedModelId].supportsReasoning && + models[actModeSelectedModelId].reasoningEffortOptions.length > 0 + ) { + actModeOcaReasoningEffort = apiConfiguration.actModeOcaReasoningEffort + ? apiConfiguration.actModeOcaReasoningEffort + : models[actModeSelectedModelId].reasoningEffortOptions[0] + } + // Build updates object based on plan/act mode setting const updates: Partial = {} @@ -108,15 +127,19 @@ export async function refreshOcaModels(controller: Controller, request: StringRe if (currentMode === "plan") { updates.planModeOcaModelId = planModeSelectedModelId updates.planModeOcaModelInfo = models[planModeSelectedModelId] + updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort } else { updates.actModeOcaModelId = actModeSelectedModelId updates.actModeOcaModelInfo = models[actModeSelectedModelId] + updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort } } else { updates.planModeOcaModelId = planModeSelectedModelId updates.planModeOcaModelInfo = models[planModeSelectedModelId] + updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort updates.actModeOcaModelId = actModeSelectedModelId updates.actModeOcaModelInfo = models[actModeSelectedModelId] + updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort } // Update state directly using batch method diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index 5fb5c6fc77a..46accd9dd1d 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -43,17 +43,15 @@ const OcaModelPicker: React.FC = ({ const fieldChangeKeys: any = { ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, } const fieldChangeValues: any = { ocaModelId: newModelId, ocaModelInfo: ocaModels[newModelId], - } - if (ocaModels[newModelId].supportsReasoning && ocaModels[newModelId].reasoningEffortOptions.length > 0) { - fieldChangeKeys["ocaReasoningEffort"] = { - plan: "planModeOcaReasoningEffort", - act: "actModeOcaReasoningEffort", - } - fieldChangeValues["ocaReasoningEffort"] = ocaModels[newModelId].reasoningEffortOptions[0] + ocaReasoningEffort: + ocaModels[newModelId].reasoningEffortOptions.length > 0 + ? ocaModels[newModelId].reasoningEffortOptions[0] + : undefined, } await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) } @@ -77,14 +75,15 @@ const OcaModelPicker: React.FC = ({ const fieldChangeKeys: any = { ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, } const fieldChangeValues: any = { ocaModelId: pendingModelId, ocaModelInfo: ocaModels[pendingModelId], - } - if (ocaModels[pendingModelId].supportsReasoning && ocaModels[pendingModelId].reasoningEffortOptions.length > 0) { - fieldChangeKeys["ocaReasoningEffort"] = { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" } - fieldChangeValues["ocaReasoningEffort"] = ocaModels[pendingModelId].reasoningEffortOptions[0] + ocaReasoningEffort: + ocaModels[pendingModelId].reasoningEffortOptions.length > 0 + ? ocaModels[pendingModelId].reasoningEffortOptions[0] + : undefined, } await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) setPendingModelId(null) @@ -194,7 +193,8 @@ const OcaModelPicker: React.FC = ({ Last refreshed at {lastRefreshedText} ) : null} - {selectedOcaModelInfo && + {!loading && + selectedOcaModelInfo && selectedOcaModelInfo.supportsReasoning && selectedOcaModelInfo.reasoningEffortOptions.length >= 0 && ( From f03e553b79a5a1ff7f59e13e189e797ac9b476ca Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Fri, 19 Dec 2025 11:56:08 -0600 Subject: [PATCH 12/14] Added inheritance logic with some slight changes --- src/core/api/providers/oca.ts | 300 ++++-------------- src/core/api/providers/openai-native.ts | 30 +- .../settings/providers/OcaModelPicker.tsx | 17 +- 3 files changed, 90 insertions(+), 257 deletions(-) diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 6bc1b51237d..1361ac4d26e 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -1,4 +1,4 @@ -import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api" +import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api" import OpenAI, { APIError, OpenAIError } from "openai" import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" @@ -12,12 +12,12 @@ import { Logger } from "@/services/logging/Logger" import { OcaModelInfo } from "@/shared/api" import { ClineStorageMessage } from "@/shared/messages/content" import { fetch } from "@/shared/net" -import { ApiHandler, type CommonApiHandlerOptions } from ".." +import { type CommonApiHandlerOptions } from ".." import { withRetry } from "../retry" import { convertToOpenAiMessages } from "../transform/openai-format" -import { convertToOpenAIResponsesInput } from "../transform/openai-response-format" import { ApiStream } from "../transform/stream" import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor" +import { OpenAiNativeHandler } from "./openai-native" export interface OcaHandlerOptions extends CommonApiHandlerOptions { ocaBaseUrl?: string @@ -30,15 +30,15 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions { ocaMode?: string // "internal" or "external" } -export class OcaHandler implements ApiHandler { - protected options: OcaHandlerOptions - protected client: OpenAI | undefined +export class OcaHandler extends OpenAiNativeHandler { + protected ocaOptions: OcaHandlerOptions - constructor(options: OcaHandlerOptions) { - this.options = options + constructor(ocaOptions: OcaHandlerOptions) { + super({}) + this.ocaOptions = ocaOptions } - protected initializeClient(options: OcaHandlerOptions) { + protected initializeClient(ocaOptions: OcaHandlerOptions) { return new (class OCIOpenAI extends OpenAI { protected override async prepareOptions(opts: any): Promise { const token = await OcaAuthService.getInstance().getAuthToken() @@ -47,7 +47,7 @@ export class OcaHandler implements ApiHandler { } opts.headers ??= {} // OCA Headers - const ociHeaders = await createOcaHeaders(token, options.taskId!) + const ociHeaders = await createOcaHeaders(token, ocaOptions.taskId!) opts.headers = { ...opts.headers, ...ociHeaders } Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`) return super.prepareOptions(opts) @@ -82,20 +82,20 @@ export class OcaHandler implements ApiHandler { } })({ baseURL: - options.ocaBaseUrl || - (options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL), + ocaOptions.ocaBaseUrl || + (ocaOptions.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL), apiKey: "noop", fetch, // Use configured fetch with proxy support }) } - protected ensureClient(): OpenAI { + override ensureClient(): OpenAI { if (!this.client) { - if (!this.options.ocaModelId) { + if (!this.ocaOptions.ocaModelId) { throw new Error("Oracle Code Assist (OCA) model is not selected") } try { - this.client = this.initializeClient(this.options) + this.client = this.initializeClient(this.ocaOptions) } catch (error) { throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`) } @@ -103,15 +103,15 @@ export class OcaHandler implements ApiHandler { return this.client } - async calculateCost(prompt_tokens: number, completion_tokens: number): Promise { + async getApiCosts(prompt_tokens: number, completion_tokens: number): Promise { // Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473 const client = this.ensureClient() - const modelId = this.options.ocaModelId || liteLlmDefaultModelId + const modelId = this.ocaOptions.ocaModelId || liteLlmDefaultModelId const token = await OcaAuthService.getInstance().getAuthToken() if (!token) { throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available") } - const ociHeaders = await createOcaHeaders(token, this.options.taskId!) + const ociHeaders = await createOcaHeaders(token, this.ocaOptions.taskId!) Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`) try { const response = await fetch(`${client.baseURL}/spend/calculate`, { @@ -142,15 +142,19 @@ export class OcaHandler implements ApiHandler { } @withRetry() - async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { - if (this.options.ocaModelInfo?.supportsResponsesApi) { - yield* this.createMessageResponsesApi(systemPrompt, messages, tools) + override async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + if (this.ocaOptions.ocaModelInfo?.supportsResponsesApi) { + yield* this.createResponseStream(systemPrompt, messages, tools ?? []) } else { - yield* this.createMessageChatApi(systemPrompt, messages, tools) + yield* this.createCompletionStream(systemPrompt, messages, tools) } } - async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + protected override async *createCompletionStream( + systemPrompt: string, + messages: ClineStorageMessage[], + tools?: OpenAITool[], + ): ApiStream { console.log("Using Chat API") const client = this.ensureClient() const formattedMessages = convertToOpenAiMessages(messages) @@ -158,23 +162,24 @@ export class OcaHandler implements ApiHandler { role: "system", content: systemPrompt, } - const modelId = this.options.ocaModelId || liteLlmDefaultModelId + const model = this.getModel() + const modelId = model.id const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini") // Configuration for extended thinking - const budgetTokens = this.options.thinkingBudgetTokens || 0 + const budgetTokens = this.ocaOptions.thinkingBudgetTokens || 0 const reasoningOn = budgetTokens !== 0 const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined - let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0 - const maxTokens: number | undefined = this.options.ocaModelInfo?.maxTokens + let temperature: number | undefined = this.ocaOptions.ocaModelInfo?.temperature ?? 0 + const maxTokens: number | undefined = this.ocaOptions.ocaModelInfo?.maxTokens if (isOminiModel && reasoningOn) { temperature = undefined // Thinking mode doesn't support temperature } // Define cache control object if prompt caching is enabled - const cacheControl = this.options.ocaUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined + const cacheControl = this.ocaOptions.ocaUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined // Add cache_control to system message if enabled const enhancedSystemMessage = { @@ -206,7 +211,7 @@ export class OcaHandler implements ApiHandler { const toolCallProcessor = new ToolCallProcessor() const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { - model: this.options.ocaModelId || liteLlmDefaultModelId, + model: modelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, stream: true, @@ -214,21 +219,18 @@ export class OcaHandler implements ApiHandler { max_tokens: maxTokens, stream_options: { include_usage: true }, ...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable - ...(this.options.taskId && { - litellm_session_id: `cline-${this.options.taskId}`, + ...(this.ocaOptions.taskId && { + litellm_session_id: `cline-${this.ocaOptions.taskId}`, ...getOpenAIToolParams(tools), }), // Add session ID for LiteLLM tracking } - if (this.options.ocaModelInfo?.supportsReasoningEffort) { - chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any) + if (this.ocaOptions.ocaModelInfo?.supportsReasoningEffort) { + chatCompletionsParams["reasoning_effort"] = this.ocaOptions.ocaReasoningEffort || ("medium" as any) } const stream = await client.chat.completions.create(chatCompletionsParams) - const inputCost = (await this.calculateCost(1e6, 0)) || 0 - const outputCost = (await this.calculateCost(0, 1e6)) || 0 - for await (const chunk of stream) { const delta = chunk.choices[0]?.delta @@ -259,9 +261,6 @@ export class OcaHandler implements ApiHandler { // Handle token usage information if (chunk.usage) { - const totalCost = - (inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6 - // Extract cache-related information if available // Need to use type assertion since these properties are not in the standard OpenAI types const usage = chunk.usage as { @@ -276,6 +275,14 @@ export class OcaHandler implements ApiHandler { const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0 const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0 + const totalCost = await this.calculateCost( + model.info, + chunk.usage.prompt_tokens, + chunk.usage.completion_tokens, + cacheWriteTokens, + cacheReadTokens, + ) + yield { type: "usage", inputTokens: usage.prompt_tokens || 0, @@ -288,208 +295,23 @@ export class OcaHandler implements ApiHandler { } } - async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { - console.log("Using Responses API") - const client = this.ensureClient() - - // Convert messages to Responses API input format - const input: OpenAI.Responses.ResponseInputItem[] = [ - { role: "system", content: systemPrompt }, - ...convertToOpenAIResponsesInput(messages), - ] - - // Convert ChatCompletion tools to Responses API format if provided - const responseTools = tools - ?.filter((tool) => tool.type === "function") - .map((tool: any) => ({ - type: "function" as const, - name: tool.function.name, - description: tool.function.description, - parameters: tool.function.parameters, - strict: tool.function.strict ?? true, // Responses API defaults to strict mode - reasoning: { effort: "medium", summary: "auto" }, - })) - - const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = { - model: this.options.ocaModelId || liteLlmDefaultModelId, - input, - stream: true, - tools: responseTools, - } - - if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) { - responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" } - } - - // Create the response using Responses API - const stream = await client.responses.create(responsesParams) - - // Process the response stream - for await (const chunk of stream) { - // Handle different event types from Responses API - if (chunk.type === "response.output_item.added") { - const item = chunk.item - if (item.type === "function_call" && item.id) { - yield { - type: "tool_calls", - id: item.id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, - }, - }, - } - } - if (item.type === "reasoning" && item.encrypted_content && item.id) { - yield { - type: "reasoning", - id: item.id, - reasoning: "", - redacted_data: item.encrypted_content, - } - } - } - if (chunk.type === "response.output_item.done") { - const item = chunk.item - if (item.type === "function_call") { - yield { - type: "tool_calls", - id: item.id || item.call_id, - tool_call: { - call_id: item.call_id, - function: { - id: item.id, - name: item.name, - arguments: item.arguments, - }, - }, - } - } - if (item.type === "reasoning") { - yield { - type: "reasoning", - id: item.id, - details: item.summary, - reasoning: "", - } - } - } - if (chunk.type === "response.reasoning_summary_part.added") { - yield { - type: "reasoning", - id: chunk.item_id, - reasoning: chunk.part.text, - } - } - if (chunk.type === "response.reasoning_summary_text.delta") { - yield { - type: "reasoning", - id: chunk.item_id, - reasoning: chunk.delta, - } - } - if (chunk.type === "response.reasoning_summary_part.done") { - yield { - type: "reasoning", - id: chunk.item_id, - details: chunk.part, - reasoning: "", - } - } - if (chunk.type === "response.output_text.delta") { - // Handle text content deltas - if (chunk.delta) { - yield { - id: chunk.item_id, - type: "text", - text: chunk.delta, - } - } - } - if (chunk.type === "response.reasoning_text.delta") { - // Handle reasoning content deltas - if (chunk.delta) { - yield { - id: chunk.item_id, - type: "reasoning", - reasoning: chunk.delta, - } - } - } - if (chunk.type === "response.function_call_arguments.delta") { - yield { - type: "tool_calls", - tool_call: { - function: { - id: chunk.item_id, - name: chunk.item_id, - arguments: chunk.delta, - }, - }, - } - } - if (chunk.type === "response.function_call_arguments.done") { - // Handle completed function call - if (chunk.item_id && chunk.name && chunk.arguments) { - yield { - type: "tool_calls", - tool_call: { - function: { - id: chunk.item_id, - name: chunk.name, - arguments: chunk.arguments, - }, - }, - } - } - } - - if ( - chunk.type === "response.incomplete" && - chunk.response?.status === "incomplete" && - chunk.response?.incomplete_details?.reason === "max_output_tokens" - ) { - console.log("Ran out of tokens") - if (chunk.response?.output_text?.length > 0) { - console.log("Partial output:", chunk.response.output_text) - } else { - console.log("Ran out of tokens during reasoning") - } - } - - if (chunk.type === "response.completed" && chunk.response?.usage) { - // Handle usage information when response is complete - const usage = chunk.response.usage - const inputTokens = usage.input_tokens || 0 - const outputTokens = usage.output_tokens || 0 - const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0 - const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 - const totalTokens = usage.total_tokens || 0 - Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) - const inputCost = (await this.calculateCost(1e6, 0)) || 0 - const outputCost = (await this.calculateCost(0, 1e6)) || 0 - const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 - const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) - yield { - type: "usage", - inputTokens: nonCachedInputTokens, - outputTokens: outputTokens, - cacheWriteTokens: cacheWriteTokens, - cacheReadTokens: cacheReadTokens, - totalCost: totalCost, - id: chunk.response.id, - } - } + override getModel() { + return { + id: this.ocaOptions.ocaModelId || liteLlmDefaultModelId, + info: this.ocaOptions.ocaModelInfo || liteLlmModelInfoSaneDefaults, } } - getModel() { - return { - id: this.options.ocaModelId || liteLlmDefaultModelId, - info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults, - } + override async calculateCost( + modelInfo: ModelInfo, + inputTokens: number, + outputTokens: number, + cacheWriteTokens: number, + cacheReadTokens: number, + ): Promise { + const inputCost = (await this.getApiCosts(1e6, 0)) || 0 + const outputCost = (await this.getApiCosts(0, 1e6)) || 0 + const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6 + return totalCost } } diff --git a/src/core/api/providers/openai-native.ts b/src/core/api/providers/openai-native.ts index debbe4e8e0e..f59ff151481 100644 --- a/src/core/api/providers/openai-native.ts +++ b/src/core/api/providers/openai-native.ts @@ -29,13 +29,13 @@ interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions { export class OpenAiNativeHandler implements ApiHandler { private options: OpenAiNativeHandlerOptions - private client: OpenAI | undefined + protected client: OpenAI | undefined constructor(options: OpenAiNativeHandlerOptions) { this.options = options } - private ensureClient(): OpenAI { + protected ensureClient(): OpenAI { if (!this.client) { if (!this.options.openAiNativeApiKey) { throw new Error("OpenAI API key is required") @@ -57,7 +57,7 @@ export class OpenAiNativeHandler implements ApiHandler { const outputTokens = usage?.completion_tokens || 0 const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0 const cacheWriteTokens = 0 - const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) + const totalCost = await this.calculateCost(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) yield { type: "usage", @@ -82,7 +82,7 @@ export class OpenAiNativeHandler implements ApiHandler { } } - private async *createCompletionStream( + protected async *createCompletionStream( systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[], @@ -146,7 +146,7 @@ export class OpenAiNativeHandler implements ApiHandler { } } - private async *createResponseStream( + protected async *createResponseStream( systemPrompt: string, messages: ClineStorageMessage[], tools: ChatCompletionTool[], @@ -333,7 +333,13 @@ export class OpenAiNativeHandler implements ApiHandler { const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0 const totalTokens = usage.total_tokens || 0 Logger.log(`Total tokens from Responses API usage: ${totalTokens}`) - const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) + const totalCost = await this.calculateCost( + model.info, + inputTokens, + outputTokens, + cacheWriteTokens, + cacheReadTokens, + ) const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) yield { type: "usage", @@ -348,7 +354,7 @@ export class OpenAiNativeHandler implements ApiHandler { } } - getModel(): { id: OpenAiNativeModelId; info: OpenAiCompatibleModelInfo } { + getModel(): { id: string; info: OpenAiCompatibleModelInfo } { const modelId = this.options.apiModelId if (modelId && modelId in openAiNativeModels) { const id = modelId as OpenAiNativeModelId @@ -360,4 +366,14 @@ export class OpenAiNativeHandler implements ApiHandler { info: { ...openAiNativeModels[openAiNativeDefaultModelId] }, } } + + async calculateCost( + modelInfo: ModelInfo, + inputTokens: number, + outputTokens: number, + cacheWriteTokens: number, + cacheReadTokens: number, + ) { + return calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) + } } diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index 46accd9dd1d..d978cba3d33 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -107,13 +107,7 @@ const OcaModelPicker: React.FC = ({ } }, [apiConfiguration, currentMode]) - const selectedOcaModelInfo = useMemo(() => { - if (currentMode == "plan") { - return apiConfiguration?.planModeOcaModelInfo - } else { - return apiConfiguration?.actModeOcaModelInfo - } - }, [apiConfiguration, currentMode]) + const reasoningEffortOptions = selectedModelInfo ? (selectedModelInfo as OcaModelInfo).reasoningEffortOptions : [] const modelIds = useMemo(() => { return Object.keys(ocaModels || []).sort((a, b) => a.localeCompare(b)) @@ -194,9 +188,10 @@ const OcaModelPicker: React.FC = ({ ) : null} {!loading && - selectedOcaModelInfo && - selectedOcaModelInfo.supportsReasoning && - selectedOcaModelInfo.reasoningEffortOptions.length >= 0 && ( + modelIds.length > 0 && + selectedModelInfo && + selectedModelInfo.supportsReasoning && + reasoningEffortOptions.length > 0 && (
@@ -208,7 +203,7 @@ const OcaModelPicker: React.FC = ({ const newValue = e.target.currentValue handleReasoningEffortChange(newValue) }}> - {selectedOcaModelInfo?.reasoningEffortOptions.map((reasoningEffort) => ( + {reasoningEffortOptions.map((reasoningEffort) => ( Date: Fri, 19 Dec 2025 12:01:04 -0600 Subject: [PATCH 13/14] Refactored some more code to look better --- .../settings/providers/OcaModelPicker.tsx | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index d978cba3d33..d70174971d8 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -40,20 +40,22 @@ const OcaModelPicker: React.FC = ({ setPendingModelId(newModelId) setShowRestrictedPopup(true) } else { - const fieldChangeKeys: any = { - ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, - ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, - ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, - } - const fieldChangeValues: any = { - ocaModelId: newModelId, - ocaModelInfo: ocaModels[newModelId], - ocaReasoningEffort: - ocaModels[newModelId].reasoningEffortOptions.length > 0 - ? ocaModels[newModelId].reasoningEffortOptions[0] - : undefined, - } - await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) + await handleModeFieldsChange( + { + ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, + ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, + }, + { + ocaModelId: newModelId, + ocaModelInfo: ocaModels[newModelId], + ocaReasoningEffort: + ocaModels[newModelId].reasoningEffortOptions.length > 0 + ? ocaModels[newModelId].reasoningEffortOptions[0] + : undefined, + }, + currentMode, + ) } } } @@ -72,20 +74,22 @@ const OcaModelPicker: React.FC = ({ const onAcknowledge = async () => { if (pendingModelId && ocaModels) { - const fieldChangeKeys: any = { - ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, - ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, - ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, - } - const fieldChangeValues: any = { - ocaModelId: pendingModelId, - ocaModelInfo: ocaModels[pendingModelId], - ocaReasoningEffort: - ocaModels[pendingModelId].reasoningEffortOptions.length > 0 - ? ocaModels[pendingModelId].reasoningEffortOptions[0] - : undefined, - } - await handleModeFieldsChange(fieldChangeKeys, fieldChangeValues, currentMode) + await handleModeFieldsChange( + { + ocaModelId: { plan: "planModeOcaModelId", act: "actModeOcaModelId" }, + ocaModelInfo: { plan: "planModeOcaModelInfo", act: "actModeOcaModelInfo" }, + ocaReasoningEffort: { plan: "planModeOcaReasoningEffort", act: "actModeOcaReasoningEffort" }, + }, + { + ocaModelId: pendingModelId, + ocaModelInfo: ocaModels[pendingModelId], + ocaReasoningEffort: + ocaModels[pendingModelId].reasoningEffortOptions.length > 0 + ? ocaModels[pendingModelId].reasoningEffortOptions[0] + : undefined, + }, + currentMode, + ) setPendingModelId(null) setShowRestrictedPopup(false) } From 540033577cbb0e122e6846d619cca3502129462c Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 23 Dec 2025 11:03:13 -0600 Subject: [PATCH 14/14] Made changees to support reasoning effort selection and also not using instructions field --- src/core/api/providers/oca.ts | 14 +++++++-- src/core/api/providers/openai-native.ts | 29 ++++++++++++++----- .../api/transform/openai-response-format.ts | 13 ++++++++- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index 1361ac4d26e..fd6976e0e67 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -143,8 +143,18 @@ export class OcaHandler extends OpenAiNativeHandler { @withRetry() override async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { - if (this.ocaOptions.ocaModelInfo?.supportsResponsesApi) { - yield* this.createResponseStream(systemPrompt, messages, tools ?? []) + const model = this.ocaOptions.ocaModelInfo + if (model?.supportsResponsesApi) { + const supportsReasoningEffort = model.supportsReasoning + const selectedReasoningEffort = this.ocaOptions.ocaReasoningEffort + yield* this.createResponseStream( + systemPrompt, + messages, + tools ?? [], + false, + supportsReasoningEffort, + selectedReasoningEffort, + ) } else { yield* this.createCompletionStream(systemPrompt, messages, tools) } diff --git a/src/core/api/providers/openai-native.ts b/src/core/api/providers/openai-native.ts index f59ff151481..1a02e81fd5d 100644 --- a/src/core/api/providers/openai-native.ts +++ b/src/core/api/providers/openai-native.ts @@ -76,7 +76,7 @@ export class OpenAiNativeHandler implements ApiHandler { if (!tools?.length) { throw new Error("Native Tool Call must be enabled in your setting for OpenAI Responses API") } - yield* this.createResponseStream(systemPrompt, messages, tools) + yield* this.createResponseStream(systemPrompt, messages, tools, true, true) } else { yield* this.createCompletionStream(systemPrompt, messages, tools) } @@ -150,12 +150,15 @@ export class OpenAiNativeHandler implements ApiHandler { systemPrompt: string, messages: ClineStorageMessage[], tools: ChatCompletionTool[], + useInstructionsParameter: boolean, + useReasoningEffort?: boolean, + reasoningEffort?: string, ): ApiStream { const client = this.ensureClient() const model = this.getModel() // Convert messages to Responses API input format - const input = convertToOpenAIResponsesInput(messages) + const input = convertToOpenAIResponsesInput(messages, systemPrompt, !useInstructionsParameter) // Convert ChatCompletion tools to Responses API format if provided const responseTools = tools @@ -172,19 +175,29 @@ export class OpenAiNativeHandler implements ApiHandler { // const lastAssistantMessage = [...messages].reverse().find((msg) => msg.role === "assistant" && msg.id) // const previous_response_id = lastAssistantMessage?.id - - // Create the response using Responses API - const stream = await client.responses.create({ + const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = { model: model.id, - instructions: systemPrompt, input, stream: true, tools: responseTools, // previous_response_id, // store: true, - reasoning: { effort: "medium", summary: "auto" }, // include: ["reasoning.encrypted_content"], - }) + } + + if (useReasoningEffort) { + responsesParams["reasoning"] = { + effort: (reasoningEffort as any) ?? "medium", + summary: "auto", + } + } + + if (useInstructionsParameter) { + responsesParams["instructions"] = systemPrompt + } + + // Create the response using Responses API + const stream = await client.responses.create(responsesParams) // Process the response stream for await (const chunk of stream) { diff --git a/src/core/api/transform/openai-response-format.ts b/src/core/api/transform/openai-response-format.ts index 6c8e6872642..09ed2bd6260 100644 --- a/src/core/api/transform/openai-response-format.ts +++ b/src/core/api/transform/openai-response-format.ts @@ -71,10 +71,21 @@ import { ClineStorageMessage } from "@/shared/messages/content" * @param messages - Array of ClineStorageMessage objects to be converted * @returns ResponseInput array containing the transformed messages with proper reasoning pairing */ -export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): ResponseInput { +export function convertToOpenAIResponsesInput( + messages: ClineStorageMessage[], + systemPrompt: string, + useSystemPrompt: boolean, +): ResponseInput { const allItems: any[] = [] const toolUseIdToCallId = new Map() + if (useSystemPrompt) { + allItems.push({ + role: "system", + content: systemPrompt, + }) + } + for (const m of messages) { if (typeof m.content === "string") { allItems.push({ role: m.role, content: [{ type: "input_text", text: m.content }] })