openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
960 lines (959 loc) • 36 kB
JavaScript
import { d as asPositiveSafeInteger } from "./number-coercion-CLj0HTDM.js";
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { t as avoidTrailingHighSurrogateBreak } from "./utf16-slice-D_ngcYKd.js";
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { At as boolean, Et as array, Rn as string, Tn as object, Xn as union, Zn as unknown, wn as number } from "./schemas-zxit8y5H.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import "./io-bdCzpGWJ.js";
import { i as logWarn } from "./logger-DwECwNVZ.js";
import { f as onAgentEventForRun, i as emitAgentEvent } from "./agent-events-CoxiItUi.js";
import { t as bindGatewayContextResolver } from "./gateway-request-scope-BCMYlsDI.js";
import { g as retainGatewayRootWorkAdmissionContinuation } from "./gateway-work-admission-R1IpuDim.js";
import { l as toOpenAiChatCompletionsUsage } from "./usage-CtmV8Xxq.js";
import { n as estimateBase64DecodedBytes } from "./base64-Vw7DZYSc.js";
import "./stream-message-shared-Cyrn1UHN.js";
import { n as readAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-BwslKylZ.js";
import { t as authorizeGatewaySessionCreation } from "./operator-role-policy-wsr1DeJv.js";
import { r as isClientToolNameConflictError } from "./agent-tool-definition-adapter-_QXAVYIM.js";
import { c as normalizeMimeList, n as DEFAULT_INPUT_IMAGE_MIMES, o as extractImageContentFromSource, r as DEFAULT_INPUT_TIMEOUT_MS, t as DEFAULT_INPUT_IMAGE_MAX_BYTES } from "./input-files-CWWFaJLH.js";
import { t as createDefaultDeps } from "./deps-CSNtqiEp.js";
import { n as agentCommandFromGatewayIngress } from "./agent-command-DK8Hh-Mq.js";
import "./agent-W7bgjwyp.js";
import { g as resolveOpenAiCompatibleHttpSenderIsOwner, h as resolveOpenAiCompatibleHttpOperatorScopes, o as authorizeOpenAiCompatibleHttpModelOverride } from "./http-auth-utils-C3lb4QXY.js";
import { h as writeDone, l as sendMissingScopeForbidden, m as watchClientDisconnect, o as sendInvalidRequest, p as setSseHeaders, r as parseGatewayJsonRequest, s as sendJson } from "./http-common-BaZaosnr.js";
import { a as isGatewaySessionKeyOverrideError, c as isUnknownGatewayAgentError, d as resolveGatewayRequestContext, f as resolveOpenAiCompatModelOverride, i as isAgentSelectionRequiredError, o as isInvalidGatewayModelError, r as authorizeOpenAiCompatibleHttpSession } from "./http-utils-BHgXp7Zb.js";
import { t as handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers-B6JLQadu.js";
import { a as validateOpenAiSamplingParams, c as IMAGE_ONLY_USER_MESSAGE, d as isReplaceableAssistantStreamEvent, f as resolveAssistantStreamDeltaText, i as resolveOpenAiCompatError, l as buildAgentMessageFromConversationEntries, n as resolveUnsatisfiedToolChoiceMessage, o as resolveAgentRunUsage, p as resolveAssistantStreamSnapshotText, r as toolChoiceConstraintPrompt, s as normalizeInputHostnameAllowlist, t as isToolChoiceConstraintSatisfied, u as renderConversationToolCall } from "./openai-tool-choice-Cml008Vd.js";
import { randomUUID } from "node:crypto";
//#region src/gateway/openai-http.ts
const OpenAiChatCompletionRequestSchema = object({
model: string().optional(),
stream: boolean().nullish(),
stream_options: object({ include_usage: boolean().optional() }).passthrough().nullish(),
tools: array(unknown()).optional(),
tool_choice: unknown().optional(),
messages: array(unknown()).optional(),
user: string().optional(),
max_tokens: number().int().positive().nullish(),
max_completion_tokens: number().int().positive().nullish(),
temperature: number().nullish(),
top_p: number().nullish(),
response_format: unknown().optional(),
frequency_penalty: number().nullish(),
presence_penalty: number().nullish(),
seed: number().nullish(),
stop: union([string(), array(string())]).nullish()
});
const DEFAULT_OPENAI_CHAT_COMPLETIONS_BODY_BYTES = 20971520;
const DEFAULT_OPENAI_MAX_IMAGE_PARTS = 8;
const DEFAULT_OPENAI_MAX_TOTAL_IMAGE_BYTES = 20971520;
const DEFAULT_OPENAI_IMAGE_LIMITS = {
allowUrl: false,
allowedMimes: new Set(DEFAULT_INPUT_IMAGE_MIMES),
maxBytes: DEFAULT_INPUT_IMAGE_MAX_BYTES,
maxRedirects: 3,
timeoutMs: DEFAULT_INPUT_TIMEOUT_MS
};
function resolveOpenAiChatCompletionsLimits(config) {
const imageConfig = config?.images;
return {
maxBodyBytes: DEFAULT_OPENAI_CHAT_COMPLETIONS_BODY_BYTES,
maxImageParts: DEFAULT_OPENAI_MAX_IMAGE_PARTS,
maxTotalImageBytes: DEFAULT_OPENAI_MAX_TOTAL_IMAGE_BYTES,
images: {
allowUrl: imageConfig?.allowUrl ?? DEFAULT_OPENAI_IMAGE_LIMITS.allowUrl,
urlAllowlist: normalizeInputHostnameAllowlist(imageConfig?.urlAllowlist),
allowedMimes: normalizeMimeList(imageConfig?.allowedMimes, DEFAULT_INPUT_IMAGE_MIMES),
maxBytes: imageConfig?.maxBytes ?? 10485760,
maxRedirects: imageConfig?.maxRedirects ?? 3,
timeoutMs: imageConfig?.timeoutMs ?? 1e4
}
};
}
function writeSse(res, data) {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
function buildAgentCommandInput(params) {
return {
message: params.prompt.message,
extraSystemPrompt: params.prompt.extraSystemPrompt,
images: params.prompt.images,
clientTools: params.clientTools,
model: params.modelOverride,
sessionKey: params.sessionKey,
runId: params.runId,
deliver: false,
messageChannel: params.messageChannel,
senderIsOwner: params.senderIsOwner,
bestEffortDeliver: false,
allowModelOverride: params.modelOverride !== void 0,
abortSignal: params.abortSignal,
streamParams: params.streamParams
};
}
function extractClientToolsFromChatRequest(tools) {
if (tools == null) return [];
if (!Array.isArray(tools)) throw new Error("tools must be an array");
const clientTools = [];
for (const tool of tools) {
if (!tool || typeof tool !== "object" || Array.isArray(tool)) throw new Error("each tool must be an object");
if (tool.type !== "function") throw new Error("only function tools are supported");
const functionValue = tool.function;
if (!functionValue || typeof functionValue !== "object" || Array.isArray(functionValue)) throw new Error("tool.function is required");
const rawName = functionValue.name;
const name = typeof rawName === "string" ? rawName.trim() : "";
if (!name) throw new Error("tool.function.name is required");
const description = functionValue.description;
const parameters = functionValue.parameters;
const strict = functionValue.strict;
clientTools.push({
type: "function",
function: {
name,
...typeof description === "string" ? { description } : {},
...parameters && typeof parameters === "object" && !Array.isArray(parameters) ? { parameters } : {},
...typeof strict === "boolean" ? { strict } : {}
}
});
}
return clientTools;
}
function applyChatToolChoice(params) {
const { tools, toolChoice } = params;
if (toolChoice == null || toolChoice === "auto") 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" || Array.isArray(toolChoice)) throw new Error("tool_choice must be a string or object");
const choiceType = toolChoice.type;
if (choiceType === "function") {
const targetName = normalizeOptionalString(toolChoice.function?.name);
if (!targetName) throw new Error("tool_choice.function.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
};
}
if (typeof choiceType !== "string") throw new Error("unsupported tool_choice type");
throw new Error(`tool_choice ${choiceType} is not supported`);
}
function writeChatCompletionChunk(res, identity, chunk) {
writeSse(res, {
id: identity.runId,
object: "chat.completion.chunk",
created: identity.created,
model: identity.model,
...chunk
});
}
function writeAssistantRoleChunk(res, params) {
writeChatCompletionChunk(res, params, { choices: [{
index: 0,
delta: { role: "assistant" },
finish_reason: null
}] });
}
function writeAssistantContentChunk(res, params) {
writeChatCompletionChunk(res, params, { choices: [{
index: 0,
delta: { content: params.content },
finish_reason: null
}] });
}
function writeAssistantFinishChunk(res, params) {
writeChatCompletionChunk(res, params, { choices: [{
index: 0,
delta: {},
finish_reason: params.finishReason
}] });
}
function splitArgumentsForStreaming(argumentsValue) {
if (!argumentsValue) return [""];
const chunkSize = 256;
const chunks = [];
for (let start = 0; start < argumentsValue.length;) {
const end = avoidTrailingHighSurrogateBreak(argumentsValue, start, Math.min(start + chunkSize, argumentsValue.length));
chunks.push(argumentsValue.slice(start, end));
start = end;
}
return chunks.length > 0 ? chunks : [""];
}
function writeAssistantToolCallsIncrementalChunks(res, params) {
for (const [index, call] of params.toolCalls.entries()) {
writeChatCompletionChunk(res, params, { choices: [{
index: 0,
delta: { tool_calls: [{
index,
id: call.id,
type: "function",
function: {
name: call.name,
arguments: ""
}
}] },
finish_reason: null
}] });
for (const argsDelta of splitArgumentsForStreaming(call.arguments)) writeChatCompletionChunk(res, params, { choices: [{
index: 0,
delta: { tool_calls: [{
index,
function: { arguments: argsDelta }
}] },
finish_reason: null
}] });
}
}
function writeUsageChunk(res, params) {
writeChatCompletionChunk(res, params, {
choices: [],
usage: params.usage
});
}
function asMessages(val) {
return Array.isArray(val) ? val : [];
}
function extractTextContent(content) {
if (typeof content === "string") return content;
if (Array.isArray(content)) return content.map((part) => {
if (!part || typeof part !== "object") return "";
const type = part.type;
const text = part.text;
const inputText = part.input_text;
if (type === "text" && typeof text === "string") return text;
if (type === "input_text" && typeof text === "string") return text;
if (typeof inputText === "string") return inputText;
return "";
}).filter(Boolean).join("\n");
return "";
}
function stringifyToolCallArguments(value) {
if (typeof value === "string") return value;
if (value == null) return "";
try {
const serialized = JSON.stringify(value);
return typeof serialized === "string" ? serialized : "";
} catch {
return "";
}
}
function extractAssistantToolCalls(value) {
if (!Array.isArray(value)) return [];
const calls = [];
for (const rawCall of value) {
if (!rawCall || typeof rawCall !== "object" || Array.isArray(rawCall)) continue;
const id = normalizeOptionalString(rawCall.id) ?? "";
const functionValue = rawCall.function;
if (!functionValue || typeof functionValue !== "object" || Array.isArray(functionValue)) continue;
const name = normalizeOptionalString(functionValue.name) ?? "";
if (!id || !name) continue;
const argumentsValue = stringifyToolCallArguments(functionValue.arguments);
calls.push({
id,
name,
arguments: argumentsValue
});
}
return calls;
}
function resolveImageUrlPart(part) {
if (!part || typeof part !== "object") return;
const imageUrl = part.image_url;
if (typeof imageUrl === "string") {
const trimmed = imageUrl.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
if (!imageUrl || typeof imageUrl !== "object") return;
const rawUrl = imageUrl.url;
if (typeof rawUrl !== "string") return;
const trimmed = rawUrl.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function extractImageUrls(content) {
const urls = [];
if (!Array.isArray(content)) return {
kind: "valid",
urls
};
for (const part of content) {
if (!part || typeof part !== "object") continue;
if (part.type !== "image_url") continue;
const url = resolveImageUrlPart(part);
if (!url) return { kind: "invalid" };
urls.push(url);
}
return {
kind: "valid",
urls
};
}
function parseImageUrlToSource(url) {
const dataUriMatch = /^data:([^,]*?),(.*)$/is.exec(url);
if (dataUriMatch) {
const metadata = normalizeOptionalString(dataUriMatch[1]) ?? "";
const data = dataUriMatch[2] ?? "";
const metadataParts = metadata.split(";").map((part) => normalizeOptionalString(part) ?? "").filter(Boolean);
if (!metadataParts.some((part) => normalizeLowercaseStringOrEmpty(part) === "base64")) throw new Error("image_url data URI must be base64 encoded");
if (!(normalizeOptionalString(data) ?? "")) throw new Error("image_url data URI is missing payload data");
return {
type: "base64",
mediaType: metadataParts.find((part) => part.includes("/")),
data
};
}
return {
type: "url",
url
};
}
function resolveActiveTurnContext(messagesUnknown) {
const messages = asMessages(messagesUnknown);
for (let i = messages.length - 1; i >= 0; i -= 1) {
const msg = messages[i];
if (!msg || typeof msg !== "object") continue;
const role = normalizeOptionalString(msg.role) ?? "";
const normalizedRole = role === "function" ? "tool" : role;
if (normalizedRole !== "user" && normalizedRole !== "tool") continue;
const imageUrls = normalizedRole === "user" ? extractImageUrls(msg.content) : {
kind: "valid",
urls: []
};
return {
activeTurnIndex: i,
activeUserMessageIndex: normalizedRole === "user" ? i : -1,
imageUrls
};
}
return {
activeTurnIndex: -1,
activeUserMessageIndex: -1,
imageUrls: {
kind: "valid",
urls: []
}
};
}
async function resolveImagesForRequest(activeTurnContext, limits) {
if (activeTurnContext.imageUrls.kind === "invalid") throw new Error("image_url part is missing a valid URL");
const urls = activeTurnContext.imageUrls.urls;
if (urls.length === 0) return [];
if (urls.length > limits.maxImageParts) throw new Error(`Too many image_url parts (${urls.length}; limit ${limits.maxImageParts})`);
const images = [];
let totalBytes = 0;
for (const url of urls) {
const source = parseImageUrlToSource(url);
if (source.type === "base64") {
const sourceBytes = estimateBase64DecodedBytes(source.data);
if (totalBytes + sourceBytes > limits.maxTotalImageBytes) throw new Error(`Total image payload too large (${totalBytes + sourceBytes}; limit ${limits.maxTotalImageBytes})`);
}
const image = await extractImageContentFromSource(source, limits.images);
totalBytes += estimateBase64DecodedBytes(image.data);
if (totalBytes > limits.maxTotalImageBytes) throw new Error(`Total image payload too large (${totalBytes}; limit ${limits.maxTotalImageBytes})`);
images.push(image);
}
return images;
}
function buildAgentPrompt(messagesUnknown, activeTurnContext) {
const messages = asMessages(messagesUnknown);
const hasActiveTurnImage = activeTurnContext.imageUrls.kind === "valid" && activeTurnContext.imageUrls.urls.length > 0;
const systemParts = [];
const conversationEntries = [];
for (const [i, msg] of messages.entries()) {
if (!msg || typeof msg !== "object") continue;
const role = normalizeOptionalString(msg.role) ?? "";
const content = extractTextContent(msg.content).trim();
if (!role) continue;
if (role === "system" || role === "developer") {
if (content) systemParts.push(content);
continue;
}
const normalizedRole = role === "function" ? "tool" : role;
if (normalizedRole !== "user" && normalizedRole !== "assistant" && normalizedRole !== "tool") continue;
const assistantToolCallsSummary = (normalizedRole === "assistant" ? extractAssistantToolCalls(msg.tool_calls) : []).map(renderConversationToolCall).join("\n");
const messageContent = [normalizedRole === "user" && !content && hasActiveTurnImage && i === activeTurnContext.activeUserMessageIndex ? IMAGE_ONLY_USER_MESSAGE : content, assistantToolCallsSummary].filter((part) => Boolean(part)).join("\n");
if (!messageContent) continue;
const name = normalizeOptionalString(msg.name) ?? "";
const toolCallId = normalizeOptionalString(msg.tool_call_id) ?? "";
const sender = normalizedRole === "assistant" ? "Assistant" : normalizedRole === "user" ? "User" : toolCallId ? `Tool:${toolCallId}` : name ? `Tool:${name}` : "Tool";
conversationEntries.push({
role: normalizedRole,
entry: {
sender,
body: messageContent
},
internalStreamError: normalizedRole === "assistant" && normalizeOptionalString(msg.stopReason) === "error" && messageContent.trim() === "[assistant turn failed before producing content]"
});
}
return {
message: buildAgentMessageFromConversationEntries(conversationEntries),
extraSystemPrompt: systemParts.length > 0 ? systemParts.join("\n\n") : void 0
};
}
function resolveAgentResponseText(result) {
const payloads = result?.payloads;
if (!Array.isArray(payloads) || payloads.length === 0) return "No response from OpenClaw.";
return payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n") || "No response from OpenClaw.";
}
function resolveAgentResponseCommentary(result) {
const payloads = result?.payloads;
if (!Array.isArray(payloads) || payloads.length === 0) return "";
return payloads.map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n\n");
}
function resolveStopReasonAndPendingToolCalls(meta) {
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return {
stopReason: void 0,
pendingToolCalls: void 0
};
const stopReasonRaw = meta.stopReason;
const stopReason = typeof stopReasonRaw === "string" ? stopReasonRaw : void 0;
const pendingRaw = meta.pendingToolCalls;
if (!Array.isArray(pendingRaw)) return {
stopReason,
pendingToolCalls: void 0
};
const pendingToolCalls = [];
for (const call of pendingRaw) {
const id = typeof call?.id === "string" ? call.id.trim() : "";
const name = typeof call?.name === "string" ? call.name.trim() : "";
const argsValue = call?.arguments;
const argumentsValue = typeof argsValue === "string" ? argsValue : argsValue == null ? "" : JSON.stringify(argsValue);
if (!id || !name) continue;
pendingToolCalls.push({
id,
name,
arguments: argumentsValue
});
}
return {
stopReason,
pendingToolCalls
};
}
function resolveChatCompletionUsage(result) {
return toOpenAiChatCompletionsUsage(resolveAgentRunUsage(result));
}
function resolveIncludeUsageForStreaming(payload) {
const streamOptions = payload.stream_options;
if (!streamOptions || typeof streamOptions !== "object" || Array.isArray(streamOptions)) return false;
return streamOptions.include_usage === true;
}
function resolveResponseFormat(value) {
if (value == null) return;
if (typeof value !== "object" || Array.isArray(value)) throw new Error("response_format must be an object");
const obj = value;
const type = obj.type;
if (type !== "text" && type !== "json_object" && type !== "json_schema") throw new Error("response_format.type must be text, json_object, or json_schema");
return obj;
}
function resolveStopSequences(value) {
if (value == null) return;
const list = typeof value === "string" ? [value] : value;
if (!Array.isArray(list)) throw new Error("stop must be a string or array of strings");
if (list.length > 4) throw new Error("stop supports at most 4 sequences");
const sequences = [];
for (const item of list) {
if (typeof item !== "string" || item.length === 0) throw new Error("stop entries must be non-empty strings");
sequences.push(item);
}
return sequences.length > 0 ? sequences : void 0;
}
function resolveChatCompletionTokenCap(value, field) {
if (value == null) return;
const maxTokens = asPositiveSafeInteger(value);
if (maxTokens === void 0) throw new Error(`${field} must be a positive safe integer`);
return maxTokens;
}
async function handleOpenAiHttpRequest(req, res, opts) {
const limits = resolveOpenAiChatCompletionsLimits(opts.config);
const handled = await handleGatewayPostJsonEndpoint(req, res, {
pathname: "/v1/chat/completions",
requiredOperatorMethod: "chat.send",
resolveOperatorScopes: resolveOpenAiCompatibleHttpOperatorScopes,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
maxBodyBytes: opts.maxBodyBytes ?? limits.maxBodyBytes
});
if (handled === false) return false;
if (!handled) return true;
const modelOverrideAuth = authorizeOpenAiCompatibleHttpModelOverride(req, handled.requestAuth);
if (!modelOverrideAuth.allowed) {
sendMissingScopeForbidden(res, modelOverrideAuth.missingScope);
return true;
}
const senderIsOwner = resolveOpenAiCompatibleHttpSenderIsOwner(req, handled.requestAuth);
const payload = parseGatewayJsonRequest(res, handled.body, OpenAiChatCompletionRequestSchema);
if (!payload) return true;
const stream = payload.stream === true;
const streamIncludeUsage = stream && resolveIncludeUsageForStreaming(payload);
const model = typeof payload.model === "string" ? payload.model : "openclaw";
const user = typeof payload.user === "string" ? payload.user : void 0;
let maxTokens;
try {
const maxCompletionTokens = resolveChatCompletionTokenCap(payload.max_completion_tokens, "max_completion_tokens");
const legacyMaxTokens = resolveChatCompletionTokenCap(payload.max_tokens, "max_tokens");
maxTokens = maxCompletionTokens ?? legacyMaxTokens;
} catch (err) {
sendInvalidRequest(res, formatErrorMessage(err).trim());
return true;
}
const temperature = typeof payload.temperature === "number" ? payload.temperature : void 0;
const topP = typeof payload.top_p === "number" ? payload.top_p : void 0;
const frequencyPenalty = typeof payload.frequency_penalty === "number" ? payload.frequency_penalty : void 0;
const presencePenalty = typeof payload.presence_penalty === "number" ? payload.presence_penalty : void 0;
const seed = typeof payload.seed === "number" ? payload.seed : void 0;
let responseFormat;
try {
responseFormat = resolveResponseFormat(payload.response_format);
} catch (err) {
sendInvalidRequest(res, `Invalid response_format: ${formatErrorMessage(err).trim()}`);
return true;
}
let stop;
try {
stop = resolveStopSequences(payload.stop);
} catch (err) {
sendInvalidRequest(res, `Invalid stop: ${formatErrorMessage(err).trim()}`);
return true;
}
const samplingError = validateOpenAiSamplingParams({
temperature: payload.temperature,
topP: payload.top_p,
frequencyPenalty: payload.frequency_penalty,
presencePenalty: payload.presence_penalty,
seed: payload.seed
});
if (samplingError) {
sendInvalidRequest(res, samplingError);
return true;
}
const streamParams = maxTokens !== void 0 || temperature !== void 0 || topP !== void 0 || responseFormat !== void 0 || frequencyPenalty !== void 0 || presencePenalty !== void 0 || seed !== void 0 || stop !== void 0 ? {
...maxTokens !== void 0 ? { maxTokens } : {},
...temperature !== void 0 ? { temperature } : {},
...topP !== void 0 ? { topP } : {},
...responseFormat !== void 0 ? { responseFormat } : {},
...frequencyPenalty !== void 0 ? { frequencyPenalty } : {},
...presencePenalty !== void 0 ? { presencePenalty } : {},
...seed !== void 0 ? { seed } : {},
...stop !== void 0 ? { stop } : {}
} : void 0;
let agentId;
let sessionKey;
let messageChannel;
try {
({agentId, sessionKey, messageChannel} = resolveGatewayRequestContext({
req,
model,
user,
sessionPrefix: "openai",
defaultMessageChannel: "webchat",
useMessageChannelHeader: true
}));
} catch (err) {
if (isAgentSelectionRequiredError(err) || isUnknownGatewayAgentError(err) || isInvalidGatewayModelError(err) || isGatewaySessionKeyOverrideError(err)) {
sendInvalidRequest(res, err.message);
return true;
}
throw err;
}
const creationAuth = authorizeGatewaySessionCreation({
cfg: getRuntimeConfig(),
...handled.requestAuth.operatorRoleActor ? { actor: handled.requestAuth.operatorRoleActor } : { profileId: handled.requestAuth.authenticatedUserProfile?.profileId },
agentId
});
if (creationAuth) {
sendJson(res, 403, { error: {
message: creationAuth.message,
type: "forbidden"
} });
return true;
}
const sessionAuth = authorizeOpenAiCompatibleHttpSession({
agentId,
sessionKey,
requestAuth: handled.requestAuth,
senderIsOwner
});
if (!sessionAuth.allowed) {
sendJson(res, 403, { error: {
message: sessionAuth.message,
type: "forbidden"
} });
return true;
}
const { modelOverride, errorMessage: modelError } = await resolveOpenAiCompatModelOverride({
req,
agentId,
model
});
if (modelError) {
sendInvalidRequest(res, modelError);
return true;
}
const activeTurnContext = resolveActiveTurnContext(payload.messages);
const prompt = buildAgentPrompt(payload.messages, activeTurnContext);
let resolvedClientTools;
let toolChoicePrompt;
let toolChoiceConstraint;
try {
const toolChoiceResult = applyChatToolChoice({
tools: extractClientToolsFromChatRequest(payload.tools),
toolChoice: payload.tool_choice
});
resolvedClientTools = toolChoiceResult.tools;
toolChoicePrompt = toolChoiceResult.extraSystemPrompt;
toolChoiceConstraint = toolChoiceResult.constraint;
} catch (err) {
sendInvalidRequest(res, `Invalid tools/tool_choice: ${formatErrorMessage(err).trim()}`);
return true;
}
let images;
try {
images = await resolveImagesForRequest(activeTurnContext, limits);
} catch (err) {
logWarn(`openai-compat: invalid image_url content: ${String(err)}`);
sendInvalidRequest(res, "Invalid image_url content in `messages`.");
return true;
}
if (!prompt.message && images.length === 0) {
sendInvalidRequest(res, "Missing user message in `messages`.");
return true;
}
const runId = `chatcmpl_${randomUUID()}`;
const created = Math.floor(Date.now() / 1e3);
const streamIdentity = {
runId,
model,
created
};
const deps = createDefaultDeps();
const abortController = new AbortController();
const mergedExtraSystemPrompt = [prompt.extraSystemPrompt, toolChoicePrompt].filter((part) => Boolean(part)).join("\n\n");
const commandInput = buildAgentCommandInput({
prompt: {
message: prompt.message,
extraSystemPrompt: mergedExtraSystemPrompt || void 0,
images: images.length > 0 ? images : void 0
},
clientTools: resolvedClientTools.length > 0 ? resolvedClientTools : void 0,
modelOverride,
sessionKey,
runId,
messageChannel,
senderIsOwner,
abortSignal: abortController.signal,
streamParams
});
const gatewayCommandInput = opts.resolveGatewayContext ? {
...commandInput,
onAdmittedRunContext: (context) => bindGatewayContextResolver(context, opts.resolveGatewayContext)
} : commandInput;
if (!stream) {
const stopWatchingDisconnect = watchClientDisconnect(req, res, abortController);
try {
const result = await agentCommandFromGatewayIngress(gatewayCommandInput, defaultRuntime, deps, {});
if (abortController.signal.aborted) return true;
const meta = result?.meta;
if (readAgentRunTerminalOutcome(result) === "failed") throw new Error("agent run failed");
const usage = resolveChatCompletionUsage(result);
const { stopReason, pendingToolCalls } = resolveStopReasonAndPendingToolCalls(meta);
if (toolChoiceConstraint && !isToolChoiceConstraintSatisfied({
constraint: toolChoiceConstraint,
pendingToolCalls
})) {
sendJson(res, 502, { error: {
message: resolveUnsatisfiedToolChoiceMessage(toolChoiceConstraint),
type: "api_error"
} });
return true;
}
if (stopReason === "tool_calls" && pendingToolCalls && pendingToolCalls.length > 0) {
const commentary = resolveAgentResponseCommentary(result);
sendJson(res, 200, {
id: runId,
object: "chat.completion",
created,
model,
choices: [{
index: 0,
message: {
role: "assistant",
content: commentary,
tool_calls: pendingToolCalls.map((call) => ({
id: call.id,
type: "function",
function: {
name: call.name,
arguments: call.arguments
}
}))
},
finish_reason: "tool_calls"
}],
usage
});
return true;
}
const content = resolveAgentResponseText(result);
sendJson(res, 200, {
id: runId,
object: "chat.completion",
created,
model,
choices: [{
index: 0,
message: {
role: "assistant",
content
},
finish_reason: "stop"
}],
usage
});
} catch (err) {
if (abortController.signal.aborted) return true;
logWarn(`openai-compat: chat completion failed: ${String(err)}`);
if (isClientToolNameConflictError(err)) {
sendInvalidRequest(res, "invalid tool configuration");
return true;
}
const mapped = resolveOpenAiCompatError(err);
if (mapped) {
sendJson(res, mapped.status, { error: mapped.error });
return true;
}
sendJson(res, 500, { error: {
message: "internal error",
type: "api_error"
} });
} finally {
stopWatchingDisconnect();
}
return true;
}
setSseHeaders(res);
let wroteStopChunk = false;
let sawAssistantDelta = false;
let streamedAssistantText = "";
let bufferedReplaceableAssistantContent = "";
let finalUsage;
let finalizeRequested = false;
let finalizeScheduled = false;
let finalizeFinishReason = "stop";
let resultResolved = false;
let closed = false;
let observedTerminalLifecycle = false;
let terminalStreamError;
let terminalLifecyclePhase = "end";
let stopWatchingDisconnect = () => {};
const maybeFinalize = () => {
if (closed || finalizeScheduled || !finalizeRequested) return;
if (!resultResolved) return;
if (streamIncludeUsage && !finalUsage) return;
finalizeScheduled = true;
queueMicrotask(() => {
if (closed) return;
if (terminalStreamError) {
finishStreamWithError(terminalStreamError);
return;
}
closed = true;
stopWatchingDisconnect();
unsubscribe();
if (!wroteStopChunk) {
writeAssistantFinishChunk(res, {
...streamIdentity,
finishReason: finalizeFinishReason
});
wroteStopChunk = true;
}
if (streamIncludeUsage && finalUsage) writeUsageChunk(res, {
...streamIdentity,
usage: finalUsage
});
writeDone(res);
res.end();
});
};
const requestFinalize = (finishReason = "stop") => {
if (!finalizeRequested || finishReason === "tool_calls") finalizeFinishReason = finishReason;
finalizeRequested = true;
maybeFinalize();
};
const unsubscribe = onAgentEventForRun(runId, (evt) => {
if (evt.runId !== runId) return;
if (closed) return;
if (evt.stream === "assistant") {
const text = evt.data?.text;
const replace = evt.data?.replace === true;
if (replace && typeof text === "string") bufferedReplaceableAssistantContent = text;
if (isReplaceableAssistantStreamEvent(evt)) {
const snapshot = resolveAssistantStreamSnapshotText(evt);
if (snapshot) bufferedReplaceableAssistantContent = snapshot;
return;
}
if (replace && typeof text === "string" && !toolChoiceConstraint && !text.startsWith(streamedAssistantText)) {
terminalStreamError ??= {
message: "Assistant output cannot be represented as an append-only response stream.",
type: "api_error"
};
return;
}
const content = typeof text === "string" && text.startsWith(streamedAssistantText) ? text.slice(streamedAssistantText.length) : resolveAssistantStreamDeltaText(evt);
if (!content) return;
streamedAssistantText += content;
if (toolChoiceConstraint) return;
sawAssistantDelta = true;
writeAssistantContentChunk(res, {
...streamIdentity,
content
});
return;
}
if (evt.stream === "lifecycle") {
const phase = evt.data?.phase;
if (phase === "start") observedTerminalLifecycle = false;
if (phase === "end" || phase === "error") {
observedTerminalLifecycle = true;
if (phase === "error" && terminalLifecyclePhase !== "error") terminalStreamError ??= {
message: normalizeOptionalString(evt.data?.error) ?? "Agent run failed",
type: "api_error"
};
requestFinalize();
}
}
});
const finishStreamWithError = (error) => {
if (closed) return;
closed = true;
stopWatchingDisconnect();
unsubscribe();
writeSse(res, { error });
writeDone(res);
res.end();
};
const releaseAgentRootWork = retainGatewayRootWorkAdmissionContinuation();
const releaseResponseRootWork = retainGatewayRootWorkAdmissionContinuation();
const releaseStreamRootWork = () => {
res.off("finish", releaseStreamRootWork);
res.off("close", releaseStreamRootWork);
releaseResponseRootWork?.();
};
res.once("finish", releaseStreamRootWork);
res.once("close", releaseStreamRootWork);
stopWatchingDisconnect = watchClientDisconnect(req, res, abortController, () => {
closed = true;
unsubscribe();
releaseStreamRootWork();
});
writeAssistantRoleChunk(res, streamIdentity);
(async () => {
try {
const result = await agentCommandFromGatewayIngress(gatewayCommandInput, defaultRuntime, deps, {});
resultResolved = true;
if (closed) return;
if (readAgentRunTerminalOutcome(result) === "failed") {
terminalLifecyclePhase = "error";
finishStreamWithError({
message: "internal error",
type: "api_error"
});
return;
}
if (terminalStreamError) {
finishStreamWithError(terminalStreamError);
return;
}
finalUsage = resolveChatCompletionUsage(result);
const meta = result?.meta;
const { stopReason, pendingToolCalls } = resolveStopReasonAndPendingToolCalls(meta);
if (toolChoiceConstraint && !isToolChoiceConstraintSatisfied({
constraint: toolChoiceConstraint,
pendingToolCalls
})) {
finishStreamWithError({
message: resolveUnsatisfiedToolChoiceMessage(toolChoiceConstraint),
type: "api_error"
});
return;
}
if (stopReason === "tool_calls" && pendingToolCalls && pendingToolCalls.length > 0) {
if (!sawAssistantDelta) {
const commentary = resolveAgentResponseCommentary(result) || streamedAssistantText || bufferedReplaceableAssistantContent;
if (commentary) {
sawAssistantDelta = true;
writeAssistantContentChunk(res, {
...streamIdentity,
content: commentary
});
}
}
writeAssistantToolCallsIncrementalChunks(res, {
...streamIdentity,
toolCalls: pendingToolCalls
});
requestFinalize("tool_calls");
return;
}
if (!sawAssistantDelta) {
const content = resolveAgentResponseCommentary(result) || bufferedReplaceableAssistantContent || resolveAgentResponseText(result) || "No response from OpenClaw.";
sawAssistantDelta = true;
writeAssistantContentChunk(res, {
...streamIdentity,
content
});
}
requestFinalize();
} catch (err) {
resultResolved = true;
if (closed || abortController.signal.aborted) return;
terminalLifecyclePhase = "error";
logWarn(`openai-compat: streaming chat completion failed: ${String(err)}`);
if (isClientToolNameConflictError(err)) {
finishStreamWithError({
message: "invalid tool configuration",
type: "invalid_request_error"
});
return;
}
const mapped = resolveOpenAiCompatError(err);
if (mapped) {
finishStreamWithError(mapped.error);
return;
}
if (terminalStreamError) {
finishStreamWithError(terminalStreamError);
return;
}
finishStreamWithError({
message: "internal error",
type: "api_error"
});
} finally {
releaseAgentRootWork?.();
if (!observedTerminalLifecycle && (terminalLifecyclePhase === "error" || !closed)) emitAgentEvent({
runId,
stream: "lifecycle",
data: { phase: terminalLifecyclePhase }
});
}
})();
return true;
}
//#endregion
export { handleOpenAiHttpRequest };