Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/jev

Call TypeSafe Jev evaluations via Cloudflare AI Gateway (same token/gateway as @kentcdodds/ai).

src/evaluate.ts

126 lines · 3.6 KB · TypeScript
/**
 * Evaluate shared state with TypeSafe Jev through Cloudflare AI Gateway.
 * Use for typed noul / choice / score answers with calibrated probabilities.
 *
 * @param input.state - Shared state (string or object) to evaluate.
 * @param input.questions - Named noul / choice / score questions.
 * @param input.accountId - Optional Cloudflare account id override.
 * @param input.gatewayId - Optional AI Gateway id override.
 * @param input.model - Optional model id (default `typesafe/jev`).
 * @returns Parsed `{ ok, answers, model, usage }` — never logs secrets.
 *
 * @example
 * import evaluate from 'kody:@kentcdodds/jev/evaluate'
 *
 * const result = await evaluate({
 *   state: 'hello',
 *   questions: {
 *     greeting: { type: 'noul', instructions: 'Is this a greeting?' },
 *   },
 * })
 */

import jevRequest from './request.ts'
import type {
	Answer,
	EvaluateInput,
	EvaluateResult,
	JevResult,
	JevUsage,
} from './types.ts'
import { resolveJevConfig } from './resolve.ts'

function asRecord(value: unknown): Record<string, unknown> | null {
	return value && typeof value === 'object' && !Array.isArray(value)
		? (value as Record<string, unknown>)
		: null
}

function unwrapJevPayload(data: unknown): JevResult | null {
	const root = asRecord(data)
	if (!root) return null

	// Cloudflare envelope: { success, result: { model, answers, usage } }
	const nested = asRecord(root.result)
	const candidate = nested && (nested.answers || nested.model) ? nested : root

	const answersRaw = asRecord(candidate.answers)
	if (!answersRaw) return null

	const answers: Record<string, Answer> = {}
	for (const [key, value] of Object.entries(answersRaw)) {
		const answer = asRecord(value)
		if (!answer || typeof answer.type !== 'string') continue
		answers[key] = answer as Answer
	}

	return {
		model: typeof candidate.model === 'string' ? candidate.model : 'typesafe/jev',
		answers,
		usage: asRecord(candidate.usage) as JevUsage | undefined,
	}
}

/**
 * Evaluate shared state with TypeSafe Jev through Cloudflare AI Gateway.
 * Use for typed noul / choice / score answers with calibrated probabilities.
 *
 * @param input.state - Shared state (string or object) to evaluate.
 * @param input.questions - Named noul / choice / score questions.
 * @param input.accountId - Optional Cloudflare account id override.
 * @param input.gatewayId - Optional AI Gateway id override.
 * @param input.model - Optional model id (default `typesafe/jev`).
 * @returns Parsed `{ ok, answers, model, usage }` — never logs secrets.
 *
 * @example
 * import evaluate from 'kody:@kentcdodds/jev/evaluate'
 *
 * const result = await evaluate({
 *   state: 'hello',
 *   questions: {
 *     greeting: { type: 'noul', instructions: 'Is this a greeting?' },
 *   },
 * })
 */
export default async function evaluate(
	input: EvaluateInput,
): Promise<EvaluateResult> {
	const config = await resolveJevConfig({
		accountId: input?.accountId,
		gatewayId: input?.gatewayId,
		model: input?.model,
	})

	const raw = await jevRequest(input)
	if (!raw.ok) {
		return {
			ok: false,
			status: raw.status,
			error: raw.error || 'Jev request failed',
			setup: raw.setup,
		}
	}

	const parsed = unwrapJevPayload(raw.data)
	if (!parsed || Object.keys(parsed.answers).length === 0) {
		return {
			ok: false,
			status: raw.status,
			error:
				'Unexpected Jev response shape (missing answers). Use ./request for the raw envelope.',
		}
	}

	return {
		ok: true,
		status: raw.status,
		answers: parsed.answers,
		model: parsed.model,
		usage: parsed.usage,
		meta: {
			accountIdSet: Boolean(config.accountId),
			gatewayId: config.gatewayId,
			model: config.model,
		},
	}
}