@ubiquity-os/plugin-sdk
Version:
SDK for plugin support.
323 lines (321 loc) • 11.1 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/llm/index.ts
var llm_exports = {};
__export(llm_exports, {
callLlm: () => callLlm
});
module.exports = __toCommonJS(llm_exports);
var EMPTY_STRING = "";
function normalizeBaseUrl(baseUrl) {
let normalized = baseUrl.trim();
while (normalized.endsWith("/")) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
var MAX_LLM_RETRIES = 2;
var RETRY_BACKOFF_MS = [250, 750];
function getRetryDelayMs(attempt) {
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getEnvString(name) {
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
return String(process.env[name] ?? EMPTY_STRING).trim();
}
function normalizeToken(value) {
return typeof value === "string" ? value.trim() : EMPTY_STRING;
}
function isGitHubToken(token) {
return token.trim().startsWith("gh");
}
function readKernelRefreshUrl(value) {
if (!value || typeof value !== "object") return EMPTY_STRING;
return normalizeToken(value.kernelRefreshUrl);
}
function getKernelRefreshUrl(input) {
const direct = readKernelRefreshUrl(input);
if (direct) return direct;
const fromConfig = readKernelRefreshUrl(input.config);
if (fromConfig) return fromConfig;
return readKernelRefreshUrl(input.settings);
}
function updateInputTokens(input, tokens) {
if ("authToken" in input) {
input.authToken = tokens.authToken;
}
if ("ubiquityKernelToken" in input) {
input.ubiquityKernelToken = tokens.kernelToken;
}
}
function getEnvTokenFromInput(input) {
if ("env" in input) {
const envValue = input.env;
if (envValue && typeof envValue === "object") {
const token = normalizeToken(envValue.UOS_AI_TOKEN);
if (token) return token;
}
}
return getEnvString("UOS_AI_TOKEN");
}
function resolveAuthToken(input, aiAuthToken) {
const explicit = normalizeToken(aiAuthToken);
if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
const envToken = getEnvTokenFromInput(input);
if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
const fallback = normalizeToken(input.authToken);
if (!fallback) {
const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
err.status = 401;
throw err;
}
return { token: fallback, isGitHub: isGitHubToken(fallback) };
}
function getAiBaseUrl(options) {
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
return normalizeBaseUrl(options.baseUrl);
}
const envBaseUrl = getEnvString("UOS_AI_URL");
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
return "https://ai-ubq-fi.deno.dev";
}
async function callLlm(options, input) {
const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
ensureMessages(messages);
const { token: resolvedAuthToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
let authToken = resolvedAuthToken;
let kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
const payload = getPayload(input);
const { owner, repo, installationId } = getRepoMetadata(payload);
const kernelRefreshUrl = getKernelRefreshUrl(input);
const hasExplicitAiAuth = Boolean(normalizeToken(aiAuthToken));
if (isGitHub && kernelRefreshUrl && kernelToken && !hasExplicitAiAuth) {
const refreshed = await refreshKernelTokens({
url: kernelRefreshUrl,
authToken,
kernelToken,
owner,
repo,
installationId
});
authToken = refreshed.authToken;
kernelToken = refreshed.kernelToken;
updateInputTokens(input, refreshed);
}
const url = buildAiUrl(options, baseUrl);
const body = JSON.stringify({
...rest,
...model ? { model } : {},
messages,
stream: isStream ?? false
});
const headers = buildHeaders(authToken, {
owner,
repo,
installationId,
ubiquityKernelToken: isGitHub ? kernelToken : void 0
});
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
if (isStream) {
if (!response.body) {
throw new Error("LLM API error: missing response body for streaming request");
}
return parseSseStream(response.body);
}
const rawText = await response.text();
try {
return JSON.parse(rawText);
} catch (err) {
const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
const error = new Error(message);
error.cause = err;
error.status = response.status;
throw error;
}
}
function ensureMessages(messages) {
if (!Array.isArray(messages) || messages.length === 0) {
const err = new Error("messages must be a non-empty array");
err.status = 400;
throw err;
}
}
function buildAiUrl(options, baseUrl) {
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
}
async function refreshKernelTokens(params) {
const headers = buildHeaders(params.authToken, {
owner: params.owner,
repo: params.repo,
installationId: params.installationId,
ubiquityKernelToken: params.kernelToken
});
const response = await fetch(params.url, { method: "POST", headers });
const text = await response.text().catch(() => EMPTY_STRING);
if (!response.ok) {
const error = new Error(`Kernel refresh error: ${response.status} - ${text}`);
error.status = response.status;
throw error;
}
let payload = {};
if (text.trim()) {
try {
payload = JSON.parse(text);
} catch (err) {
const details = err instanceof Error ? ` (${err.message})` : "";
const error = new Error(`Kernel refresh error: failed to parse JSON response${details}`);
error.status = response.status;
throw error;
}
}
const authToken = normalizeToken(payload.authToken);
const kernelToken = normalizeToken(payload.ubiquityKernelToken);
if (!authToken || !kernelToken) {
const error = new Error("Kernel refresh error: response missing authToken or ubiquityKernelToken");
error.status = 502;
throw error;
}
return {
authToken,
kernelToken,
expiresAt: typeof payload.expiresAt === "string" ? payload.expiresAt : null
};
}
async function fetchWithRetry(url, options, maxRetries) {
let attempt = 0;
let lastError;
while (attempt <= maxRetries) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
throw await buildResponseError(response);
} catch (error) {
lastError = error;
if (!shouldRetryError(error, attempt, maxRetries)) throw error;
await sleep(getRetryDelayMs(attempt));
attempt += 1;
}
}
throw lastError ?? new Error("LLM API error: request failed after retries");
}
async function buildResponseError(response) {
const errText = await response.text();
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
error.status = response.status;
return error;
}
function shouldRetryError(error, attempt, maxRetries) {
if (attempt >= maxRetries) return false;
const status = getErrorStatus(error);
if (typeof status === "number") {
return status >= 500;
}
return true;
}
function getErrorStatus(error) {
return typeof error?.status === "number" ? error.status : void 0;
}
function getPayload(input) {
if ("payload" in input) {
return input.payload;
}
return input.eventPayload;
}
function getRepoMetadata(payload) {
const repoPayload = payload;
return {
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
installationId: repoPayload?.installation?.id
};
}
function buildHeaders(authToken, options) {
const headers = {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json"
};
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
headers["X-GitHub-Installation-Id"] = String(options.installationId);
}
if (options.ubiquityKernelToken) {
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
}
return headers;
}
async function* parseSseStream(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = EMPTY_STRING;
try {
while (true) {
const { value, done: isDone } = await reader.read();
if (isDone) break;
buffer += decoder.decode(value, { stream: true });
const { events, remainder } = splitSseEvents(buffer);
buffer = remainder;
for (const event of events) {
const data = getEventData(event);
if (!data) continue;
if (data.trim() === "[DONE]") return;
yield parseEventData(data);
}
}
} finally {
reader.releaseLock();
}
}
function splitSseEvents(buffer) {
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const parts = normalized.split("\n\n");
const remainder = parts.pop() ?? EMPTY_STRING;
return { events: parts, remainder };
}
function getEventData(event) {
if (!event.trim()) return null;
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
if (!dataLines.length) return null;
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
return data || null;
}
function parseEventData(data) {
try {
return JSON.parse(data);
} catch (error) {
if (data.includes("\n")) {
const collapsed = data.replace(/\n/g, EMPTY_STRING);
try {
return JSON.parse(collapsed);
} catch {
}
}
const message = error instanceof Error ? error.message : String(error);
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
callLlm
});