@robota-sdk/openai
Version:
OpenAI integration for Robota SDK - GPT-4, GPT-3.5, function calling, and tool integration with OpenAI's API
485 lines (481 loc) • 15.1 kB
JavaScript
import { BaseAIProvider, logger } from '@robota-sdk/core';
import * as fs from 'fs';
import * as path from 'path';
// src/provider.ts
// src/adapter.ts
var OpenAIConversationAdapter = class {
/**
* Filter messages for OpenAI compatibility
*
* OpenAI has specific requirements:
* - Tool messages must have valid toolCallId
* - Messages must be in proper sequence
* - Tool messages without toolCallId should be excluded
*/
static filterMessagesForOpenAI(messages) {
return messages.filter((msg) => {
if (msg.role === "user" || msg.role === "assistant" || msg.role === "system") {
return true;
}
if (msg.role === "tool") {
const toolMsg = msg;
return !!(toolMsg.toolCallId && toolMsg.toolCallId.trim() !== "" && toolMsg.toolCallId !== "unknown");
}
return false;
});
}
/**
* Convert UniversalMessage array to OpenAI message format
* Now properly handles tool messages for OpenAI's tool calling feature
*/
static toOpenAIFormat(messages) {
const filteredMessages = this.filterMessagesForOpenAI(messages);
return filteredMessages.map((msg) => this.convertMessage(msg));
}
/**
* Convert a single UniversalMessage to OpenAI format
* Handles all message types including tool messages
*/
static convertMessage(msg) {
const messageRole = msg.role;
if (messageRole === "user") {
const userMsg = msg;
const result = {
role: "user",
content: userMsg.content
};
if (userMsg.name) {
result.name = userMsg.name;
}
return result;
}
if (messageRole === "assistant") {
const assistantMsg = msg;
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
const result = {
role: "assistant",
content: assistantMsg.content || null,
tool_calls: assistantMsg.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments
}
}))
};
return result;
}
return {
role: "assistant",
content: assistantMsg.content || ""
};
}
if (messageRole === "system") {
const systemMsg = msg;
return {
role: "system",
content: systemMsg.content
};
}
if (messageRole === "tool") {
const toolMsg = msg;
const result = {
role: "tool",
content: toolMsg.content,
tool_call_id: toolMsg.toolCallId || "unknown"
};
return result;
}
const _exhaustiveCheck = msg;
return _exhaustiveCheck;
}
/**
* Add system prompt to message array if needed
*/
static addSystemPromptIfNeeded(messages, systemPrompt) {
if (!systemPrompt) {
return messages;
}
const hasSystemMessage = messages.some((msg) => msg.role === "system");
if (hasSystemMessage) {
return messages;
}
return [
{ role: "system", content: systemPrompt },
...messages
];
}
};
var PayloadLogger = class {
enabled;
logDir;
includeTimestamp;
constructor(enabled = false, logDir = "./logs/api-payloads", includeTimestamp = true) {
this.enabled = enabled;
this.logDir = logDir;
this.includeTimestamp = includeTimestamp;
if (this.enabled) {
this.ensureLogDirectoryExists();
}
}
/**
* Log API payload to file
* @param payload - The API request payload
* @param type - Type of request ('chat' or 'stream')
*/
async logPayload(payload, type = "chat") {
if (!this.enabled) {
return;
}
try {
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const filename = this.includeTimestamp ? `openai-${type}-${timestamp}.json` : `openai-${type}-${Date.now()}.json`;
const filepath = path.join(this.logDir, filename);
const logData = {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
type,
provider: "openai",
payload: this.sanitizePayload(payload)
};
await fs.promises.writeFile(
filepath,
JSON.stringify(logData, null, 2),
"utf8"
);
} catch (error) {
}
}
/**
* Ensure log directory exists
*/
ensureLogDirectoryExists() {
if (!fs.existsSync(this.logDir)) {
try {
fs.mkdirSync(this.logDir, { recursive: true });
} catch (error) {
}
}
}
/**
* Sanitize payload to remove sensitive information
* @param payload - Raw payload object
* @returns Sanitized payload
*/
sanitizePayload(payload) {
const sanitized = JSON.parse(JSON.stringify(payload));
return sanitized;
}
/**
* Check if logging is enabled
*/
isEnabled() {
return this.enabled;
}
};
// src/provider.ts
var OpenAIProvider = class extends BaseAIProvider {
/**
* Provider identifier name
* @readonly
*/
name = "openai";
/**
* OpenAI client instance
* @internal
*/
client;
/**
* Client type identifier
* @readonly
*/
type = "openai";
/**
* OpenAI client instance (alias for backwards compatibility)
* @readonly
* @deprecated Use the private client property instead
*/
instance;
/**
* Provider configuration options
* @readonly
*/
options;
/**
* Payload logger for API request logging
* @internal
*/
payloadLogger;
/**
* Create a new OpenAI provider instance
*
* @param options - Configuration options for the OpenAI provider
*
* @throws {Error} When client is not provided in options
*/
constructor(options) {
super();
this.options = {
temperature: 0.7,
maxTokens: void 0,
...options
};
if (!options.client) {
throw new Error("OpenAI client is not injected. The client option is required.");
}
this.client = options.client;
this.instance = options.client;
this.payloadLogger = new PayloadLogger(
this.options.enablePayloadLogging || false,
this.options.payloadLogDir || "./logs/api-payloads",
this.options.includeTimestampInLogFiles !== false
);
}
/**
* Convert function definitions to OpenAI tool format
*
* Transforms universal function definitions into OpenAI's specific tool format
* required by the Chat Completions API.
*
* @param functions - Array of universal function definitions
* @returns Array of OpenAI-formatted tools
*/
formatFunctions(functions) {
return functions.map((fn) => ({
type: "function",
function: {
name: fn.name,
description: fn.description || "",
parameters: fn.parameters
}
}));
}
/**
* Filter conversation history for OpenAI API compatibility
*
* Converts messages to OpenAI format and filters out invalid tool messages.
*
* @param messages - Array of messages to filter
* @returns OpenAI-formatted messages array
*/
filterHistory(messages) {
return OpenAIConversationAdapter.toOpenAIFormat(messages);
}
/**
* Configure tools for OpenAI API request
*
* Transforms function schemas into OpenAI tool format and sets tool_choice.
*
* @param tools - Array of function schemas
* @returns OpenAI tool configuration object
*/
configureTools(tools) {
if (!tools || !Array.isArray(tools)) {
return void 0;
}
return {
tools: this.formatFunctions(tools),
tool_choice: "auto"
// Force new tool_calls format
};
}
/**
* Send a chat request to OpenAI and receive a complete response
*
* Processes the provided context and sends it to OpenAI's Chat Completions API.
* Handles message format conversion, error handling, and response parsing.
*
* @param model - Model name to use (e.g., 'gpt-4', 'gpt-3.5-turbo')
* @param context - Context object containing messages and system prompt
* @param options - Optional generation parameters and tools
* @returns Promise resolving to the model's response
*
* @throws {Error} When context is invalid
* @throws {Error} When messages array is invalid
* @throws {Error} When message format conversion fails
* @throws {Error} When OpenAI API call fails
*/
async chat(model, context, options) {
this.validateContext(context);
const openaiMessages = this.filterHistory(context.messages);
const completionOptions = {
model,
messages: openaiMessages,
max_tokens: options?.maxTokens || this.options.maxTokens,
temperature: options?.temperature || this.options.temperature
};
const toolConfig = this.configureTools(options?.tools);
if (toolConfig) {
completionOptions.tools = toolConfig.tools;
completionOptions.tool_choice = toolConfig.tool_choice;
}
if (this.options.responseFormat) {
if (this.options.responseFormat === "text") {
completionOptions.response_format = { type: "text" };
} else if (this.options.responseFormat === "json_object") {
completionOptions.response_format = { type: "json_object" };
} else if (this.options.responseFormat === "json_schema") {
if (!this.options.jsonSchema) {
throw new Error('jsonSchema is required when responseFormat is "json_schema"');
}
completionOptions.response_format = {
type: "json_schema",
json_schema: this.options.jsonSchema
};
}
}
try {
await this.payloadLogger.logPayload(completionOptions, "chat");
const response = await this.client.chat.completions.create(completionOptions);
return this.parseResponse(response);
} catch (error) {
this.handleApiError(error, "chat");
}
}
/**
* Convert OpenAI API response to universal ModelResponse format
*
* Transforms the OpenAI-specific response format into the standard format
* used across all providers in Robota.
*
* @param response - Raw response from OpenAI Chat Completions API
* @returns Parsed model response in universal format
*
* @internal
*/
parseResponse(response) {
const message = response.choices[0].message;
const result = {
content: message.content || void 0,
usage: response.usage ? {
promptTokens: response.usage.prompt_tokens,
completionTokens: response.usage.completion_tokens,
totalTokens: response.usage.total_tokens
} : void 0,
metadata: {
model: response.model,
finishReason: response.choices[0].finish_reason,
systemFingerprint: response.system_fingerprint
}
};
if (message.tool_calls && message.tool_calls.length > 0) {
result.toolCalls = message.tool_calls.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments
}
}));
}
return result;
}
/**
* Convert OpenAI streaming response chunk to universal format
*
* Transforms individual chunks from OpenAI's streaming response into the
* standard StreamingResponseChunk format used across all providers.
*
* @param chunk - Raw chunk from OpenAI streaming API
* @returns Parsed streaming response chunk
*
* @internal
*/
parseStreamingChunk(chunk) {
const delta = chunk.choices[0].delta;
const result = {
content: delta.content || void 0,
isComplete: chunk.choices[0].finish_reason !== null
};
if (delta.tool_calls && delta.tool_calls.length > 0) {
const toolCall = delta.tool_calls[0];
if (toolCall.type === "function") {
result.functionCall = {
name: toolCall.function?.name,
arguments: toolCall.function?.arguments
};
}
}
return result;
}
/**
* Send a streaming chat request to OpenAI and receive response chunks
*
* Similar to chat() but returns an async iterator that yields response chunks
* as they arrive from OpenAI's streaming API. Useful for real-time display
* of responses or handling large responses incrementally.
*
* @param model - Model name to use
* @param context - Context object containing messages and system prompt
* @param options - Optional generation parameters and tools
* @returns Async generator yielding response chunks
*
* @throws {Error} When context is invalid
* @throws {Error} When messages array is invalid
* @throws {Error} When message format conversion fails
* @throws {Error} When OpenAI streaming API call fails
*
* @see {@link ../../../apps/examples/01-basic | Basic Usage Examples}
*/
async *chatStream(model, context, options) {
if (!context || typeof context !== "object") {
logger.error("[OpenAIProvider] Invalid context:", context);
throw new Error("Valid Context object is required");
}
const { messages } = context;
if (!Array.isArray(messages)) {
logger.error("[OpenAIProvider] Invalid message array:", messages);
throw new Error("Valid message array is required");
}
const openaiMessages = OpenAIConversationAdapter.toOpenAIFormat(context.messages);
const completionOptions = {
model,
messages: openaiMessages,
max_tokens: options?.maxTokens || this.options.maxTokens,
temperature: options?.temperature || this.options.temperature,
stream: true
};
if (options?.tools && Array.isArray(options.tools)) {
completionOptions.tools = this.formatFunctions(options.tools);
completionOptions.tool_choice = "auto";
}
if (this.options.responseFormat) {
if (this.options.responseFormat === "text") {
completionOptions.response_format = { type: "text" };
} else if (this.options.responseFormat === "json_object") {
completionOptions.response_format = { type: "json_object" };
} else if (this.options.responseFormat === "json_schema") {
if (!this.options.jsonSchema) {
throw new Error('jsonSchema is required when responseFormat is "json_schema"');
}
completionOptions.response_format = {
type: "json_schema",
json_schema: this.options.jsonSchema
};
}
}
try {
await this.payloadLogger.logPayload(completionOptions, "stream");
const stream = await this.client.chat.completions.create(completionOptions);
for await (const chunk of stream) {
yield this.parseStreamingChunk(chunk);
}
} catch (error) {
logger.error("[OpenAIProvider] Streaming API call error:", error);
throw error;
}
}
/**
* Release resources and close connections
*
* Performs cleanup operations when the provider is no longer needed.
* OpenAI client doesn't require explicit cleanup, so this is a no-op.
*
* @returns Promise that resolves when cleanup is complete
*/
async close() {
}
};
export { OpenAIConversationAdapter, OpenAIProvider, PayloadLogger };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map