@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
761 lines (760 loc) • 39 kB
TypeScript
import type { AIProviderName } from "../constants/enums.js";
import type { EvaluationData } from "../index.js";
import type { NeuroLink } from "../neurolink.js";
import type { FileToolRootPolicy, UnknownRecord, MiddlewareFactoryOptions, StreamOptions, StreamResult, AIProvider, AnalyticsData, EnhancedGenerateResult, TextGenerationOptions, TextGenerationResult, ValidationSchema, EmbedInput, DecisionRequest, DecisionResult } from "../types/index.js";
import { TelemetryHandler } from "./modules/TelemetryHandler.js";
import type { LanguageModel, ModelMessage, RawUsageObject, Tool, ToolCallRepairFunction, ToolSet } from "../types/index.js";
export declare abstract class BaseProvider implements AIProvider {
protected modelName: string;
protected readonly providerName: AIProviderName;
protected readonly defaultTimeout: number;
protected middlewareOptions?: MiddlewareFactoryOptions;
protected readonly directTools: {};
protected mcpTools?: Record<string, Tool>;
private fileToolRootsBound;
protected customTools?: Map<string, unknown>;
protected toolExecutor?: (toolName: string, params: unknown) => Promise<unknown>;
protected sessionId?: string;
protected userId?: string;
protected neurolink?: NeuroLink;
setTraceContext(ctx: {
traceId: string;
parentSpanId: string;
} | null): void;
private messageBuilder;
private streamHandler;
protected telemetryHandler: TelemetryHandler;
private utilities;
private readonly toolsManager;
constructor(modelName?: string, providerName?: AIProviderName, neurolink?: NeuroLink, middleware?: MiddlewareFactoryOptions);
/**
* Update modelName and rebuild composition handlers with the new value.
*
* Auto-discovery providers (lm-studio, llamacpp) call this once they have
* resolved the loaded model from `/v1/models`. Without this, handlers
* (TelemetryHandler, MessageBuilder, ...) keep the pre-discovery name and
* pricing / span / log metadata reports the stale value.
*/
protected refreshHandlersForModel(model: string): void;
/**
* Check if this provider supports tool/function calling
* Override in subclasses to disable tools for specific providers or models
* @returns the current model's registered capability, or true when unknown
*/
supportsTools(): boolean;
/**
* Whether this provider implements the opt-in `executionControl` contract.
*
* Default false, and that default is load-bearing: a provider that has not
* implemented the contract must REJECT it, not ignore it. Silently dropping
* a caller-set execution policy is invisible until a long turn dies at a
* ceiling its owner believed had been removed. Overridden only where the
* control is genuinely honoured end to end.
*/
supportsExecutionControl(): boolean;
/**
* Apply the shared tool gate and optionally report registry-backed
* suppression at the request entry point.
*/
private shouldUseTools;
/**
* Primary streaming method - implements AIProvider interface
* When tools are involved, falls back to generate() with synthetic streaming
*/
stream(optionsOrPrompt: StreamOptions | string, analysisSchema?: ValidationSchema): Promise<StreamResult>;
/**
* Swap in a fallback model when a stream dies on a retired id before it has
* produced anything.
*
* OpenAI-compatible streaming is lazy: executeStream() returns a
* StreamResult without touching the network, and the request only goes out
* on the consumer's first pull. A retired model therefore does NOT fail in
* stream()'s try/catch — it fails deep inside iteration. This wrapper sits
* UNDER wrapStreamWithLifecycleCallbacks precisely so that a failed first
* attempt is invisible: onChunk, onFinish and onError all belong to the
* layer above and never observe it.
*
* The `yielded` guards are the safety property. A stream is only retried
* while it has emitted nothing; once a single chunk has reached the
* consumer the output is committed, and any later failure propagates
* untouched rather than replaying a half-delivered response.
*/
private withStreamModelFallback;
/**
* The eager half of the retired-model stream fallback, for providers whose
* executeStream() reaches the network before returning (see
* runGenerateWithModelFallback for the generate() half and the reasoning).
*
* Returns a working StreamResult when a fallback model succeeds, or
* undefined to mean "not recoverable — carry on with the original error".
* Returning rather than throwing is deliberate: every existing path in the
* caller's catch (the broad fake-streaming fallback, the terminal-error
* re-throw, the onError firing) is left exactly as it was, so behaviour
* changes only when a fallback actually succeeds.
*/
private retryStreamWithFallbackModel;
/**
* Wrap a StreamResult with consumer-facing lifecycle callbacks.
*
* `options.onChunk`, `options.onFinish`, `options.onError` are translated
* by NeuroLink.applyStreamLifecycleMiddleware() into
* `options.middleware.middlewareConfig.lifecycle.config`. The AI SDK's
* lifecycle middleware only sees these via the wrapped LanguageModel —
* which is bypassed by providers that stream via raw HTTP fetch (Ollama
* over /api/chat, custom OpenAI-compatible servers, etc). Wrapping the
* user-facing stream here ensures the callbacks fire regardless of the
* underlying transport.
*
* `teardownController`, when supplied, is the call's own abort source (see
* `stream()`): the cancel hook fires it on abandonment so the transport's
* in-flight HTTP request is cancelled, not just this iterator chain.
*/
private wrapStreamWithLifecycleCallbacks;
/**
* Fire the consumer-supplied onError callback before throwing. Used in
* error branches inside stream() that re-throw without emitting any
* stream chunks (which would otherwise hide the failure from a caller
* that supplied `onError`).
*/
private fireLifecycleErrorCallback;
/**
* Build the fake-stream output and apply the same incremental TTS wrapper
* used by the standard NeuroLink stream path.
*/
private createFakeStreamingOutput;
/**
* Execute fake streaming - extracted method for reusability
*/
private executeFakeStreaming;
/**
* Apply per-call tool filtering (whitelist/blacklist) to a tools record.
*
* All filtering surfaces are merged into one ResolvedToolPolicy by
* `resolveToolPolicy()` — per-call `toolFilter` (whitelist),
* `enabledToolNames` (merged into the whitelist, as its docs always
* promised), `excludeTools` (denylist, applied after the whitelist), and
* the instance-level `tools` config (enabled/include/exclude, `*` globs) —
* then applied by `applyToolGate()`. This is the single filter semantics
* for every generate/stream path.
*/
private getToolPolicy;
private applyToolFiltering;
/**
* Deterministic name-sorted key order. External MCP servers connect and
* discover in parallel, so insertion order varies across process restarts;
* providers serialize this record in key order (and Anthropic pins its
* cache_control breakpoint to the LAST tool), so an unstable order silently
* busts provider prompt caches. Runs AFTER the dedup pass — dedup's
* keep-first policy must see phase order (built-ins first) so duplicate
* winners don't flip when an MCP tool name sorts earlier.
*/
private sortToolRecord;
/**
* On-demand discovery (`tools.discovery: true`): defer external MCP tool
* schemas behind one `search_tools` meta-tool. Built-in tools, per-call
* tools, explicitly whitelisted tools, and session-pinned (previously
* discovered) tools always stay hot. No-op when discovery is off — with a
* one-time WARN when the catalog is large enough that selection accuracy
* measurably degrades.
*/
private applyToolDiscovery;
private static warnedLargeCatalog;
private warnLargeCatalogOnce;
/**
* Opt-in signature dedup — runs AFTER whitelist/blacklist filtering and
* BEFORE the tool set reaches the provider call. Fails open: any error
* inside dedupeTools returns the original filtered set unchanged.
*/
private applyDedupPass;
/**
* Prepare generation context including tools and model
*/
private prepareGenerationContext;
/**
* Get merged tools for streaming: combines base tools (MCP/built-in) with
* user-provided tools (e.g., RAG tools passed via options.tools).
*
* This is the canonical tool-merge pattern for executeStream() implementations.
* All providers should call this instead of getAllTools() directly.
*/
protected getToolsForStream(options: StreamOptions | TextGenerationOptions): Promise<Record<string, Tool>>;
/**
* Create (or reuse) the per-call ToolExecutionRecorder and wrap the final
* tool record with it. The recorder rides on the options object so provider
* loops and result assembly observe the same capture state; wrapping is
* idempotent, so paths that re-enter (stream→generate fallback) never
* double-record.
*/
protected wrapToolsForExecutionCapture(tools: Record<string, Tool>, options: StreamOptions | TextGenerationOptions): Record<string, Tool>;
/**
* Build messages array for generation - delegated to MessageBuilder
*/
private buildMessages;
/**
* Build messages array for streaming operations - delegated to MessageBuilder
* This is a protected helper method that providers can use to build messages
* with automatic multimodal detection, eliminating code duplication
*
* @param options - Stream options or text generation options
* @returns Promise resolving to ModelMessage array ready for AI SDK
*/
protected buildMessagesForStream(options: StreamOptions | TextGenerationOptions): Promise<ModelMessage[]>;
/**
* Record performance metrics - delegated to TelemetryHandler
*/
protected recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
/**
* Text generation method - implements AIProvider interface
* Tools are always available unless explicitly disabled
*
* Supports Text-to-Speech (TTS) audio generation in two modes:
* 1. Direct synthesis (default): TTS synthesizes the input text without AI generation
* 2. AI response synthesis: TTS synthesizes the AI-generated response after generation
*
* When TTS is enabled with useAiResponse=false (default), the method returns early with
* only the audio result, skipping AI generation entirely for optimal performance.
*
* When TTS is enabled with useAiResponse=true, the method performs full AI generation
* and then synthesizes the AI response to audio.
*
* @param optionsOrPrompt - Generation options or prompt string
* @param _analysisSchema - Optional analysis schema (not used)
* @returns Enhanced result with optional audio field containing TTSResult
*
* IMPLEMENTATION NOTE: Uses streamText() under the hood and accumulates results
* for consistency and better performance
*/
/**
* Ensure runtime-discovered model limits (context window, output-token
* ceiling) are registered before any budget math runs. Generation
* pipelines await this BEFORE `checkContextBudget`, and generate()/stream()
* await it before options normalization so `getSafeMaxTokens` sees the
* discovered output ceiling.
*
* Default no-op. Providers with a runtime discovery source override it
* (e.g. LiteLLM's `/model/info`). Implementations must NEVER reject —
* discovery failure degrades to the static defaults.
*/
ensureModelLimits(): Promise<void>;
generate(optionsOrPrompt: TextGenerationOptions | string, _analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
/**
* Models the catalog names are the models a vendor served when the entry was
* last verified. Vendors retire them without warning, and until this existed
* the runtime had no answer for that: an InvalidModelError is classified
* non-retryable (correctly — another *provider* cannot fix a bad model id),
* so the fallback chain stopped dead and the caller got an error even though
* the same provider was still serving four other models listed right there
* in the catalog's `fallbacks`.
*
* So on an invalid-model error only, walk this provider's fallbacks. Every
* switch is a loud WARN: the caller asked for one model and is getting
* another, which they must be able to see in the logs. Any other error type
* propagates untouched on the first attempt.
*
* stream() gets the same treatment via retryStreamWithFallbackModel, which
* is safe for the same reason it is cheap: the retry happens before any
* lifecycle callback has fired and before a chunk has reached the consumer,
* so there is no observable output to replay.
*/
/**
* Protected rather than private so a provider with a native generate path can
* reuse it. Overriding `generate()` otherwise skips this wrapper silently,
* and a retired default model stops degrading to the next live one in the
* catalog — which is exactly what the provider contract gate checks.
*/
protected runGenerateWithModelFallback(attempt: () => Promise<EnhancedGenerateResult | null>, callerOwnsFallback: boolean, options?: TextGenerationOptions): Promise<EnhancedGenerateResult | null>;
/**
* Model ids to try when this provider rejects its current model as invalid.
* Empty by default; catalog-driven providers return their `fallbacks`.
*/
protected getModelFallbacks(): string[];
/**
* The fallbacks an invalid-model retry may try for this request. A catalog
* orders `fallbacks` for text, so a request carrying an image keeps only the
* vision-capable ones: a text-only candidate fails the vision guard, and
* that error names the fallback instead of the retired model the caller
* asked for. With no vision-capable candidate left, the caller gets the
* original invalid-model error.
*/
protected modelFallbacksFor(options: TextGenerationOptions | StreamOptions, requestedModel: string): string[];
/**
* Alias for generate method - implements AIProvider interface
*/
gen(optionsOrPrompt: TextGenerationOptions | string, analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
private runGenerateInActiveContext;
protected handleDirectTTSSynthesis(options: TextGenerationOptions, startTime: number): Promise<EnhancedGenerateResult>;
private handleVideoFrameGeneration;
/**
* Close out a turn produced by a provider's own native generate loop.
*
* The native loops bypass `executeGeneration`, and with it every post-call
* step the standard path runs. Each one that was missed had to be found
* separately — `onFinish` stopped firing, TTS synthesis silently produced no
* audio, and OTEL saw no usage for any native provider. Routing all of them
* through one method is what stops the next native path from rediscovering
* the same list.
*
* Order matters and mirrors the standard path: metrics are recorded before
* synthesis so telemetry sees the model's own usage, and `enhanceResult`
* runs last so analytics and evaluation observe the final content.
*/
protected finalizeNativeGenerate(result: EnhancedGenerateResult, options: TextGenerationOptions, startTime: number): Promise<EnhancedGenerateResult>;
/**
* Invoke a caller's `onFinish` for a native turn.
*
* On the standard path `onFinish` is converted into lifecycle middleware and
* applied by wrapping the model — a step the native loops skip, so they have
* to call it themselves. Failures are logged and swallowed: a caller's
* callback must not be able to fail their generation.
*/
protected fireGenerateOnFinish(options: TextGenerationOptions, result: EnhancedGenerateResult | null, startTime: number): void;
protected synthesizeAIResponseIfNeeded(enhancedResult: EnhancedGenerateResult, options: TextGenerationOptions): Promise<EnhancedGenerateResult>;
/**
* Build the public TTS failure detail.
*
* The message is redacted through the shared `sanitizeErrorCause` before it
* leaves this method. It used to be an internal value that only reached the
* logger; it is now carried on `result.ttsMetadata` and therefore reaches
* SDK callers, who may forward it to an end user. Provider errors quote the
* request URL often enough that a key in a query string is a real vector —
* `redactUrlsInText` strips exactly that while leaving a bare URL readable,
* so the diagnosis survives and the credential does not.
*/
private getTTSErrorDetails;
/**
* BACKWARD COMPATIBILITY: Legacy generateText method
* Converts EnhancedGenerateResult to TextGenerationResult format
* Ensures existing scripts using createAIProvider().generateText() continue to work
*/
generateText(options: TextGenerationOptions): Promise<TextGenerationResult>;
/**
* Generate embeddings for text
*
* This is a default implementation that throws an error.
* Providers that support embeddings (OpenAI, Google Vertex, Amazon Bedrock)
* should override this method with their specific implementation.
*
* @param input - Text string or EmbedInput with text/image/mimeType
* @param _modelName - Optional embedding model name (provider-specific)
* @returns Promise resolving to the embedding vector (array of numbers)
* @throws Error if the provider does not support embeddings
*
* @example
* ```typescript
* const provider = await ProviderFactory.createProvider('openai', 'text-embedding-3-small');
* const embedding = await provider.embed('Hello world');
* console.log(embedding); // [0.123, -0.456, ...]
*
* // Multi-modal embedding (Bedrock Titan Image / Nova)
* const multiEmbedding = await provider.embed({ text: "a photo", image: imageBuffer, mimeType: "image/png" });
* ```
*/
embed(input: string | EmbedInput, _modelName?: string): Promise<number[]>;
/**
* Generate embeddings for multiple texts in a single batch
*
* This is a default implementation that throws an error.
* Providers that support embeddings should override this method.
* The AI SDK's embedMany automatically handles chunking for models with batch limits.
*
* @param texts - The texts to embed
* @param _modelName - Optional embedding model name (provider-specific)
* @returns Promise resolving to an array of embedding vectors
* @throws Error if the provider does not support embeddings
*/
embedMany(texts: string[], _modelName?: string): Promise<number[][]>;
/**
* Get the default embedding model for this provider
*
* Override in subclasses to provide provider-specific defaults.
* Returns undefined for providers that don't support embeddings.
*
* @returns The default embedding model name, or undefined if not supported
*/
protected getDefaultEmbeddingModel(): string | undefined;
/**
* Evaluate a state against a batch of typed questions.
*
* Default implementation throws, exactly as `embed()` does: `decide` is an
* optional capability, declared per provider by `"decide"` in its
* descriptor's `inferenceKinds`. Callers that want a silent no-op on an
* unsupporting provider should check the descriptor first rather than
* catching this.
*
* Latency on a decision model is flat in question count while concurrent
* requests queue, so this method is deliberately batch-shaped: one call
* carries every question a caller needs. There is no single-question
* convenience wrapper, because it would invite fanning out.
*
* @throws Error if the provider does not serve the `decide` inference type
*/
decide(_request: DecisionRequest): Promise<DecisionResult>;
/**
* Get the default decision model for this provider.
*
* Mirrors {@link getDefaultEmbeddingModel}. Returns undefined for providers
* that do not serve the `decide` inference type.
*/
protected getDefaultDecisionModel(): string | undefined;
/**
* Create an `experimental_repairToolCall` handler for streamText/generateText.
* Dynamically reads the tool's JSON schema to repair wrong names and params.
* Returns undefined when repair is disabled via options.
*/
protected getToolCallRepairFn(options?: StreamOptions | TextGenerationOptions): ToolCallRepairFunction<ToolSet> | undefined;
/**
* Opt-in streaming hook. A provider that produces an async iterable of text
* chunks plus promises for how the turn ended implements this and gets a
* working `executeStream` for free.
*
* Deliberately optional rather than abstract: every provider that already
* overrides `executeStream` directly — the older and still perfectly valid
* pattern — never needs it.
*
* `finishReason` and `usage` are promises because both are only knowable
* once the underlying stream has finished. Resolve them when it does; the
* default `executeStream` chains off them rather than waiting for a
* consumer, so they must settle even if nobody drains the stream.
*/
protected doStream?(options: StreamOptions): Promise<{
stream: AsyncIterable<{
content: string;
}>;
finishReason: Promise<string>;
usage: Promise<{
inputTokens: number;
outputTokens: number;
}>;
warnings?: string[];
}>;
/**
* Provider-specific streaming implementation (only used when tools are
* disabled).
*
* This used to be `protected abstract`, which meant a provider had exactly
* two options: write the whole adapter by hand, or not stream at all. The
* failure mode that produced was SageMaker's — a complete, working
* `doStream` sitting one property access away from an `executeStream` that
* unconditionally threw "not yet fully implemented". Providers that
* implement `doStream` now inherit a correct implementation, and providers
* that override this method are unaffected.
*/
protected executeStream(options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
/**
* Get the provider name
*/
protected abstract getProviderName(): AIProviderName;
/**
* Get the default model for this provider
*/
protected abstract getDefaultModel(): string;
/**
* REQUIRED: Every provider MUST implement this method
* Returns the Vercel AI SDK model instance for this provider
*/
/**
* Run one whole turn under the caller's timeout contract.
*
* Every generate path must go through this, including the native loops that
* bypass `executeGeneration`. When the Anthropic native path was first added
* it built its own call and never composed a timer, so `timeout: 1000`
* against a three-second upstream simply returned the late response — the
* turn budget silently stopped existing for that provider.
*
* An explicit, valid `turnTimeoutMs` is the caller's whole-turn contract and
* owns this hard abort; `timeout` then keeps its per-model-call meaning (it
* reaches the model layer via `providerOptions.neurolink`). Before that
* split, `timeout` alone bounded the ENTIRE multi-step loop, so a caller
* asking for a 40-minute turn of 5-minute calls was killed at 5 minutes
* flat — mid-loop, dressed as "Request was aborted.".
*
* `descriptorGenerateMs` only ever RAISES the 3-minute floor, never lowers
* it: several descriptors carry aspirational sub-180s numbers (openai 30s,
* bedrock 45s) that were never enforced, and enforcing them now would break
* long-running generations that have always been allowed.
*/
protected withTurnTimeout<T>(options: TextGenerationOptions, descriptorGenerateMs: number | undefined, run: (timedOptions: TextGenerationOptions) => Promise<T>): Promise<T>;
/**
* Emit a `turn:lifecycle` event on the SDK emitter (alongside
* tool:start/tool:end) so loop conditions that previously only reached
* process logs — step-cap, time-limit, stall, abort, tool timeouts,
* malformed-call retries — are observable by consumers' own pipelines.
*
* Lives here rather than on one provider because every native agentic loop
* has the same conditions to report, and a second copy is how two loops
* come to emit subtly different payloads for the same event name.
*/
protected emitTurnEvent(payload: {
phase: "step-cap" | "context-cap" | "time-limit" | "stalled" | "aborted" | "provider-error" | "tool-timeout" | "malformed-retry";
step?: number;
maxSteps?: number;
toolName?: string;
toolCallCount?: number;
elapsedMs?: number;
}): void;
/**
* The per-provider generate budget from the descriptor, for native paths
* that call `withTurnTimeout` directly.
*/
protected getDescriptorGenerateMs(): number | undefined;
/** Per-provider `decide` budget from the descriptor, when one is declared. */
protected getDescriptorDecideMs(): number | undefined;
protected abstract getAISDKModel(): LanguageModel | Promise<LanguageModel>;
/**
* Public handle on this provider's model object.
*
* `getAISDKModel()` is protected because only the generation pipeline should
* drive it. The browser bundle needs the same handle to back its public
* provider factories without reaching into provider internals, so this is the
* one sanctioned way out. The union return lets providers that resolve their
* model synchronously (Anthropic) and asynchronously (the OpenAI-compatible
* family) both satisfy it.
*/
getModel(): LanguageModel | Promise<LanguageModel>;
/**
* Get AI SDK model with middleware applied
* This method wraps the base model with any configured middleware
* TODO(#1576): Implement global level middlewares that can be used
*/
protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
/**
* Apply the configured middleware chain to a caller-supplied base model.
*
* `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
* the model the non-streaming path drives. Streaming paths build a
* different base — one whose `doStream` starts the provider's own stream
* loop — and need the same chain applied to it, so the wrapping is split
* out here rather than duplicated per provider.
*/
protected applyMiddlewareToModel(baseModel: LanguageModel, options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
/**
* Extract middleware options - delegated to Utilities
*/
private extractMiddlewareOptions;
/**
* Check if a schema is a Zod schema - delegated to Utilities
*/
private isZodSchema;
/**
* Convert tool execution result - delegated to Utilities
*/
private convertToolResult;
/**
* Fix JSON Schema for OpenAI strict mode - delegated to Utilities
*/
private fixSchemaForOpenAIStrictMode;
/**
* Get all available tools - delegated to ToolsManager
*/
/**
* Bind the built-in file tools to one request's root policy. NeuroLink
* calls this before every provider call that carries the caller's options,
* because several providers override generate()/stream() and assemble
* their tools without passing through BaseProvider's entry points.
* Providers are created per request, so a bound policy never reaches
* another request.
*/
setFileToolRootPolicy(policy: FileToolRootPolicy | undefined): void;
/**
* Resolve roots for a provider called directly, without NeuroLink. A
* policy NeuroLink already bound is left alone unless these options carry
* their own. An invalid root throws, before any model request.
*/
private applyFileToolRoots;
protected getAllTools(): Promise<Record<string, Tool>>;
/**
* Calculate actual cost - delegated to TelemetryHandler
*/
private calculateActualCost;
/**
* Create a permissive Zod schema - delegated to Utilities
*/
private createPermissiveZodSchema;
/**
* Set session context for MCP tools - delegated to ToolsManager
*/
setSessionContext(sessionId?: string, userId?: string): void;
/**
* Provider-specific error formatting.
* Subclasses implement this to produce human-readable error messages
* (e.g., "❌ Google Vertex AI Provider Error\n\n...").
*/
protected abstract formatProviderError(error: unknown): Error;
/**
* Handle provider errors with abort passthrough.
* AbortErrors are never wrapped — they must propagate with their
* original identity so that isAbortError() can detect them in
* retry/fallback loops (directProviderGeneration, performMCPGenerationRetries).
*/
/**
* Classify an error that escaped while the consumer was ITERATING a stream.
*
* `stream()` only awaits the CONSTRUCTION of the provider's stream object, and a provider
* that discovers its failure lazily throws on first pull instead — so the raw upstream
* error reached the consumer with no provider tag and no classification, while the same
* failure through `generate()` was classified normally. Measured on OpenAI:
*
* streaming "You have no credits remaining."
* non-streaming "[openai] OpenAI quota exhausted — this will not resolve by retrying..."
*
* Three guards. Only the third is load-bearing against today's code; the first two are
* deliberate depth against a hazard that is real but not currently reachable. Measured,
* rather than assumed — see the note after the list.
*
* 1. ALREADY STAMPED — everything that went through `handleProviderError` carries the mark,
* including the two formatters whose results escape the ProviderError hierarchy
* (`amazonSagemaker` returns SageMakerError, `replicate` returns NeuroLinkError; both
* extend Error directly). Without this they would be classified a second time and
* degraded.
* 2. ALREADY A ProviderError — covers providers that call `formatProviderError` DIRECTLY,
* bypassing `handleProviderError` and therefore the stamp. Anthropic does this in its
* own streaming catch (`anthropic/client.ts`), and its result is a ProviderError.
* 3. HAS AN HTTP STATUS — without this, an ordinary bug is relabelled as a provider
* failure. `classifyProviderError` ends in an unconditional catch-all
* (`utils/errorClassifier.ts`: `if (!rule) return new ProviderError(...)`) that is not
* gated on the error having come off the wire. Measured with the guard absent:
* TypeError "Cannot read properties of undefined (reading 'content')"
* became ProviderError "[openai] openai error: Cannot read properties of undefined..."
* which would hide a real defect behind a plausible provider message.
*
* WHY 1 AND 2 ARE NOT CURRENTLY REACHABLE, and why they stay anyway. A statusCode is
* attached to a formatted error in exactly one place — `handleProviderError` below — and
* that same method applies the stamp. So a ProviderError carrying a status is always
* stamped (caught by guard 1's own condition), and a ProviderError produced by a direct
* `formatProviderError` call carries no status, so guard 3 already returns it untouched.
* Verified by disabling guards 1 and 2 together, rebuilding, and re-running: the mocked
* provider contract suite stayed 64/64 and a mocked Anthropic streaming 429 was
* byte-identical (`RateLimitError`, one `[anthropic]` prefix).
*
* They remain because the hazard they cover is measured and real: `handleProviderError`
* is NOT idempotent — it copies statusCode onto its own output, so a second pass
* re-matches the bare 429 rule and DEGRADES the classification:
* pass 1 ProviderError "...quota exhausted — this will not resolve by retrying..."
* pass 2 RateLimitError "...rate limit exceeded..."
* The day any provider attaches a status to an error it formats itself, guard 3 stops
* covering that case and this degradation becomes live. Cheap insurance, not dead code —
* but do not cite guards 1 and 2 as proven-by-failure the way guard 3 is.
*/
protected classifyStreamError(error: unknown): Error;
protected handleProviderError(error: unknown): Error;
/**
* Image generation method. Providers that support it should override this.
* By default, it throws an error indicating that the functionality is not supported.
* @param _options The generation options.
* @returns A promise that resolves to the generation result.
*/
protected executeImageGeneration(_options: TextGenerationOptions): Promise<EnhancedGenerateResult>;
/**
* Execute operation with timeout and proper cleanup
* Consolidates identical timeout handling from 8/10 providers
*/
protected executeWithTimeout<T>(operation: () => Promise<T>, options: {
timeout?: number | string;
operationType?: string;
}): Promise<T>;
/**
* Validate stream options - delegated to StreamHandler
*/
protected validateStreamOptions(options: StreamOptions): void;
/**
* Create text stream transformation - delegated to StreamHandler.
* Reviewer follow-up: forwards the optional `getUnderlyingError`
* callback so providers can capture upstream errors via
* `streamText`'s `onError` and have them flow into the
* NoOutputGeneratedError sentinel's `providerError` /
* `modelResponseRaw`.
*/
protected createTextStream(result: {
textStream: AsyncIterable<string>;
finishReason?: Promise<unknown> | unknown;
totalUsage?: Promise<unknown> | unknown;
}, getUnderlyingError?: () => unknown): AsyncGenerator<{
content: string;
} | import("../types/index.js").StreamNoOutputSentinel>;
/**
* Create standardized stream result - delegated to StreamHandler
*/
protected createStreamResult(stream: AsyncGenerator<{
content: string;
}>, additionalProps?: Partial<StreamResult>): StreamResult;
/**
* Create stream analytics - delegated to StreamHandler
*/
protected createStreamAnalytics(result: UnknownRecord, startTime: number, options: StreamOptions): Promise<UnknownRecord | undefined>;
/**
* Handle common error patterns - delegated to Utilities
*/
protected handleCommonErrors(error: unknown): Error | null;
/**
* Set up tool executor - delegated to ToolsManager
* @param sdk - The NeuroLinkSDK instance for tool execution
* @param functionTag - Function name for logging
*/
setupToolExecutor(sdk: {
customTools: Map<string, unknown>;
executeTool: (toolName: string, params: unknown) => Promise<unknown>;
}, functionTag: string): void;
/**
* Normalize text generation options - delegated to Utilities
*/
protected normalizeTextOptions(optionsOrPrompt: TextGenerationOptions | string): TextGenerationOptions;
/**
* Normalize stream options - delegated to Utilities
*/
protected normalizeStreamOptions(optionsOrPrompt: StreamOptions | string): StreamOptions;
protected enhanceResult(result: EnhancedGenerateResult, options: TextGenerationOptions, startTime: number): Promise<EnhancedGenerateResult>;
/**
* Handle video generation mode
*
* Generates video from input image + text prompt using Vertex AI Veo 3.1.
*
* @param options - Text generation options with video configuration
* @param startTime - Generation start timestamp for metrics
* @returns Enhanced result with video data
*
* @example
* ```typescript
* const result = await provider.generate({
* input: { text: "Product showcase", images: [imageBuffer] },
* output: { mode: "video", video: { resolution: "1080p" } }
* });
* // result.video contains the generated video
* ```
*/
protected handleVideoGeneration(options: TextGenerationOptions, startTime: number): Promise<EnhancedGenerateResult>;
/**
* Create analytics - delegated to TelemetryHandler
*/
protected createAnalytics(result: EnhancedGenerateResult, responseTime: number, options: TextGenerationOptions): Promise<AnalyticsData>;
/**
* Create evaluation - delegated to TelemetryHandler
*/
protected createEvaluation(result: EnhancedGenerateResult, options: TextGenerationOptions): Promise<EvaluationData>;
/**
* Validate text generation options - delegated to Utilities
*/
protected validateOptions(options: TextGenerationOptions): void;
/**
* Get provider information - delegated to Utilities
*/
protected getProviderInfo(): {
provider: string;
model: string;
};
/**
* Get timeout value in milliseconds - delegated to Utilities
*/
getTimeout(options: TextGenerationOptions | StreamOptions): number;
/**
* Check if tool executions should be stored and handle storage
*/
protected handleToolExecutionStorage(toolCalls: unknown[], toolResults: unknown[], options: TextGenerationOptions | StreamOptions, currentTime: Date): Promise<void>;
/**
* Utility method to chunk large prompts into smaller pieces
* @param prompt The prompt to chunk
* @param maxChunkSize Maximum size per chunk (default: 900,000 characters)
* @param overlap Overlap between chunks to maintain context (default: 100 characters)
* @returns Array of prompt chunks
*/
static chunkPrompt(prompt: string, maxChunkSize?: number, overlap?: number): string[];
}