← Public packages
@kentcdodds/jev
Call TypeSafe Jev evaluations via Cloudflare AI Gateway (same token/gateway as @kentcdodds/ai).
src/request.ts
152 lines · 3.9 KB · TypeScript/**
* Escape-hatch raw Cloudflare `/ai/run` call for `typesafe/jev`.
* Prefer `./evaluate` for parsed answers; use this when you need the full envelope.
*
* @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 (sent as `cf-aig-gateway-id`).
* @param input.model - Optional model id (default `typesafe/jev`).
* @returns `{ ok, status, data }` — never logs secrets.
*
* @example
* import jevRequest from 'kody:@kentcdodds/jev/request'
*
* const raw = await jevRequest({
* state: 'hello',
* questions: {
* greeting: { type: 'noul', instructions: 'Is this a greeting?' },
* },
* })
*/
import {
cloudflareApiTokenPlaceholder,
missingSetup,
resolveJevConfig,
} from './resolve.ts'
import type { JevRequestInput, JevRequestResult } from './types.ts'
function stringify(value: unknown) {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
/**
* Escape-hatch raw Cloudflare `/ai/run` call for `typesafe/jev`.
* Prefer `./evaluate` for parsed answers; use this when you need the full envelope.
*
* @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 (sent as `cf-aig-gateway-id`).
* @param input.model - Optional model id (default `typesafe/jev`).
* @returns `{ ok, status, data }` — never logs secrets.
*
* @example
* import jevRequest from 'kody:@kentcdodds/jev/request'
*
* const raw = await jevRequest({
* state: 'hello',
* questions: {
* greeting: { type: 'noul', instructions: 'Is this a greeting?' },
* },
* })
*/
export default async function jevRequest(
input: JevRequestInput,
): Promise<JevRequestResult> {
if (!input || typeof input !== 'object') {
return { ok: false, status: 0, data: null, error: 'input is required' }
}
if (input.state === undefined || input.state === null) {
return { ok: false, status: 0, data: null, error: 'state is required' }
}
if (!input.questions || typeof input.questions !== 'object') {
return {
ok: false,
status: 0,
data: null,
error: 'questions must be a non-empty object',
}
}
const config = await resolveJevConfig({
accountId: input.accountId,
gatewayId: input.gatewayId,
model: input.model,
})
if (!config.accountId) {
return {
ok: false,
status: 0,
data: null,
error: 'cloudflareAccountId is not set',
setup: missingSetup(''),
}
}
const url =
'https://api.cloudflare.com/client/v4/accounts/' +
config.accountId +
'/ai/run'
const headers: Record<string, string> = {
Authorization: 'Bearer ' + cloudflareApiTokenPlaceholder,
'Content-Type': 'application/json',
'cf-aig-gateway-id': config.gatewayId,
...(input.headers || {}),
}
const body = {
model: config.model,
input: {
state: input.state,
questions: input.questions,
},
}
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
})
const text = await response.text()
let data: unknown = text
try {
data = text ? JSON.parse(text) : null
} catch {
data = text
}
if (!response.ok) {
return {
ok: false,
status: response.status,
data,
error:
'Cloudflare /ai/run failed: HTTP ' +
response.status +
' ' +
(typeof data === 'string' ? data.slice(0, 500) : stringify(data).slice(0, 500)),
setup:
response.status === 401 || response.status === 403
? missingSetup(config.accountId)
: undefined,
}
}
return { ok: true, status: response.status, data }
} catch (error) {
return {
ok: false,
status: 0,
data: null,
error: error instanceof Error ? error.message : String(error),
setup: missingSetup(config.accountId),
}
}
}