Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
217 lines
7.3 KiB
TypeScript
217 lines
7.3 KiB
TypeScript
/**
|
|
* Codex-backed media understanding provider for bounded image description and
|
|
* structured extraction turns.
|
|
*/
|
|
import {
|
|
type JsonSchemaObject,
|
|
validateJsonSchemaValue,
|
|
} from "openclaw/plugin-sdk/json-schema-runtime";
|
|
import type {
|
|
ImagesDescriptionRequest,
|
|
ImagesDescriptionResult,
|
|
MediaUnderstandingProvider,
|
|
StructuredExtractionRequest,
|
|
StructuredExtractionResult,
|
|
} from "openclaw/plugin-sdk/media-understanding";
|
|
import { CODEX_PROVIDER_ID, FALLBACK_CODEX_MODELS } from "./provider-catalog.js";
|
|
import {
|
|
runBoundedCodexAppServerTurn,
|
|
type CodexBoundedTurnOptions,
|
|
} from "./src/app-server/bounded-turn.js";
|
|
import type { CodexUserInput } from "./src/app-server/protocol.js";
|
|
|
|
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.";
|
|
|
|
export type CodexMediaUnderstandingProviderOptions = CodexBoundedTurnOptions;
|
|
|
|
/**
|
|
* Builds the media-understanding provider that delegates image tasks to an
|
|
* isolated Codex app-server session.
|
|
*/
|
|
export function buildCodexMediaUnderstandingProvider(
|
|
options: CodexMediaUnderstandingProviderOptions = {},
|
|
): MediaUnderstandingProvider {
|
|
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: ImagesDescriptionRequest,
|
|
options: CodexMediaUnderstandingProviderOptions,
|
|
): Promise<ImagesDescriptionResult> {
|
|
const model = req.model.trim();
|
|
if (!model) {
|
|
throw new Error("Codex image understanding requires model id.");
|
|
}
|
|
|
|
const { text } = await runBoundedCodexAppServerTurn({
|
|
config: req.cfg,
|
|
model: { mode: "required", id: model },
|
|
profile: req.profile,
|
|
timeoutMs: req.timeoutMs,
|
|
agentDir: req.agentDir,
|
|
authProfileStore: req.authStore,
|
|
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" as const,
|
|
url: `data:${image.mime ?? "image/png"};base64,${image.buffer.toString("base64")}`,
|
|
})),
|
|
],
|
|
requiredModalities: ["text", "image"],
|
|
isolation: "configured-transport",
|
|
});
|
|
return { text, model };
|
|
}
|
|
|
|
async function extractCodexStructured(
|
|
req: StructuredExtractionRequest,
|
|
options: CodexMediaUnderstandingProviderOptions,
|
|
): Promise<StructuredExtractionResult> {
|
|
const model = req.model.trim();
|
|
if (!model) {
|
|
throw new Error("Codex structured extraction requires model id.");
|
|
}
|
|
const instructions = req.instructions.trim();
|
|
if (!instructions) {
|
|
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.");
|
|
}
|
|
|
|
const { text } = await runBoundedCodexAppServerTurn({
|
|
config: req.cfg,
|
|
model: { mode: "required", id: model },
|
|
profile: req.profile,
|
|
timeoutMs: req.timeoutMs,
|
|
agentDir: req.agentDir,
|
|
authProfileStore: req.authStore,
|
|
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(),
|
|
isolation: "configured-transport",
|
|
});
|
|
return normalizeStructuredExtractionResult({ text, model, provider: req.provider, req });
|
|
}
|
|
|
|
function buildCodexImagePrompt(req: ImagesDescriptionRequest): string {
|
|
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(): string[] {
|
|
return ["text", "image"];
|
|
}
|
|
|
|
function buildCodexStructuredInput(req: StructuredExtractionRequest): CodexUserInput[] {
|
|
return [
|
|
{ type: "text", text: buildStructuredExtractionPrompt(req), text_elements: [] },
|
|
...req.input.map((entry) => {
|
|
if (entry.type === "text") {
|
|
return { type: "text" as const, text: entry.text, text_elements: [] };
|
|
}
|
|
return {
|
|
type: "image" as const,
|
|
url: `data:${entry.mime ?? "image/png"};base64,${entry.buffer.toString("base64")}`,
|
|
};
|
|
}),
|
|
];
|
|
}
|
|
|
|
function buildStructuredExtractionPrompt(req: StructuredExtractionRequest): string {
|
|
return [
|
|
req.instructions.trim(),
|
|
req.schemaName ? `Schema name: ${req.schemaName}` : undefined,
|
|
req.jsonSchema ? `JSON schema:\n${JSON.stringify(req.jsonSchema)}` : undefined,
|
|
req.jsonMode === false
|
|
? "Return the extraction as concise text."
|
|
: "Return valid JSON only. Do not wrap the JSON in Markdown fences.",
|
|
]
|
|
.filter((part): part is string => Boolean(part))
|
|
.join("\n\n");
|
|
}
|
|
|
|
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function normalizeStructuredExtractionResult(params: {
|
|
text: string;
|
|
model: string;
|
|
provider: string;
|
|
req: StructuredExtractionRequest;
|
|
}): StructuredExtractionResult {
|
|
const result: StructuredExtractionResult = {
|
|
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;
|
|
}
|