From 0cf0d2a3893b3d865c28cfbaf70f068b4a761c7b Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 10 Nov 2025 09:27:12 -0700 Subject: [PATCH] Misc --- src/api/providers/openai-native.ts | 52 ++++++++++++++++++++++- src/core/webview/ClineProvider.ts | 38 +++++------------ src/core/webview/webviewMessageHandler.ts | 50 +++++++++++++++++++++- 3 files changed, 108 insertions(+), 32 deletions(-) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index e39d02b486..bab2673443 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -1,11 +1,15 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import * as fsSync from "fs" +import * as pathSync from "path" +import * as osSync from "os" import { type ModelInfo, openAiNativeDefaultModelId, OpenAiNativeModelId, - getOpenAiNativeModels, + openAiNativeModels, + validateModelInfoRecord, OPENAI_NATIVE_DEFAULT_TEMPERATURE, GPT5_DEFAULT_TEMPERATURE, type ReasoningEffort, @@ -31,6 +35,50 @@ export type OpenAiNativeModel = ReturnType // Constants for model identification const GPT5_MODEL_PREFIX = "gpt-5" +/** + * Host-only sync loader for OpenAI Native models merged with ~/.roo overrides. + * Avoids the browser-safe loader in @roo-code/types which may not resolve extras in the extension host bundle. + */ +function loadMergedOpenAiNativeModelsOnHostSync(): Record { + try { + // 1) Inline JSON override + let extras: Record = {} + try { + const inline = process.env?.ROO_OPENAI_NATIVE_MODELS_JSON + if (inline) { + const parsed = JSON.parse(inline) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + extras = validateModelInfoRecord(parsed) + } + } + } catch { + // ignore + } + + // 2) File-based load when no inline provided + if (Object.keys(extras).length === 0) { + try { + const customPath = + process.env.ROO_OPENAI_NATIVE_MODELS_PATH || + pathSync.join(osSync.homedir(), ".roo", "models", "openai-native.json") + if (customPath && fsSync.existsSync(customPath)) { + const raw = fsSync.readFileSync(customPath, "utf8") + const parsed = JSON.parse(raw) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + extras = validateModelInfoRecord(parsed) + } + } + } catch { + // ignore file errors + } + } + + return { ...openAiNativeModels, ...extras } as Record + } catch { + return openAiNativeModels as Record + } +} + export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI @@ -1222,7 +1270,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio const modelId = this.options.apiModelId // Get merged models (built-in + user-defined from ~/.roo/models/openai-native.json) - const allModels = getOpenAiNativeModels() + const allModels = loadMergedOpenAiNativeModelsOnHostSync() let id = modelId && modelId in allModels ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index aa459bd737..58026eebee 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -42,7 +42,6 @@ import { ORGANIZATION_ALLOW_ALL, DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, - getModelId, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud" @@ -1296,31 +1295,6 @@ export class ClineProvider // Provider Profile Management - /** - * Updates the current task's API handler if the provider or model has changed. - * This prevents unnecessary context condensing when only non-model settings change. - * @param providerSettings The new provider settings to apply - */ - private updateTaskApiHandlerIfNeeded(providerSettings: ProviderSettings): void { - const task = this.getCurrentTask() - - if (task && task.apiConfiguration) { - // Only rebuild API handler if provider or model actually changed - // to avoid triggering unnecessary context condensing - const currentProvider = task.apiConfiguration.apiProvider - const newProvider = providerSettings.apiProvider - const currentModelId = getModelId(task.apiConfiguration) - const newModelId = getModelId(providerSettings) - - if (currentProvider !== newProvider || currentModelId !== newModelId) { - task.api = buildApiHandler(providerSettings) - } - } else if (task) { - // Fallback: rebuild if apiConfiguration is not available - task.api = buildApiHandler(providerSettings) - } - } - getProviderProfileEntries(): ProviderSettingsEntry[] { return this.contextProxy.getValues().listApiConfigMeta || [] } @@ -1368,7 +1342,11 @@ export class ClineProvider // Change the provider for the current task. // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). - this.updateTaskApiHandlerIfNeeded(providerSettings) + const task = this.getCurrentTask() + + if (task) { + task.api = buildApiHandler(providerSettings) + } } else { await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) } @@ -1425,7 +1403,11 @@ export class ClineProvider } // Change the provider for the current task. - this.updateTaskApiHandlerIfNeeded(providerSettings) + const task = this.getCurrentTask() + + if (task) { + task.api = buildApiHandler(providerSettings) + } await this.postStateToWebview() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8432f5b484..29f9f8a6a9 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -11,7 +11,7 @@ import { type ClineMessage, type TelemetrySetting, type ModelInfo, - getOpenAiNativeModels, + validateModelInfoRecord, TelemetryEventName, UserSettingsConfig, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, @@ -55,6 +55,7 @@ import { getWorkspacePath } from "../../utils/path" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" import { GetModelsOptions } from "../../shared/api" +import { openAiNativeModels } from "@roo-code/types" import { generateSystemPrompt } from "./generateSystemPrompt" import { getCommand } from "../../utils/commands" @@ -99,6 +100,51 @@ export const webviewMessageHandler = async ( ) } + /** + * Host-only loader: merge built-in OpenAI native models with user additions from: + * - ROO_OPENAI_NATIVE_MODELS_JSON (JSON string) + * - ROO_OPENAI_NATIVE_MODELS_PATH or ~/.roo/models/openai-native.json + * Returns the merged models record. + */ + async function getMergedOpenAiNativeModelsOnHost(): Promise> { + try { + // 1) Env JSON override + let extras: Record = {} + const inline = process.env?.ROO_OPENAI_NATIVE_MODELS_JSON + if (inline) { + try { + const parsed = JSON.parse(inline) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + extras = validateModelInfoRecord(parsed) + } + } catch { + // ignore malformed env json + } + } + + // 2) File-based + if (Object.keys(extras).length === 0) { + const customPath = + process.env.ROO_OPENAI_NATIVE_MODELS_PATH || + path.join(os.homedir(), ".roo", "models", "openai-native.json") + try { + const raw = await fs.readFile(customPath, "utf8") + const parsed = JSON.parse(raw) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + extras = validateModelInfoRecord(parsed) + } + } catch { + // missing/invalid file: ignore + } + } + + const merged = { ...openAiNativeModels, ...extras } + return merged as Record + } catch { + return openAiNativeModels as Record + } + } + /** * Removes the target message and all subsequent messages */ @@ -985,7 +1031,7 @@ export const webviewMessageHandler = async ( case "requestOpenAiNativeModels": { // Return merged built-ins + user-defined from ~/.roo/models/openai-native.json try { - const mergedModels = getOpenAiNativeModels() + const mergedModels = await getMergedOpenAiNativeModelsOnHost() provider.postMessageToWebview({ type: "openAiNativeModels", openAiNativeModels: mergedModels }) } catch (error) { console.error("Failed to load OpenAI Native models:", error)