UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,253 lines (1,252 loc) 38.1 kB
import { D as resolveIntegerOption } from "./number-coercion-CJQ8TR--.js"; import { n as defaultRuntime } from "./runtime-B4lgFmsS.js"; import { At as boolean, Bt as discriminatedUnion, Et as array, Nn as record, Rn as string, Tn as object, Xn as union, Zn as unknown, dn as literal, wn as number, yt as _enum } from "./schemas-6cH6bZ7o.js"; import { n as ZodIssueCode } from "./compat-B-THjfQ8.js"; import { a as logWarn } from "./logger-lqqYRtFw.js"; import { i as emitAgentEvent, l as onAgentEvent } from "./agent-events-C1B8VhOg.js"; import { i as isClientToolNameConflictError } from "./tool-split-jrKfShnK.js"; import { f as extractFileContentFromSource, g as resolveInputFileLimits, m as normalizeMimeList, p as extractImageContentFromSource, u as DEFAULT_INPUT_IMAGE_MIMES } from "./runner.entries-B23jIAju.js"; import { i as wrapExternalContent } from "./external-content-pX-Pk1Iu.js"; import { t as renderFileContextBlock } from "./file-context-M6CFQU8x.js"; import { n as agentCommandFromIngress } from "./agent-command-DimMXeog.js"; import { t as createDefaultDeps } from "./deps-CWqGdIJS.js"; import "./agent-COt_BYfw.js"; import { a as sendJson, d as writeDone, l as setSseHeaders, u as watchClientDisconnect } from "./http-common-DunL6val.js"; import { a as getHeader, i as getBearerToken, l as resolveOpenAiCompatibleHttpOperatorScopes } from "./http-auth-utils-Du8mRZIw.js"; import { a as resolveGatewayRequestContext, o as resolveOpenAiCompatModelOverride, r as resolveAgentIdForRequest } from "./http-utils-leZWdpma.js"; import { t as handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers-BkKm9tZp.js"; import { c as resolveAssistantStreamDeltaText, i as resolveOpenAiCompatError, n as resolveUnsatisfiedToolChoiceMessage, o as normalizeInputHostnameAllowlist, r as toolChoiceConstraintPrompt, s as buildAgentMessageFromConversationEntries, t as isToolChoiceConstraintSatisfied } from "./openai-tool-choice-CG8IZWcK.js"; import { createHash, randomUUID } from "node:crypto"; //#region src/gateway/open-responses.schema.ts /** * OpenResponses API Zod Schemas * * Zod schemas for the OpenResponses `/v1/responses` endpoint. * This module is isolated from gateway imports to enable future codegen and prevent drift. * * @see https://www.open-responses.com/ */ const InputTextContentPartSchema = object({ type: literal("input_text"), text: string() }).strict(); const OutputTextContentPartSchema = object({ type: literal("output_text"), text: string() }).strict(); const InputImageSourceSchema = discriminatedUnion("type", [object({ type: literal("url"), url: string().url() }), object({ type: literal("base64"), media_type: _enum([ "image/jpeg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif" ]), data: string().min(1) })]); const InputImageContentPartSchema = object({ type: literal("input_image"), source: InputImageSourceSchema }).strict(); const InputFileSourceSchema = discriminatedUnion("type", [object({ type: literal("url"), url: string().url() }), object({ type: literal("base64"), media_type: string().min(1), data: string().min(1), filename: string().optional() })]); const ContentPartSchema = discriminatedUnion("type", [ InputTextContentPartSchema, OutputTextContentPartSchema, InputImageContentPartSchema, object({ type: literal("input_file"), source: InputFileSourceSchema }).strict() ]); const MessageItemRoleSchema = _enum([ "system", "developer", "user", "assistant" ]); const AssistantPhaseSchema = _enum(["commentary", "final_answer"]); const ItemParamSchema = discriminatedUnion("type", [ object({ type: literal("message"), role: MessageItemRoleSchema, content: union([string(), array(ContentPartSchema)]), phase: AssistantPhaseSchema.optional() }).strict().superRefine((value, ctx) => { if (value.phase !== void 0 && value.role !== "assistant") ctx.addIssue({ code: ZodIssueCode.custom, path: ["phase"], message: "`phase` is only valid on assistant messages." }); }), object({ type: literal("function_call"), id: string().optional(), call_id: string().optional(), name: string(), arguments: string() }).strict(), object({ type: literal("function_call_output"), call_id: string(), output: string() }).strict(), object({ type: literal("reasoning"), content: string().optional(), encrypted_content: string().optional(), summary: string().optional() }).strict(), object({ type: literal("item_reference"), id: string() }).strict() ]); const ToolDefinitionSchema = object({ type: literal("function"), name: string().min(1, "Tool name cannot be empty"), description: string().optional(), parameters: record(string(), unknown()).optional(), strict: boolean().optional() }).strict(); const ToolChoiceSchema = union([ literal("auto"), literal("none"), literal("required"), object({ type: literal("function"), name: string().min(1) }).strict(), object({ type: literal("function"), function: object({ name: string().min(1) }) }).strict() ]); const CreateResponseBodySchema = object({ model: string(), input: union([string(), array(ItemParamSchema)]), instructions: string().optional(), tools: array(ToolDefinitionSchema).optional(), tool_choice: ToolChoiceSchema.optional(), stream: boolean().optional(), max_output_tokens: number().int().positive().optional(), max_tool_calls: number().int().positive().optional(), user: string().optional(), temperature: number().min(0).max(2).optional(), top_p: number().min(0).max(1).optional(), metadata: record(string(), string()).optional(), store: boolean().optional(), previous_response_id: string().optional(), reasoning: object({ effort: _enum([ "low", "medium", "high" ]).optional(), summary: _enum([ "auto", "concise", "detailed" ]).optional() }).optional(), truncation: _enum(["auto", "disabled"]).optional() }).strict(); const ResponseStatusSchema = _enum([ "in_progress", "completed", "failed", "cancelled", "incomplete" ]); const OutputItemSchema = discriminatedUnion("type", [ object({ type: literal("message"), id: string(), role: literal("assistant"), content: array(OutputTextContentPartSchema), phase: AssistantPhaseSchema.optional(), status: _enum(["in_progress", "completed"]).optional() }).strict(), object({ type: literal("function_call"), id: string(), call_id: string(), name: string(), arguments: string(), status: _enum(["in_progress", "completed"]).optional() }).strict(), object({ type: literal("reasoning"), id: string(), content: string().optional(), summary: string().optional() }).strict() ]); const UsageSchema = object({ input_tokens: number().int().nonnegative(), output_tokens: number().int().nonnegative(), total_tokens: number().int().nonnegative() }); const ResponseResourceSchema = object({ id: string(), object: literal("response"), created_at: number().int(), status: ResponseStatusSchema, model: string(), output: array(OutputItemSchema), usage: UsageSchema, error: object({ code: string(), message: string() }).optional() }); object({ type: literal("response.created"), response: ResponseResourceSchema }); object({ type: literal("response.in_progress"), response: ResponseResourceSchema }); object({ type: literal("response.completed"), response: ResponseResourceSchema }); object({ type: literal("response.failed"), response: ResponseResourceSchema }); object({ type: literal("response.output_item.added"), output_index: number().int().nonnegative(), item: OutputItemSchema }); object({ type: literal("response.output_item.done"), output_index: number().int().nonnegative(), item: OutputItemSchema }); object({ type: literal("response.content_part.added"), item_id: string(), output_index: number().int().nonnegative(), content_index: number().int().nonnegative(), part: OutputTextContentPartSchema }); object({ type: literal("response.content_part.done"), item_id: string(), output_index: number().int().nonnegative(), content_index: number().int().nonnegative(), part: OutputTextContentPartSchema }); object({ type: literal("response.output_text.delta"), item_id: string(), output_index: number().int().nonnegative(), content_index: number().int().nonnegative(), delta: string() }); object({ type: literal("response.output_text.done"), item_id: string(), output_index: number().int().nonnegative(), content_index: number().int().nonnegative(), text: string() }); //#endregion //#region src/gateway/openresponses-file-content.ts /** Wraps untrusted file content for OpenResponses input blocks. */ function wrapUntrustedFileContent(content) { return wrapExternalContent(content, { source: "unknown", includeWarning: false }); } //#endregion //#region src/gateway/openresponses-prompt.ts function extractTextContent(content) { if (typeof content === "string") return content; return content.map((part) => { if (part.type === "input_text") return part.text; if (part.type === "output_text") return part.text; return ""; }).filter(Boolean).join("\n"); } /** Build the user message and optional system prompt from Responses API input. */ function buildAgentPrompt(input) { if (typeof input === "string") return { message: input }; const systemParts = []; const conversationEntries = []; for (const item of input) if (item.type === "message") { const content = extractTextContent(item.content).trim(); if (!content) continue; if (item.role === "system" || item.role === "developer") { systemParts.push(content); continue; } const normalizedRole = item.role === "assistant" ? "assistant" : "user"; const sender = normalizedRole === "assistant" ? "Assistant" : "User"; conversationEntries.push({ role: normalizedRole, entry: { sender, body: content } }); } else if (item.type === "function_call_output") conversationEntries.push({ role: "tool", entry: { sender: `Tool:${item.call_id}`, body: item.output } }); return { message: buildAgentMessageFromConversationEntries(conversationEntries), extraSystemPrompt: systemParts.length > 0 ? systemParts.join("\n\n") : void 0 }; } //#endregion //#region src/gateway/openresponses-shape.ts /** Creates an assistant output message item for OpenResponses-compatible responses. */ function createAssistantOutputItem(params) { return { type: "message", id: params.id, role: "assistant", content: [{ type: "output_text", text: params.text }], ...params.phase ? { phase: params.phase } : {}, status: params.status }; } /** Creates a function-call output item for OpenResponses-compatible responses. */ function createFunctionCallOutputItem(params) { return { type: "function_call", id: params.id, call_id: params.callId, name: params.name, arguments: params.arguments, status: params.status }; } //#endregion //#region src/gateway/openresponses-http.ts /** * OpenResponses HTTP Handler * * Implements the OpenResponses `/v1/responses` endpoint for OpenClaw Gateway. * * @see https://www.open-responses.com/ */ const DEFAULT_BODY_BYTES = 20 * 1024 * 1024; const DEFAULT_MAX_URL_PARTS = 8; const RESPONSE_SESSION_TTL_MS = 1800 * 1e3; const MAX_RESPONSE_SESSION_ENTRIES = 500; const responseSessionMap = /* @__PURE__ */ new Map(); function normalizeResponseSessionScope(scope) { const authSubject = scope.authSubject.trim(); const requestedSessionKey = scope.requestedSessionKey?.trim(); return { authSubject, agentId: scope.agentId, requestedSessionKey: requestedSessionKey || void 0 }; } function resolveResponseSessionAuthSubject(params) { const bearer = getBearerToken(params.req); if (bearer) return `bearer:${createHash("sha256").update(bearer).digest("hex")}`; if (params.auth.mode === "trusted-proxy" && params.auth.trustedProxy?.userHeader) { const user = getHeader(params.req, params.auth.trustedProxy.userHeader)?.trim(); if (user) return `trusted-proxy:${user}`; } return `gateway-auth:${params.auth.mode}`; } function createResponseSessionScope(params) { return normalizeResponseSessionScope({ authSubject: resolveResponseSessionAuthSubject({ req: params.req, auth: params.auth }), agentId: params.agentId, requestedSessionKey: getHeader(params.req, "x-openclaw-session-key") }); } function matchesResponseSessionScope(entry, scope) { return entry.authSubject === scope.authSubject && entry.agentId === scope.agentId && entry.requestedSessionKey === scope.requestedSessionKey; } function pruneExpiredResponseSessions(now) { while (responseSessionMap.size > 0) { const oldest = responseSessionMap.entries().next().value; if (!oldest) return; const [oldestKey, oldestValue] = oldest; if (now - oldestValue.ts <= RESPONSE_SESSION_TTL_MS) return; responseSessionMap.delete(oldestKey); } } function evictOverflowResponseSessions() { while (responseSessionMap.size > MAX_RESPONSE_SESSION_ENTRIES) { const oldestKey = responseSessionMap.keys().next().value; if (!oldestKey) return; responseSessionMap.delete(oldestKey); } } function storeResponseSession(responseId, sessionKey, scope, now = Date.now()) { responseSessionMap.delete(responseId); responseSessionMap.set(responseId, { ...scope, sessionKey, ts: now }); pruneExpiredResponseSessions(now); evictOverflowResponseSessions(); } function lookupResponseSession(responseId, scope, now = Date.now()) { if (!responseId) return; const entry = responseSessionMap.get(responseId); if (!entry) return; if (now - entry.ts > RESPONSE_SESSION_TTL_MS) { responseSessionMap.delete(responseId); return; } if (!matchesResponseSessionScope(entry, scope)) return; return entry.sessionKey; } const testing = { resetResponseSessionState() { responseSessionMap.clear(); }, wrapUntrustedFileContent, storeResponseSessionAt(responseId, sessionKey, now, scope = { authSubject: "test", agentId: "main" }) { storeResponseSession(responseId, sessionKey, normalizeResponseSessionScope(scope), now); }, lookupResponseSessionAt(responseId, now, scope = { authSubject: "test", agentId: "main" }) { return lookupResponseSession(responseId, normalizeResponseSessionScope(scope), now); }, getResponseSessionIds() { return [...responseSessionMap.keys()]; }, resolveResponsesLimits }; function writeSseEvent(res, event) { res.write(`event: ${event.type}\n`); res.write(`data: ${JSON.stringify(event)}\n\n`); } function resolveResponsesLimits(config) { const files = config?.files; const images = config?.images; const fileLimits = resolveInputFileLimits(files); return { maxBodyBytes: config?.maxBodyBytes ?? DEFAULT_BODY_BYTES, maxUrlParts: resolveIntegerOption(config?.maxUrlParts, DEFAULT_MAX_URL_PARTS, { min: 0 }), files: { ...fileLimits, urlAllowlist: normalizeInputHostnameAllowlist(files?.urlAllowlist) }, images: { allowUrl: images?.allowUrl ?? true, urlAllowlist: normalizeInputHostnameAllowlist(images?.urlAllowlist), allowedMimes: normalizeMimeList(images?.allowedMimes, DEFAULT_INPUT_IMAGE_MIMES), maxBytes: images?.maxBytes ?? 10485760, maxRedirects: images?.maxRedirects ?? 3, timeoutMs: images?.timeoutMs ?? 1e4 } }; } function extractClientTools(body) { return (body.tools ?? []).map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters, strict: tool.strict } })); } function applyToolChoice(params) { const { tools, toolChoice } = params; if (!toolChoice) return { tools }; if (toolChoice === "none") return { tools: [] }; if (toolChoice === "required") { if (tools.length === 0) throw new Error("tool_choice=required but no tools were provided"); const constraint = { type: "required" }; return { tools, extraSystemPrompt: toolChoiceConstraintPrompt(constraint), constraint }; } if (typeof toolChoice === "object" && toolChoice.type === "function") { const targetName = ("name" in toolChoice ? toolChoice.name : toolChoice.function.name).trim(); if (!targetName) throw new Error("tool_choice.name is required"); const matched = tools.filter((tool) => tool.function?.name === targetName); if (matched.length === 0) throw new Error(`tool_choice requested unknown tool: ${targetName}`); const constraint = { type: "function", name: targetName }; return { tools: matched, extraSystemPrompt: toolChoiceConstraintPrompt(constraint), constraint }; } return { tools }; } function createEmptyUsage() { return { input_tokens: 0, output_tokens: 0, total_tokens: 0 }; } function toUsage(value) { if (!value) return createEmptyUsage(); const input = value.input ?? 0; const output = value.output ?? 0; const cacheRead = value.cacheRead ?? 0; const cacheWrite = value.cacheWrite ?? 0; const total = value.total ?? input + output + cacheRead + cacheWrite; return { input_tokens: Math.max(0, input), output_tokens: Math.max(0, output), total_tokens: Math.max(0, total) }; } function extractUsageFromResult(result) { const meta = result?.meta; return toUsage(meta && typeof meta === "object" ? meta.agentMeta?.usage : void 0); } function resolveStopReasonAndPendingToolCalls(meta) { if (!meta || typeof meta !== "object") return { stopReason: void 0, pendingToolCalls: void 0 }; const record = meta; return { stopReason: record.stopReason, pendingToolCalls: record.pendingToolCalls }; } function createResponseResource(params) { return { id: params.id, object: "response", created_at: Math.floor(Date.now() / 1e3), status: params.status, model: params.model, output: params.output, usage: params.usage ?? createEmptyUsage(), error: params.error }; } async function runResponsesAgentCommand(params) { return agentCommandFromIngress({ message: params.message, images: params.images.length > 0 ? params.images : void 0, clientTools: params.clientTools.length > 0 ? params.clientTools : void 0, extraSystemPrompt: params.extraSystemPrompt || void 0, model: params.modelOverride, streamParams: params.streamParams ?? void 0, sessionKey: params.sessionKey, runId: params.runId, deliver: false, messageChannel: params.messageChannel, bestEffortDeliver: false, allowModelOverride: true, abortSignal: params.abortSignal }, defaultRuntime, params.deps); } async function handleOpenResponsesHttpRequest(req, res, opts) { const limits = resolveResponsesLimits(opts.config); const maxBodyBytes = opts.maxBodyBytes ?? (opts.config?.maxBodyBytes ? limits.maxBodyBytes : Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2)); const handled = await handleGatewayPostJsonEndpoint(req, res, { pathname: "/v1/responses", requiredOperatorMethod: "chat.send", resolveOperatorScopes: resolveOpenAiCompatibleHttpOperatorScopes, auth: opts.auth, trustedProxies: opts.trustedProxies, allowRealIpFallback: opts.allowRealIpFallback, rateLimiter: opts.rateLimiter, maxBodyBytes }); if (handled === false) return false; if (!handled) return true; const parseResult = CreateResponseBodySchema.safeParse(handled.body); if (!parseResult.success) { const issue = parseResult.error.issues[0]; sendJson(res, 400, { error: { message: issue ? `${issue.path.join(".")}: ${issue.message}` : "Invalid request body", type: "invalid_request_error" } }); return true; } const payload = parseResult.data; const stream = Boolean(payload.stream); const model = payload.model; const user = payload.user; const { modelOverride, errorMessage: modelError } = await resolveOpenAiCompatModelOverride({ req, agentId: resolveAgentIdForRequest({ req, model }), model }); if (modelError) { sendJson(res, 400, { error: { message: modelError, type: "invalid_request_error" } }); return true; } let images = []; const fileContexts = []; let urlParts = 0; const markUrlPart = () => { urlParts += 1; if (urlParts > limits.maxUrlParts) throw new Error(`Too many URL-based input sources: ${urlParts} (limit: ${limits.maxUrlParts})`); }; try { if (Array.isArray(payload.input)) { for (const item of payload.input) if (item.type === "message" && typeof item.content !== "string") for (const part of item.content) { if (part.type === "input_image") { const source = part.source; const sourceType = source.type === "base64" || source.type === "url" ? source.type : void 0; if (!sourceType) throw new Error("input_image must have 'source.url' or 'source.data'"); if (sourceType === "url") markUrlPart(); const image = await extractImageContentFromSource(sourceType === "url" ? { type: "url", url: source.url ?? "", mediaType: source.media_type } : { type: "base64", data: source.data ?? "", mediaType: source.media_type }, limits.images); images.push(image); continue; } if (part.type === "input_file") { const source = part.source; const sourceType = source.type === "base64" || source.type === "url" ? source.type : void 0; if (!sourceType) throw new Error("input_file must have 'source.url' or 'source.data'"); if (sourceType === "url") markUrlPart(); const file = await extractFileContentFromSource({ source: sourceType === "url" ? { type: "url", url: source.url ?? "", mediaType: source.media_type, filename: source.filename } : { type: "base64", data: source.data ?? "", mediaType: source.media_type, filename: source.filename }, limits: limits.files }); const rawText = file.text; if (rawText?.trim()) fileContexts.push(renderFileContextBlock({ filename: file.filename, content: wrapUntrustedFileContent(rawText) })); else if (file.images && file.images.length > 0) fileContexts.push(renderFileContextBlock({ filename: file.filename, content: "[PDF content rendered to images]", surroundContentWithNewlines: false })); if (file.images && file.images.length > 0) images = images.concat(file.images); } } } } catch (err) { logWarn(`openresponses: request parsing failed: ${String(err)}`); sendJson(res, 400, { error: { message: "invalid request", type: "invalid_request_error" } }); return true; } const clientTools = extractClientTools(payload); let toolChoicePrompt; let toolChoiceConstraint; let resolvedClientTools = clientTools; try { const toolChoiceResult = applyToolChoice({ tools: clientTools, toolChoice: payload.tool_choice }); resolvedClientTools = toolChoiceResult.tools; toolChoicePrompt = toolChoiceResult.extraSystemPrompt; toolChoiceConstraint = toolChoiceResult.constraint; } catch (err) { logWarn(`openresponses: tool configuration failed: ${String(err)}`); sendJson(res, 400, { error: { message: "invalid tool configuration", type: "invalid_request_error" } }); return true; } const resolved = resolveGatewayRequestContext({ req, model, user, sessionPrefix: "openresponses", defaultMessageChannel: "webchat", useMessageChannelHeader: true }); const responseSessionScope = createResponseSessionScope({ req, auth: opts.auth, agentId: resolved.agentId }); const sessionKey = lookupResponseSession(payload.previous_response_id, responseSessionScope) ?? resolved.sessionKey; const messageChannel = resolved.messageChannel; const prompt = buildAgentPrompt(payload.input); const fileContext = fileContexts.length > 0 ? fileContexts.join("\n\n") : void 0; const toolChoiceContext = toolChoicePrompt?.trim(); const extraSystemPrompt = [ payload.instructions, prompt.extraSystemPrompt, toolChoiceContext, fileContext ].filter(Boolean).join("\n\n"); if (!prompt.message) { sendJson(res, 400, { error: { message: "Missing user message in `input`.", type: "invalid_request_error" } }); return true; } const responseId = `resp_${randomUUID()}`; const rememberResponseSession = () => storeResponseSession(responseId, sessionKey, responseSessionScope); const outputItemId = `msg_${randomUUID()}`; const deps = createDefaultDeps(); const abortController = new AbortController(); const streamMaxTokens = typeof payload.max_output_tokens === "number" ? payload.max_output_tokens : void 0; const streamTemperature = typeof payload.temperature === "number" ? payload.temperature : void 0; const streamTopP = typeof payload.top_p === "number" ? payload.top_p : void 0; const streamParams = streamMaxTokens !== void 0 || streamTemperature !== void 0 || streamTopP !== void 0 ? { ...streamMaxTokens !== void 0 ? { maxTokens: streamMaxTokens } : {}, ...streamTemperature !== void 0 ? { temperature: streamTemperature } : {}, ...streamTopP !== void 0 ? { topP: streamTopP } : {} } : void 0; if (!stream) { const stopWatchingDisconnect = watchClientDisconnect(req, res, abortController); try { const result = await runResponsesAgentCommand({ message: prompt.message, images, clientTools: resolvedClientTools, extraSystemPrompt, modelOverride, streamParams, sessionKey, runId: responseId, messageChannel, deps, abortSignal: abortController.signal }); if (abortController.signal.aborted) return true; const payloads = result?.payloads; const usage = extractUsageFromResult(result); const meta = result?.meta; const { stopReason, pendingToolCalls } = resolveStopReasonAndPendingToolCalls(meta); if (toolChoiceConstraint && !isToolChoiceConstraintSatisfied({ constraint: toolChoiceConstraint, pendingToolCalls })) { const failed = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "api_error", message: resolveUnsatisfiedToolChoiceMessage(toolChoiceConstraint) }, usage }); rememberResponseSession(); sendJson(res, 502, failed); return true; } if (stopReason === "tool_calls" && pendingToolCalls && pendingToolCalls.length > 0) { const assistantText = Array.isArray(payloads) && payloads.length > 0 ? payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n") : ""; const output = []; if (assistantText) output.push(createAssistantOutputItem({ id: outputItemId, text: assistantText, phase: "commentary", status: "completed" })); for (const functionCall of pendingToolCalls) output.push(createFunctionCallOutputItem({ id: `call_${randomUUID()}`, callId: functionCall.id, name: functionCall.name, arguments: functionCall.arguments })); const response = createResponseResource({ id: responseId, model, status: "incomplete", output, usage }); rememberResponseSession(); sendJson(res, 200, response); return true; } const response = createResponseResource({ id: responseId, model, status: "completed", output: [createAssistantOutputItem({ id: outputItemId, text: Array.isArray(payloads) && payloads.length > 0 ? payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n") : "No response from OpenClaw.", phase: "final_answer", status: "completed" })], usage }); rememberResponseSession(); sendJson(res, 200, response); } catch (err) { if (abortController.signal.aborted) return true; logWarn(`openresponses: non-stream response failed: ${String(err)}`); if (isClientToolNameConflictError(err)) { sendJson(res, 400, createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "invalid_request_error", message: "invalid tool configuration" } })); return true; } const response = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "api_error", message: "internal error" } }); const mapped = resolveOpenAiCompatError(err); if (mapped) { const mappedResponse = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: mapped.error.type, message: mapped.error.message } }); rememberResponseSession(); sendJson(res, mapped.status, mappedResponse); return true; } rememberResponseSession(); sendJson(res, 500, response); } finally { stopWatchingDisconnect(); } return true; } setSseHeaders(res); let accumulatedText = ""; let sawAssistantDelta = false; let closed = false; let unsubscribe = () => {}; let stopWatchingDisconnect = () => {}; let finalUsage; let finalizeRequested = null; const maybeFinalize = () => { if (closed) return; if (!finalizeRequested) return; if (!finalUsage) return; const usage = finalUsage; closed = true; stopWatchingDisconnect(); unsubscribe(); writeSseEvent(res, { type: "response.output_text.done", item_id: outputItemId, output_index: 0, content_index: 0, text: finalizeRequested.text }); writeSseEvent(res, { type: "response.content_part.done", item_id: outputItemId, output_index: 0, content_index: 0, part: { type: "output_text", text: finalizeRequested.text } }); const completedItem = createAssistantOutputItem({ id: outputItemId, text: finalizeRequested.text, phase: finalizeRequested.status === "completed" ? "final_answer" : "commentary", status: "completed" }); writeSseEvent(res, { type: "response.output_item.done", output_index: 0, item: completedItem }); const finalResponse = createResponseResource({ id: responseId, model, status: finalizeRequested.status, output: [completedItem], usage }); rememberResponseSession(); writeSseEvent(res, { type: "response.completed", response: finalResponse }); writeDone(res); res.end(); }; const requestFinalize = (status, text) => { if (finalizeRequested) return; finalizeRequested = { status, text }; maybeFinalize(); }; const initialResponse = createResponseResource({ id: responseId, model, status: "in_progress", output: [] }); writeSseEvent(res, { type: "response.created", response: initialResponse }); writeSseEvent(res, { type: "response.in_progress", response: initialResponse }); writeSseEvent(res, { type: "response.output_item.added", output_index: 0, item: createAssistantOutputItem({ id: outputItemId, text: "", status: "in_progress" }) }); writeSseEvent(res, { type: "response.content_part.added", item_id: outputItemId, output_index: 0, content_index: 0, part: { type: "output_text", text: "" } }); unsubscribe = onAgentEvent((evt) => { if (evt.runId !== responseId) return; if (closed) return; if (evt.stream === "assistant") { const text = evt.data?.text; if (evt.data?.replace === true && typeof text === "string") accumulatedText = text; const content = resolveAssistantStreamDeltaText(evt); if (!content) return; if (toolChoiceConstraint) { accumulatedText += content; return; } sawAssistantDelta = true; accumulatedText += content; writeSseEvent(res, { type: "response.output_text.delta", item_id: outputItemId, output_index: 0, content_index: 0, delta: content }); return; } if (evt.stream === "lifecycle") { const phase = evt.data?.phase; if (phase === "end" || phase === "error") requestFinalize(phase === "error" ? "failed" : "completed", accumulatedText || "No response from OpenClaw."); } }); stopWatchingDisconnect = watchClientDisconnect(req, res, abortController, () => { closed = true; unsubscribe(); }); (async () => { try { const result = await runResponsesAgentCommand({ message: prompt.message, images, clientTools: resolvedClientTools, extraSystemPrompt, modelOverride, streamParams, sessionKey, runId: responseId, messageChannel, deps, abortSignal: abortController.signal }); finalUsage = extractUsageFromResult(result); const resultAny = result; const meta = resultAny.meta; const { stopReason, pendingToolCalls } = resolveStopReasonAndPendingToolCalls(meta); if (!closed && toolChoiceConstraint && !isToolChoiceConstraintSatisfied({ constraint: toolChoiceConstraint, pendingToolCalls })) { const failed = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "api_error", message: resolveUnsatisfiedToolChoiceMessage(toolChoiceConstraint) }, usage: finalUsage ?? createEmptyUsage() }); closed = true; stopWatchingDisconnect(); unsubscribe(); rememberResponseSession(); writeSseEvent(res, { type: "response.failed", response: failed }); writeDone(res); res.end(); return; } if (!closed && stopReason === "tool_calls" && pendingToolCalls && pendingToolCalls.length > 0) { const usage = finalUsage ?? createEmptyUsage(); const finalText = accumulatedText || (Array.isArray(resultAny.payloads) ? resultAny.payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n") : ""); if (toolChoiceConstraint && accumulatedText) { sawAssistantDelta = true; writeSseEvent(res, { type: "response.output_text.delta", item_id: outputItemId, output_index: 0, content_index: 0, delta: accumulatedText }); } writeSseEvent(res, { type: "response.output_text.done", item_id: outputItemId, output_index: 0, content_index: 0, text: finalText }); writeSseEvent(res, { type: "response.content_part.done", item_id: outputItemId, output_index: 0, content_index: 0, part: { type: "output_text", text: finalText } }); const completedItem = createAssistantOutputItem({ id: outputItemId, text: finalText, phase: "commentary", status: "completed" }); writeSseEvent(res, { type: "response.output_item.done", output_index: 0, item: completedItem }); const functionCallItems = []; let nextStreamOutputIndex = 1; for (const functionCall of pendingToolCalls) { const functionCallItemId = `call_${randomUUID()}`; const functionCallItem = createFunctionCallOutputItem({ id: functionCallItemId, callId: functionCall.id, name: functionCall.name, arguments: functionCall.arguments }); writeSseEvent(res, { type: "response.output_item.added", output_index: nextStreamOutputIndex, item: functionCallItem }); const completedFunctionCallItem = createFunctionCallOutputItem({ id: functionCallItemId, callId: functionCall.id, name: functionCall.name, arguments: functionCall.arguments, status: "completed" }); writeSseEvent(res, { type: "response.output_item.done", output_index: nextStreamOutputIndex, item: completedFunctionCallItem }); functionCallItems.push(functionCallItem); nextStreamOutputIndex += 1; } const incompleteResponse = createResponseResource({ id: responseId, model, status: "incomplete", output: [completedItem, ...functionCallItems], usage }); closed = true; stopWatchingDisconnect(); unsubscribe(); rememberResponseSession(); writeSseEvent(res, { type: "response.completed", response: incompleteResponse }); writeDone(res); res.end(); return; } maybeFinalize(); if (closed) return; if (!sawAssistantDelta) { const payloads = resultAny.payloads; const content = Array.isArray(payloads) && payloads.length > 0 ? payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n") : "No response from OpenClaw."; accumulatedText = content; sawAssistantDelta = true; writeSseEvent(res, { type: "response.output_text.delta", item_id: outputItemId, output_index: 0, content_index: 0, delta: content }); } } catch (err) { if (closed || abortController.signal.aborted) return; logWarn(`openresponses: streaming response failed: ${String(err)}`); finalUsage = finalUsage ?? createEmptyUsage(); if (isClientToolNameConflictError(err)) { writeSseEvent(res, { type: "response.failed", response: createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "invalid_request_error", message: "invalid tool configuration" }, usage: finalUsage }) }); emitAgentEvent({ runId: responseId, stream: "lifecycle", data: { phase: "error" } }); return; } const errorResponse = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: "api_error", message: "internal error" }, usage: finalUsage }); const mapped = resolveOpenAiCompatError(err); if (mapped) { const mappedResponse = createResponseResource({ id: responseId, model, status: "failed", output: [], error: { code: mapped.error.type, message: mapped.error.message }, usage: finalUsage }); rememberResponseSession(); writeSseEvent(res, { type: "response.failed", response: mappedResponse }); emitAgentEvent({ runId: responseId, stream: "lifecycle", data: { phase: "error" } }); return; } rememberResponseSession(); writeSseEvent(res, { type: "response.failed", response: errorResponse }); emitAgentEvent({ runId: responseId, stream: "lifecycle", data: { phase: "error" } }); } finally { if (!closed) emitAgentEvent({ runId: responseId, stream: "lifecycle", data: { phase: "end" } }); } })(); return true; } //#endregion export { testing as __testing, testing, buildAgentPrompt, handleOpenResponsesHttpRequest };