import { kody } from 'kody:runtime' import { generateText, listModels } from './complete.ts' import { generateObject } from './generate-object.ts' import { PROVIDER_IDS, defaultSecretName, providerSecretUrl } from './providers.ts' import aiSettings from './settings.ts' import { parseProviderId, type ProviderId } from './validation.ts' function secretEntries( result: unknown, ): Array<{ name?: string; scope?: string }> { if ( result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets) ) { return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets } return Array.isArray(result) ? result : [] } async function listUserSecretNames(): Promise> { try { const listed = await kody.secretList({ scope: 'user' }) const names = new Set() for (const entry of secretEntries(listed)) { if (entry?.name && (entry.scope === 'user' || !entry.scope)) { names.add(entry.name) } } return names } catch { return new Set() } } function firstConfiguredProvider(names: Set): ProviderId | null { const preferred: Array = [ 'openai', 'anthropic', 'groq', 'openai-compatible', 'cloudflare', ] for (const id of preferred) { if (names.has(defaultSecretName(id))) return id } return null } /** * Local helper checks, plus a live models list when a provider secret exists. * * Without secrets this still returns `{ ok: true, live: false }` and the * prefilled setup URLs so the package is forkable. With a secret it proves * host approval via GET /models — it never spends completion tokens. * * @example * import smokeTest from 'kody:@kody/ai/smoke-test' * const result = await smokeTest() */ export async function smokeTest(params: { provider?: string } = {}) { const preview = await generateText({ provider: 'openai', messages: [{ role: 'user', content: 'kody ai dry-run smoke' }], dryRun: true, }) if (!('dryRun' in preview) || preview.dryRun !== true) { throw new Error('generateText dry-run self-check failed.') } const objectPreview = await generateObject({ provider: 'openai', messages: [{ role: 'user', content: 'label this: ok' }], schema: { type: 'object', properties: { label: { type: 'string' } }, required: ['label'], }, dryRun: true, }) if (!('dryRun' in objectPreview) || objectPreview.dryRun !== true) { throw new Error('generateObject dry-run self-check failed.') } const settingsPreview = await aiSettings({ provider: 'openai', model: 'gpt-4o-mini', dryRun: true, }) if (!('dryRun' in settingsPreview) || settingsPreview.dryRun !== true) { throw new Error('settings dry-run self-check failed.') } const names = await listUserSecretNames() const requested = params.provider ? parseProviderId(params.provider) : undefined const provider = requested ?? firstConfiguredProvider(names) const setup = { secrets: Object.fromEntries( PROVIDER_IDS.map((id) => { const secret = defaultSecretName(id) return [secret, names.has(secret)] }), ), nextSteps: PROVIDER_IDS.map((id) => providerSecretUrl(id)), } if (!provider || !names.has(defaultSecretName(provider))) { return { ok: true, live: false, selfCheck: { generateTextDryRun: true, generateObjectDryRun: true, settingsDryRun: true, }, setup, } } try { const models = await listModels({ provider }) if (!models || ('ok' in models && models.ok === false)) { return { ok: true, live: false, provider, selfCheck: { generateTextDryRun: true, generateObjectDryRun: true, settingsDryRun: true, }, models, setup, } } return { ok: true, live: true, provider, selfCheck: { generateTextDryRun: true, generateObjectDryRun: true, settingsDryRun: true, }, models, setup: { secrets: setup.secrets, nextSteps: [], }, } } catch (error) { return { ok: true, live: false, provider, error: error instanceof Error ? error.message : String(error), selfCheck: { generateTextDryRun: true, generateObjectDryRun: true, settingsDryRun: true, }, setup, } } } export default smokeTest