Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ export type GoogleConfig = {
baseUrl?: string
}

export type ApiKeysConfig = {
openai?: string
anthropic?: string
google?: string
xai?: string
openrouter?: string
zai?: string
apify?: string
firecrawl?: string
fal?: string
}

export type LoggingLevel = 'debug' | 'info' | 'warn' | 'error'
export type LoggingFormat = 'json' | 'pretty'
export type LoggingConfig = {
Expand Down Expand Up @@ -179,6 +191,12 @@ export type SummarizeConfig = {
google?: GoogleConfig
xai?: XaiConfig
logging?: LoggingConfig
/**
* API keys for LLM providers and services.
*
* Precedence: environment variables > config file apiKeys.
*/
apiKeys?: ApiKeysConfig
}

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down Expand Up @@ -1000,6 +1018,37 @@ export function loadSummarizeConfig({ env }: { env: Record<string, string | unde
const google = parseProviderBaseUrlConfig(parsed.google, path, 'google')
const xai = parseProviderBaseUrlConfig(parsed.xai, path, 'xai')

const apiKeys = (() => {
const value = (parsed as Record<string, unknown>).apiKeys
if (typeof value === 'undefined') return undefined
if (!isRecord(value)) {
throw new Error(`Invalid config file ${path}: "apiKeys" must be an object.`)
}
const keys: Record<string, string> = {}
const allowed = [
'openai',
'anthropic',
'google',
'xai',
'openrouter',
'zai',
'apify',
'firecrawl',
'fal',
]
for (const [key, val] of Object.entries(value)) {
const k = key.trim().toLowerCase()
if (!allowed.includes(k)) {
throw new Error(`Invalid config file ${path}: unknown apiKeys provider "${key}".`)
}
if (typeof val !== 'string' || val.trim().length === 0) {
throw new Error(`Invalid config file ${path}: "apiKeys.${key}" must be a non-empty string.`)
}
keys[k] = val.trim()
}
return Object.keys(keys).length > 0 ? (keys as import('./config.js').ApiKeysConfig) : undefined
})()

return {
config: {
...(model ? { model } : {}),
Expand All @@ -1017,6 +1066,7 @@ export function loadSummarizeConfig({ env }: { env: Record<string, string | unde
...(google ? { google } : {}),
...(xai ? { xai } : {}),
...(logging ? { logging } : {}),
...(apiKeys ? { apiKeys } : {}),
},
path,
}
Expand Down
31 changes: 22 additions & 9 deletions src/run/run-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export function resolveEnvState({
envForRun: Record<string, string | undefined>
configForCli: SummarizeConfig | null
}): EnvState {
const xaiKeyRaw = typeof envForRun.XAI_API_KEY === 'string' ? envForRun.XAI_API_KEY : null
const cfgKeys = configForCli?.apiKeys
const xaiKeyRaw =
typeof envForRun.XAI_API_KEY === 'string' ? envForRun.XAI_API_KEY : (cfgKeys?.xai ?? null)
const openaiBaseUrl = (() => {
const envValue = normalizeBaseUrl(envForRun.OPENAI_BASE_URL)
if (envValue) return envValue
Expand All @@ -69,23 +71,29 @@ export function resolveEnvState({
? envForRun.Z_AI_API_KEY
: typeof envForRun.ZAI_API_KEY === 'string'
? envForRun.ZAI_API_KEY
: null
: (cfgKeys?.zai ?? null)
const zaiBaseUrlRaw =
typeof envForRun.Z_AI_BASE_URL === 'string'
? envForRun.Z_AI_BASE_URL
: typeof envForRun.ZAI_BASE_URL === 'string'
? envForRun.ZAI_BASE_URL
: null
const openRouterKeyRaw =
typeof envForRun.OPENROUTER_API_KEY === 'string' ? envForRun.OPENROUTER_API_KEY : null
typeof envForRun.OPENROUTER_API_KEY === 'string'
? envForRun.OPENROUTER_API_KEY
: (cfgKeys?.openrouter ?? null)
const openaiKeyRaw =
typeof envForRun.OPENAI_API_KEY === 'string' ? envForRun.OPENAI_API_KEY : null
typeof envForRun.OPENAI_API_KEY === 'string'
? envForRun.OPENAI_API_KEY
: (cfgKeys?.openai ?? null)
const apiKey =
typeof openaiBaseUrl === 'string' && /openrouter\.ai/i.test(openaiBaseUrl)
? (openRouterKeyRaw ?? openaiKeyRaw)
: openaiKeyRaw
const apifyToken =
typeof envForRun.APIFY_API_TOKEN === 'string' ? envForRun.APIFY_API_TOKEN : null
typeof envForRun.APIFY_API_TOKEN === 'string'
? envForRun.APIFY_API_TOKEN
: (cfgKeys?.apify ?? null)
const ytDlpPath = (() => {
const explicit = typeof envForRun.YT_DLP_PATH === 'string' ? envForRun.YT_DLP_PATH.trim() : ''
if (explicit.length > 0) return explicit
Expand All @@ -101,19 +109,24 @@ export function resolveEnvState({
const value = raw.trim()
return value.length > 0 ? value : null
})()
const falApiKey = typeof envForRun.FAL_KEY === 'string' ? envForRun.FAL_KEY : null
const falApiKey =
typeof envForRun.FAL_KEY === 'string' ? envForRun.FAL_KEY : (cfgKeys?.fal ?? null)
const firecrawlKey =
typeof envForRun.FIRECRAWL_API_KEY === 'string' ? envForRun.FIRECRAWL_API_KEY : null
typeof envForRun.FIRECRAWL_API_KEY === 'string'
? envForRun.FIRECRAWL_API_KEY
: (cfgKeys?.firecrawl ?? null)
const anthropicKeyRaw =
typeof envForRun.ANTHROPIC_API_KEY === 'string' ? envForRun.ANTHROPIC_API_KEY : null
typeof envForRun.ANTHROPIC_API_KEY === 'string'
? envForRun.ANTHROPIC_API_KEY
: (cfgKeys?.anthropic ?? null)
const googleKeyRaw =
typeof envForRun.GEMINI_API_KEY === 'string'
? envForRun.GEMINI_API_KEY
: typeof envForRun.GOOGLE_GENERATIVE_AI_API_KEY === 'string'
? envForRun.GOOGLE_GENERATIVE_AI_API_KEY
: typeof envForRun.GOOGLE_API_KEY === 'string'
? envForRun.GOOGLE_API_KEY
: null
: (cfgKeys?.google ?? null)

const firecrawlApiKey = firecrawlKey && firecrawlKey.trim().length > 0 ? firecrawlKey : null
const firecrawlConfigured = firecrawlApiKey !== null
Expand Down
Loading