/** * @when you need a cheap, fast, calibrated classifier (yes/no, pick-one, or a score) instead of a full LLM grading round — the cost tier below judge/Evaluator.rubric * @fails classify() throws ValidationError (stamped lib:'bare-agent') on a bad request or a malformed/mismatched Jev reply; ProviderError on HTTP/transport (401/403/422/429/529, socket cut); a governance HaltError from onLlmResult propagates clean. * @signature new JevProvider(options?) * @example * import { JevProvider } from 'bare-agent/providers'; * const jev = new JevProvider({ apiKey, rates: { in: 0.042 / 1000, out: 0 } }); // per-1K tokens * const { answers } = await jev.classify('I was charged twice, please refund.', { * route: { type: 'choice', instructions: 'Route this ticket.', * criteria: { billing: 'payments/refunds', technical: 'bugs', account: 'login' } }, * }); * // answers.route.choice === 'billing' */ export class JevProvider { /** * @param {object} [options] * @param {string} [options.apiKey] - Bearer key. Required at classify() time. * @param {string} [options.model='jev-latest'] - Model id ('jev-latest' | 'jev-preview' | a pinned 'jev-1.13.0'). * @param {string} [options.baseUrl='https://api.typesafe.ai'] * @param {number} [options.timeoutMs] - Idle-socket timeout (ms); default from provider-http, 0/Infinity disable. * @param {number} [options.deadlineMs] - Total call-duration deadline (ms); 0 disables (default). * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [options.rates] - Per-1K-token USD rates for authoritative pricing (Jev: `{ in: 0.042/1000, out: 0 }`). * @param {boolean} [options.exposeErrorBody=false] - Include the raw error body on a ProviderError (default off). * @param {boolean} [options.harden=true] - Wrap each question's instructions with a defensive preamble (prefixed for a string, prepended as element 0 for an array, or added under a reserved key for an object), treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`. NOTE: the preamble is part of the question, so hardened and unhardened calls can return DIFFERENT values for the same `state` — the shift can be large enough to move a decision cutoff. Tune any decision threshold with hardening in the SAME state you ship it in — a cutoff tuned against `harden:false` or the raw API will be off once hardening is on. */ constructor(options?: { apiKey?: string | undefined; model?: string | undefined; baseUrl?: string | undefined; timeoutMs?: number | undefined; deadlineMs?: number | undefined; rates?: { in: number; out: number; cacheReadMult?: number; cacheWriteMult?: number; } | undefined; exposeErrorBody?: boolean | undefined; harden?: boolean | undefined; }); apiKey: string | undefined; model: string; baseUrl: string; timeoutMs: number | undefined; deadlineMs: number | undefined; rates: { in: number; out: number; cacheReadMult?: number; cacheWriteMult?: number; } | null; exposeErrorBody: boolean; harden: boolean; /** * Classify `state` against one or more typed `questions`. See the class doc for the primitive tags. * * Jev answers are CALIBRATED, not DETERMINISTIC — repeated identical calls can differ slightly, * and a value sitting on a threshold can cross it between runs. Assert on RANGES or on the side of * a threshold, never on an exact value (`toBe(0.62)` will flake). * @param {string|object|any[]} state - The shared input all questions judge (Jev's `state`). * @param {Record} questions - Keyed questions; each judged independently against `state`. * @param {object} [opts] * @param {string} [opts.model] - Override the model for this call. * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [opts.rates] - Override rates for this call. * @param {number} [opts.timeoutMs] - Override idle timeout for this call. * @param {number} [opts.deadlineMs] - Override deadline for this call. * @param {boolean} [opts.harden] - Override injection hardening for this call (constructor default otherwise). * @param {(payload: {usage: any, model: string|null, kind: 'classify', costUsd: number|null, rateSource: 'provider'|'caller'|'tier'|'default'|null}) => any} [opts.onLlmResult] - Budget hook; forwarded before return. * @returns {Promise<{model: string, answers: Record, usage: any, costUsd: number|null, rateSource: 'provider'|'caller'|'tier'|'default'|null, raw: any}>} * `raw` is the full, unmodified parsed Jev response (mirrors `judge()`'s `raw`) — closes the silent drop of any * top-level field beyond `answers`/`usage`/`model` (e.g. a request id, warnings, moderation flags, timing). */ classify(state: string | object | any[], questions: Record, opts?: { model?: string | undefined; rates?: { in: number; out: number; cacheReadMult?: number; cacheWriteMult?: number; } | undefined; timeoutMs?: number | undefined; deadlineMs?: number | undefined; harden?: boolean | undefined; onLlmResult?: ((payload: { usage: any; model: string | null; kind: "classify"; costUsd: number | null; rateSource: "provider" | "caller" | "tier" | "default" | null; }) => any) | undefined; }): Promise<{ model: string; answers: Record; usage: any; costUsd: number | null; rateSource: "provider" | "caller" | "tier" | "default" | null; raw: any; }>; /** * Request-side loud validation — fail before spending a token. @param {any} state @param {any} questions */ _validateRequest(state: any, questions: any): void; /** * Build a hardened COPY of `questions` (new object, new nested question objects, new * nested instructions containers) with {@link HARDENING_PREAMBLE} woven into each * `instructions`, shape-aware so a structured form is wrapped rather than stringified: * a string gets the preamble prefixed, an array gets it prepended as element 0, and an * object gets it added under the reserved {@link HARDENING_KEY}. Never mutates the * caller's `questions` argument or its nested objects/arrays. * @param {Record} questions @returns {Record} */ _hardenQuestions(questions: Record): Record; /** * Answer-side validation — Jev's reply is UNTRUSTED model output; every answer must * match the question that asked it. Only the discriminator field (`noul`/`choice`/`score`) is * validated here — `probabilities`/`confidence` on a `choice`/`score` answer pass through * UNVALIDATED (untrusted passthrough; the caller decides what to do with them). * @param {any} questions @param {any} raw @returns {Record} */ _validateAnswers(questions: any, raw: any): Record; /** * Normalize Jev usage to bareagent's neutral shape (no cache tiers). @param {any} u */ _normalizeUsage(u: any): { inputTokens: any; outputTokens: any; cacheReadTokens: number; cacheCreationTokens: number; } | null; /** * @param {string} path @param {Record} body @param {number} [timeoutMs=0] @param {number} [deadlineMs=0] * @returns {Promise} */ _request(path: string, body: Record, timeoutMs?: number, deadlineMs?: number): Promise; }