Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/with-typesafe/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# next.js
/.next/
/out/

# production
/build

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
26 changes: 26 additions & 0 deletions examples/with-typesafe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# JEV-based PDF form filling (Typesafe × SimplePDF)

_Built with Next.js, Tailwind CSS, `@simplepdf/react-embed-pdf`, and Typesafe's **JEV** ("System One") model._

Pick a form (an **IRS W-9** or a **medication prior-authorization**), and JEV auto-fills it from a demo record in the SimplePDF editor: it maps each CSV column to a field, ticks the right checkboxes and picks constrained-field options, and gates every write on a calibrated confidence. Fills below the auto-threshold surface as **low-confidence** items to confirm, a JEV plausibility pass flags **fictional / implausible** values, and any field it can't fill is a manual-entry prompt. A human confirms or fixes each one, re-validates, and finalizes.

The shape is the message: **code owns control flow, JEV answers narrow structured questions** (which column fits this field? should this box be checked? is this value plausible?). No agent, no hallucination.

## Run

```sh
npm install
npm run dev
```

Open `http://localhost:3001`. The editor loads under the whitelisted `spdf-jev` demo origin (no Pro account needed).

## JEV API key (BYOK, in-memory)

The JEV key is **yours** and is held **in memory only**: no server, no persistence, no localStorage. The call is proxied same-origin through `/api/jev` (JEV has no browser CORS), and the key is forwarded, never stored. Get a key at `https://console.typesafe.ai/settings/keys`.

**What reaches JEV:** field labels, the demo CSV record, and the filled text values (for the plausibility pass). Checkbox/option states, signature data, and picture data are never sent.

For local dev you can pre-seed the key: copy it into `.env.local` as `NEXT_PUBLIC_TYPESAFE_API_KEY=<key>` (gitignored, never committed). The shipped path is still typing the key into the UI.

> Illustrative demo, not tax advice.
46 changes: 46 additions & 0 deletions examples/with-typesafe/app/api/jev/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Same-origin proxy to JEV. api.typesafe.ai sends no Access-Control-Allow-Origin,
// so a browser-direct call is blocked; this route forwards it server-side (no CORS)
// with the caller's BYOK Authorization header, which it never stores. The path is
// allowlisted to the endpoints the SDK uses, so it is not an open relay.
// CF: plans/P107-typesafe-jev-example.md

const JEV_UPSTREAM = "https://api.typesafe.ai"
const ALLOWED_PATHS = new Set(["v1/systemone"])
const UPSTREAM_TIMEOUT_MS = 15_000

export const POST = async (
request: Request,
context: { params: Promise<{ path: string[] }> },
): Promise<Response> => {
const { path } = await context.params
const joined = path.join("/")
if (!ALLOWED_PATHS.has(joined)) {
return Response.json({ error: "Not a permitted JEV path" }, { status: 404 })
}

const authorization = request.headers.get("authorization")
const body = await request.text()

try {
const upstream = await fetch(`${JEV_UPSTREAM}/${joined}`, {
method: "POST",
headers: {
"content-type": "application/json",
...(authorization !== null ? { authorization } : {}),
},
body,
// Never follow a redirect: a 3xx could otherwise replay this POST (and the BYOK
// Authorization header) to an arbitrary host. The upstream is a fixed API origin.
redirect: "error",
// Forward the caller's cancellation AND cap the round trip.
signal: AbortSignal.any([request.signal, AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)]),
})
const responseBody = await upstream.text()
return new Response(responseBody, {
status: upstream.status,
headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" },
})
} catch {
return Response.json({ error: "JEV upstream is unavailable" }, { status: 502 })
}
}
75 changes: 75 additions & 0 deletions examples/with-typesafe/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@import "tailwindcss";

/* TypeSafe-inspired dark palette (typesafe.ai): near-black canvas, pink/magenta
brand accents, calibrated-confidence semantics (green=valid, amber=low, red=error).
Tokens are defined once on :root and mapped into Tailwind v4's theme below. */
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-brand: hsl(var(--brand));
--color-brand-2: hsl(var(--brand-2));
--color-valid: hsl(var(--valid));
--color-warn: hsl(var(--warn));
--color-error: hsl(var(--error));
--color-info: hsl(var(--info));
}

