/** * Jev wire adapter — the ONLY module that knows the Jev/"System One" endpoint * paths and wire JSON. * * Maps a provider-neutral {@link DecisionRequest} to a `POST {base}/v1/systemone` * body, maps the response back to a {@link DecisionOutcome}, and validates the * mapped answers with the contract zod schemas. Any service implementing the * Jev wire format (layahost today) is reachable with just a base URL and an * API key. * * Wire notes (from the provider's published OpenAPI document): * - Request: `{ model?, state, questions: { name: { type, instructions, criteria } } }`. * `model` is sent only when the request names one; the server applies its * default otherwise. No model identifier is hard-coded here (arch gate 13). * - A `noul` question's wire `criteria` is `{ "true": }`; the * contract's single criteria string becomes that description, and it also * serves as the wire `instructions` (the question text) unless the request * supplies its own instructions. * - Response answers: noul → `{ noul: , confidence }`; choice → * `{ choice, probabilities: { option: p }, confidence }`; score → * `{ score: , probabilities: { level: p }, confidence }`. * `confidence` is OPTIONAL on the wire (T12790 follow-up, `jev-wire/4`): a * plain Jev host (e.g. `jev-1.13.0`) answers `{ type, noul }` with no * `confidence`; layahost sends one. When absent, the adapter derives it * with {@link deriveJevConfidence} (the chosen outcome's probability margin * over the others), so the contract's required `confidence` stays satisfied. * Score probabilities are keyed by level text (or, defensively, by 0-based * index) and re-aligned to the question's `criteria` order. * - Errors, per the layahost OpenAPI 1.0.0 (test fixture * `__tests__/fixtures/layahost-openapi.trimmed.json`): 401, 402, 422, 429, * 503, each with a Jev-style body `{detail:{error_type,message}}`. 402, and a * 429 whose `error_type` is `insufficient_quota` (empty balance), are credit * exhaustion (`insufficient_credits`), never `unauthorized` and never a rate * limit. 503 is `overloaded` (with `retry-after`). The error body is read, * size-capped, to classify. * UNVERIFIED tolerant extras (in the provider docs, not the OpenAPI): a 403 * `key_limit_exceeded` (the key's monthly limit, NOT a credential failure; * any other 403 is `unauthorized`) and a 529 (`overloaded`). * - Cost (T12664, T12715, `jev-wire/3`): read from the response body only — * `meta.cost_micros` (integer micro-dollars) and `meta.cost_usd`, both in * the OpenAPI `SystemOneResponse.meta`; `meta.checkpoint` likewise. When * only `cost_usd` is present, micros are derived from it. The layahost * OpenAPI 1.0.0 declares no response headers at all, so the * `x-layahost-cost-micros` / `x-layahost-balance-micros` headers named in * the prose docs are NOT read (T12715); the balance comes from * `GET /v1/usage` (`balance.micros`) instead. A plain Jev host sends no * `meta` and keeps working. * - Extensions, gated by {@link JevProviderOptions.capabilities} (detected by * {@link detectJevCapabilities} from `/v1/usage` and `/v1/templates` * responses, never from the host name): the `lang` and `cache` request * fields, `POST /v1/systemone/batch` and `GET /v1/usage`. * * - Model: the provider's OpenAPI marks `SystemOneRequest.model` REQUIRED and * documents no server-side default for `/v1/systemone` (the `laya-auto` * default is documented for `/v1/decide` only). The body therefore carries * `req.model ?? connection.model`; a request with neither is sent without a * model and the provider may reject it (422 → heuristic fallback). * - `GET /v1/models` → `{ models: [{ name, description?, ... }] }`; used by * {@link listJevModels} for the reachability probe. * * `/v1/decide` (single question, different body shape) is not used: every * single-question call is a one-question `/v1/systemone` call, so one mapping * covers every request. * * @task T12490 * @epic T12486 */ import type { DecisionOutcome, DecisionProviderCapabilities, DecisionProviderUsage, DecisionQuestion, DecisionRequest } from '@cleocode/contracts'; import { type DecisionProvider, type DecisionProviderConnection, DecisionProviderError, type DecisionProviderErrorKind } from './provider.js'; import { type DecideFetch } from './transport.js'; /** * Capabilities of a provider that answers `GET /v1/usage` (layahost): batch * of 64 requests / 256 questions, usage, cache control, lang hint, cost in * micros. Templates are added only when `/v1/templates` lists them. */ export declare const LAYAHOST_EXTENSION_CAPABILITIES: DecisionProviderCapabilities; /** * Adapter identity + version; part of every cache key so a mapping change * invalidates the cache. `/2`: cost micros, balance and checkpoint are read. * `/3` (T12715): cost is read from `meta` only (no headers); micros are * derived from `meta.cost_usd` when `meta.cost_micros` is absent. * `/4`: an answer's `confidence` is optional; when a plain Jev host omits it, * it is derived from the probabilities ({@link deriveJevConfidence}). */ export declare const JEV_ADAPTER_VERSION = "jev-wire/4"; /** One question as the Jev wire expects it. */ interface JevWireQuestion { readonly type: DecisionQuestion['type']; readonly instructions: DecisionQuestion['instructions']; readonly criteria: DecisionQuestion['criteria'] | { readonly true: string; }; } /** The `/v1/systemone` request body. */ export interface JevSystemOneBody { /** Model identifier; omitted to use the server default. */ readonly model?: string; /** The state being judged. */ readonly state: DecisionRequest['state']; /** Question name → wire question. */ readonly questions: Readonly>; /** Language hint (only with the `langHint` capability). */ readonly lang?: string; /** Provider answer cache (only with the `cacheControl` capability). */ readonly cache?: boolean; } /** * Map a contract request to the `/v1/systemone` wire body. * * `lang` and `cache` are sent only when `capabilities` says the provider * honours them; the default (Jev minimum) sends neither, so a plain Jev host * sees exactly the Jev body. * * @param req - Provider-neutral request. * @param capabilities - The provider's capabilities. Default: the Jev minimum. * @returns The JSON body to POST. */ export declare function toJevSystemOneBody(req: DecisionRequest, capabilities?: DecisionProviderCapabilities): JevSystemOneBody; /** * Confidence for an answer whose wire body carries none (plain Jev hosts). * * Defined as the probability margin of the CHOSEN outcome over the most * likely of the others, clamped to [0, 1]: * `p(chosen) − max(p(other))`. The chosen outcome need not be the argmax (a * host may name a `choice` that is not the most probable option), in which * case the margin is negative and clamps to 0. * - noul: chosen is yes when `p >= 0.5`, so this is `|2p − 1|` — 0 at a coin * flip, 1 at certainty. * - choice / score: `p(chosen option or level) − max(p of the others)` (a * single outcome counts as a margin of `p(chosen)`). * * The definitions agree on a two-outcome distribution, so a noul and a * two-option choice with the same odds get the same confidence. A reported * `confidence` is always preferred over this derivation. * * @param probabilities - The outcome probabilities (for noul: `[p, 1 − p]`). * @param chosen - Index of the chosen outcome in `probabilities`. * @returns The derived confidence in [0, 1] (0 for an empty distribution or * an out-of-range `chosen`). */ export declare function deriveJevConfidence(probabilities: readonly number[], chosen: number): number; /** * Map a `/v1/systemone` response body to a contract outcome. * * Every question in `req` must be answered with the matching type, and every * mapped answer must pass {@link decisionAnswerSchema}; otherwise this throws * `DecisionProviderError('invalid_response')`. * * @param req - The request that was sent (needed to align score levels). * @param body - Parsed JSON response body. * @param latencyMs - Measured wall-clock latency of the call. * @returns The outcome, with `source: 'provider'`. */ export declare function fromJevSystemOneResponse(req: DecisionRequest, body: unknown, latencyMs: number): DecisionOutcome; /** * Parse a `retry-after` header (delta seconds or HTTP date) into milliseconds, * capped at {@link MAX_RATE_LIMIT_COOLDOWN_MS}. * * Negative seconds, past dates and anything unparseable are ignored * (`undefined`, so the budget applies its default cool-down). A numeric-looking * value is never re-read as a date: `Date.parse('-5')` is a valid year. * * @param header - Raw header value, or null. * @param now - Current epoch ms (injectable for tests). * @returns Milliseconds to wait (≤ 60 000), or undefined when absent, negative or unparseable. */ export declare function parseRetryAfterMs(header: string | null, now?: number): number | undefined; /** The provider's `error_type` from an error body (`{detail:{error_type}}` or `{error:{type}}`). */ export declare function errorTypeOf(body: unknown): string | undefined; /** * Classify a non-2xx response. * * @param status - HTTP status. * @param retryAfterMs - Parsed `retry-after`, when present. * @param errorType - The body's `error_type`, when readable. * @returns The provider error. */ export declare function errorForStatus(status: number, retryAfterMs: number | undefined, errorType?: string): DecisionProviderError; /** Options for {@link createJevProvider}. */ export interface JevProviderOptions { /** * Transport; defaults to {@link decideFetch}, which releases every DNS, * socket and TLS handle on abort (the global `fetch` does not). Tests * inject a stub. */ readonly fetch?: DecideFetch; /** Monotonic clock in ms; defaults to `performance.now`. */ readonly now?: () => number; /** * Detected capabilities (see {@link detectJevCapabilities}). Default: the * Jev minimum — no extension is used until detection says it exists. */ readonly capabilities?: DecisionProviderCapabilities; } /** * Create a {@link DecisionProvider} speaking the Jev wire format. * * @param connection - Base URL + API key. The key is only ever placed in the * `Authorization` header. * @param opts - Injectable `fetch` and clock. * @returns A provider whose `decide` throws {@link DecisionProviderError} on any failure. * * @example * ```ts * const provider = createJevProvider({ baseUrl: 'https://decide.example', apiKey }); * const outcome = await provider.decide(request, AbortSignal.timeout(300)); * ``` */ export declare function createJevProvider(connection: DecisionProviderConnection, opts?: JevProviderOptions): DecisionProvider; /** * Map a `GET /v1/usage` body onto {@link DecisionProviderUsage}. Unknown or * malformed fields are dropped, never guessed. * * @param body - Parsed JSON. * @returns The usage fields the body carried. */ export declare function parseJevUsage(body: unknown): DecisionProviderUsage; /** * Detection failures that say nothing about the provider's capabilities * (network, timeout/abort, 5xx, 503/529, 429): a detection that hit one is * partial, never proof that a capability is absent (T12715). */ export declare const TRANSIENT_DETECTION_ERRORS: ReadonlySet; /** * Detect what a Jev-compatible host supports beyond the Jev minimum, from its * responses only (spec §2.2: never from the host name). * * - `GET /v1/usage` answers 2xx with at least one usage field → the layahost extensions * ({@link LAYAHOST_EXTENSION_CAPABILITIES}). * - `GET /v1/templates` answers 2xx with names → `templates`. * - Anything else (404, error, timeout) → the Jev minimum. Never throws. * * A transient failure (see {@link TRANSIENT_DETECTION_ERRORS}) of either * probe is returned as `error`, including when `/v1/usage` succeeded and only * `/v1/templates` failed: such a detection is partial, and callers must not * cache it as "capability absent". * * Both endpoints count toward the provider's rate limit, so callers cache the * result (`cleo decide status` refreshes it at most every 10 minutes). * * @param connection - Base URL + API key. * @param signal - Aborts both probes. * @param opts - Injectable `fetch`. * @returns The detected capabilities and, when read, the usage. */ export declare function detectJevCapabilities(connection: Pick, signal: AbortSignal, opts?: Pick): Promise<{ capabilities: DecisionProviderCapabilities; usage?: DecisionProviderUsage; error?: DecisionProviderError; }>; /** * Allowed shape of a model name: word characters plus `. : / @ -`, 1-128 * characters. Anything else (ANSI escapes, control characters, spaces) is * never stored, sent or printed. */ export declare const DECISION_MODEL_NAME_PATTERN: RegExp; /** Byte cap on a `GET /v1/models` response body. */ export declare const MAX_MODELS_RESPONSE_BYTES: number; /** Cap on the number of model names kept from a listing. */ export declare const MAX_MODELS_LISTED = 500; /** * Whether `name` is a safe model identifier (see {@link DECISION_MODEL_NAME_PATTERN}). * * @param name - Candidate model name. * @returns True when it may be stored, sent and printed. */ export declare function isValidDecisionModelName(name: string): boolean; /** * List the models the key may use (`GET {base}/v1/models`). * * The body is capped at {@link MAX_MODELS_RESPONSE_BYTES}; names failing * {@link DECISION_MODEL_NAME_PATTERN} are skipped and at most * {@link MAX_MODELS_LISTED} names are returned. * * @param connection - Base URL + API key (only ever placed in the `Authorization` header). * @param signal - Aborts the request. * @param opts - Injectable `fetch`. * @returns Safe model names in the order the provider lists them. * @throws {DecisionProviderError} On any HTTP, network, abort, size or shape failure. */ export declare function listJevModels(connection: Pick, signal: AbortSignal, opts?: Pick): Promise; export {}; //# sourceMappingURL=jev-wire.d.ts.map