fix: harden provider recommendation safety
This commit is contained in:
@@ -6,24 +6,20 @@ import {
|
||||
normalizeRecommendationGoal,
|
||||
recommendOllamaModel,
|
||||
} from '../src/utils/providerRecommendation.ts'
|
||||
import {
|
||||
buildOllamaProfileEnv,
|
||||
buildOpenAIProfileEnv,
|
||||
createProfileFile,
|
||||
selectAutoProfile,
|
||||
type ProfileFile,
|
||||
type ProviderProfile,
|
||||
} from '../src/utils/providerProfile.ts'
|
||||
import {
|
||||
getOllamaChatBaseUrl,
|
||||
hasLocalOllama,
|
||||
listOllamaModels,
|
||||
} from './provider-discovery.ts'
|
||||
|
||||
type ProviderProfile = 'openai' | 'ollama'
|
||||
|
||||
type ProfileFile = {
|
||||
profile: ProviderProfile
|
||||
env: {
|
||||
OPENAI_BASE_URL?: string
|
||||
OPENAI_MODEL?: string
|
||||
OPENAI_API_KEY?: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
function parseArg(name: string): string | null {
|
||||
const args = process.argv.slice(2)
|
||||
const idx = args.indexOf(name)
|
||||
@@ -37,25 +33,16 @@ function parseProviderArg(): ProviderProfile | 'auto' {
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
function sanitizeApiKey(key: string | null): string | undefined {
|
||||
if (!key || key === 'SUA_CHAVE') return undefined
|
||||
return key
|
||||
}
|
||||
|
||||
async function resolveOllamaModel(
|
||||
argModel: string | null,
|
||||
argBaseUrl: string | null,
|
||||
goal: ReturnType<typeof normalizeRecommendationGoal>,
|
||||
): Promise<string> {
|
||||
) : Promise<string | null> {
|
||||
if (argModel) return argModel
|
||||
|
||||
const discovered = await listOllamaModels(argBaseUrl || undefined)
|
||||
const recommended = recommendOllamaModel(discovered, goal)
|
||||
if (recommended) {
|
||||
return recommended.name
|
||||
}
|
||||
|
||||
return process.env.OPENAI_MODEL || 'llama3.1:8b'
|
||||
return recommended?.name ?? null
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -68,37 +55,57 @@ async function main(): Promise<void> {
|
||||
)
|
||||
|
||||
let selected: ProviderProfile
|
||||
let resolvedOllamaModel: string | null = null
|
||||
if (provider === 'auto') {
|
||||
selected = (await hasLocalOllama(argBaseUrl || undefined)) ? 'ollama' : 'openai'
|
||||
if (await hasLocalOllama(argBaseUrl || undefined)) {
|
||||
resolvedOllamaModel = await resolveOllamaModel(argModel, argBaseUrl, goal)
|
||||
selected = selectAutoProfile(resolvedOllamaModel)
|
||||
} else {
|
||||
selected = 'openai'
|
||||
}
|
||||
} else {
|
||||
selected = provider
|
||||
}
|
||||
|
||||
const env: ProfileFile['env'] = {}
|
||||
let env: ProfileFile['env']
|
||||
if (selected === 'ollama') {
|
||||
env.OPENAI_BASE_URL = getOllamaChatBaseUrl(argBaseUrl || undefined)
|
||||
env.OPENAI_MODEL = await resolveOllamaModel(argModel, argBaseUrl, goal)
|
||||
const key = sanitizeApiKey(argApiKey || process.env.OPENAI_API_KEY || null)
|
||||
if (key) env.OPENAI_API_KEY = key
|
||||
resolvedOllamaModel ??= await resolveOllamaModel(argModel, argBaseUrl, goal)
|
||||
if (!resolvedOllamaModel) {
|
||||
console.error('No viable Ollama chat model was discovered. Pull a chat model first or pass --model explicitly.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
env = buildOllamaProfileEnv(
|
||||
resolvedOllamaModel,
|
||||
{
|
||||
baseUrl: argBaseUrl,
|
||||
getOllamaChatBaseUrl,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
env.OPENAI_BASE_URL = argBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
env.OPENAI_MODEL =
|
||||
argModel ||
|
||||
process.env.OPENAI_MODEL ||
|
||||
getGoalDefaultOpenAIModel(goal)
|
||||
const key = sanitizeApiKey(argApiKey || process.env.OPENAI_API_KEY || null)
|
||||
if (!key) {
|
||||
const builtEnv = buildOpenAIProfileEnv({
|
||||
goal,
|
||||
model:
|
||||
argModel ||
|
||||
process.env.OPENAI_MODEL ||
|
||||
getGoalDefaultOpenAIModel(goal),
|
||||
apiKey: argApiKey || process.env.OPENAI_API_KEY || null,
|
||||
processEnv: {
|
||||
...process.env,
|
||||
OPENAI_BASE_URL:
|
||||
argBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
||||
},
|
||||
})
|
||||
|
||||
if (!builtEnv) {
|
||||
console.error('OpenAI profile requires a real API key. Use --api-key or set OPENAI_API_KEY.')
|
||||
process.exit(1)
|
||||
}
|
||||
env.OPENAI_API_KEY = key
|
||||
|
||||
env = builtEnv
|
||||
}
|
||||
|
||||
const profile: ProfileFile = {
|
||||
profile: selected,
|
||||
env,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
const profile = createProfileFile(selected, env)
|
||||
|
||||
const outputPath = resolve(process.cwd(), '.openclaude-profile.json')
|
||||
writeFileSync(outputPath, JSON.stringify(profile, null, 2), 'utf8')
|
||||
|
||||
@@ -3,27 +3,21 @@ import { spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
getGoalDefaultOpenAIModel,
|
||||
normalizeRecommendationGoal,
|
||||
recommendOllamaModel,
|
||||
} from '../src/utils/providerRecommendation.ts'
|
||||
import {
|
||||
buildLaunchEnv,
|
||||
selectAutoProfile,
|
||||
type ProfileFile,
|
||||
type ProviderProfile,
|
||||
} from '../src/utils/providerProfile.ts'
|
||||
import {
|
||||
getOllamaChatBaseUrl,
|
||||
hasLocalOllama,
|
||||
listOllamaModels,
|
||||
} from './provider-discovery.ts'
|
||||
|
||||
type ProviderProfile = 'openai' | 'ollama'
|
||||
|
||||
type ProfileFile = {
|
||||
profile: ProviderProfile
|
||||
env?: {
|
||||
OPENAI_BASE_URL?: string
|
||||
OPENAI_MODEL?: string
|
||||
OPENAI_API_KEY?: string
|
||||
}
|
||||
}
|
||||
|
||||
type LaunchOptions = {
|
||||
requestedProfile: ProviderProfile | 'auto' | null
|
||||
passthroughArgs: string[]
|
||||
@@ -93,10 +87,10 @@ function loadPersistedProfile(): ProfileFile | null {
|
||||
|
||||
async function resolveOllamaDefaultModel(
|
||||
goal: ReturnType<typeof normalizeRecommendationGoal>,
|
||||
): Promise<string> {
|
||||
): Promise<string | null> {
|
||||
const models = await listOllamaModels()
|
||||
const recommended = recommendOllamaModel(models, goal)
|
||||
return recommended?.name || process.env.OPENAI_MODEL || 'llama3.1:8b'
|
||||
return recommended?.name ?? null
|
||||
}
|
||||
|
||||
function runCommand(command: string, env: NodeJS.ProcessEnv): Promise<number> {
|
||||
@@ -113,41 +107,6 @@ function runCommand(command: string, env: NodeJS.ProcessEnv): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
async function buildEnv(
|
||||
profile: ProviderProfile,
|
||||
persisted: ProfileFile | null,
|
||||
goal: ReturnType<typeof normalizeRecommendationGoal>,
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
const persistedEnv = persisted?.env ?? {}
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
CLAUDE_CODE_USE_OPENAI: '1',
|
||||
}
|
||||
|
||||
if (profile === 'ollama') {
|
||||
env.OPENAI_BASE_URL =
|
||||
persistedEnv.OPENAI_BASE_URL ||
|
||||
process.env.OPENAI_BASE_URL ||
|
||||
getOllamaChatBaseUrl()
|
||||
env.OPENAI_MODEL =
|
||||
persistedEnv.OPENAI_MODEL ||
|
||||
process.env.OPENAI_MODEL ||
|
||||
await resolveOllamaDefaultModel(goal)
|
||||
if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === 'SUA_CHAVE') {
|
||||
delete env.OPENAI_API_KEY
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
env.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL || persistedEnv.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
env.OPENAI_MODEL =
|
||||
process.env.OPENAI_MODEL ||
|
||||
persistedEnv.OPENAI_MODEL ||
|
||||
getGoalDefaultOpenAIModel(goal)
|
||||
env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || persistedEnv.OPENAI_API_KEY
|
||||
return env
|
||||
}
|
||||
|
||||
function applyFastFlags(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
env.CLAUDE_CODE_SIMPLE ??= '1'
|
||||
env.CLAUDE_CODE_DISABLE_THINKING ??= '1'
|
||||
@@ -181,18 +140,36 @@ async function main(): Promise<void> {
|
||||
|
||||
const persisted = loadPersistedProfile()
|
||||
let profile: ProviderProfile
|
||||
let resolvedOllamaModel: string | null = null
|
||||
|
||||
if (requestedProfile === 'auto') {
|
||||
if (persisted) {
|
||||
profile = persisted.profile
|
||||
} else if (await hasLocalOllama()) {
|
||||
resolvedOllamaModel = await resolveOllamaDefaultModel(options.goal)
|
||||
profile = selectAutoProfile(resolvedOllamaModel)
|
||||
} else {
|
||||
profile = (await hasLocalOllama()) ? 'ollama' : 'openai'
|
||||
profile = 'openai'
|
||||
}
|
||||
} else {
|
||||
profile = requestedProfile
|
||||
}
|
||||
|
||||
const env = await buildEnv(profile, persisted, options.goal)
|
||||
if (profile === 'ollama' && persisted?.profile !== 'ollama') {
|
||||
resolvedOllamaModel ??= await resolveOllamaDefaultModel(options.goal)
|
||||
if (!resolvedOllamaModel) {
|
||||
console.error('No viable Ollama chat model was discovered. Pull a chat model first or save one with `bun run profile:init -- --provider ollama --model <model>`.')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const env = await buildLaunchEnv({
|
||||
profile,
|
||||
persisted,
|
||||
goal: options.goal,
|
||||
getOllamaChatBaseUrl,
|
||||
resolveOllamaDefaultModel: async () => resolvedOllamaModel || 'llama3.1:8b',
|
||||
})
|
||||
if (options.fast) {
|
||||
applyFastFlags(env)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,21 @@ import { resolve } from 'node:path'
|
||||
import {
|
||||
applyBenchmarkLatency,
|
||||
getGoalDefaultOpenAIModel,
|
||||
isViableOllamaChatModel,
|
||||
normalizeRecommendationGoal,
|
||||
rankOllamaModels,
|
||||
selectRecommendedOllamaModel,
|
||||
type BenchmarkedOllamaModel,
|
||||
type RecommendationGoal,
|
||||
} from '../src/utils/providerRecommendation.ts'
|
||||
import {
|
||||
buildOllamaProfileEnv,
|
||||
buildOpenAIProfileEnv,
|
||||
createProfileFile,
|
||||
sanitizeApiKey,
|
||||
type ProfileFile,
|
||||
type ProviderProfile,
|
||||
} from '../src/utils/providerProfile.ts'
|
||||
import {
|
||||
benchmarkOllamaModel,
|
||||
getOllamaChatBaseUrl,
|
||||
@@ -17,18 +27,6 @@ import {
|
||||
listOllamaModels,
|
||||
} from './provider-discovery.ts'
|
||||
|
||||
type ProviderProfile = 'openai' | 'ollama'
|
||||
|
||||
type ProfileFile = {
|
||||
profile: ProviderProfile
|
||||
env: {
|
||||
OPENAI_BASE_URL?: string
|
||||
OPENAI_MODEL?: string
|
||||
OPENAI_API_KEY?: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type CliOptions = {
|
||||
apply: boolean
|
||||
benchmark: boolean
|
||||
@@ -90,11 +88,6 @@ function parseOptions(argv: string[]): CliOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
function sanitizeApiKey(key: string | undefined): string | undefined {
|
||||
if (!key || key === 'SUA_CHAVE') return undefined
|
||||
return key
|
||||
}
|
||||
|
||||
function printHumanSummary(payload: {
|
||||
goal: RecommendationGoal
|
||||
recommendedProfile: ProviderProfile
|
||||
@@ -138,29 +131,27 @@ async function maybeApplyProfile(
|
||||
goal: RecommendationGoal,
|
||||
baseUrl: string | null,
|
||||
): Promise<boolean> {
|
||||
const env: ProfileFile['env'] = {}
|
||||
let env: ProfileFile['env'] | null
|
||||
if (profile === 'ollama') {
|
||||
env.OPENAI_BASE_URL = getOllamaChatBaseUrl(baseUrl ?? undefined)
|
||||
env.OPENAI_MODEL = model
|
||||
const key = sanitizeApiKey(process.env.OPENAI_API_KEY)
|
||||
if (key) env.OPENAI_API_KEY = key
|
||||
env = buildOllamaProfileEnv(model, {
|
||||
baseUrl,
|
||||
getOllamaChatBaseUrl,
|
||||
})
|
||||
} else {
|
||||
const key = sanitizeApiKey(process.env.OPENAI_API_KEY)
|
||||
if (!key) {
|
||||
env = buildOpenAIProfileEnv({
|
||||
goal,
|
||||
model: model || getGoalDefaultOpenAIModel(goal),
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
processEnv: process.env,
|
||||
})
|
||||
|
||||
if (!env) {
|
||||
console.error('Cannot apply an OpenAI profile without OPENAI_API_KEY.')
|
||||
return false
|
||||
}
|
||||
env.OPENAI_BASE_URL =
|
||||
process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
env.OPENAI_MODEL = model || getGoalDefaultOpenAIModel(goal)
|
||||
env.OPENAI_API_KEY = key
|
||||
}
|
||||
|
||||
const profileFile: ProfileFile = {
|
||||
profile,
|
||||
env,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
const profileFile = createProfileFile(profile, env)
|
||||
|
||||
writeFileSync(
|
||||
resolve(process.cwd(), '.openclaude-profile.json'),
|
||||
@@ -180,7 +171,9 @@ async function main(): Promise<void> {
|
||||
: []
|
||||
|
||||
const heuristicRanked = rankOllamaModels(ollamaModels, options.goal)
|
||||
const benchmarkInput = options.benchmark ? heuristicRanked.slice(0, 3) : []
|
||||
const benchmarkInput = options.benchmark
|
||||
? heuristicRanked.filter(isViableOllamaChatModel).slice(0, 3)
|
||||
: []
|
||||
|
||||
const benchmarkResults: Record<string, number | null> = {}
|
||||
for (const model of benchmarkInput) {
|
||||
@@ -197,7 +190,7 @@ async function main(): Promise<void> {
|
||||
benchmarkMs: null,
|
||||
}))
|
||||
|
||||
const recommendedOllama = rankedModels[0] ?? null
|
||||
const recommendedOllama = selectRecommendedOllamaModel(rankedModels)
|
||||
const openAIConfigured = Boolean(sanitizeApiKey(process.env.OPENAI_API_KEY))
|
||||
|
||||
let recommendedProfile: ProviderProfile
|
||||
|
||||
Reference in New Issue
Block a user