:root {
--background: 0 0% 12%;
--foreground: 0 0% 98%;
--card: 0 0% 15%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 12%;
--popover-foreground: 0 0% 98%;
--primary: 345 82% 74%;
--primary-foreground: 0 0% 12%;
--secondary: 0 0% 18%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 18%;
--muted-foreground: 0 0% 52%;
--accent: 0 0% 18%;
--accent-foreground: 0 0% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 22%;
--input: 0 0% 22%;
--ring: 345 82% 74%;
--brand: 345 82% 74%;
--brand-2: 314 58% 59%;
--valid: 152 95% 34%;
--warn: 38 92% 50%;
--error: 0 84% 60%;
--info: 175 90% 36%;
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Tailwind v4 drops the default button pointer; restore it for interactive elements. */
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]) {
cursor: pointer;
}
}
21 changes: 21 additions & 0 deletions examples/with-typesafe/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from 'next'
import type { ReactElement, ReactNode } from 'react'
import './globals.css'

export const metadata: Metadata = {
title: 'JEV-based PDF form filling · SimplePDF',
description:
'Fill a PDF from a CSV with JEV (Typesafe): auto-fill the fields, raise low-confidence and validation issues, human signs off. Built on the SimplePDF editor.',
}

export default function RootLayout({
children,
}: Readonly<{
children: ReactNode
}>): ReactElement {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions examples/with-typesafe/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return null
}
Binary file added examples/with-typesafe/app/page.tsx
Binary file not shown.
147 changes: 147 additions & 0 deletions examples/with-typesafe/benchmark/backends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Benchmark-only (delete with benchmark/ before the PR). Two backends run the SAME work and return
// BOTH their timing AND their decisions, so the harness can check the outputs are equivalent before
// trusting the latency numbers. JEV via the Typesafe SDK (structured choice/noul); any
// OpenAI-compatible chat model via JSON mode. Node runs server-side, so no CORS and no proxy.
import { TypeSafeClient, choice, noul } from "@typesafe-ai/sdk"
import type { BenchField, BenchInput } from "./fixtures"

export type PhaseTiming = { mapMs: number; classifyMs: number }
export type ModelOutput = {
timing: PhaseTiming
mapping: Record<string, string> // fieldId -> chosen column / option / "none"
plausibility: Record<string, number> // field label -> genuine score (0..1)
}
// `extraBody` is merged into each request, so a reasoning model can be tuned per provider without a
// code change (e.g. { reasoning_effort: "low" }, { thinking: false }).
export type OpenAICompatibleConfig = { baseUrl: string; model: string; apiKey: string; extraBody: Record<string, unknown> }

const JEV_BASE_URL = "https://api.typesafe.ai"
const NONE = "none"
const REQUEST_TIMEOUT_MS = 60_000

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null

const isFillable = (field: BenchField): boolean => field.type !== "SIGNATURE" && field.type !== "PICTURE"
const fieldOptions = (field: BenchField): string[] | null =>
field.options !== null && field.options.length > 0 ? field.options : null

// --- JEV (Typesafe SDK) — the exact structured questions the app asks ------------------------

const jevMappingQuestions = (input: BenchInput): Record<string, ReturnType<typeof choice>> => {
const columnCriteria: Record<string, string> = {
...Object.fromEntries(input.record.columns.map((column) => [column, `CSV column "${column}" = "${input.record.values[column] ?? ""}"`])),
[NONE]: "No CSV column fits this field",
}
return Object.fromEntries(
input.fields
.filter(isFillable)
.map((field): [string, ReturnType<typeof choice>] => {
const options = fieldOptions(field)
if (options !== null) {
return [
field.fieldId,
choice(`Given the record, which value fits the field labeled "${field.name}" (type ${field.type})? Answer "${NONE}" to leave it blank.`, {
...Object.fromEntries(options.map((option) => [option, `The correct value for this field is "${option}"`])),
[NONE]: "No option fits the record; leave the field blank",
}),
]
}
return [
field.fieldId,
choice(`Which CSV column should fill the form field labeled "${field.name}" (type ${field.type})? Answer "${NONE}" if no column fits.`, columnCriteria),
]
}),
)
}

