diff --git a/proto/cline/models.proto b/proto/cline/models.proto index 159afb81f86..bde344d558d 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -380,6 +380,14 @@ 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; + // Supports reasoning + optional bool supports_reasoning = 21; + // reasoning effort options + repeated string reasoning_effort_options = 22; } // Aggregated OCA model catalog keyed by model identifier @@ -602,12 +610,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; @@ -643,10 +652,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/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 7b02d7ee88c..fd6976e0e67 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, 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" @@ -9,33 +9,36 @@ 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 { type CommonApiHandlerOptions } from ".." import { withRetry } from "../retry" import { convertToOpenAiMessages } from "../transform/openai-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 ocaModelId?: string - ocaModelInfo?: LiteLLMModelInfo + ocaModelInfo?: OcaModelInfo + ocaReasoningEffort?: string thinkingBudgetTokens?: number ocaUsePromptCache?: boolean taskId?: string 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() @@ -44,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) @@ -79,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}`) } @@ -100,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`, { @@ -139,30 +142,54 @@ export class OcaHandler implements ApiHandler { } @withRetry() - async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + override async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream { + 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) + } + } + + protected override async *createCompletionStream( + 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 = { 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 = { @@ -193,8 +220,8 @@ export class OcaHandler implements ApiHandler { const toolCallProcessor = new ToolCallProcessor() - const stream = await client.chat.completions.create({ - model: this.options.ocaModelId || liteLlmDefaultModelId, + const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, stream: true, @@ -202,14 +229,17 @@ 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 - }) + } - const inputCost = (await this.calculateCost(1e6, 0)) || 0 - const outputCost = (await this.calculateCost(0, 1e6)) || 0 + if (this.ocaOptions.ocaModelInfo?.supportsReasoningEffort) { + chatCompletionsParams["reasoning_effort"] = this.ocaOptions.ocaReasoningEffort || ("medium" as any) + } + + const stream = await client.chat.completions.create(chatCompletionsParams) for await (const chunk of stream) { const delta = chunk.choices[0]?.delta @@ -241,9 +271,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 { @@ -258,6 +285,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, @@ -270,10 +305,23 @@ export class OcaHandler implements ApiHandler { } } - getModel() { + override getModel() { return { - id: this.options.ocaModelId || liteLlmDefaultModelId, - info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults, + id: this.ocaOptions.ocaModelId || liteLlmDefaultModelId, + info: this.ocaOptions.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..1a02e81fd5d 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", @@ -76,13 +76,13 @@ 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) } } - private async *createCompletionStream( + protected async *createCompletionStream( systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[], @@ -146,16 +146,19 @@ export class OpenAiNativeHandler implements ApiHandler { } } - private async *createResponseStream( + protected async *createResponseStream( 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) { @@ -333,7 +346,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 +367,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 +379,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/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 }] }) diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index 1dc4c12b1d8..f134568508f 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" @@ -57,6 +62,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 +79,10 @@ 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), + supportsReasoning: modelInfo.is_reasoning_model || false, + reasoningEffortOptions: modelInfo.reasoning_effort_options || [], }) } console.log("OCA models fetched", models) @@ -91,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 = {} @@ -98,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/src/core/storage/StateManager.ts b/src/core/storage/StateManager.ts index 787e3d8677c..38f821a07d1 100644 --- a/src/core/storage/StateManager.ts +++ b/src/core/storage/StateManager.ts @@ -597,6 +597,7 @@ export class StateManager { planModeHuaweiCloudMaasModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -635,6 +636,7 @@ export class StateManager { actModeHuaweiCloudMaasModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, @@ -677,6 +679,7 @@ export class StateManager { planModeHuaweiCloudMaasModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -716,6 +719,7 @@ export class StateManager { actModeHuaweiCloudMaasModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, @@ -1265,6 +1269,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 +1341,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..3e2f3b791fa 100644 --- a/src/core/storage/utils/state-helpers.ts +++ b/src/core/storage/utils/state-helpers.ts @@ -389,6 +389,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 +463,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 +604,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis planModeBasetenModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaReasoningEffort, planModeHicapModelId, planModeHicapModelInfo, planModeAihubmixModelId, @@ -640,6 +643,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis actModeBasetenModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaReasoningEffort, actModeHicapModelId, actModeHicapModelInfo, actModeAihubmixModelId, 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..1e735045470 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -171,6 +171,7 @@ export interface ApiHandlerOptions { planModeHuaweiCloudMaasModelInfo?: ModelInfo planModeOcaModelId?: string planModeOcaModelInfo?: OcaModelInfo + planModeOcaReasoningEffort?: string planModeAihubmixModelId?: string planModeAihubmixModelInfo?: OpenAiCompatibleModelInfo planModeHicapModelId?: string @@ -211,6 +212,7 @@ export interface ApiHandlerOptions { actModeHuaweiCloudMaasModelInfo?: ModelInfo actModeOcaModelId?: string actModeOcaModelInfo?: OcaModelInfo + actModeOcaReasoningEffort?: string actModeAihubmixModelId?: string actModeAihubmixModelInfo?: OpenAiCompatibleModelInfo actModeHicapModelId?: string @@ -276,6 +278,10 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo { surveyId?: string banner?: string surveyContent?: string + 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 a3731eb5bca..6ba92683c11 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -108,6 +108,10 @@ function convertOcaModelInfoToProtoOcaModelInfo(info: OcaModelInfo | undefined): surveyId: info.surveyId, banner: info.banner, modelName: info.modelName, + supportsChatApi: info.supportsChatApi, + supportsResponsesApi: info.supportsResponsesApi, + supportsReasoning: info.supportsReasoning, + reasoningEffortOptions: info.reasoningEffortOptions, } } @@ -131,6 +135,10 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi surveyId: info.surveyId, banner: info.banner, modelName: info.modelName, + supportsChatApi: info.supportsChatApi, + supportsResponsesApi: info.supportsResponsesApi, + supportsReasoning: info.supportsReasoning, + reasoningEffortOptions: info.reasoningEffortOptions, } } @@ -528,6 +536,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, @@ -567,6 +576,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, @@ -698,6 +708,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, @@ -738,6 +749,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 31cedcbd64e..5d84f699223 100644 --- a/src/shared/proto-conversions/models/typeConversion.ts +++ b/src/shared/proto-conversions/models/typeConversion.ts @@ -144,6 +144,10 @@ export function fromProtobufOcaModelInfo(protoInfo: ProtoOcaModelInfo): OcaModel surveyId: protoInfo.surveyId, banner: protoInfo.banner, surveyContent: protoInfo.surveyContent, + supportsChatApi: protoInfo.supportsChatApi, + supportsResponsesApi: protoInfo.supportsResponsesApi, + supportsReasoning: protoInfo.supportsReasoning, + reasoningEffortOptions: protoInfo.reasoningEffortOptions, } } diff --git a/src/shared/storage/state-keys.ts b/src/shared/storage/state-keys.ts index db3735a62c5..17efdabbf05 100644 --- a/src/shared/storage/state-keys.ts +++ b/src/shared/storage/state-keys.ts @@ -165,6 +165,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 +204,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/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) } diff --git a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx index b0c19e8d62f..d70174971d8 100644 --- a/webview-ui/src/components/settings/providers/OcaModelPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaModelPicker.tsx @@ -44,10 +44,15 @@ const OcaModelPicker: React.FC = ({ { 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, ) @@ -55,16 +60,33 @@ 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( { 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, ) @@ -81,6 +103,16 @@ const OcaModelPicker: React.FC = ({ return normalizeApiConfiguration(apiConfiguration, currentMode) }, [apiConfiguration, currentMode]) + const selectedReasoningEffort = useMemo(() => { + if (currentMode == "plan") { + return apiConfiguration?.planModeOcaReasoningEffort + } else { + return apiConfiguration?.actModeOcaReasoningEffort + } + }, [apiConfiguration, currentMode]) + + const reasoningEffortOptions = selectedModelInfo ? (selectedModelInfo as OcaModelInfo).reasoningEffortOptions : [] + const modelIds = useMemo(() => { return Object.keys(ocaModels || []).sort((a, b) => a.localeCompare(b)) }, [ocaModels]) @@ -108,6 +140,10 @@ const OcaModelPicker: React.FC = ({ max-height: 100px; overflow: auto; } + #reasoning-effort-dropdown::part(listbox){ + max-height: 100px; + overflow: auto; + } `}
@@ -155,6 +191,40 @@ const OcaModelPicker: React.FC = ({ Last refreshed at {lastRefreshedText}
) : null} + {!loading && + modelIds.length > 0 && + selectedModelInfo && + selectedModelInfo.supportsReasoning && + reasoningEffortOptions.length > 0 && ( + + +
+ { + const newValue = e.target.currentValue + handleReasoningEffortChange(newValue) + }}> + {reasoningEffortOptions.map((reasoningEffort) => ( + + {reasoningEffort} + + ))} + +
+
+ )} {selectedModelInfo && ( <> {showBudgetSlider && }