UNPKG

@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
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; }