hono-jev-router

September 18, 2026 · View on GitHub

Route HTTP requests by meaning.

hono-jev-router is an experimental router for Hono. Instead of a method and a path, you describe a request in plain words, and Jev by TypeSafe AI decides which description fits the incoming request.

import { Hono } from 'hono'
import { JevRouter } from 'hono-jev-router'

const app = new Hono({
  router: new JevRouter({ apiKey: process.env.TYPESAFE_API_KEY }),
})

app.on('jev', 'a request from an AI agent', (c) => {
  return c.text('# Documentation', 200, { 'Content-Type': 'text/markdown' })
})

app.on('jev', 'a request from a human browser', (c) => {
  return c.html('<h1>Documentation</h1>')
})

app.on('jev', 'suspicious automated traffic', (c) => {
  return c.text('Forbidden', 403)
})

export default app

Warning

This is an experiment. Every semantically routed request calls a model, so it adds latency and cost, and the answer is a probability, not a guarantee.

Before you use it

  • Do not use it for authentication or authorization. The request is the input of the model, so whoever sends the request can try to steer the answer. 'suspicious automated traffic' is a heuristic, not a security boundary.
  • Requests are sent to a third party. The method, URL, headers and the first maxBodyLength bytes of a textual body go to Jev. The values of Authorization, Cookie, Proxy-Authorization, X-API-Key and X-Auth-Token are replaced with [redacted]. Change the list with redactHeaders.
  • Every semantically routed request is a model call you pay for. Put a rate limit in front of it.

Install

npm i hono-jev-router

How it works

HTTP Request → JevRouter → Jev: "does the request match this description?" → first match → handler → Response
  • app.on('jev', '<description>', handler) registers a semantic route.
  • The method, URL, headers and body of the request are sent to Jev with one yes/no (Noul) question per description. They are evaluated in parallel in a single call.
  • Like a normal router, the first registered route that matches wins. A route matches when its probability reaches the threshold.
  • The handler is a normal Hono handler: c.req.method and c.req.path are those of the real request.
  • If no description matches, the request falls through, usually to app.notFound().
  • Everything else works as usual. Path routes and middleware are handled by Hono's TrieRouter, and a path route that returns a response wins before Jev is asked.
app.use(logger())
app.get('/health', (c) => c.text('ok')) // Jev is not called

// Order matters: put specific descriptions first, broad ones last
app.on('jev', 'suspicious automated traffic', (c) => c.text('Forbidden', 403))
app.on('jev', 'any GET request', (c) => c.text('Hello!'))

The result

The decision is available as c.get('jev').

import type { JevResult } from 'hono-jev-router'

const app = new Hono<{ Variables: { jev: JevResult } }>({ router: new JevRouter({ apiKey }) })

app.on('jev', 'a request from an AI agent', (c) => {
  const { route, confidence, probabilities } = c.get('jev')
  // route: 'a request from an AI agent'
  // confidence: 0.94
  // probabilities: { 'a request from an AI agent': 0.94, 'a request from a human browser': 0.2 }
  return c.json({ route, confidence, probabilities })
})

The probabilities are independent of each other, so they do not add up to 1.

Options

new JevRouter({
  apiKey, // string | (c) => string
  baseURL, // default: 'https://api.typesafe.ai'
  threshold, // a route matches when its probability is at least this. default: 0.5
  maxBodyLength, // how much of the request body Jev sees. default: 4096
  redactHeaders, // header values Jev must not see. default: authorization, cookie, ...
  run, // reach Jev some other way
  choose, // replace the whole decision
})

threshold

Raise it to make every route stricter. Requests that match nothing fall through:

const app = new Hono({ router: new JevRouter({ apiKey, threshold: 0.8 }) })

app.notFound((c) => c.text('Not sure what you are', 404))

apiKey

Your TypeSafe API key as a string:

new JevRouter({ apiKey: process.env.TYPESAFE_API_KEY })

On Cloudflare Workers, process.env holds your variables and secrets only when the nodejs_compat flag is on and the compatibility date is 2025-04-01 or later. Otherwise pass a function. It receives the Context, so the key can come from c.env:

new JevRouter({ apiKey: (c) => c.env.TYPESAFE_API_KEY })

run: Workers AI / AI Gateway

Jev is available on Cloudflare as typesafe/jev, so you can use the AI binding instead of an API key:

type Bindings = { AI: Ai }

const app = new Hono<{ Bindings: Bindings }>({
  router: new JevRouter({
    run: async (c, request) => {
      // If 'typesafe/jev' is not in your generated types yet, cast `c.env.AI`
      const { result } = await c.env.AI.run('typesafe/jev', request, {
        gateway: { id: 'default' },
      })
      return result
    },
  }),
})

choose

Replaces the decision itself. It receives the request state, the descriptions and the threshold, and returns a JevResult. Useful for tests, caching, or delegating to something that holds the credentials:

new JevRouter({
  choose: (c, input) => c.env.JEV.choose(input), // input: { state, routes, threshold }
})

Author

Yusuke Wada https://github.com/yusukebe

License

MIT