@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
145 lines (144 loc) • 7.65 kB
TypeScript
import type { AIProviderName } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { ProviderError } from "../types/index.js";
import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionPreparedMedia, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
/**
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
* while still bounded. Every internal caller is fail-open, so this only bites
* on a genuine stall.
*/
export declare const DEFAULT_DECISION_TIMEOUT_MS = 5000;
export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
export declare const asNumber: (v: unknown) => number | undefined;
/**
* FastAPI's request-validation envelope, `{"detail":[{loc, msg, input}]}`,
* reduced to its field paths and messages. `input` echoes the caller's own
* request back — possibly the whole state — so it is never surfaced.
*/
export declare function describeValidationErrors(detail: readonly unknown[]): string;
/**
* A LiteLLM proxy echoes credentials back in its error texts, in more than one
* wording: a masked key plus its hash on a rejected key, the whole key when it
* does not look like a LiteLLM key, a key hash on a rate limit. None of it
* belongs in an error message or a log, whatever the wording, so the
* configured key, anything shaped like a LiteLLM key, any long hex run and any
* embedded `data:` URL are all removed.
*/
export declare function redactCredentials(message: string, apiKey: string): string;
/**
* The shared half of every "System One" decision provider — a model that takes
* one `state` plus typed questions and returns one typed answer each, with no
* text anywhere.
*
* Vendors differ in where the request goes, how it is authenticated, what the
* body carries and what their error envelopes look like. They do not differ in
* anything else, so everything else lives here: the missing-key check, the
* auth circuit breaker, the timeout and retry loop, answer parsing and usage.
* A new decision provider implements the abstract hooks and nothing more.
*/
export declare abstract class SystemOneDecisionProvider extends BaseProvider {
protected readonly proxyFetch: typeof fetch;
private readonly maxRetries;
/**
* Set once the key has been rejected. A bad key cannot fix itself, and
* without this every later request would pay a full round trip (~265ms
* measured on Jev) to be told so again — a permanent tax on the hot path for
* a misconfiguration.
*/
private authFailed;
protected constructor(modelName: string | undefined, providerName: AIProviderName, sdk: unknown);
/** Short vendor name for messages and logs, e.g. "TypeSafe". */
protected abstract vendorLabel(): string;
/** How the model is named when describing it, e.g. "TypeSafe (Jev)". */
protected abstract vendorDisplayName(): string;
protected abstract decisionApiKey(): string;
protected abstract missingKeyMessage(): string;
/**
* Why this instance cannot send a request even though it has a key, such as
* a provider with no built-in endpoint and none configured. Undefined when
* nothing is missing.
*/
protected missingConfigMessage(): string | undefined;
/**
* Where the request goes. The model asked for is passed in because one
* vendor names it in the URL path; every other provider ignores it.
*/
protected abstract decisionEndpoint(model: string): string;
protected abstract decisionHeaders(): Record<string, string>;
protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
protected abstract parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
protected abstract readRequestId(headers: Headers): string | undefined;
/**
* The System One wire calls the yes/no primitive `noul`; this codebase and
* every SDK exposing it call it `boolean`.
*/
protected encodeQuestion(question: DecisionQuestion): Record<string, unknown>;
/**
* How long a rate-limited or unavailable response asks the caller to wait.
* Undefined keeps the default backoff. A wait longer than what is left of the
* attempt's timeout is not taken: the error is thrown at once, so a fail-open
* consumer is not held for the length of a vendor's cooldown.
*/
protected readRetryAfterMs(_headers: Headers): number | undefined;
/**
* The per-attempt timeout when the caller sets none. A provider whose latency
* grows with the number of questions scales the descriptor's allowance here.
*/
protected defaultTimeoutMs(_questionCount: number): number | undefined;
/**
* The part of a successful response that holds `answers`, `usage` and
* `model`. The default is the response itself; a vendor that wraps every
* answer in an envelope returns what is inside it. Errors are not passed
* through here: they are read from the whole response.
*/
protected readDecisionPayload(payload: unknown): unknown;
/** Per-question confidence a transport reports outside the answer objects. */
protected reportedConfidence(_payload: unknown): Record<string, number>;
protected resolveResponseModel(payload: Record<string, unknown>, requestedModel: string): string;
/** Extra fields for the credential-rejected warning. */
protected diagnosticContext(): Record<string, unknown>;
protected getProviderName(): AIProviderName;
protected getDefaultDecisionModel(): string | undefined;
supportsTools(): boolean;
/**
* A decision model produces no text, so there is no chat model behind it.
* Unreachable while the descriptor declares `inferenceKinds: ["decide"]`;
* kept actionable for the case where something routes here anyway.
*/
protected getAISDKModel(): LanguageModel;
protected executeStream(_options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
/**
* Must RETURN the error, never throw it (CLAUDE.md rule 6).
*/
protected formatProviderError(error: unknown): Error;
/**
* Evaluate a state against a batch of typed questions.
*
* Throws a {@link ProviderError} carrying a normalised {@link DecisionError}
* in `cause`. Internal hot paths should prefer `NeuroLink.tryDecide()`,
* which returns null instead.
*/
decide(request: DecisionRequest): Promise<DecisionResult>;
/**
* Turn the request's images and video into data URLs, or refuse it. Refused
* as a non-retryable `invalid_request`: resending the same media cannot help.
*/
private prepareMedia;
/**
* The encoded body, not just the media, is what the server's size limit
* applies to.
*/
private assertWithinRequestBytes;
/**
* Refuse a request the model cannot read in full. An encoder cuts the state
* off past its window without saying so, so without this a decision would
* be made on input the model never saw — and reported as if it had.
*/
private assertWithinDecisionLimits;
/**
* Wrap a normalised decision failure in the provider error type, keeping
* the structured detail reachable via `cause` for callers that branch on
* `kind` (authentication vs rate_limit vs max_tokens_exceeded).
*/
protected decisionError(detail: DecisionError): ProviderError;
}