This commit is contained in:
Hannes Rudolph 2025-11-10 09:27:12 -07:00
parent 3ac3240d42
commit 0cf0d2a389
3 changed files with 108 additions and 32 deletions

View file

@ -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<OpenAiNativeHandler["getModel"]>
// 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<string, ModelInfo> {
try {
// 1) Inline JSON override
let extras: Record<string, ModelInfo> = {}
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<string, ModelInfo>
} catch {
return openAiNativeModels as Record<string, ModelInfo>
}
}
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

View file

@ -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()

View file

@ -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<Record<string, ModelInfo>> {
try {
// 1) Env JSON override
let extras: Record<string, any> = {}
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<string, ModelInfo>
} catch {
return openAiNativeModels as Record<string, ModelInfo>
}
}
/**
* 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)