fix(openai-native): consolidate loaders + tolerant validation; surface custom models in OpenAI/OpenAI Native UI; fix lint types

This commit is contained in:
Hannes Rudolph 2025-11-07 20:05:04 -07:00
parent 27f404426c
commit cc7c50c959
3 changed files with 30 additions and 21 deletions

View file

@ -1,4 +1,4 @@
import type { ModelInfo } from "../model.js"
import { modelInfoSchema, type ModelInfo } from "../model.js"
// https://openai.com/api/pricing/
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
@ -328,12 +328,7 @@ function loadUserOpenAiNativeModels(): Record<string, ModelInfo> {
try {
const parsed = JSON.parse(inlineJson)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
const envResult: Record<string, ModelInfo> = {}
for (const [modelId, info] of Object.entries(parsed)) {
if (info && typeof info === "object" && !Array.isArray(info)) {
envResult[modelId] = info as ModelInfo
}
}
const envResult = validateModelInfoRecord(parsed)
return envResult
}
} catch {
@ -363,20 +358,32 @@ function loadUserOpenAiNativeModels(): Record<string, ModelInfo> {
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
// Best-effort shallow validation; only keep object entries
const result: Record<string, ModelInfo> = {}
for (const [modelId, info] of Object.entries(parsed)) {
if (info && typeof info === "object" && !Array.isArray(info)) {
result[modelId] = info as ModelInfo
}
}
return result
// Use shared validator (merges sane defaults and accepts partials)
return validateModelInfoRecord(parsed)
} catch {
// On any error (missing file, invalid JSON, restricted env), fall back to empty extras
return {}
}
}
/**
* Validate an arbitrary record of model info objects, keeping only entries that conform.
*/
export function validateModelInfoRecord(input: unknown): Record<string, ModelInfo> {
if (!input || typeof input !== "object" || Array.isArray(input)) return {}
const out: Record<string, ModelInfo> = {}
for (const [id, info] of Object.entries(input as Record<string, unknown>)) {
if (info && typeof info === "object" && !Array.isArray(info)) {
const parsedInfo = modelInfoSchema.partial().safeParse(info)
const merged = parsedInfo.success
? { ...openAiModelInfoSaneDefaults, ...parsedInfo.data }
: { ...openAiModelInfoSaneDefaults, ...(info as Record<string, unknown>) }
out[id] = merged as ModelInfo
}
}
return out
}
/**
* Returns built-in OpenAI native models merged with user-defined additions.
* User models override built-ins on key collision.

View file

@ -9,6 +9,7 @@ import {
openAiNativeDefaultModelId,
OpenAiNativeModelId,
openAiNativeModels,
validateModelInfoRecord,
OPENAI_NATIVE_DEFAULT_TEMPERATURE,
GPT5_DEFAULT_TEMPERATURE,
type ReasoningEffort,
@ -47,7 +48,7 @@ function loadMergedOpenAiNativeModelsOnHostSync(): Record<string, ModelInfo> {
if (inline) {
const parsed = JSON.parse(inline)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
extras = parsed as Record<string, ModelInfo>
extras = validateModelInfoRecord(parsed)
}
}
} catch {
@ -64,7 +65,7 @@ function loadMergedOpenAiNativeModelsOnHostSync(): Record<string, ModelInfo> {
const raw = fsSync.readFileSync(customPath, "utf8")
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
extras = parsed as Record<string, ModelInfo>
extras = validateModelInfoRecord(parsed)
}
}
} catch {

View file

@ -11,6 +11,7 @@ import {
type ClineMessage,
type TelemetrySetting,
type ModelInfo,
validateModelInfoRecord,
TelemetryEventName,
UserSettingsConfig,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
@ -114,7 +115,7 @@ export const webviewMessageHandler = async (
try {
const parsed = JSON.parse(inline)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
extras = parsed
extras = validateModelInfoRecord(parsed)
}
} catch {
// ignore malformed env json
@ -130,7 +131,7 @@ export const webviewMessageHandler = async (
const raw = await fs.readFile(customPath, "utf8")
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
extras = parsed
extras = validateModelInfoRecord(parsed)
}
} catch {
// missing/invalid file: ignore
@ -1030,8 +1031,8 @@ export const webviewMessageHandler = async (
case "requestOpenAiNativeModels": {
// Return merged built-ins + user-defined from ~/.roo/models/openai-native.json
try {
const openAiNativeModels = await getMergedOpenAiNativeModelsOnHost()
provider.postMessageToWebview({ type: "openAiNativeModels", openAiNativeModels })
const mergedModels = await getMergedOpenAiNativeModelsOnHost()
provider.postMessageToWebview({ type: "openAiNativeModels", openAiNativeModels: mergedModels })
} catch (error) {
console.error("Failed to load OpenAI Native models:", error)
provider.postMessageToWebview({ type: "openAiNativeModels", openAiNativeModels: {} })