openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
350 lines (349 loc) • 13.9 kB
JavaScript
import { j as resolveTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js";
import { t as validateJsonSchemaValue } from "./schema-validator-CHU-4IBb.js";
import "./number-runtime-DBLVDypr.js";
import "./json-schema-runtime-CovmnlP6.js";
import { i as FALLBACK_CODEX_MODELS, r as CODEX_PROVIDER_ID } from "./provider-catalog-BdolWBnQ.js";
import { d as resolveCodexAppServerRuntimeOptions } from "./config-i2AzQJy8.js";
import { i as assertCodexTurnStartResponse, l as readCodexTurnCompletedNotification, o as readCodexErrorNotification, r as assertCodexThreadStartResponse } from "./protocol-validators-B48C6S9O.js";
import { r as readModelListResult } from "./models-BlR_JQQS.js";
import { t as isJsonObject } from "./protocol-oeJQu4rs.js";
import { r as buildCodexRuntimeThreadConfig } from "./thread-lifecycle-nq1AIao_.js";
//#region extensions/codex/media-understanding-provider.ts
/**
* Codex-backed media understanding provider for bounded image description and
* structured extraction turns.
*/
const DEFAULT_CODEX_IMAGE_MODEL = FALLBACK_CODEX_MODELS.find((model) => model.inputModalities.includes("image"))?.id ?? FALLBACK_CODEX_MODELS[0]?.id;
const DEFAULT_CODEX_IMAGE_PROMPT = "Describe the image.";
/**
* Builds the media-understanding provider that delegates image tasks to an
* isolated Codex app-server session.
*/
function buildCodexMediaUnderstandingProvider(options = {}) {
return {
id: CODEX_PROVIDER_ID,
capabilities: ["image"],
...DEFAULT_CODEX_IMAGE_MODEL ? { defaultModels: { image: DEFAULT_CODEX_IMAGE_MODEL } } : {},
describeImage: async (req) => describeCodexImages({
images: [{
buffer: req.buffer,
fileName: req.fileName,
mime: req.mime
}],
provider: req.provider,
model: req.model,
prompt: req.prompt,
maxTokens: req.maxTokens,
timeoutMs: req.timeoutMs,
profile: req.profile,
preferredProfile: req.preferredProfile,
authStore: req.authStore,
agentDir: req.agentDir,
cfg: req.cfg
}, options),
describeImages: async (req) => describeCodexImages(req, options),
extractStructured: async (req) => extractCodexStructured(req, options)
};
}
async function describeCodexImages(req, options) {
const model = req.model.trim();
if (!model) throw new Error("Codex image understanding requires model id.");
return {
text: await runBoundedCodexVisionTurn({
model,
profile: req.profile,
timeoutMs: req.timeoutMs,
agentDir: req.agentDir,
options,
taskLabel: "image understanding",
developerInstructions: "You are OpenClaw's bounded image-understanding worker. Describe only the provided image content. Do not call tools, edit files, or ask follow-up questions.",
input: [{
type: "text",
text: buildCodexImagePrompt(req),
text_elements: []
}, ...req.images.map((image) => ({
type: "image",
url: `data:${image.mime ?? "image/png"};base64,${image.buffer.toString("base64")}`
}))],
requiredModalities: ["text", "image"]
}),
model
};
}
async function runBoundedCodexVisionTurn(params) {
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.options.pluginConfig });
const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 100, 100);
const ownsClient = !params.options.clientFactory;
const client = params.options.clientFactory ? await params.options.clientFactory(appServer.start, params.profile) : await import("./shared-client-CYX2KUjs.js").then(({ createIsolatedCodexAppServerClient }) => createIsolatedCodexAppServerClient({
startOptions: appServer.start,
timeoutMs,
authProfileId: params.profile
}));
const abortController = new AbortController();
const timeout = setTimeout(() => abortController.abort("timeout"), timeoutMs);
timeout.unref?.();
try {
await assertCodexModelSupportsInput({
client,
model: params.model,
requiredModalities: params.requiredModalities,
timeoutMs,
signal: abortController.signal
});
const thread = assertCodexThreadStartResponse(await client.request("thread/start", {
model: params.model,
modelProvider: "openai",
cwd: params.agentDir || process.cwd(),
approvalPolicy: "on-request",
sandbox: "read-only",
serviceName: "OpenClaw",
developerInstructions: params.developerInstructions,
config: buildCodexRuntimeThreadConfig(void 0, { nativeCodeModeEnabled: false }),
environments: [],
dynamicTools: [],
experimentalRawEvents: true,
persistExtendedHistory: false,
ephemeral: true
}, {
timeoutMs,
signal: abortController.signal
}));
const collector = createCodexTurnCollector(thread.thread.id, params.taskLabel);
const cleanup = client.addNotificationHandler(collector.handleNotification);
const requestCleanup = client.addRequestHandler(denyCodexImageApprovalRequest);
try {
const turn = assertCodexTurnStartResponse(await client.request("turn/start", {
threadId: thread.thread.id,
input: params.input,
cwd: params.agentDir || process.cwd(),
approvalPolicy: "on-request",
model: params.model,
effort: "low"
}, {
timeoutMs,
signal: abortController.signal
}));
return await collector.collect(turn.turn, {
timeoutMs,
signal: abortController.signal
});
} finally {
requestCleanup();
cleanup();
}
} finally {
clearTimeout(timeout);
if (ownsClient) client.close();
}
}
async function extractCodexStructured(req, options) {
const model = req.model.trim();
if (!model) throw new Error("Codex structured extraction requires model id.");
if (!req.instructions.trim()) throw new Error("Codex structured extraction requires instructions.");
if (req.input.length === 0) throw new Error("Codex structured extraction requires at least one input.");
if (!req.input.some((entry) => entry.type === "image")) throw new Error("Codex structured extraction requires at least one image input.");
return normalizeStructuredExtractionResult({
text: await runBoundedCodexVisionTurn({
model,
profile: req.profile,
timeoutMs: req.timeoutMs,
agentDir: req.agentDir,
options,
taskLabel: "structured extraction",
developerInstructions: "You are OpenClaw's bounded structured-extraction worker. Return only the requested extraction. Do not call tools, edit files, ask follow-up questions, or include secrets.",
input: buildCodexStructuredInput(req),
requiredModalities: requiredStructuredModalities()
}),
model,
provider: req.provider,
req
});
}
function denyCodexImageApprovalRequest(request) {
if (request.method === "item/commandExecution/requestApproval" || request.method === "item/fileChange/requestApproval") return {
decision: "decline",
reason: "OpenClaw Codex image understanding does not grant tool or file approvals."
};
if (request.method === "item/permissions/requestApproval") return {
permissions: {},
scope: "turn"
};
if (request.method.includes("requestApproval")) return {
decision: "decline",
reason: "OpenClaw Codex image understanding does not grant native approvals."
};
if (request.method === "mcpServer/elicitation/request") return { action: "decline" };
}
async function assertCodexModelSupportsInput(params) {
const match = readModelListResult(await params.client.request("model/list", {
limit: 100,
cursor: null,
includeHidden: false
}, {
timeoutMs: Math.min(params.timeoutMs, 5e3),
signal: params.signal
})).models.find((entry) => entry.model === params.model || entry.id === params.model);
if (!match) throw new Error(`Codex app-server model not found: ${params.model}`);
if (params.requiredModalities.includes("image") && !match.inputModalities.includes("image")) throw new Error(`Codex app-server model does not support images: ${params.model}`);
if (params.requiredModalities.includes("text") && !match.inputModalities.includes("text")) throw new Error(`Codex app-server model does not support text: ${params.model}`);
}
function buildCodexImagePrompt(req) {
const prompt = req.prompt?.trim() || DEFAULT_CODEX_IMAGE_PROMPT;
if (req.images.length <= 1) return prompt;
return `${prompt}\n\nAnalyze all ${req.images.length} images together.`;
}
function requiredStructuredModalities() {
return ["text", "image"];
}
function buildCodexStructuredInput(req) {
return [{
type: "text",
text: buildStructuredExtractionPrompt(req),
text_elements: []
}, ...req.input.map((entry) => {
if (entry.type === "text") return {
type: "text",
text: entry.text,
text_elements: []
};
return {
type: "image",
url: `data:${entry.mime ?? "image/png"};base64,${entry.buffer.toString("base64")}`
};
})];
}
function buildStructuredExtractionPrompt(req) {
return [
req.instructions.trim(),
req.schemaName ? `Schema name: ${req.schemaName}` : void 0,
req.jsonSchema ? `JSON schema:\n${JSON.stringify(req.jsonSchema)}` : void 0,
req.jsonMode === false ? "Return the extraction as concise text." : "Return valid JSON only. Do not wrap the JSON in Markdown fences."
].filter((part) => Boolean(part)).join("\n\n");
}
function isJsonSchemaObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeStructuredExtractionResult(params) {
const result = {
text: params.text,
model: params.model,
provider: params.provider,
contentType: params.req.jsonMode === false ? "text" : "json"
};
if (params.req.jsonMode !== false) {
try {
result.parsed = JSON.parse(params.text);
} catch {
throw new Error("Codex structured extraction returned invalid JSON.");
}
if (isJsonSchemaObject(params.req.jsonSchema)) {
const validation = validateJsonSchemaValue({
schema: params.req.jsonSchema,
cacheKey: "codex.media-understanding.extractStructured",
value: result.parsed,
cache: false
});
if (!validation.ok) {
const message = validation.errors.map((error) => error.text).join("; ") || "invalid";
throw new Error(`Codex structured extraction JSON did not match schema: ${message}`);
}
result.parsed = validation.value;
}
}
return result;
}
function createCodexTurnCollector(threadId, taskLabel) {
let turnId;
let completedTurn;
let promptError;
const pending = [];
const assistantTextByItem = /* @__PURE__ */ new Map();
const assistantItemOrder = [];
let resolveCompletion;
const completion = new Promise((resolve) => {
resolveCompletion = resolve;
});
const rememberAssistantText = (itemId, text) => {
if (!text) return;
if (!assistantTextByItem.has(itemId)) assistantItemOrder.push(itemId);
assistantTextByItem.set(itemId, text);
};
const handleNotification = (notification) => {
const params = isJsonObject(notification.params) ? notification.params : void 0;
if (!params || readString(params, "threadId") !== threadId) return;
if (!turnId) {
pending.push(notification);
return;
}
if (readNotificationTurnId(params) !== turnId) return;
if (notification.method === "item/agentMessage/delta") {
const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "assistant";
const delta = readString(params, "delta") ?? "";
rememberAssistantText(itemId, `${assistantTextByItem.get(itemId) ?? ""}${delta}`);
return;
}
if (notification.method === "turn/completed") {
completedTurn = readCodexTurnCompletedNotification(notification.params)?.turn ?? completedTurn;
resolveCompletion?.();
return;
}
if (notification.method === "error") {
promptError = readCodexErrorNotification(notification.params)?.error.message ?? `codex app-server ${taskLabel} turn failed`;
resolveCompletion?.();
}
};
return {
handleNotification,
async collect(startedTurn, options) {
turnId = startedTurn.id;
if (isTerminalTurn(startedTurn)) completedTurn = startedTurn;
for (const notification of pending.splice(0)) handleNotification(notification);
if (!completedTurn && !promptError) await waitForTurnCompletion({
completion,
timeoutMs: options.timeoutMs,
signal: options.signal,
taskLabel
});
if (promptError) throw new Error(promptError);
if (completedTurn?.status === "failed") throw new Error(completedTurn.error?.message ?? `codex app-server ${taskLabel} turn failed`);
const itemText = collectAssistantTextFromItems(completedTurn?.items);
const deltaText = assistantItemOrder.map((itemId) => assistantTextByItem.get(itemId)?.trim()).filter((text) => Boolean(text)).join("\n\n").trim();
const text = (itemText || deltaText).trim();
if (!text) throw new Error(`Codex app-server ${taskLabel} turn returned no text.`);
return text;
}
};
}
async function waitForTurnCompletion(params) {
let timeout;
let cleanupAbort;
try {
await Promise.race([params.completion, new Promise((_, reject) => {
timeout = setTimeout(() => reject(/* @__PURE__ */ new Error(`codex app-server ${params.taskLabel} turn timed out`)), params.timeoutMs);
timeout.unref?.();
const abortListener = () => reject(/* @__PURE__ */ new Error(`codex app-server ${params.taskLabel} turn aborted`));
params.signal.addEventListener("abort", abortListener, { once: true });
cleanupAbort = () => params.signal.removeEventListener("abort", abortListener);
})]);
} finally {
if (timeout) clearTimeout(timeout);
cleanupAbort?.();
}
}
function collectAssistantTextFromItems(items) {
return (items ?? []).filter((item) => item.type === "agentMessage").map((item) => item.text.trim()).filter(Boolean).join("\n\n").trim();
}
function readNotificationTurnId(record) {
const direct = readString(record, "turnId");
if (direct) return direct;
return isJsonObject(record.turn) ? readString(record.turn, "id") : void 0;
}
function readString(record, key) {
const value = record[key];
return typeof value === "string" ? value : void 0;
}
function isTerminalTurn(turn) {
return turn.status === "completed" || turn.status === "interrupted" || turn.status === "failed";
}
//#endregion
export { buildCodexMediaUnderstandingProvider as t };