Files
orcs-code/src/utils/providerProfile.ts
Kevin Codex 903a30916a Merge pull request #107 from rithulkamesh/main
feat: GitHub Models provider + interactive onboard (keychain-backed)
2026-04-02 20:14:51 +08:00

352 lines
9.6 KiB
TypeScript

import {
DEFAULT_CODEX_BASE_URL,
DEFAULT_OPENAI_BASE_URL,
isCodexBaseUrl,
resolveCodexApiCredentials,
resolveProviderRequest,
} from '../services/api/providerConfig.ts'
import {
getGoalDefaultOpenAIModel,
type RecommendationGoal,
} from './providerRecommendation.ts'
const DEFAULT_GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai'
const DEFAULT_GEMINI_MODEL = 'gemini-2.0-flash'
export type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' | 'atomic-chat'
export type ProfileEnv = {
OPENAI_BASE_URL?: string
OPENAI_MODEL?: string
OPENAI_API_KEY?: string
CODEX_API_KEY?: string
CHATGPT_ACCOUNT_ID?: string
CODEX_ACCOUNT_ID?: string
GEMINI_API_KEY?: string
GEMINI_MODEL?: string
GEMINI_BASE_URL?: string
}
export type ProfileFile = {
profile: ProviderProfile
env: ProfileEnv
createdAt: string
}
export function sanitizeApiKey(
key: string | null | undefined,
): string | undefined {
if (!key || key === 'SUA_CHAVE') return undefined
return key
}
export function buildOllamaProfileEnv(
model: string,
options: {
baseUrl?: string | null
getOllamaChatBaseUrl: (baseUrl?: string) => string
},
): ProfileEnv {
return {
OPENAI_BASE_URL: options.getOllamaChatBaseUrl(options.baseUrl ?? undefined),
OPENAI_MODEL: model,
}
}
export function buildAtomicChatProfileEnv(
model: string,
options: {
baseUrl?: string | null
getAtomicChatChatBaseUrl: (baseUrl?: string) => string
},
): ProfileEnv {
return {
OPENAI_BASE_URL: options.getAtomicChatChatBaseUrl(options.baseUrl ?? undefined),
OPENAI_MODEL: model,
}
}
export function buildGeminiProfileEnv(options: {
model?: string | null
baseUrl?: string | null
apiKey?: string | null
processEnv?: NodeJS.ProcessEnv
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(
options.apiKey ??
processEnv.GEMINI_API_KEY ??
processEnv.GOOGLE_API_KEY,
)
if (!key) {
return null
}
const env: ProfileEnv = {
GEMINI_MODEL:
options.model || processEnv.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
GEMINI_API_KEY: key,
}
const baseUrl = options.baseUrl || processEnv.GEMINI_BASE_URL
if (baseUrl) {
env.GEMINI_BASE_URL = baseUrl
}
return env
}
export function buildOpenAIProfileEnv(options: {
goal: RecommendationGoal
model?: string | null
baseUrl?: string | null
apiKey?: string | null
processEnv?: NodeJS.ProcessEnv
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(options.apiKey ?? processEnv.OPENAI_API_KEY)
if (!key) {
return null
}
const defaultModel = getGoalDefaultOpenAIModel(options.goal)
const shellOpenAIRequest = resolveProviderRequest({
model: processEnv.OPENAI_MODEL,
baseUrl: processEnv.OPENAI_BASE_URL,
fallbackModel: defaultModel,
})
const useShellOpenAIConfig = shellOpenAIRequest.transport === 'chat_completions'
return {
OPENAI_BASE_URL:
options.baseUrl ||
(useShellOpenAIConfig ? processEnv.OPENAI_BASE_URL : undefined) ||
DEFAULT_OPENAI_BASE_URL,
OPENAI_MODEL:
options.model ||
(useShellOpenAIConfig ? processEnv.OPENAI_MODEL : undefined) ||
defaultModel,
OPENAI_API_KEY: key,
}
}
export function buildCodexProfileEnv(options: {
model?: string | null
baseUrl?: string | null
apiKey?: string | null
processEnv?: NodeJS.ProcessEnv
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(options.apiKey ?? processEnv.CODEX_API_KEY)
const credentialEnv = key
? ({ ...processEnv, CODEX_API_KEY: key } as NodeJS.ProcessEnv)
: processEnv
const credentials = resolveCodexApiCredentials(credentialEnv)
if (!credentials.apiKey || !credentials.accountId) {
return null
}
const env: ProfileEnv = {
OPENAI_BASE_URL: options.baseUrl || DEFAULT_CODEX_BASE_URL,
OPENAI_MODEL: options.model || 'codexplan',
}
if (key) {
env.CODEX_API_KEY = key
}
env.CHATGPT_ACCOUNT_ID = credentials.accountId
return env
}
export function createProfileFile(
profile: ProviderProfile,
env: ProfileEnv,
): ProfileFile {
return {
profile,
env,
createdAt: new Date().toISOString(),
}
}
export function selectAutoProfile(
recommendedOllamaModel: string | null,
): ProviderProfile {
return recommendedOllamaModel ? 'ollama' : 'openai'
}
export async function buildLaunchEnv(options: {
profile: ProviderProfile
persisted: ProfileFile | null
goal: RecommendationGoal
processEnv?: NodeJS.ProcessEnv
getOllamaChatBaseUrl?: (baseUrl?: string) => string
resolveOllamaDefaultModel?: (goal: RecommendationGoal) => Promise<string>
getAtomicChatChatBaseUrl?: (baseUrl?: string) => string
resolveAtomicChatDefaultModel?: () => Promise<string | null>
}): Promise<NodeJS.ProcessEnv> {
const processEnv = options.processEnv ?? process.env
const persistedEnv =
options.persisted?.profile === options.profile
? options.persisted.env ?? {}
: {}
const shellGeminiKey = sanitizeApiKey(
processEnv.GEMINI_API_KEY ?? processEnv.GOOGLE_API_KEY,
)
const persistedGeminiKey = sanitizeApiKey(persistedEnv.GEMINI_API_KEY)
if (options.profile === 'gemini') {
const env: NodeJS.ProcessEnv = {
...processEnv,
CLAUDE_CODE_USE_GEMINI: '1',
}
delete env.CLAUDE_CODE_USE_OPENAI
delete env.CLAUDE_CODE_USE_GITHUB
env.GEMINI_MODEL =
processEnv.GEMINI_MODEL ||
persistedEnv.GEMINI_MODEL ||
DEFAULT_GEMINI_MODEL
env.GEMINI_BASE_URL =
processEnv.GEMINI_BASE_URL ||
persistedEnv.GEMINI_BASE_URL ||
DEFAULT_GEMINI_BASE_URL
const geminiKey = shellGeminiKey || persistedGeminiKey
if (geminiKey) {
env.GEMINI_API_KEY = geminiKey
} else {
delete env.GEMINI_API_KEY
}
delete env.GOOGLE_API_KEY
delete env.OPENAI_BASE_URL
delete env.OPENAI_MODEL
delete env.OPENAI_API_KEY
delete env.CODEX_API_KEY
delete env.CHATGPT_ACCOUNT_ID
delete env.CODEX_ACCOUNT_ID
return env
}
const env: NodeJS.ProcessEnv = {
...processEnv,
CLAUDE_CODE_USE_OPENAI: '1',
}
delete env.CLAUDE_CODE_USE_GEMINI
delete env.CLAUDE_CODE_USE_GITHUB
delete env.GEMINI_API_KEY
delete env.GEMINI_MODEL
delete env.GEMINI_BASE_URL
delete env.GOOGLE_API_KEY
if (options.profile === 'ollama') {
const getOllamaBaseUrl =
options.getOllamaChatBaseUrl ?? (() => 'http://localhost:11434/v1')
const resolveOllamaModel =
options.resolveOllamaDefaultModel ?? (async () => 'llama3.1:8b')
env.OPENAI_BASE_URL = persistedEnv.OPENAI_BASE_URL || getOllamaBaseUrl()
env.OPENAI_MODEL =
persistedEnv.OPENAI_MODEL ||
(await resolveOllamaModel(options.goal))
delete env.OPENAI_API_KEY
delete env.CODEX_API_KEY
delete env.CHATGPT_ACCOUNT_ID
delete env.CODEX_ACCOUNT_ID
return env
}
if (options.profile === 'atomic-chat') {
const getAtomicChatBaseUrl =
options.getAtomicChatChatBaseUrl ?? (() => 'http://127.0.0.1:1337/v1')
const resolveModel =
options.resolveAtomicChatDefaultModel ?? (async () => null as string | null)
env.OPENAI_BASE_URL = persistedEnv.OPENAI_BASE_URL || getAtomicChatBaseUrl()
env.OPENAI_MODEL =
persistedEnv.OPENAI_MODEL ||
(await resolveModel()) ||
''
delete env.OPENAI_API_KEY
delete env.CODEX_API_KEY
delete env.CHATGPT_ACCOUNT_ID
delete env.CODEX_ACCOUNT_ID
return env
}
if (options.profile === 'codex') {
env.OPENAI_BASE_URL =
persistedEnv.OPENAI_BASE_URL && isCodexBaseUrl(persistedEnv.OPENAI_BASE_URL)
? persistedEnv.OPENAI_BASE_URL
: DEFAULT_CODEX_BASE_URL
env.OPENAI_MODEL = persistedEnv.OPENAI_MODEL || 'codexplan'
delete env.OPENAI_API_KEY
const codexKey =
sanitizeApiKey(processEnv.CODEX_API_KEY) ||
sanitizeApiKey(persistedEnv.CODEX_API_KEY)
const liveCodexCredentials = resolveCodexApiCredentials(processEnv)
const codexAccountId =
processEnv.CHATGPT_ACCOUNT_ID ||
processEnv.CODEX_ACCOUNT_ID ||
liveCodexCredentials.accountId ||
persistedEnv.CHATGPT_ACCOUNT_ID ||
persistedEnv.CODEX_ACCOUNT_ID
if (codexKey) {
env.CODEX_API_KEY = codexKey
} else {
delete env.CODEX_API_KEY
}
if (codexAccountId) {
env.CHATGPT_ACCOUNT_ID = codexAccountId
} else {
delete env.CHATGPT_ACCOUNT_ID
}
delete env.CODEX_ACCOUNT_ID
return env
}
const defaultOpenAIModel = getGoalDefaultOpenAIModel(options.goal)
const shellOpenAIRequest = resolveProviderRequest({
model: processEnv.OPENAI_MODEL,
baseUrl: processEnv.OPENAI_BASE_URL,
fallbackModel: defaultOpenAIModel,
})
const persistedOpenAIRequest = resolveProviderRequest({
model: persistedEnv.OPENAI_MODEL,
baseUrl: persistedEnv.OPENAI_BASE_URL,
fallbackModel: defaultOpenAIModel,
})
const useShellOpenAIConfig = shellOpenAIRequest.transport === 'chat_completions'
const usePersistedOpenAIConfig =
(!persistedEnv.OPENAI_MODEL && !persistedEnv.OPENAI_BASE_URL) ||
persistedOpenAIRequest.transport === 'chat_completions'
env.OPENAI_BASE_URL =
(useShellOpenAIConfig ? processEnv.OPENAI_BASE_URL : undefined) ||
(usePersistedOpenAIConfig ? persistedEnv.OPENAI_BASE_URL : undefined) ||
DEFAULT_OPENAI_BASE_URL
env.OPENAI_MODEL =
(useShellOpenAIConfig ? processEnv.OPENAI_MODEL : undefined) ||
(usePersistedOpenAIConfig ? persistedEnv.OPENAI_MODEL : undefined) ||
defaultOpenAIModel
env.OPENAI_API_KEY = processEnv.OPENAI_API_KEY || persistedEnv.OPENAI_API_KEY
delete env.CODEX_API_KEY
delete env.CHATGPT_ACCOUNT_ID
delete env.CODEX_ACCOUNT_ID
return env
}