@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
1,045 lines (1,044 loc) • 42.5 kB
TypeScript
import type { AIProviderName } from "../constants/enums.js";
import type { EvaluationData } from "./evaluation.js";
import type { RAGConfig } from "./rag.js";
import type { KnowledgeGroundingMetadata, KnowledgeRequestScope } from "./knowledge.js";
import type { SkillsCallOptions } from "./skills.js";
import type { AnalyticsData, ToolExecutionEvent, ToolExecutionSummary } from "../types/index.js";
import type { MiddlewareFactoryOptions, OnChunkCallback, OnErrorCallback, OnFinishCallback } from "../types/middleware.js";
import type { TokenUsage } from "./analytics.js";
import type { JsonValue, UnknownRecord } from "./common.js";
import type { TerminalAgentModeOption, TerminalAgentModeVersion } from "./agentMode.js";
import type { Content, ImageWithAltText } from "./content.js";
import type { ChatMessage, ConversationModality, ToolReplayMode } from "./conversation.js";
import type { StreamNoOutputSentinel } from "./noOutputSentinel.js";
import type { GenerateStopReason, TTSMetadata, ToolExecutionCaptureOptions } from "./generate.js";
import type { MemoryCallOptions } from "./memory.js";
import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js";
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
import type { STTOptions, STTResult } from "./stt.js";
import type { StandardRecord, ValidationSchema } from "./aliases.js";
import type { CSVProcessorOptions, FileWithMetadata, VideoProcessorOptions, VisionImageOutputFormat, OfficeProcessorOptions } from "./file.js";
import type { WorkflowConfig } from "./workflow.js";
import type { LanguageModel, StepResult } from "./providers.js";
import type { Tool, ToolChoice } from "./tools.js";
import type { ProcessorPipelineConfig } from "./ioProcessor.js";
/**
* Progress tracking and metadata for streaming operations
*/
export type StreamingProgressData = {
chunkCount: number;
totalBytes: number;
chunkSize: number;
elapsedTime: number;
estimatedRemaining?: number;
streamId?: string;
phase: "initializing" | "streaming" | "processing" | "complete" | "error";
};
/**
* Streaming metadata for performance tracking
*/
export type StreamingMetadata = {
startTime: number;
endTime?: number;
totalDuration?: number;
averageChunkSize: number;
maxChunkSize: number;
minChunkSize: number;
throughputBytesPerSecond?: number;
streamingProvider: string;
modelUsed: string;
};
/**
* Options for AI requests with unified provider configuration
*/
export type StreamingOptions = {
providers: AIModelProviderConfig[];
temperature?: number;
maxTokens?: number;
systemPrompt?: string;
};
/**
* Progress callback for streaming operations
*/
export type ProgressCallback = (progress: StreamingProgressData) => void | Promise<void>;
/**
* Type for tool execution calls (AI SDK compatible)
*/
export type StreamToolCall = {
type?: "tool-call";
toolCallId?: string;
toolName: string;
parameters?: UnknownRecord;
args?: UnknownRecord;
id?: string;
};
/**
* Type for tool execution results - Enhanced for type safety
*/
export type StreamToolResult = {
toolName: string;
status: "success" | "failure";
output?: JsonValue;
error?: string;
id?: string;
executionTime?: number;
metadata?: {
[key: string]: JsonValue;
} & {
serverId?: string;
toolCategory?: string;
isExternal?: boolean;
};
};
/**
* Tool Call Results Array - High Reusability
*/
export type ToolCallResults = Array<StreamToolResult>;
/**
* Tool Calls Array - High Reusability
*/
export type ToolCalls = Array<StreamToolCall>;
/**
* Stream function options type - Primary method for streaming content
* Future-ready for multi-modal capabilities while maintaining text focus
*/
export type PCMEncoding = "PCM16LE";
export type AudioInputSpec = {
frames: AsyncIterable<Buffer>;
sampleRateHz?: number;
encoding?: PCMEncoding;
channels?: 1;
};
export type AudioChunk = {
data: Buffer;
sampleRateHz: number;
channels: number;
encoding: PCMEncoding;
};
/**
* Stream chunk type using discriminated union for type safety
*
* Used in streaming responses to deliver either text or TTS audio chunks.
* The discriminated union ensures type safety - only one variant can exist at a time.
*
* @example Processing text chunks
* ```typescript
* for await (const chunk of result.stream) {
* if (chunk.type === "text") {
* console.log(chunk.content); // TypeScript knows 'content' exists
* }
* }
* ```
*
* @example Processing audio chunks
* ```typescript
* const audioBuffer: Buffer[] = [];
* for await (const chunk of result.stream) {
* if (chunk.type === "tts_audio") {
* audioBuffer.push(chunk.audio.data); // TypeScript knows 'audio' exists
* if (chunk.audio.isFinal) {
* const fullAudio = Buffer.concat(audioBuffer);
* fs.writeFileSync('output.mp3', fullAudio);
* }
* }
* }
* ```
*
* @example Processing both text and audio
* ```typescript
* for await (const chunk of result.stream) {
* switch (chunk.type) {
* case "text":
* process.stdout.write(chunk.content);
* break;
* case "tts_audio":
* playAudioChunk(chunk.audio.data);
* break;
* }
* }
* ```
*/
export type StreamChunk = {
/** Discriminator for text chunks */
type: "text";
/** Text content chunk */
content: string;
} | {
/** Discriminator for synthesized TTS audio chunks. Uses `tts_audio`
* (not `audio`) to avoid colliding with realtime AudioChunk and to
* match the runtime shape emitted by `BaseProvider.stream()`. */
type: "tts_audio";
/** TTS audio chunk data */
audio: TTSChunk;
};
/** Provider-level chunks accepted by NeuroLink's core streaming pipeline. */
export type ProviderStreamChunk = {
content: string;
} | {
type: "audio";
audio: AudioChunk;
} | {
type: "tts_audio";
audio: TTSChunk;
} | {
type: "image";
imageOutput: {
base64: string;
};
};
/**
* What a `beforeStep` callback is told at a step boundary.
*
* The boundary is reached only after that step's tool results have settled and
* been written into the conversation, and before the step cap is re-checked —
* so a decision taken here applies to the NEXT step of the SAME turn, never to
* a step already in flight.
*/
export type ExecutionControlStepContext = {
/** Zero-based index of the step that just settled. */
stepIndex: number;
/** Steps that have completed in this turn, including the one that just settled. */
stepsCompleted: number;
/** The step cap currently in force — the number a renewal must exceed. */
maxSteps: number;
/** Milliseconds since the turn's first request was built. */
elapsedMs: number;
/** Names of the tools dispatched on the step that just settled, in order. */
toolNames: string[];
/**
* Fires when the turn is cancelled or the callback outlives its own budget.
* A callback that does real work (reading a live budget, asking a service)
* must honour it — the turn does not wait for a callback that ignores it.
*/
signal: AbortSignal;
};
/**
* What a `beforeStep` callback may change about the rest of the turn.
*
* Returning nothing is a decision too: it declines the renewal, so the
* existing cap stands and a turn that has reached it ends as a step-limit
* outcome exactly as it would with no callback at all.
*/
export type ExecutionControlDecision = {
/**
* A new absolute step cap. Must be finite and greater than the cap in force;
* anything else is ignored, so a callback cannot shorten a turn by returning
* a smaller number or unbound one by returning Infinity.
*/
maxSteps?: number;
/**
* A planning nudge appended to the conversation before the next step, in the
* same loop and the same history — this is how a caller tells the model that
* its budget changed without restarting the turn.
*/
nudge?: string;
};
/**
* Opt-in execution policy for one streamed turn. Supported only on the native
* Anthropic stream path today; any other provider REJECTS it rather than
* ignoring it, because silently dropping a policy the caller set is how a turn
* ends at a limit its owner believed it had removed.
*
* Absent, none of the turn-level policy here applies: the turn is bounded by
* `timeout` / `turnTimeoutMs` / `maxSteps` as it always was.
*
* One thing is NOT conditional on this option, and it is worth stating because
* it changed: the agentic engine bounds every tool call, at
* `toolTimeoutMs` or the 300s default, whether or not `executionControl` is
* present. Loops that previously ran tools with no per-tool timer at all — the
* Bedrock and Google AI Studio paths — therefore acquired one. A caller that
* relied on unbounded tool execution says so with `toolTimeoutMs: null`, which
* restores exactly the old behaviour.
*/
export type ExecutionControlOptions = {
/**
* Hard deadline for a single HTTP request (ms). Required, finite, positive.
*
* This is the floor that makes the rest of the contract safe: whatever the
* turn-level policy is — including no lifetime ceiling at all — a stalled
* upstream is always caught here, with the timer's own identity on the
* error. A step boundary cannot reset a deadline already running.
*/
requestTimeoutMs: number;
/**
* The turn's wall-clock ceiling (ms).
*
* - `null` — no lifetime timer is armed at all. This is the case that cannot
* be expressed any other way: a very large number is still a ceiling, and
* it fires eventually, in the middle of work, dressed as a cancel. With no
* lifetime timer the per-tool deadline is the turn's last bound, so
* `toolTimeoutMs: null` is refused alongside it.
* - a finite positive number — an explicit cap, reported as `time-limit`.
* - `0`, negative, or non-finite — rejected.
* - absent — the legacy handling (`turnTimeoutMs`, else the provider
* timeout) is inherited untouched.
*
* Set to anything other than absent, this OWNS the turn's lifetime timer, so
* a `turnTimeoutMs` on the same request would be read by nothing. That
* combination is rejected rather than silently resolved — set one or the
* other. Note that "absent" means the property is not there AND that it is
* present holding `undefined`: both say "no opinion", and both inherit.
*/
lifetimeTimeoutMs?: number | null;
/**
* Runs at each step boundary. May renew the step cap and append a planning
* nudge. It is itself bounded by `beforeStepTimeoutMs` and cancelled with
* the turn; a callback that throws, or outlives its budget, is treated as
* declining to renew.
*/
beforeStep?: (context: ExecutionControlStepContext) => ExecutionControlDecision | undefined | Promise<ExecutionControlDecision | undefined>;
/**
* Bound on `beforeStep` itself (ms, finite and positive; default 30_000).
* A boundary callback sits between two model calls, so an unbounded one
* stalls the turn in a place no other timer is watching.
*/
beforeStepTimeoutMs?: number;
};
export type StreamOptions = {
/**
* Fraction of the model's context window at which compaction runs for this
* request, replacing the 0.8 default.
*
* A request that needs only the current message does not need the whole
* window kept warm for it, and compacting earlier is free; a request that
* depends on the entire conversation should compact as late as possible.
* When the classifier router runs with a decision model it fills this in
* per request — always at or below the default, never above, because a
* request that overflows the window fails hard and `ModelPool` records that
* failure as a permanent cooldown.
*/
compactionThreshold?: number;
/**
* Opt this stream call into the knowledge grounding configured on the
* NeuroLink instance. Defaults to `false` when omitted.
*/
useKnowledgeGrounding?: boolean;
/**
* Enabled integrations used to scope knowledge retrieval for this turn.
* Used only when `useKnowledgeGrounding` is true and knowledge grounding is
* enabled on the NeuroLink instance.
*/
knowledgeContext?: KnowledgeRequestScope;
input: {
/** Prompt text. Optional for media-only modes (avatar, music) that are driven by uploaded files rather than a prompt. */
text?: string;
audio?: AudioInputSpec;
/**
* Images to include in the request.
* Supports simple image data (Buffer, string) or objects with alt text for accessibility.
*
* @example Simple usage
* ```typescript
* images: [imageBuffer, "https://example.com/image.jpg"]
* ```
*
* @example With alt text for accessibility
* ```typescript
* images: [
* { data: imageBuffer, altText: "Product screenshot showing main dashboard" },
* { data: "https://example.com/chart.png", altText: "Sales chart for Q3 2024" }
* ]
* ```
*/
images?: Array<Buffer | string | ImageWithAltText>;
csvFiles?: Array<Buffer | string>;
pdfFiles?: Array<Buffer | string>;
audioFiles?: Array<Buffer | string>;
videoFiles?: Array<Buffer | string>;
files?: Array<Buffer | string | FileWithMetadata>;
content?: Content[];
};
output?: {
format?: "text" | "structured" | "json";
streaming?: {
chunkSize?: number;
bufferSize?: number;
enableProgress?: boolean;
};
};
csvOptions?: CSVProcessorOptions;
/**
* Video processing options (#478, #433).
*
* Reconstructed option objects have to carry this the same way they carry
* `csvOptions` and `pdfOptions`: the message builder hands it to the
* detector, which hands it to `VideoProcessor`, and anything that rebuilds
* the options along the way and omits it restores the defaults without
* saying so.
*/
videoOptions?: VideoProcessorOptions;
/**
* Office document processing options. Currently consumed by the XLSX path
* (`sheetName`, `formatStyle`); see OfficeProcessorOptions.
*/
officeOptions?: OfficeProcessorOptions;
/** PDF processing options (#258). */
pdfOptions?: {
/** Password for an encrypted PDF (image-conversion fallback path). */
password?: string;
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
maxCanvasPixels?: number;
/**
* Render scale for the image fallback used by providers without native PDF
* support (#297). Higher is sharper but costs roughly the square in memory
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
*/
scale?: number;
/**
* Max pages converted by the image fallback (#297). Pages beyond this are
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
*/
maxPages?: number;
};
/**
* Options for images that need transcoding before a vision provider can
* read them (HEIC, TIFF, BMP, ICO, JPEG 2000, AVIF — see
* `adapters/imageFormatSupport.ts`). Mirrors `GenerateOptions.imageOptions`.
*/
imageOptions?: {
/**
* Transcode target for an incompatible image. Defaults to `"png"` — the
* module's own default, unchanged unless a caller opts in here.
*/
outputFormat?: VisionImageOutputFormat;
};
/**
* Audio transcription options for attached audio files (#413/#440).
*
* Mirrors `GenerateOptions.audioOptions`. `stream()` spreads its options
* rather than rebuilding them field by field, so declaring it here is what
* lets it reach the message builder.
*/
audioOptions?: {
/** Backend: "openai" (aliases "whisper"), "google" or "azure". */
provider?: string;
/** Transcription model, e.g. "whisper-1". Backend-specific. */
transcriptionModel?: string;
/** Language hint, e.g. "en". */
language?: string;
/** OpenAI/Whisper-only context prompt to bias transcription; ignored by Google and Azure. */
prompt?: string;
};
/**
* Text-to-Speech (TTS) configuration for streaming
*
* Enable audio generation from the streamed text response. Audio chunks will be
* delivered through the stream alongside text chunks as TTSChunk objects.
*
* @example Basic streaming TTS
* ```typescript
* const result = await neurolink.stream({
* input: { text: "Tell me a story" },
* provider: "google-ai",
* tts: { enabled: true, voice: "en-US-Neural2-C" }
* });
*
* for await (const chunk of result.stream) {
* if (chunk.type === "text") {
* process.stdout.write(chunk.content);
* } else if (chunk.type === "tts_audio") {
* // Handle audio chunk
* playAudioChunk(chunk.audio.data);
* }
* }
* ```
*
* @example Advanced streaming TTS with audio buffer
* ```typescript
* const result = await neurolink.stream({
* input: { text: "Speak slowly" },
* provider: "google-ai",
* tts: {
* enabled: true,
* voice: "en-US-Neural2-D",
* speed: 0.8,
* format: "mp3",
* quality: "hd"
* }
* });
* ```
*/
tts?: TTSOptions;
/**
* Speech-to-Text (STT) configuration for streaming
*
* When enabled, audio from `stt.audio` is transcribed before streaming begins.
*/
stt?: STTOptions & {
provider?: string;
audio?: Buffer | ArrayBuffer;
};
/**
* Thinking/reasoning configuration for extended thinking models
*
* Enables extended thinking capabilities for supported models.
*
* **Gemini 3 Models** (gemini-3.1-pro-preview, gemini-3-flash-preview):
* Use `thinkingLevel` to control reasoning depth:
* - `minimal` - Near-zero thinking (Flash only)
* - `low` - Fast reasoning for simple tasks
* - `medium` - Balanced reasoning/latency
* - `high` - Maximum reasoning depth (default for Pro)
*
* **Anthropic Claude** (claude-3-7-sonnet, etc.):
* Use `budgetTokens` to set token budget for thinking.
*
* @example Gemini 3 with thinking level (streaming)
* ```typescript
* const result = await neurolink.stream({
* input: { text: "Solve this complex problem..." },
* provider: "google-ai",
* model: "gemini-3.1-pro-preview",
* thinkingConfig: {
* thinkingLevel: "high"
* }
* });
* ```
*
* @example Anthropic with budget tokens (streaming)
* ```typescript
* const result = await neurolink.stream({
* input: { text: "Solve this complex math problem..." },
* provider: "anthropic",
* model: "claude-3-7-sonnet-20250219",
* thinkingConfig: {
* enabled: true,
* budgetTokens: 10000
* }
* });
* ```
*/
thinkingConfig?: {
enabled?: boolean;
type?: "enabled" | "disabled";
/** Token budget for thinking (Anthropic models) */
budgetTokens?: number;
/** Thinking level for Gemini 3 models: minimal, low, medium, high */
thinkingLevel?: "minimal" | "low" | "medium" | "high";
};
provider?: AIProviderName | string;
model?: string;
region?: string;
temperature?: number;
maxTokens?: number;
/** Top-p (nucleus) sampling parameter. Controls diversity of generated tokens. */
topP?: number;
/** Top-k sampling parameter. Limits the number of tokens considered. (Google/Gemini models only) */
topK?: number;
/** Stop sequences that will halt generation when encountered. */
stopSequences?: string[];
systemPrompt?: string;
/** Opt-in terminal agent mode. See GenerateOptions.agentMode. */
agentMode?: TerminalAgentModeOption;
schema?: ValidationSchema;
tools?: Record<string, Tool>;
timeout?: number | string;
/** Wall-clock cap for the whole agentic turn (ms). See GenerateOptions.turnTimeoutMs. */
turnTimeoutMs?: number;
/**
* Opt-in execution policy for this turn (native Anthropic streaming only).
* See ExecutionControlOptions. Absent means the legacy bounds apply
* unchanged; present on any other provider is an error, not a no-op.
*
* Two consequences of that "error, not a no-op" stance are worth knowing
* before you set it:
*
* - **It is incompatible with provider fallback.** Only the native Anthropic
* stream path implements the contract, so a turn that falls back to any
* other provider — internal fallback, or a configured fallback chain —
* fails there with a ValidationError instead of being served without the
* policy. That is deliberate: a fallback that silently dropped the policy
* would produce exactly the invisible ceiling this option exists to
* remove. Pair it with `disableInternalFallback: true` when you want the
* turn to stay on Anthropic, and handle the error if you do not.
* - **It cannot be combined with `turnTimeoutMs`** when it sets
* `lifetimeTimeoutMs`, because both name the turn's wall-clock ceiling and
* only one of them can win. Supplying both is rejected up front rather
* than resolved in silence.
* - **`lifetimeTimeoutMs: null` cannot be combined with
* `toolTimeoutMs: null`.** Removing the turn's ceiling leaves the per-tool
* deadline as the only thing that will ever end a tool which never
* returns; removing that as well leaves the turn with no bound anywhere.
* Also rejected up front.
*/
executionControl?: ExecutionControlOptions;
/** Max time with no progress before the turn ends as "stalled" (ms). Native Vertex loops only — see GenerateOptions.stallTimeoutMs. */
stallTimeoutMs?: number;
/** Remaining-time threshold that triggers the wrap-up nudge (ms). See GenerateOptions.wrapupTimeLeadMs. */
wrapupTimeLeadMs?: number;
/** Per-tool-execution timeout (ms, default 300_000; `null` for no bound). See GenerateOptions.toolTimeoutMs. */
toolTimeoutMs?: number | null;
/** AbortSignal for external cancellation of the AI call */
abortSignal?: AbortSignal;
/** Bounds for tool execution capture. See GenerateOptions.toolExecutionCapture. */
toolExecutionCapture?: ToolExecutionCaptureOptions;
disableTools?: boolean;
/** Disable the schema-driven tool call repair mechanism (BZ-665). Default: false (repair enabled). */
disableToolCallRepair?: boolean;
maxSteps?: number;
/** Directories the built-in file tools may touch for this call; see GenerateOptions.toolRoots; can only narrow tools.fileRoots. */
toolRoots?: string[];
/**
* Tool choice configuration for streaming generation.
* Mirrors generate() so translated/fallback requests can preserve forced tool use.
* A forced choice (`"required"` or a named tool) is applied only to the
* first `toolChoiceSteps` steps (default 1); see GenerateOptions.toolChoice.
*/
toolChoice?: ToolChoice<Record<string, Tool>>;
/** Leading steps a forced `toolChoice` stays in force (default 1). See GenerateOptions.toolChoiceSteps. */
toolChoiceSteps?: number;
/** How stored tool steps are replayed into the prompt. See GenerateOptions.replayToolSteps. */
replayToolSteps?: ToolReplayMode;
/**
* Optional callback that runs before each stream step in a multi-step generation.
* Only a returned `toolChoice` is honoured (for that step); `activeTools` is
* accepted but ignored. See GenerateOptions.prepareStep.
*/
prepareStep?: (options: {
steps: StepResult<Record<string, Tool>>[];
stepNumber: number;
maxSteps: number;
model: LanguageModel;
}) => PromiseLike<{
toolChoice?: ToolChoice<Record<string, Tool>>;
activeTools?: Record<string, Tool>;
} | undefined>;
/** Include only these tools by name (whitelist). If set, only matching tools are available. */
toolFilter?: string[];
/**
* Filter available tools by name.
* Used by dynamic arguments to dynamically select which tools to enable.
* Merged into `toolFilter` before tool filtering runs.
*/
enabledToolNames?: string[];
/** Exclude these tools by name (blacklist). Applied after toolFilter. */
excludeTools?: string[];
/** Disable tool result caching for this request (overrides global mcp.cache.enabled) */
disableToolCache?: boolean;
/**
* Disable NeuroLink's internal provider fallback for this request.
* Used by the Claude proxy so the proxy itself can own fallback order.
*/
disableInternalFallback?: boolean;
/**
* Whether a turn that ended at the caller's own `maxSteps` bound may still
* trigger the internal no-output provider fallback. Reaching `maxSteps` is
* a budget the caller set, not a provider failure, but such a turn can end
* with no text output and would otherwise be retried on a different
* provider — spending that provider's tokens because the caller's own
* budget ran out. Set `false` to surface the capped turn as-is
* (`metadata.stopReason === "step-cap"`). Default (unset/true) preserves
* the existing fallback behaviour.
*/
fallbackOnMaxSteps?: boolean;
/**
* Skip injecting tool schemas into the system prompt.
* When true, tools are ONLY passed natively via the provider's `tools` parameter,
* avoiding duplicate tool definitions (~30K tokens savings per call).
* Default: false (backward compatible — tool schemas are injected into system prompt).
*/
skipToolPromptInjection?: boolean;
enableEvaluation?: boolean;
enableAnalytics?: boolean;
context?: UnknownRecord;
evaluationDomain?: string;
toolUsageContext?: string;
conversationHistory?: Array<{
role: string;
content: string;
}>;
factoryConfig?: {
domainType?: string;
domainConfig?: StandardRecord;
enhancementType?: "domain-configuration" | "streaming-optimization" | "mcp-integration" | "legacy-migration" | "context-conversion";
preserveLegacyFields?: boolean;
validateDomainData?: boolean;
};
streaming?: {
enabled?: boolean;
chunkSize?: number;
bufferSize?: number;
enableProgress?: boolean;
fallbackToGenerate?: boolean;
};
conversationMessages?: ChatMessage[];
middleware?: MiddlewareFactoryOptions;
workflow?: string;
workflowConfig?: WorkflowConfig;
enableSummarization?: boolean;
/**
* How this turn was entered. Stamped on the stored messages' metadata so
* history readers can tell a spoken turn from a typed one. Leave it unset
* for typed input: the stored turn is then identical to what it was before
* the field existed.
*/
modality?: ConversationModality;
/**
* Maximum cumulative cost (USD) for this session.
* Once the session spend reaches this limit, subsequent stream() calls
* will throw a SESSION_BUDGET_EXCEEDED error instead of making API calls.
*
* @example
* ```typescript
* const result = await neurolink.stream({
* input: { text: "Summarize this" },
* maxBudgetUsd: 1.00
* });
* ```
*/
maxBudgetUsd?: number;
/**
* RAG (Retrieval-Augmented Generation) configuration.
*
* When provided, NeuroLink automatically loads the specified files, chunks them,
* generates embeddings, and creates a search tool that the AI model can invoke
* on demand to find relevant context before answering.
*
* @example Basic RAG streaming
* ```typescript
* const stream = await neurolink.stream({
* input: { text: "What is RAG?" },
* provider: "vertex",
* rag: {
* files: ["./docs/guide.md"],
* }
* });
* ```
*/
rag?: RAGConfig;
/** BZ-1341: Override fallback provider name (takes precedence over env/model config). */
fallbackProvider?: string;
/** BZ-1341: Override fallback model name (takes precedence over env/model config). */
fallbackModel?: string;
/** Callback invoked when streaming completes successfully. */
onFinish?: OnFinishCallback;
/** Callback invoked when streaming encounters an error. */
onError?: OnErrorCallback;
/** Callback invoked for each streaming chunk. */
onChunk?: OnChunkCallback;
/** Pre-validated user context for the request */
requestContext?: Record<string, unknown>;
/** Raw auth token — validated by configured auth provider */
auth?: {
token: string;
};
/**
* Per-provider credential overrides for this request.
* Overrides instance-level credentials set in `new NeuroLink({ credentials })`.
* Unset providers fall through to instance credentials, then environment variables.
*/
credentials?: NeurolinkCredentials;
/**
* Curator P2-3: per-call fallback callback. Overrides any
* instance-level `providerFallback` set on `new NeuroLink({...})`.
* Invoked for any error thrown while establishing the stream, except a
* genuine caller cancel — i.e. this call's `abortSignal` fired (network
* errors, 5xx, timeouts, auth failures, model-access-denied, and
* internal watchdog aborts all invoke it); receives the error
* unmodified. There is no mid-stream resume once chunks are flowing.
* Return `{ provider, model }` to retry, `null` to bubble.
*/
providerFallback?: (error: unknown) => Promise<{
provider?: string;
model?: string;
} | null>;
/**
* Curator P2-3: per-call ordered model chain. Overrides any
* instance-level `modelChain`. Without an explicit `providerFallback`
* callback the chain only advances on model-access-denied errors —
* other failures (network, 5xx, timeouts) bubble immediately.
*/
modelChain?: string[];
/**
* Per-call memory control.
*
* Override the global memory SDK behavior for this specific call.
* All flags default to `true` when the global memory SDK is enabled.
* If the global memory SDK is disabled, these flags have no effect.
* Shared with `GenerateOptions` — see `MemoryCallOptions`.
*/
memory?: MemoryCallOptions;
/** PII detection — scans and optionally redacts PII from input before the LLM call. */
piiDetection?: {
enabled?: boolean;
action?: "redact" | "abort" | "warn";
detectTypes?: Array<"email" | "phone" | "ssn" | "creditCard" | "ipAddress" | "address" | "name" | "dateOfBirth" | "passport" | "driversLicense">;
customPatterns?: RegExp[];
allowList?: string[];
redactionText?: string;
};
/** Response validation — validates accumulated stream content after completion. */
responseValidation?: {
minLength?: number;
maxLength?: number;
requiredPhrases?: string[];
forbiddenPhrases?: string[];
jsonSchema?: Record<string, unknown>;
customValidator?: (text: string) => {
category: string;
severity: "error" | "warning" | "info";
message: string;
} | null;
truncationAction?: "abort" | "retry" | "truncate" | "warn";
truncationSuffix?: string;
retryOnFailure?: boolean;
maxRetries?: number;
};
/** Input validation — validates input text before any processing. */
inputValidation?: {
trimWhitespace?: boolean;
minLength?: number;
maxLength?: number;
requireContent?: boolean;
};
/** @deprecated Use `piiDetection`, `responseValidation`, and `inputValidation` instead. */
processors?: ProcessorPipelineConfig;
/**
* Per-call skills control. Only effective when the instance was
* constructed with `skills.enabled: true`. Lets a call disable the
* prompt index, or narrow it by scope/tags. Per-call wins over
* instance config.
*/
skills?: SkillsCallOptions;
};
/**
* Stream function result type - Primary output format for streaming
* Future-ready for multi-modal outputs while maintaining text focus
*/
export type StreamResult = {
/** Knowledge-grounding diagnostics for this turn (present only when grounding ran). */
knowledge?: KnowledgeGroundingMetadata;
/** Terminal agent instructions version applied (present only with `agentMode`). */
agentModeVersion?: TerminalAgentModeVersion;
stream: AsyncIterable<{
content: string;
reasoning?: string;
} | StreamNoOutputSentinel | {
type: "audio";
audio: AudioChunk;
} | {
type: "tts_audio";
audio: TTSChunk;
} | {
type: "image";
imageOutput: {
base64: string;
};
} | {
content: string;
type?: "preliminary" | "final";
}>;
provider?: string;
model?: string;
usage?: TokenUsage;
finishReason?: string;
/**
* Why the agentic turn ended (see GenerateStopReason). For background-loop
* streams (the native Vertex paths and the native Anthropic stream path)
* prefer `metadata.stopReason` after draining the stream — this top-level
* field may be a getter that resolves late, and wrapper spreads can
* snapshot it before the loop finishes.
*/
stopReason?: GenerateStopReason;
/** Verbatim provider finish/stop reason for the turn's terminal model call. */
rawFinishReason?: string;
toolCalls?: StreamToolCall[];
toolResults?: StreamToolResult[];
toolEvents?: AsyncIterable<ToolExecutionEvent>;
toolExecutions?: ToolExecutionSummary[];
toolsUsed?: string[];
metadata?: {
streamId?: string;
startTime?: number;
totalChunks?: number;
estimatedDuration?: number;
responseTime?: number;
preliminaryTime?: number;
fallback?: boolean;
totalToolExecutions?: number;
toolExecutionTime?: number;
hasToolErrors?: boolean;
guardrailsBlocked?: boolean;
error?: string;
finishReason?: string;
stopReason?: GenerateStopReason;
rawFinishReason?: string;
stepsUsed?: number;
/**
* Parsed structured output for a `stream({ schema })` turn, available
* AFTER the stream has been drained.
*
* Lives on metadata for the same reason `finishReason` above does: it is a
* mutable reference the loop fills in, so result-object spreads in stream
* wrappers cannot snapshot it before it resolves. A plain field rather than
* a promise, deliberately — when a middleware short-circuits the request
* and the loop never runs, an unresolved promise would hang every reader,
* whereas an absent field is simply absent. Readers must tolerate that:
* absence means the model never produced the object.
*/
structuredData?: unknown;
/**
* Tokens spent by the tool-free re-ask that produced `structuredData`,
* when one was needed.
*
* That re-ask is a second, separately-billed model call, and the stream's
* own `usage` has already resolved by the time it runs — so folding these
* tokens into it would mutate a value a caller may have read, and leaving
* them out entirely would under-report what the turn cost. Reported here
* instead: same delivery as `structuredData`, filled at the same moment,
* read at the same moment. Absent when no re-ask was needed, which is the
* common case. The generate path accounts for its equivalent re-ask
* inline, since there the usage has not been handed out yet.
*/
structuredDataUsage?: {
inputTokens: number;
outputTokens: number;
};
thoughtSignature?: string;
thoughts?: Array<{
id?: string;
type?: string;
content?: string;
}>;
};
analytics?: AnalyticsData | Promise<AnalyticsData>;
evaluation?: EvaluationData | Promise<EvaluationData>;
events?: Array<{
type: string;
seq: number;
timestamp: number;
[key: string]: unknown;
}>;
workflow?: {
originalResponse: string;
processedResponse: string;
ensembleResponses: Array<{
provider: string;
model: string;
content: string;
responseTime: number;
status: "success" | "failure" | "timeout" | "partial";
error?: string;
}>;
judgeScores?: {
scores: Record<string, number>;
reasoning?: string;
selectedModel: string;
};
selectedModel: string;
metrics: {
totalTime: number;
ensembleTime: number;
judgeTime?: number;
conditioningTime?: number;
};
workflowId: string;
workflowName: string;
};
/** STT transcription result (when stt option is used) */
transcription?: STTResult;
/**
* Streaming TTS result (when `tts.enabled`). `stream()` synthesizes the AI
* response incrementally; `useAiResponse` continues to select input vs
* response synthesis for non-streaming generation.
* Resolves with the synthesized audio after the caller drains `stream` to
* completion; like other stream-final fields, it remains pending while the
* lazy stream is unconsumed. It resolves with the aggregate of whatever
* segments were synthesized: a synthesis failure part-way through still
* resolves with the earlier segments rather than discarding them. It resolves
* to undefined only when no segment was produced — TTS was not enabled, no
* handler resolved for the requested provider, the model stream errored, every
* synthesis failed, or the caller stopped draining `stream` before it ended
* (an abandoned stream settles undefined rather than a partial aggregate).
* Audio is also yielded incrementally as ordered
* `tts_audio` chunks. Each chunk, including the final one, contains only its
* own buffered segment. The aggregate is a byte concatenation of those
* independently synthesized segments, so what it is depends on the format's
* framing: for frame- or sample-stream formats (`mp3`, `mpeg`, `mpga`,
* `pcm16`) it is one playable stream; for header-bearing container formats
* (`wav`, `flac`, `m4a`, `mp4`, `webm`) it is not a valid file, because each
* segment carries its own header; for `ogg`/`opus` it is a chained stream that
* some decoders read only through its first segment. Use the individual chunk
* buffers when each segment must be a valid container file.
*/
audio?: Promise<TTSResult | undefined>;
/**
* Outcome metadata for streaming TTS synthesis. This is a mutable reference
* whose success and latency fields are finalized asynchronously; read it
* after draining `stream`.
*/
ttsMetadata?: TTSMetadata;
};
/**
* Enhanced provider type with stream method
*/
export type EnhancedStreamProvider = {
stream(options: StreamOptions): Promise<StreamResult>;
getName(): string;
isAvailable(): Promise<boolean>;
};
/**
* Stream text result from AI SDK (compatible with both v4 and v6)
*
* AI SDK v6 changed Promise → PromiseLike and renamed usage fields
* (promptTokens → inputTokens, completionTokens → outputTokens).
* This type accepts either shape so callers don't need casts.
*/
export type StreamTextResult = {
textStream: AsyncIterable<string>;
fullStream?: AsyncIterable<unknown>;
text: PromiseLike<string>;
usage: PromiseLike<AISDKUsage | undefined>;
response: PromiseLike<{
id?: string;
model?: string;
timestamp?: number | Date;
} | undefined>;
finishReason: PromiseLike<"stop" | "length" | "content-filter" | "tool-calls" | "error" | "other" | "unknown">;
/**
* Tool results. Accepts both NeuroLink StreamToolResult[] and AI SDK TypedToolResult[],
* since the analytics collector passes them through as `unknown` anyway.
*/
toolResults?: PromiseLike<StreamToolResult[] | ReadonlyArray<unknown>>;
/**
* Tool calls. Accepts both NeuroLink StreamToolCall[] and AI SDK TypedToolCall[].
*/
toolCalls?: PromiseLike<StreamToolCall[] | ReadonlyArray<unknown>>;
};
/**
* Raw usage data from Vercel AI SDK.
*
* Covers both v4 (promptTokens / completionTokens) and
* v6 (inputTokens / outputTokens) field names.
* extractTokenUsage() in tokenUtils.ts already handles both shapes.
*/
export type AISDKUsage = {
/** @deprecated AI SDK v4 name — use inputTokens */
promptTokens?: number;
/** @deprecated AI SDK v4 name — use outputTokens */
completionTokens?: number;
/** @deprecated AI SDK v4 name — use totalTokens */
totalTokens?: number;
/** AI SDK v6 name for prompt / input tokens */
inputTokens?: number;
/** AI SDK v6 name for completion / output tokens */
outputTokens?: number;
[key: string]: unknown;
};
/**
* Stream analytics collector type
*/
export type StreamAnalyticsCollector = {
collectUsage(result: StreamTextResult): Promise<TokenUsage>;
collectMetadata(result: StreamTextResult): Promise<ResponseMetadata>;
createAnalytics(provider: string, model: string, result: StreamTextResult, startTime: number, context?: Record<string, unknown>): Promise<AnalyticsData>;
};
/**
* Response metadata from stream
*/
export type ResponseMetadata = {
id?: string;
model?: string;
timestamp?: number | Date;
finishReason?: string;
};