const jevPlausibilityQuestions = (input: BenchInput): Record<string, ReturnType<typeof noul>> =>
Object.fromEntries(
input.filled.map((field): [string, ReturnType<typeof noul>] => [
field.name,
noul(`Is "${field.value}" a genuine, plausible value for the field labeled "${field.name}" (not fictional, a placeholder, or obviously wrong)?`),
]),
)

export const runJev = async (apiKey: string, input: BenchInput): Promise<ModelOutput> => {
const client = new TypeSafeClient({ apiKey, baseURL: JEV_BASE_URL })

const mapStart = performance.now()
const { answers: mapAnswers } = await client.systemOne({ state: input.record.values, questions: jevMappingQuestions(input) })
const mapMs = performance.now() - mapStart
const mapping = Object.fromEntries(Object.entries(mapAnswers).map(([fieldId, answer]) => [fieldId, answer.choice]))

const plausibilityState = { fields: Object.fromEntries(input.filled.map((field) => [field.name, field.value])) }
const classifyStart = performance.now()
const { answers: plausAnswers } = await client.systemOne({ state: plausibilityState, questions: jevPlausibilityQuestions(input) })
const classifyMs = performance.now() - classifyStart
const plausibility = Object.fromEntries(Object.entries(plausAnswers).map(([label, answer]) => [label, answer.noul]))

return { timing: { mapMs, classifyMs }, mapping, plausibility }
}

// --- OpenAI-compatible chat model (JSON mode) — the same work, one batched call per phase -------

const chatJson = async (config: OpenAICompatibleConfig, system: string, user: string): Promise<Record<string, unknown>> => {
const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey}` },
body: JSON.stringify({
model: config.model,
messages: [
{ role: "system", content: system },
{ role: "user", content: user },
],
response_format: { type: "json_object" },
temperature: 0,
...config.extraBody,
}),
// Fail fast instead of hanging the whole benchmark if the endpoint stalls.
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})
if (!response.ok) {
const detail = await response.text()
throw new Error(`Model request failed (${response.status}): ${detail.slice(0, 300)}`)
}
const data: unknown = await response.json()
const content = isRecord(data) && Array.isArray(data.choices) && isRecord(data.choices[0]) && isRecord(data.choices[0].message)
? data.choices[0].message.content
: null
if (typeof content !== "string") {
throw new Error("Model response missing choices[0].message.content")
}
const parsed: unknown = JSON.parse(content)
return isRecord(parsed) ? parsed : {}
}

const toStringMap = (raw: Record<string, unknown>): Record<string, string> =>
Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, typeof value === "string" ? value : String(value)]))

const toNumberMap = (raw: Record<string, unknown>): Record<string, number> =>
Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, typeof value === "number" ? value : Number(value)]))

export const runOpenAICompatible = async (config: OpenAICompatibleConfig, input: BenchInput): Promise<ModelOutput> => {
const fillable = input.fields.filter(isFillable)
const fieldLines = fillable
.map((field) => `- id=${field.fieldId} label="${field.name}" type=${field.type}${field.options !== null ? ` options=[${field.options.join(", ")}]` : ""}`)
.join("\n")
const recordLines = input.record.columns.map((column) => `- ${column} = "${input.record.values[column] ?? ""}"`).join("\n")

const mapStart = performance.now()
const mapRaw = await chatJson(
config,
'You fill PDF form fields from a CSV record. For each field, pick the CSV column whose value belongs in it, or "none". For a field with options, pick one of its options (or "none"). Reply ONLY with a JSON object mapping each field id to the chosen column/option/"none".',
`CSV record:\n${recordLines}\n\nForm fields:\n${fieldLines}`,
)
const mapMs = performance.now() - mapStart

const filledLines = input.filled.map((field) => `- label="${field.name}" value="${field.value}"`).join("\n")
const classifyStart = performance.now()
const plausRaw = await chatJson(
config,
"You judge whether each filled PDF field value is genuine and plausible (not fictional, a placeholder, or obviously wrong). Reply ONLY with a JSON object mapping each field label to a number from 0 (implausible) to 1 (genuine).",
`Filled fields:\n${filledLines}`,
)
const classifyMs = performance.now() - classifyStart

return { timing: { mapMs, classifyMs }, mapping: toStringMap(mapRaw), plausibility: toNumberMap(plausRaw) }
}
Loading