@llumiverse/core
Version:
Provide an universal API to LLMs. Support for existing LLMs can be added by writing a driver.
300 lines • 12.2 kB
JavaScript
;
/**
* Classes to handle the execution of an interaction in an execution environment.
* Base abstract class is then implemented by each environment
* (eg: OpenAI, HuggingFace, etc.)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbstractDriver = void 0;
exports.createLogger = createLogger;
const common_1 = require("@llumiverse/common");
const CompletionStream_js_1 = require("./CompletionStream.js");
const index_js_1 = require("./formatters/index.js");
const validation_js_1 = require("./validation.js");
// Helper to create logger methods that support both message-only and object-first signatures
function createConsoleLoggerMethod(consoleMethod) {
return ((objOrMsg, msgOrNever, ...args) => {
if (typeof objOrMsg === 'string') {
// Message-only: logger.info("message", ...args)
consoleMethod(objOrMsg, msgOrNever, ...args);
}
else if (msgOrNever !== undefined) {
// Object-first: logger.info({ obj }, "message", ...args)
consoleMethod(msgOrNever, objOrMsg, ...args);
}
else {
// Object-only: logger.info({ obj })
consoleMethod(objOrMsg, ...args);
}
});
}
const ConsoleLogger = {
debug: createConsoleLoggerMethod(console.debug.bind(console)),
info: createConsoleLoggerMethod(console.info.bind(console)),
warn: createConsoleLoggerMethod(console.warn.bind(console)),
error: createConsoleLoggerMethod(console.error.bind(console)),
};
const noop = () => void 0;
const NoopLogger = {
debug: noop,
info: noop,
warn: noop,
error: noop,
};
function createLogger(logger) {
if (logger === "console") {
return ConsoleLogger;
}
else if (logger) {
return logger;
}
else {
return NoopLogger;
}
}
/**
* To be implemented by each driver
*/
class AbstractDriver {
options;
logger;
constructor(opts) {
this.options = opts;
this.logger = createLogger(opts.logger);
}
async createTrainingPrompt(options) {
const prompt = await this.createPrompt(options.segments, { result_schema: options.schema, model: options.model });
return JSON.stringify({
prompt,
completion: typeof options.completion === 'string' ? options.completion : JSON.stringify(options.completion)
});
}
startTraining(_dataset, _options) {
throw new Error("Method not implemented.");
}
cancelTraining(_jobId) {
throw new Error("Method not implemented.");
}
getTrainingJob(_jobId) {
throw new Error("Method not implemented.");
}
validateResult(result, options) {
if (!result.tool_use && !result.error && options.result_schema) {
try {
result.result = (0, validation_js_1.validateResult)(result.result, options.result_schema);
}
catch (error) {
const errorMessage = `[${this.provider}] [${options.model}] ${error.code ? '[' + error.code + '] ' : ''}Result validation error: ${error.message}`;
this.logger.error({ err: error, data: result.result }, errorMessage);
result.error = {
code: error.code || error.name,
message: error.message,
data: result.result,
};
}
}
}
async execute(segments, options) {
const prompt = await this.createPrompt(segments, options);
return this._execute(prompt, options).catch((error) => {
// Don't wrap if already a LlumiverseError
if (common_1.LlumiverseError.isLlumiverseError(error)) {
throw error;
}
throw this.formatLlumiverseError(error, {
provider: this.provider,
model: options.model,
operation: 'execute',
});
});
}
async _execute(prompt, options) {
try {
const start = Date.now();
let result;
if (this.isImageModel(options.model)) {
this.logger.debug(`[${this.provider}] Executing prompt on ${options.model}, image pathway.`);
result = await this.requestImageGeneration(prompt, options);
}
else {
this.logger.debug(`[${this.provider}] Executing prompt on ${options.model}, text pathway.`);
result = await this.requestTextCompletion(prompt, options);
this.validateResult(result, options);
}
const execution_time = Date.now() - start;
return { ...result, prompt, execution_time };
}
catch (error) {
// Don't wrap if already a LlumiverseError
if (common_1.LlumiverseError.isLlumiverseError(error)) {
throw error;
}
// Log the original error for debugging
this.logger.error({ err: error, data: { provider: this.provider, model: options.model, operation: 'execute', prompt } }, `Error during execution in provider ${this.provider}:`);
throw this.formatLlumiverseError(error, {
provider: this.provider,
model: options.model,
operation: 'execute',
});
}
}
isImageModel(_model) {
return false;
}
// by default no stream is supported. we block and we return all at once
async stream(segments, options) {
this.logger.debug(options, `Executing prompt with provider ${this.provider} with options: ${JSON.stringify(options)}`);
const prompt = await this.createPrompt(segments, options);
const canStream = await this.canStream(options);
if (canStream) {
return new CompletionStream_js_1.DefaultCompletionStream(this, prompt, options);
}
else if (this.isImageModel(options.model)) {
return new CompletionStream_js_1.FallbackCompletionStream(this, prompt, options);
}
else {
return new CompletionStream_js_1.FallbackCompletionStream(this, prompt, options);
}
}
/**
* Override this method to provide a custom prompt formatter
* @param segments
* @param options
* @returns
*/
async formatPrompt(segments, opts) {
return (0, index_js_1.formatTextPrompt)(segments, opts.result_schema);
}
async createPrompt(segments, opts) {
return await (opts.format ? opts.format(segments, opts.result_schema) : this.formatPrompt(segments, opts));
}
/**
* Must be overridden if the implementation cannot stream.
* Some implementation may be able to stream for certain models but not for others.
* You must overwrite and return false if the current model doesn't support streaming.
* The default implementation returns true, so it is assumed that the streaming can be done.
* If this method returns false then the streaming execution will fallback on a blocking execution streaming the entire response as a single event.
* @param options the execution options containing the target model name.
* @returns true if the execution can be streamed false otherwise.
*/
canStream(_options) {
return Promise.resolve(true);
}
/**
* Get a list of models that can be trained.
* The default is to return an empty array
* @returns
*/
async listTrainableModels() {
return [];
}
/**
* Build the conversation context after streaming completion.
* Override this in driver implementations that support multi-turn conversations.
*
* @param prompt - The prompt that was sent (includes prior conversation context)
* @param result - The completion results from the streamed response
* @param toolUse - The tool calls from the streamed response (if any)
* @param options - The execution options
* @returns The updated conversation context, or undefined if not supported
*/
buildStreamingConversation(_prompt, _result, _toolUse, _options) {
// Default implementation returns undefined - drivers can override
return undefined;
}
/**
* Format an error into LlumiverseError. Override in driver implementations
* to provide provider-specific error parsing.
*
* The default implementation uses common patterns:
* - Status 429, 408: retryable (rate limit, timeout)
* - Status 529: retryable (overloaded)
* - Status 5xx: retryable (server errors)
* - Status 4xx (except above): not retryable (client errors)
* - Error messages containing "rate limit", "timeout", etc.: retryable
*
* @param error - The error to format
* @param context - Context about where the error occurred
* @returns A standardized LlumiverseError
*/
formatLlumiverseError(error, context) {
// Extract status code from common locations (only if numeric)
let code;
const rawCode = error?.status
|| error?.statusCode
|| error?.code;
if (typeof rawCode === 'number') {
code = rawCode;
}
// Extract error name if available
const errorName = error?.name;
// Extract message
const message = error instanceof Error
? error.message
: String(error);
// Determine retryability
const retryable = this.isRetryableError(code, message);
return new common_1.LlumiverseError(`[${this.provider}] ${message}`, retryable, context, error, code, errorName);
}
/**
* Determine if an error is retryable based on status code and message.
* Can be overridden by drivers for provider-specific logic.
*
* @param statusCode - The HTTP status code (if available)
* @param message - The error message
* @returns True if retryable, false if not retryable, undefined if unknown
*/
isRetryableError(statusCode, message) {
// Numeric status codes
if (statusCode !== undefined) {
if (statusCode === 429 || statusCode === 408)
return true; // Rate limit, timeout
if (statusCode === 529)
return true; // Overloaded
if (statusCode >= 500 && statusCode < 600)
return true; // Server errors
return false; // 4xx client errors not retryable
}
// Message-based detection for non-HTTP errors
const lowerMessage = message.toLowerCase();
// Rate limit variations
if (lowerMessage.includes('rate') && lowerMessage.includes('limit'))
return true;
// Timeout variations (timeout, timed out, time out)
if (lowerMessage.includes('timeout'))
return true;
if (lowerMessage.includes('timed') && lowerMessage.includes('out'))
return true;
if (lowerMessage.includes('time') && lowerMessage.includes('out'))
return true;
// Resource exhausted variations
if (lowerMessage.includes('resource') && lowerMessage.includes('exhaust'))
return true;
// Other retryable patterns
if (lowerMessage.includes('retry'))
return true;
if (lowerMessage.includes('overload'))
return true;
if (lowerMessage.includes('throttl'))
return true;
if (lowerMessage.includes('429'))
return true;
if (lowerMessage.includes('529'))
return true;
// Unknown errors - let consumer decide retry strategy
return undefined;
}
async requestImageGeneration(_prompt, _options) {
throw new Error("Image generation not implemented.");
//Cannot be made abstract, as abstract methods are required in the derived class
}
/**
* Cleanup method called when the driver is evicted from the cache.
* Override this in driver implementations that need to release resources.
*/
destroy() {
// No-op by default
}
}
exports.AbstractDriver = AbstractDriver;
//# sourceMappingURL=Driver.js.map