naisys
Version:
NAISYS - Autonomous AI agent runner with built-in context management and cost tracking
194 lines • 7.65 kB
JavaScript
import { LlmApiType } from "@naisys/common";
import OpenAI from "openai";
import { extractDesktopActions, formatInputWithComputerUse, } from "../../computer-use/vendors/openai-computer-use.js";
const clientCache = new Map();
function getClient(apiKey, baseURL) {
const cacheKey = `${baseURL || ""}|${apiKey}`;
let client = clientCache.get(cacheKey);
if (!client) {
client = new OpenAI({ baseURL, apiKey });
clientCache.set(cacheKey, client);
}
return client;
}
function formatOpenAiResponseFailure(response) {
if (response.error) {
return `OpenAI Responses error: ${response.error.code || "unknown"}: ${response.error.message || "no message"}`;
}
if (response.incomplete_details?.reason) {
return `OpenAI Responses incomplete: ${response.incomplete_details.reason}`;
}
return "OpenAI Responses stream failed.";
}
function toOpenAiReasoningEffort(level) {
return level === "max" ? "xhigh" : level;
}
function toOpenAiReasoning(level) {
return level ? { effort: toOpenAiReasoningEffort(level) } : undefined;
}
async function createStreamingOpenAiResponse(openAI, params, abortSignal) {
const stream = await openAI.responses.create(params, { signal: abortSignal });
let response;
let outputText = "";
const outputItems = [];
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
outputText += event.delta;
}
else if (event.type === "response.output_item.done") {
outputItems.push(event.item);
}
else if (event.type === "response.completed" ||
event.type === "response.incomplete") {
response = event.response;
}
else if (event.type === "response.failed") {
throw new Error(formatOpenAiResponseFailure(event.response));
}
else if (event.type === "error") {
throw new Error(event.code ? `${event.code}: ${event.message}` : event.message);
}
}
if (!response) {
throw new Error("OpenAI Responses stream ended without a final response.");
}
return {
...response,
output: response.output?.length ? response.output : outputItems,
output_text: response.output_text || outputText,
};
}
export async function sendWithOpenAiStandard(deps, modelKey, systemMessage, context, source, apiKey, abortSignal) {
const { modelService, costTracker, tools, useToolsForLlmConsoleResponses, desktopConfig, } = deps;
const model = modelService.getLlmModel(modelKey);
if (!apiKey) {
throw `Error, set ${model.apiKeyVar} variable`;
}
const openAI = getClient(apiKey, model.baseUrl);
const isOpenAiOauth = model.apiType === LlmApiType.OpenAIOAuth;
const useConsoleTools = source === "console" &&
useToolsForLlmConsoleResponses &&
model.supportsToolUse === true;
// Build tools array — console and desktop tools can coexist
const toolsDefs = [];
if (useConsoleTools) {
const functionTool = {
type: "function",
name: tools.consoleToolOpenAI.function.name,
description: tools.consoleToolOpenAI.function.description,
parameters: tools.consoleToolOpenAI.function
.parameters,
strict: false,
};
toolsDefs.push(functionTool);
}
if (desktopConfig) {
toolsDefs.push({ type: "computer" });
}
// Force console tool only when desktop is not also enabled
let toolChoice = undefined;
if (useConsoleTools && !desktopConfig) {
toolChoice = {
type: "function",
name: tools.consoleToolOpenAI.function.name,
};
}
const createParams = {
model: model.versionName,
instructions: systemMessage,
input: (desktopConfig
? formatInputWithComputerUse(context, formatContentBlocks, formatSingleBlock)
: context.map((m) => ({
role: m.role === "assistant" ? "assistant" : "user",
content: formatContentBlocks(m.content, m.role),
}))),
reasoning: toOpenAiReasoning(model.reasoningLevel),
tools: toolsDefs.length > 0 ? toolsDefs : undefined,
tool_choice: toolChoice,
};
const response = isOpenAiOauth
? await createStreamingOpenAiResponse(openAI, {
...createParams,
store: false,
stream: true,
}, abortSignal)
: await openAI.responses.create(createParams, { signal: abortSignal });
if (!response.usage) {
throw "Error, no usage data returned from OpenAI Responses API.";
}
const inputTokens = response.usage.input_tokens || 0;
const outputTokens = response.usage.output_tokens || 0;
const messagesTokenCount = inputTokens;
const cacheReadTokens = response.usage.input_tokens_details?.cached_tokens || 0;
const nonCachedPromptTokens = Math.max(0, inputTokens - cacheReadTokens);
costTracker.recordTokens(source, model.key, nonCachedPromptTokens, outputTokens, 0, cacheReadTokens);
// Extract desktop actions (computer_call items); coords stay in scaled-pixel space
const desktopActions = desktopConfig
? extractDesktopActions(response.output)
: [];
// Extract console commands (function_call items)
const consoleCommands = useConsoleTools
? extractConsoleCommands(response.output, tools)
: undefined;
// Extract text
const textParts = response.output_text
? [response.output_text]
: [];
// Desktop actions take priority (same pattern as Anthropic vendor)
if (desktopActions.length > 0) {
const allText = [...textParts, ...(consoleCommands || [])];
return { responses: allText, messagesTokenCount, desktopActions };
}
if (consoleCommands) {
return { responses: consoleCommands, messagesTokenCount };
}
return {
responses: textParts.length > 0 ? textParts : [""],
messagesTokenCount,
};
}
function extractConsoleCommands(output, tools) {
const toolCalls = output
.filter((item) => item.type === "function_call")
.map((item) => ({
type: "function",
function: { name: item.name, arguments: item.arguments },
}));
return tools.getCommandsFromOpenAiToolUse(toolCalls) || undefined;
}
// --- Content formatting helpers (shared with openai-computer-use) ---
function makeTextPart(text, role) {
return role === "assistant"
? { type: "output_text", text }
: { type: "input_text", text };
}
function formatContentBlocks(content, role) {
if (typeof content === "string") {
return [makeTextPart(content, role)];
}
return content
.map((b) => formatSingleBlock(b, role))
.filter((p) => p !== null);
}
function formatSingleBlock(block, role) {
switch (block.type) {
case "text":
return makeTextPart(block.text, role);
case "image":
return {
type: "input_image",
image_url: `data:${block.mimeType};base64,${block.base64}`,
detail: "auto",
};
case "tool_use":
// Fallback when desktop is not enabled — include as text description
return makeTextPart(`ns-tool desktop-action ${JSON.stringify(block.input)}`, role);
case "tool_result":
return makeTextPart("[Desktop screenshot]", role);
case "audio":
return null;
default:
return null;
}
}
//# sourceMappingURL=openai-standard.js.map