jorel
Version:
A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.
344 lines (343 loc) • 14.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIProvider = void 0;
const openai_1 = require("openai");
const providers_1 = require("../../providers");
const shared_1 = require("../../shared");
const tools_1 = require("../../tools");
const convert_inputs_1 = require("./convert-inputs");
const convert_llm_message_1 = require("./convert-llm-message");
/** Provides access to OpenAI and other compatible services */
class OpenAIProvider {
constructor(options = {}) {
if (options.azure) {
this.name = options.name || OpenAIProvider.defaultName + "-azure";
this.client = new openai_1.AzureOpenAI({
endpoint: options.apiUrl || process.env.AZURE_OPENAI_ENDPOINT,
apiKey: options.apiKey || process.env.AZURE_OPENAI_API_KEY,
apiVersion: options.apiVersion || process.env.AZURE_OPENAI_API_VERSION || "2024-12-01-preview",
maxRetries: options.maxRetries || 3,
timeout: options.timeout,
});
this.isAzure = true;
}
else {
this.name = options.name || OpenAIProvider.defaultName;
this.client = new openai_1.OpenAI({
apiKey: options.apiKey || process.env.OPENAI_API_KEY,
baseURL: options.apiUrl || process.env.OPENAI_API_URL,
maxRetries: options.maxRetries,
timeout: options.timeout,
});
this.isAzure = false;
}
}
// Helper method for parsing OpenAI API errors
parseOpenAiError(error) {
let errorMessage;
let errorType = "unknown";
const status = error instanceof openai_1.APIError ? error.status : undefined;
errorMessage = error instanceof Error ? error.message : String(error);
// Clean up error messages that start with HTTP status codes
// e.g., "404 The model `gpt-5-mini-1` does not exist..." -> "The model `gpt-5-mini-1` does not exist..."
const statusCodeMatch = errorMessage.match(/^(\d{3})\s+(.+)$/);
if (statusCodeMatch) {
errorMessage = statusCodeMatch[2];
}
// Map error types based on OpenAI error classes and status codes
if (error instanceof openai_1.BadRequestError || status === 400) {
errorType = "invalid_request";
}
else if (error instanceof openai_1.AuthenticationError || status === 401) {
errorType = "authentication_error";
}
else if (error instanceof openai_1.PermissionDeniedError || status === 403) {
errorType = "authentication_error";
}
else if (error instanceof openai_1.NotFoundError || status === 404) {
errorType = "invalid_request";
}
else if (error instanceof openai_1.UnprocessableEntityError || status === 422) {
errorType = "invalid_request";
}
else if (error instanceof openai_1.RateLimitError || status === 429) {
// 429 can mean either rate limit or quota exceeded
const lowerMessage = errorMessage.toLowerCase();
if (lowerMessage.includes("quota") || lowerMessage.includes("exceeded your current quota")) {
errorType = "quota_exceeded";
}
else {
errorType = "rate_limit";
}
}
else if (error instanceof openai_1.InternalServerError || (status && status >= 500)) {
errorType = "server_error";
}
else if (error instanceof openai_1.APIConnectionTimeoutError) {
errorType = "timeout";
}
else if (error instanceof openai_1.APIConnectionError) {
errorType = "network_error";
}
return { message: errorMessage, type: errorType };
}
async generateResponse(model, messages, config = {}) {
const start = Date.now();
const temperature = config.temperature ?? undefined;
let response;
try {
response = await this.client.chat.completions.create({
model,
messages: await (0, convert_llm_message_1.convertLlmMessagesToOpenAiMessages)(messages),
temperature,
response_format: (0, convert_inputs_1.jsonResponseToOpenAi)(config.json, config.jsonDescription),
max_tokens: config.maxTokens,
max_completion_tokens: config.maxCompletionTokens,
parallel_tool_calls: config.tools && config.tools.hasTools ? config.tools.allowParallelCalls : undefined,
tool_choice: (0, convert_inputs_1.toolChoiceToOpenAi)(config.toolChoice),
tools: config.tools?.asLlmFunctions,
reasoning_effort: config.reasoningEffort,
verbosity: config.verbosity,
}, {
signal: config.abortSignal,
});
}
catch (error) {
if (error instanceof openai_1.OpenAIError && error.message.toLowerCase().includes("aborted")) {
throw new shared_1.JorElAbortError("Request was aborted");
}
const { message, type } = this.parseOpenAiError(error);
throw new shared_1.JorElLlmError(`[OpenAIProvider] Error generating content: ${message}`, type);
}
const durationMs = Date.now() - start;
const inputTokens = response.usage?.prompt_tokens;
const outputTokens = response.usage?.completion_tokens;
const reasoningTokens = response.usage?.completion_tokens_details?.reasoning_tokens;
const message = response.choices[0].message;
const toolCalls = message.tool_calls?.map((call) => {
if (call.type === "custom") {
throw new Error(`Unsupported tool call type: ${call.type}`);
}
return {
id: (0, shared_1.generateUniqueId)(),
request: {
id: call.id,
function: {
name: call.function.name,
arguments: tools_1.LlmToolKit.deserialize(call.function.arguments),
},
},
approvalState: config.tools?.getTool(call.function.name)?.requiresConfirmation
? "requiresApproval"
: "noApprovalRequired",
executionState: "pending",
result: null,
error: null,
};
});
const provider = this.name;
const reasoningContent = null;
return {
...(0, providers_1.generateAssistantMessage)(message.content, reasoningContent, toolCalls),
meta: {
model,
provider,
temperature,
durationMs,
inputTokens,
outputTokens,
reasoningTokens,
},
};
}
async *generateResponseStream(model, messages, config = {}) {
const start = Date.now();
const provider = this.name;
const temperature = config.temperature ?? undefined;
let response;
try {
response = await this.client.chat.completions.create({
model,
messages: await (0, convert_llm_message_1.convertLlmMessagesToOpenAiMessages)(messages),
temperature,
response_format: (0, convert_inputs_1.jsonResponseToOpenAi)(config.json, config.jsonDescription),
max_tokens: config.maxTokens,
max_completion_tokens: config.maxCompletionTokens,
stream: true,
tools: config.tools?.asLlmFunctions,
parallel_tool_calls: config.tools && config.tools.hasTools ? config.tools.allowParallelCalls : undefined,
tool_choice: (0, convert_inputs_1.toolChoiceToOpenAi)(config.toolChoice),
stream_options: {
include_usage: true,
},
reasoning_effort: config.reasoningEffort,
verbosity: config.verbosity,
}, {
signal: config.abortSignal,
});
}
catch (error) {
const isAbort = (error instanceof openai_1.OpenAIError && error.message.toLowerCase().includes("aborted")) ||
(error instanceof Error && error.name === "AbortError");
const stopReason = isAbort ? "userCancelled" : "generationError";
yield {
type: "response",
role: "assistant",
content: "",
reasoningContent: null,
meta: {
model,
provider,
temperature,
durationMs: 0,
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
},
stopReason,
error: stopReason === "generationError" ? this.parseOpenAiError(error) : undefined,
};
return;
}
let inputTokens;
let outputTokens;
let reasoningTokens;
const _toolCalls = [];
let content = "";
const reasoningContent = "";
let error;
try {
for await (const chunk of response) {
const delta = (0, shared_1.firstEntry)(chunk.choices)?.delta;
if (delta?.content) {
content += delta.content;
const chunkId = (0, shared_1.generateUniqueId)();
yield { type: "chunk", content: delta.content, chunkId };
}
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
const _toolCall = _toolCalls[toolCall.index] || { id: "", function: { name: "", arguments: "" } };
if (toolCall.id)
_toolCall.id += toolCall.id;
if (toolCall.function) {
if (toolCall.function.name)
_toolCall.function.name += toolCall.function.name;
if (toolCall.function.arguments)
_toolCall.function.arguments += toolCall.function.arguments;
}
_toolCalls[toolCall.index] = _toolCall;
}
}
if (chunk.usage) {
inputTokens = (inputTokens || 0) + (chunk.usage?.prompt_tokens ?? 0);
outputTokens = (outputTokens || 0) + (chunk.usage?.completion_tokens ?? 0);
reasoningTokens = (reasoningTokens || 0) + (chunk.usage?.completion_tokens_details?.reasoning_tokens ?? 0);
}
}
}
catch (e) {
error = this.parseOpenAiError(e);
}
const durationMs = Date.now() - start;
const toolCalls = _toolCalls.map((call) => {
let parsedArgs = null;
let parseError = null;
try {
parsedArgs = tools_1.LlmToolKit.deserialize(call.function.arguments);
}
catch (e) {
parseError = e instanceof Error ? e : new Error("Unable to parse tool call arguments");
}
const approvalState = config.tools?.getTool(call.function.name)
?.requiresConfirmation
? "requiresApproval"
: "noApprovalRequired";
const base = {
id: (0, shared_1.generateUniqueId)(),
request: {
id: call.id,
function: {
name: call.function.name,
arguments: parsedArgs ?? {},
},
},
approvalState,
};
if (parseError) {
return {
...base,
executionState: "error",
result: null,
error: {
type: parseError.name || "ToolArgumentParseError",
message: parseError.message || "Invalid tool call arguments",
numberOfAttempts: 1,
lastAttempt: new Date(),
},
};
}
return {
...base,
executionState: "pending",
result: null,
error: null,
};
});
// Determine stop reason and error message
const stopReason = config.abortSignal?.aborted ? "userCancelled" : error ? "generationError" : "completed";
// Log non-abort errors
if (error && stopReason === "generationError") {
config.logger?.error("OpenAIProvider", `Stream error: ${error.message}`);
}
const meta = {
model,
provider,
temperature,
durationMs,
inputTokens: inputTokens ?? undefined,
outputTokens: outputTokens ?? undefined,
reasoningTokens: reasoningTokens ?? undefined,
};
if (_toolCalls.length > 0) {
yield {
type: "response",
role: "assistant_with_tools",
content,
reasoningContent: reasoningContent || null,
toolCalls,
meta,
stopReason,
error: stopReason === "generationError" ? error : undefined,
};
}
else {
yield {
type: "response",
role: "assistant",
content,
reasoningContent: reasoningContent || null,
meta,
stopReason,
error: stopReason === "generationError" ? error : undefined,
};
}
}
async getAvailableModels() {
const models = await this.client.models.list();
return models.data.map((model) => model.id);
}
async createEmbedding(model, text, abortSignal) {
const response = await this.client.embeddings.create({
model,
input: text,
}, {
signal: abortSignal,
});
if (!response || !response.data || !response.data || response.data.length === 0) {
throw new Error("Failed to create embedding");
}
return response.data[0].embedding;
}
}
exports.OpenAIProvider = OpenAIProvider;
OpenAIProvider.defaultName = "openai";