@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-kev

npm · 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.0

Works 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.ts JSDoc, 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
  • KEV_API_KEY (optional) — Kev server API key

Runtime Dependencies

  • @molecule/api-ai-decisions

  • @molecule/api-secrets

  • You run the model. KEV_URL (default http://localhost:8008, the server's own default port) points at a kev.serve host; its README examples use --port 8009, so set KEV_URL to match. kev.serve binds 127.0.0.1 unless you pass --host 0.0.0.0 — and it is OPEN by default: set KEV_API_KEY on 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.

  • model never 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 images field; this bond throws if you pass any. Use @molecule/api-ai-decisions-intern-decision or -llm for 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=fp32 on the server gives the exact evaluation path.

  • Ignore the wire confidence. The server sends TypeSafe's dispersion statistic, not an accuracy rate; this bond recomputes confidence as the probability of the reported answer. Tune minConfidence on 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 /health route: probe GET /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/systemone servers. Point baseUrl at any server of the same protocol and lower maxOptions to its limit (a self-hosted Nimble: model: 'nimble-latest', maxOptions: 26 — check its license first).

  • Use the core's setProvider, not bond('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.