openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
187 lines (186 loc) • 7.42 kB
JavaScript
import { D as resolveExpiresAtMsFromDurationMs, g as isFutureDateTimestampMs } from "./number-coercion-CuVhdCBs.mjs";
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { t as findNormalizedProviderKey } from "./provider-id-D1VImdKk.mjs";
import "./model-catalog-normalize-Dxe_x7wi.mjs";
import { a as normalizeConfiguredProviderCatalogModelId } from "./model-ref-shared-KBg0y299.mjs";
import { i as resolveProviderRequestCapabilities } from "./provider-attribution-DCsKu1Yv.mjs";
import "./provider-catalog-Io9OWhUB.mjs";
import { n as recordLiveCatalogExpiry } from "./provider-catalog-expiry-BajE8fJo.mjs";
import { createHash } from "node:crypto";
import { addAbortListener } from "node:events";
//#region src/plugin-sdk/provider-catalog-shared.ts
const liveCatalogCache = /* @__PURE__ */ new Map();
async function consumeLiveCatalog(entry, signal) {
signal?.throwIfAborted();
entry.controller.signal.throwIfAborted();
entry.consumers += 1;
const pending = signal && !entry.settled ? createDeferredCore() : void 0;
if (pending) (entry.waiters ??= /* @__PURE__ */ new Set()).add(pending);
let released = false;
const release = () => {
if (released) return;
released = true;
if (pending) entry.waiters?.delete(pending);
entry.consumers -= 1;
if (!entry.settled && entry.consumers === 0) entry.controller.abort(signal?.reason);
};
let listener;
try {
if (signal && pending) listener = addAbortListener(signal, () => {
release();
pending.reject(signal.reason);
});
const value = await (pending?.promise ?? entry.value);
if (entry.retained) recordLiveCatalogExpiry(entry.expiresAt);
return value;
} finally {
listener?.[Symbol.dispose]();
release();
}
}
function buildLiveCatalogCacheKey(parts) {
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
}
/**
* Shares pending loads and caches successful values for a short TTL after completion.
*/
async function getCachedLiveCatalogValue(params) {
params.signal?.throwIfAborted();
const rawNow = params.now?.() ?? Date.now();
const ttlMs = params.ttlMs ?? 3e4;
const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNow });
if (expiresAt === void 0) return await params.load(params.signal);
const key = buildLiveCatalogCacheKey(params.keyParts);
const existing = liveCatalogCache.get(key);
if (existing) {
if (!existing.controller.signal.aborted && isFutureDateTimestampMs(existing.expiresAt, { nowMs: rawNow })) return await consumeLiveCatalog(existing, params.signal);
liveCatalogCache.delete(key);
}
const completion = createDeferredCore();
completion.promise.catch(() => void 0);
const entry = {
expiresAt,
controller: new AbortController(),
consumers: 0,
settled: false,
retained: false,
value: completion.promise
};
pruneMapToMaxSize(liveCatalogCache, 99);
liveCatalogCache.set(key, entry);
const consumed = consumeLiveCatalog(entry, params.signal);
(async () => {
let retain = false;
try {
entry.controller.signal.throwIfAborted();
const resolved = await params.load(entry.controller.signal);
retain = !entry.controller.signal.aborted && (params.shouldCache?.(resolved) ?? true);
if (retain) {
const completedExpiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: params.now?.() ?? Date.now() });
retain = completedExpiresAt !== void 0;
if (completedExpiresAt !== void 0) entry.expiresAt = completedExpiresAt;
}
completion.resolve(resolved);
for (const waiter of entry.waiters ?? []) waiter.resolve(resolved);
} catch (error) {
completion.reject(error);
for (const waiter of entry.waiters ?? []) waiter.reject(error);
} finally {
entry.waiters = void 0;
entry.retained = retain;
entry.settled = true;
if (!retain && liveCatalogCache.get(key) === entry) liveCatalogCache.delete(key);
}
})();
return await consumed;
}
/**
* Clears the process-local live catalog cache for tests and isolated plugin probes.
*/
function clearLiveCatalogCacheForTests() {
liveCatalogCache.clear();
}
function normalizeConfiguredCatalogModelInput(input) {
if (!Array.isArray(input)) return;
const normalized = input.filter((item) => item === "text" || item === "image" || item === "audio" || item === "video" || item === "document");
return normalized.length > 0 ? normalized : void 0;
}
function resolveConfiguredProviderModels(config, providerId) {
const providers = config?.models?.providers;
if (!providers || typeof providers !== "object") return [];
const providerKey = findNormalizedProviderKey(providers, providerId);
if (!providerKey) return [];
const providerConfig = providers[providerKey];
if (!providerConfig || typeof providerConfig !== "object") return [];
return Array.isArray(providerConfig.models) ? providerConfig.models : [];
}
/**
* Reads user-configured provider models as catalog entries for plugin discovery output.
*/
function readConfiguredProviderCatalogEntries(params) {
const provider = params.publishedProviderId ?? params.providerId;
const models = resolveConfiguredProviderModels(params.config, params.providerId);
const entries = [];
for (const model of models) {
if (!model || typeof model !== "object") continue;
const id = typeof model.id === "string" ? model.id.trim() : "";
if (!id) continue;
const normalizedId = normalizeConfiguredProviderCatalogModelId(provider, id);
const name = (typeof model.name === "string" ? model.name : normalizedId).trim() || normalizedId;
const contextWindow = typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : void 0;
const reasoning = typeof model.reasoning === "boolean" ? model.reasoning : void 0;
const input = normalizeConfiguredCatalogModelInput(model.input);
entries.push({
provider,
id: normalizedId,
name,
...contextWindow ? { contextWindow } : {},
...reasoning !== void 0 ? { reasoning } : {},
...input ? { input } : {}
});
}
return entries;
}
function withStreamingUsageCompat(provider) {
if (!Array.isArray(provider.models) || provider.models.length === 0) return provider;
let changed = false;
const models = provider.models.map((model) => {
if (model.compat?.supportsUsageInStreaming !== void 0) return model;
changed = true;
return {
...model,
compat: {
...model.compat,
supportsUsageInStreaming: true
}
};
});
return changed ? {
...provider,
models
} : provider;
}
/**
* Returns whether a provider transport can report native usage while streaming.
*/
function supportsNativeStreamingUsageCompat(params) {
return resolveProviderRequestCapabilities({
provider: params.providerId,
api: "openai-completions",
baseUrl: params.baseUrl,
capability: "llm",
transport: "stream"
}).supportsNativeStreamingUsageCompat;
}
/**
* Marks models as streaming-usage compatible when provider transport capabilities allow it.
*/
function applyProviderNativeStreamingUsageCompat(params) {
return supportsNativeStreamingUsageCompat({
providerId: params.providerId,
baseUrl: params.providerConfig.baseUrl
}) ? withStreamingUsageCompat(params.providerConfig) : params.providerConfig;
}
//#endregion
export { supportsNativeStreamingUsageCompat as a, readConfiguredProviderCatalogEntries as i, clearLiveCatalogCacheForTests as n, getCachedLiveCatalogValue as r, applyProviderNativeStreamingUsageCompat as t };