import { packageStorage } from 'kody:runtime'
import { PROVIDER_IDS, providerCatalog, providerSecretUrl } from './providers.ts'
import {
assertHttpsUrl,
inputRecord,
isMutationDryRun,
optionalString,
parseProviderId,
type ProviderId,
} from './validation.ts'
export const SETTING_KEYS = {
provider: 'provider',
model: 'model',
baseUrl: 'baseUrl',
cloudflareAccountId: 'cloudflareAccountId',
cloudflareAiGatewayId: 'cloudflareAiGatewayId',
apiKeySecret: 'apiKeySecret',
} as const
export type AiSettings = {
provider: ProviderId | null
model: string | null
baseUrl: string | null
cloudflareAccountId: string | null
cloudflareAiGatewayId: string | null
apiKeySecret: string | null
}
const STORAGE_NOTE =
"These defaults live in this package's packageStorage. Live @kody/ai storage is the platform bucket — fork or invoke your own copy before setting them."
async function readString(key: string): Promise<string | null> {
try {
const store = packageStorage()
if (!store) return null
const raw = await store.get(key)
return typeof raw === 'string' && raw.trim() ? raw.trim() : null
} catch {
return null
}
}
async function writeString(key: string, value: string) {
const store = packageStorage()
await store.set(key, value)
}
export async function getSettings(): Promise<AiSettings> {
const providerRaw = await readString(SETTING_KEYS.provider)
let provider: ProviderId | null = null
if (providerRaw) {
try {
provider = parseProviderId(providerRaw) ?? null
} catch {
provider = null
}
}
return {
provider,
model: await readString(SETTING_KEYS.model),
baseUrl: await readString(SETTING_KEYS.baseUrl),
cloudflareAccountId: await readString(SETTING_KEYS.cloudflareAccountId),
cloudflareAiGatewayId: await readString(SETTING_KEYS.cloudflareAiGatewayId),
apiKeySecret: await readString(SETTING_KEYS.apiKeySecret),
}
}
export type SettingsInput = {
provider?: string
model?: string
baseUrl?: string
cloudflareAccountId?: string
cloudflareAiGatewayId?: string
apiKeySecret?: string
dryRun?: boolean
confirm?: boolean
}
/**
* Read or update fork-local provider defaults.
*
* Writes default to dry-run. A live persist requires `confirm: true`.
* Never stores API keys — only provider, model, base URL, and Cloudflare ids.
*/
export default async function aiSettings(input: SettingsInput = {}) {
const parsed = inputRecord(input ?? {})
const next: Partial<AiSettings> = {}
const provider = optionalString(parsed, 'provider')
if (provider) next.provider = parseProviderId(provider)
const model = optionalString(parsed, 'model')
if (model) next.model = model
const baseUrl = optionalString(parsed, 'baseUrl')
if (baseUrl) next.baseUrl = assertHttpsUrl(baseUrl, 'baseUrl')
const cloudflareAccountId = optionalString(parsed, 'cloudflareAccountId')
if (cloudflareAccountId) next.cloudflareAccountId = cloudflareAccountId
const cloudflareAiGatewayId = optionalString(parsed, 'cloudflareAiGatewayId')
if (cloudflareAiGatewayId) next.cloudflareAiGatewayId = cloudflareAiGatewayId
const apiKeySecret = optionalString(parsed, 'apiKeySecret')
if (apiKeySecret) next.apiKeySecret = apiKeySecret
const updates = Object.entries(next).filter(([, value]) => value != null)
if (updates.length === 0) {
return {
settings: await getSettings(),
note: STORAGE_NOTE,
providers: PROVIDER_IDS.map((id) => {
const entry = providerCatalog(id)
return {
id,
secret: entry.secretPrefix,
hosts: entry.hosts,
secretUrl: providerSecretUrl(id),
}
}),
}
}
if (isMutationDryRun(parsed)) {
return {
dryRun: true as const,
wouldUpdate: Object.fromEntries(updates),
settings: await getSettings(),
note: 'Pass confirm: true to persist these defaults on this fork.',
}
}
for (const [key, value] of updates) {
await writeString(key, String(value))
}
return {
updated: updates.map(([key]) => key),
settings: await getSettings(),
note: STORAGE_NOTE,
}
}
export { STORAGE_NOTE }