Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/ai

src/settings.ts

138 lines · 3.9 KB · TypeScript
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 }