@molecule/api-ai-decisions-kev
Provider bond · ai-decisions · API (Node) · v1.0.0 · Apache-2.0
Kev decisions provider for molecule.dev — typed choice/score/yes-no answers from the open-weights Kev models (Qwen-based, 0.8B–27B) on your own kev.serve host
npm install @molecule/api-ai-decisions-kevnpm · Source on GitHub · Implements @molecule/api-ai-decisions
How it works
@molecule/api-ai-decisions-kev is a provider bond on the API (Node) side: it implements the ai-decisions core interface (@molecule/api-ai-decisions) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
# Run the model server (NVIDIA/AMD GPU, or Apple silicon via MLX; Python 3.12/3.13)
git clone https://github.com/jaredpalmer/kev.git
KEV_API_KEY=change-me uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b@v1.0Works with: @molecule/api-ai-decisions, @molecule/api-secrets
Secrets: KEV_URL (optional), KEV_API_KEY (optional)
Reference
Kev decisions provider for molecule.dev — typed decisions from the
open-weights Kev models on a kev.serve host you run.
Kev (github.com/jaredpalmer/kev; code and weights Apache-2.0) is a family of
Qwen-based decision models — Kev-0.8B, 4B and 9B (LoRA adapters on
Qwen3.5-*-Base) and Kev-27B (a full fine-tune of Qwen3.8-27B) — that answer
choice, score and yes/no questions with calibrated probabilities and
generate no text. Its README recommends Kev-4B as the default. kev.serve
speaks TypeSafe Jev's /v1/systemone protocol, so swapping to
@molecule/api-ai-decisions-jev, -laya or -jeff is a one-line change.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
# Run the model server (NVIDIA/AMD GPU, or Apple silicon via MLX; Python 3.12/3.13)
git clone https://github.com/jaredpalmer/kev.git
KEV_API_KEY=change-me uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b@v1.0
import { setProvider, requireProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-kev'
setProvider(provider) // reads KEV_URL + KEV_API_KEY on first use
const { answers } = await requireProvider().decide({
state: 'My card was charged twice, please refund one of them',
questions: {
queue: {
type: 'choice',
instructions: 'Which team?',
criteria: { billing: 'charges, refunds', tech: 'bugs, login' },
},
refund: { type: 'yesNo', instructions: 'The customer wants a refund.' },
},
})
answers.queue.choice // 'billing'
answers.refund.probability // 0.93
Type
provider
Installation
npm install @molecule/api-ai-decisions-kev @molecule/api-ai-decisions @molecule/api-secrets
API
Interfaces
KevConfig
Configuration for the Kev decisions provider.
interface KevConfig {
/** Base URL of the `kev.serve` host. Defaults to `KEV_URL`, then `http://localhost:8008`. */
baseUrl?: string
/** Bearer token, when the server sets `KEV_API_KEY`. Defaults to the `KEV_API_KEY` env var. */
apiKey?: string
/**
* Extra request headers, resolved before each call and merged over the
* defaults — for a host whose auth expires or is not a bearer token (a Cloud
* Run ID token, Modal proxy auth). Pair with `@molecule/api-model-hosting`:
* `headers: () => hosting.authHeaders(endpoint.id)`.
*/
headers?: () => Record<string, string> | Promise<Record<string, string>>
/**
* Model id sent on every request. `kev.serve` accepts any string and echoes
* it back; it never selects a checkpoint (the server answers with the one
* it loaded via `--run`). Defaults to `'kev-latest'`.
*/
model?: string
/**
* Most `choice` options / `score` levels sent in one question. Defaults to
* 255, `kev.serve`'s own limit. Lower it for another `/v1/systemone` server
* with a smaller one (Nimble: 26).
*/
maxOptions?: number
/**
* Milliseconds to wait for one `decide()` (retries included) before
* aborting. Defaults to 120000 — long enough for a scale-to-zero host's
* cold start (~35 s on Modal) plus the model load.
*/
timeoutMs?: number
}
WireQuestion
One question as sent on the wire.
interface WireQuestion {
type: 'choice' | 'score' | 'noul'
instructions: string
criteria?: Record<string, string> | string[]
}
WireAnswer
One answer as received on the wire (only the fields we read).
interface WireAnswer {
type?: string
choice?: string
score?: number
noul?: number
probabilities?: Record<string, number>
}
WireResponse
The response body (only the fields we read).
interface WireResponse {
model?: string
answers?: Record<string, WireAnswer>
usage?: { input_tokens?: number; output_tokens?: number }
}
PostOptions
Options for postSystemOne.
interface PostOptions {
/** Full endpoint URL. */
url: string
/** Bearer token, if any. */
apiKey?: string
/** Extra headers resolved before the request (merged over the defaults). */
headers?: () => Record<string, string> | Promise<Record<string, string>>
/** Request body. */
body: Record<string, unknown>
/** Abort signal. */
signal?: AbortSignal
/** Label for error messages (`'Kev'`). */
label: string
/** Max retries on a retryable status (default 3). */
maxRetries?: number
}
Functions
createProvider(config)
Creates a Kev decisions provider.
function createProvider(config?: KevConfig): AIDecisionsProvider
config— Base URL, API key, headers hook, model id, option limit and timeout.
Returns: An AIDecisionsProvider backed by a kev.serve host.
toWireQuestions(questions)
Converts the core's questions into wire questions (yesNo → noul).
function toWireQuestions(questions: Record<string, DecisionQuestion>): Record<string, WireQuestion>
questions— The questions keyed by id.
Returns: The wire questions object.
fromWireAnswer(id, question, wire, minConfidence)
Converts one wire answer into the core's answer for question.
function fromWireAnswer(
id: string,
question: DecisionQuestion,
wire: WireAnswer | undefined,
minConfidence?: number,
): DecisionAnswer
id— The question id (for error messages).question— The question that was asked.wire— The wire answer.minConfidence— Optional low-confidence threshold.
Returns: The typed answer.
fromWireResponse(questions, body, minConfidence)
Converts a whole wire response into the core's result.
function fromWireResponse(questions: Q, body: WireResponse, minConfidence?: number): DecideResult<Q>
questions— The questions that were asked.body— The parsed response body.minConfidence— Optional low-confidence threshold.
Returns: The typed result.
postSystemOne(opts)
POSTs a /v1/systemone request with retry on 429/5xx-busy, honouring
Retry-After. Throws an Error carrying status on a non-2xx response.
function postSystemOne(opts: PostOptions): Promise<WireResponse>
opts— Request options.
Returns: The parsed response body.
Constants
DEFAULT_KEV_URL
kev.serve's default bind (--port defaults to 8008; its README examples use 8009).
const DEFAULT_KEV_URL: 'http://localhost:8008'
DEFAULT_KEV_MODEL
The model id kev.serve documents; any string is echoed back, none selects a checkpoint.
const DEFAULT_KEV_MODEL: 'kev-latest'
DEFAULT_KEV_MAX_OPTIONS
Most choice options or score levels kev.serve accepts (MAX_OPTIONS in kev/api.py).
const DEFAULT_KEV_MAX_OPTIONS: 255
DEFAULT_KEV_TIMEOUT_MS
Default per-call timeout: room for a scale-to-zero cold start (~35 s on Modal).
const DEFAULT_KEV_TIMEOUT_MS: 120000
provider
The provider implementation — lazy, so env vars are read on first use.
const provider: AIDecisionsProvider
aiDecisionsKevSecretDefinitions
Secret definitions used by the Kev decisions bond.
const aiDecisionsKevSecretDefinitions: SecretDefinition[]
Core Interface
Implements @molecule/api-ai-decisions interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-kev'
export function setupAiDecisionsKev(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai-decisions^1.1.0@molecule/api-secrets^1.0.1
Environment Variables
KEV_URL(optional) — Kev server URL- Setup: The base URL of your kev.serve host (clone github.com/jaredpalmer/kev, uv sync --extra serve, then uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b@v1.0). Defaults to http://localhost:8008.
- Get it here: https://github.com/jaredpalmer/kev#readme
- Example:
http://localhost:8008
KEV_API_KEY(optional) — Kev server API key- Setup: The bearer token your kev.serve host requires, if you set KEV_API_KEY on the server.
- Get it here: https://github.com/jaredpalmer/kev#readme
- Example:
a-long-random-string
Runtime Dependencies
-
@molecule/api-ai-decisions -
@molecule/api-secrets -
You run the model.
KEV_URL(defaulthttp://localhost:8008, the server's own default port) points at akev.servehost; its README examples use--port 8009, so setKEV_URLto match.kev.servebinds127.0.0.1unless you pass--host 0.0.0.0— and it is OPEN by default: setKEV_API_KEYon BOTH sides before exposing it, or you publish an unauthenticated GPU endpoint. -
Validated context. The server accepts states up to 65,536 tokens, but accuracy holds only to 8,192 tokens for Kev-0.8B/4B/9B (trained mostly on states ≤384 tokens) and 65,536 for Kev-27B. A longer state is REFUSED with a 422 naming its token count (unless the server runs
KEV_TRUNCATE_STATES=1); this bond surfaces it verbatim and never retries a 422. Shorten the state instead. -
modelnever selects a checkpoint. Any string is echoed back; the server answers with the one checkpoint it loaded via--run. Run a second server for a second size. -
1–255 options or levels per question (Kev's own limit — not Jeff's 26). This bond refuses more before sending. Option order can change an answer: questions are isolated from each other, options within one are not.
-
Text only. There is no
imagesfield; this bond throws if you pass any. Use@molecule/api-ai-decisions-intern-decisionor-llmfor images. -
Never use Kev-0.8B for tool routing — it scores below chance on When2Call (0.133). Date arithmetic is weak below 27B: start the server with
KEV_DATE_FACTS=1. Knowledge questions are capped by the base model, and smaller sizes trail Jev by 13–31 points out of domain. -
bf16 vs fp32. Serving runs in bf16, so probabilities differ from the published fp32 numbers by up to ~0.03 (GPU) / ~0.05 (Mac) and the top answer flips on ~1 question in 300.
KEV_DTYPE=fp32on the server gives the exact evaluation path. -
Ignore the wire
confidence. The server sends TypeSafe's dispersion statistic, not an accuracy rate; this bond recomputesconfidenceas the probability of the reported answer. TuneminConfidenceon your own labelled data — Kev-0.8B can automate far fewer decisions at a fixed error budget than 4B/9B/27B. -
Not a CPU model. It runs on CUDA/ROCm or Apple MLX (Kev-4B: ~42 ms on an L40S, ~721 ms on an M5). There is no
/healthroute: probeGET /v1/models. A scale-to-zero host (Modal) takes ~35 s on the first call after idle;createProvider({ timeoutMs })defaults to 120 s for that. -
Hosted on rented compute? Pass
headers: () => hosting.authHeaders(endpoint.id)(@molecule/api-model-hosting) when the host's auth expires or is not a bearer token — e.g. Modal proxy auth. It runs before every request and is merged over the defaults. -
Other
/v1/systemoneservers. PointbaseUrlat any server of the same protocol and lowermaxOptionsto its limit (a self-hosted Nimble:model: 'nimble-latest',maxOptions: 26— check its license first). -
Use the core's
setProvider, notbond('ai-decisions', …)directly.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Each flow that makes a decision (routing, triage, moderation, a guardrail) runs it from the real UI, and the answer DRIVES what happens next (the item lands in the chosen queue, the badge shows, the action is blocked) — not just printed.
- Both directions: a clearly-billing input routes to billing AND a clearly-technical one routes elsewhere. One label for every input is a broken integration.
- A low-confidence answer takes the app's fallback path (human review, "unsure" state) instead of being acted on.
- Provider errors (service down, bad key) show a visible, recoverable state — never a blank screen or an unhandled rejection.
- The call runs server-side: no provider request or key in the browser's Network tab.