export type InputRecord = Record<string, unknown>
export type ProviderId =
| 'openai'
| 'anthropic'
| 'groq'
| 'cloudflare'
| 'openai-compatible'
export const PROVIDER_IDS = [
'openai',
'anthropic',
'groq',
'cloudflare',
'openai-compatible',
] as const satisfies ReadonlyArray<ProviderId>
const ACCOUNT_LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/
const SECRET_PREFIXES = [
'openaiApiKey',
'anthropicApiKey',
'groqApiKey',
'cloudflareApiToken',
'openaiCompatibleApiKey',
] as const
export type SecretPrefix = (typeof SECRET_PREFIXES)[number]
export function inputRecord(value: unknown): InputRecord {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Input must be an object.')
}
return value as InputRecord
}
export function optionalString(
input: InputRecord,
key: string,
): string | undefined {
const value = input[key]
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') throw new Error(key + ' must be a string.')
const trimmed = value.trim()
return trimmed || undefined
}
export function requiredString(input: InputRecord, key: string): string {
const value = optionalString(input, key)
if (!value) throw new Error(key + ' is required.')
return value
}
export function optionalBoolean(
input: InputRecord,
key: string,
): boolean | undefined {
const value = input[key]
if (value === undefined || value === null) return undefined
if (typeof value !== 'boolean') throw new Error(key + ' must be a boolean.')
return value
}
export function accountLabel(value: string | undefined): string | null {
const trimmed = (value ?? '').trim()
if (!trimmed || trimmed === 'default') return null
if (!ACCOUNT_LABEL_PATTERN.test(trimmed)) {
throw new Error(
'account must be a short label such as "work" or "live" (letters, numbers, _ or -).',
)
}
return trimmed
}
export function isProviderId(value: string): value is ProviderId {
return (PROVIDER_IDS as ReadonlyArray<string>).includes(value)
}
export function parseProviderId(value: string | undefined): ProviderId | undefined {
if (!value) return undefined
if (!isProviderId(value)) {
throw new Error(
'provider must be openai, anthropic, groq, cloudflare, or openai-compatible.',
)
}
return value
}
export function secretNameFor(
prefix: SecretPrefix,
account?: string,
override?: string,
): string {
if (override != null && override.trim()) {
const name = override.trim()
const allowed = SECRET_PREFIXES.some(
(item) => name === item || name.startsWith(item + '-'),
)
if (!allowed) {
throw new Error(
'apiKeySecret must be one of ' +
SECRET_PREFIXES.join(', ') +
' or the same name with an account suffix.',
)
}
return name
}
const label = accountLabel(account)
return label ? prefix + '-' + label : prefix
}
export function secretPlaceholder(secretName: string): string {
return '{{secret:' + secretName + '|scope=user}}'
}
/**
* Settings writes default to dry-run. A live persist requires `confirm: true`
* and `dryRun` not set to true.
*/
export function isMutationDryRun(input: {
dryRun?: unknown
confirm?: unknown
}): boolean {
if (input.dryRun === true) return true
if (input.confirm === true) return false
return true
}
export function isPreviewDryRun(input: { dryRun?: unknown }): boolean {
return input.dryRun === true
}
export function boundedMaxTokens(value: unknown, fallback = 4096): number {
if (value === undefined || value === null || value === '') return fallback
if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 32768) {
throw new Error('maxTokens must be an integer from 1 through 32768.')
}
return Number(value)
}
export function assertHttpsUrl(value: string, key: string): string {
let parsed: URL
try {
parsed = new URL(value)
} catch {
throw new Error(key + ' must be an https URL.')
}
if (parsed.protocol !== 'https:') {
throw new Error(key + ' must be an https URL.')
}
if (parsed.username || parsed.password) {
throw new Error(key + ' must not include credentials.')
}
return parsed.origin + parsed.pathname.replace(/\/$/, '')
}
export function hostFromHttpsUrl(value: string): string {
return new URL(value).host
}
export function asRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
return value as Record<string, unknown>
}
export function stringify(value: unknown): string {
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
export function clean(value: unknown): string {
return String(value ?? '').trim()
}
export function assertNever(value: never, label: string): never {
throw new Error('Unhandled ' + label + ': ' + String(value))
}