openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
624 lines (623 loc) • 23.5 kB
JavaScript
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { l as calculateCost, n as transformMessages, t as sanitizeSurrogates, u as clampThinkingLevel } from "./sanitize-unicode-CgzZKr22.js";
import { n as parseStreamingJson } from "./json-parse-CydVzlvP.js";
import { t as headersToRecord } from "./headers-CaXpIDsu.js";
import { c as isResponsesTextContentPartType, i as resolveOpenAIStrictToolSetting, n as normalizeOpenAIStrictToolParameters, r as resolveOpenAIStrictToolFlagForInventory, s as isAzureResponsesTextDeltaEvent, t as findOpenAIStrictToolSchemaDiagnostics } from "./openai-tool-schema-CT6-f1IZ.js";
import { t as shortHash } from "./hash-mbqDyGo7.js";
import { createHash } from "node:crypto";
//#region src/llm/providers/openai-responses-tools.ts
const log = createSubsystemLogger("llm/openai-responses");
const MAX_STRICT_TOOL_DOWNGRADE_DIAGNOSTIC_KEYS = 64;
const loggedStrictToolDowngradeDiagnosticKeys = /* @__PURE__ */ new Set();
/** Converts tools to deterministic OpenAI Responses function tool definitions. */
function convertResponsesTools(tools, options) {
const strict = resolveResponsesStrictToolFlag(tools, resolveResponsesStrictToolSetting(options), options?.model);
return sortResponsesToolsByName(tools).map((tool) => {
const result = {
type: "function",
name: tool.name,
description: tool.description,
parameters: normalizeOpenAIStrictToolParameters(tool.parameters, strict === true, options?.model?.compat)
};
if (strict !== void 0) result.strict = strict;
return result;
});
}
function resolveResponsesStrictToolSetting(options) {
if (options?.strict !== void 0) return options.strict;
if (options?.model) return resolveOpenAIStrictToolSetting(options.model, {
transport: "stream",
supportsStrictMode: options.supportsStrictMode
});
return false;
}
function resolveResponsesStrictToolFlag(tools, strictSetting, model) {
const strict = resolveOpenAIStrictToolFlagForInventory(tools, strictSetting);
if (strictSetting === true && strict === false && model && log.isEnabled("debug", "any")) {
const diagnostics = findOpenAIStrictToolSchemaDiagnostics(tools);
if (shouldLogStrictToolDowngradeDiagnostic(diagnostics, model)) {
const sample = diagnostics.slice(0, 5).map((entry) => ({
tool: entry.toolName ?? `tool[${entry.toolIndex}]`,
violations: entry.violations.slice(0, 8)
}));
log.debug(`OpenAI responses tool schema strict mode downgraded to strict=false for ${model.provider ?? "unknown"}/${model.id ?? "unknown"} because ${diagnostics.length} tool schema(s) are not strict-compatible`, {
provider: model.provider,
model: model.id,
incompatibleToolCount: diagnostics.length,
sample
});
}
}
return strict;
}
function shouldLogStrictToolDowngradeDiagnostic(diagnostics, model) {
const key = createHash("sha256").update(JSON.stringify({
provider: model.provider,
model: model.id,
diagnostics: diagnostics.map((entry) => ({
toolIndex: entry.toolIndex,
toolName: entry.toolName ?? null,
violations: entry.violations
}))
})).digest("hex");
if (loggedStrictToolDowngradeDiagnosticKeys.has(key)) return false;
if (loggedStrictToolDowngradeDiagnosticKeys.size >= MAX_STRICT_TOOL_DOWNGRADE_DIAGNOSTIC_KEYS) loggedStrictToolDowngradeDiagnosticKeys.clear();
loggedStrictToolDowngradeDiagnosticKeys.add(key);
return true;
}
function compareToolText(left, right) {
const leftText = left ?? "";
const rightText = right ?? "";
if (leftText < rightText) return -1;
if (leftText > rightText) return 1;
return 0;
}
function sortResponsesToolsByName(tools) {
return tools.toSorted((left, right) => compareToolText(left.name, right.name) || compareToolText(left.description, right.description));
}
//#endregion
//#region src/llm/providers/openai-responses-shared.ts
function normalizeResponsesReasoningReplayItem(params) {
const next = { ...params.item };
if (!Array.isArray(next.summary)) next.summary = [];
if (!params.replayResponsesItemIds) delete next.id;
return next;
}
function encodeTextSignatureV1(id, phase) {
const payload = {
v: 1,
id
};
if (phase) payload.phase = phase;
return JSON.stringify(payload);
}
function parseTextSignature(signature) {
if (!signature) return;
if (signature.startsWith("{")) try {
const parsed = JSON.parse(signature);
if (parsed.v === 1) {
const id = typeof parsed.id === "string" ? parsed.id : void 0;
const phase = parsed.phase === "commentary" || parsed.phase === "final_answer" ? parsed.phase : void 0;
if (id !== void 0 || phase !== void 0) return {
id,
phase
};
return;
}
} catch {}
return { id: signature };
}
function resolveReplayableResponsesMessageId(params) {
if (!params.textSignatureId) return params.fallbackOrdinal === 0 ? params.fallbackId : `${params.fallbackId}_${params.fallbackOrdinal}`;
return params.previousReplayItemWasReasoning ? params.textSignatureId : void 0;
}
function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
const messages = [];
const shouldReplayResponsesItemIds = options?.replayResponsesItemIds ?? true;
const normalizeIdPart = (part) => {
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
return (sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized).replace(/_+$/, "");
};
const buildForeignResponsesItemId = (itemId) => {
const normalized = `fc_${shortHash(itemId)}`;
return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
};
const normalizeToolCallId = (id, targetModel, source) => {
if (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id);
if (!id.includes("|")) return normalizeIdPart(id);
const [callId, itemId] = id.split("|");
const normalizedCallId = normalizeIdPart(callId);
let normalizedItemId = source.provider !== model.provider || source.api !== model.api ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);
if (!normalizedItemId.startsWith("fc_")) normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
return `${normalizedCallId}|${normalizedItemId}`;
};
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
if ((options?.includeSystemPrompt ?? true) && context.systemPrompt) {
const role = model.reasoning ? "developer" : "system";
messages.push({
type: "message",
role,
content: [{
type: "input_text",
text: sanitizeSurrogates(context.systemPrompt)
}]
});
}
let msgIndex = 0;
for (const msg of transformedMessages) {
if (msg.role === "user") if (typeof msg.content === "string") messages.push({
type: "message",
role: "user",
content: [{
type: "input_text",
text: sanitizeSurrogates(msg.content)
}]
});
else {
const content = msg.content.map((item) => {
if (item.type === "text") return {
type: "input_text",
text: sanitizeSurrogates(item.text)
};
return {
type: "input_image",
detail: "auto",
image_url: `data:${item.mimeType};base64,${item.data}`
};
});
if (content.length === 0) continue;
messages.push({
type: "message",
role: "user",
content
});
}
else if (msg.role === "assistant") {
const output = [];
let textFallbackOrdinal = 0;
const assistantMsg = msg;
let previousReplayItemWasReasoning = false;
const isDifferentModel = assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
for (const block of msg.content) if (block.type === "thinking") {
if (block.thinkingSignature) {
const reasoningItem = normalizeResponsesReasoningReplayItem({
item: JSON.parse(block.thinkingSignature),
replayResponsesItemIds: shouldReplayResponsesItemIds
});
output.push(reasoningItem);
previousReplayItemWasReasoning = true;
}
} else if (block.type === "text") {
const textBlock = block;
const parsedSignature = parseTextSignature(textBlock.textSignature);
let msgId = shouldReplayResponsesItemIds ? resolveReplayableResponsesMessageId({
textSignatureId: parsedSignature?.id,
fallbackId: `msg_${msgIndex}`,
fallbackOrdinal: textFallbackOrdinal,
previousReplayItemWasReasoning
}) : void 0;
if (!parsedSignature?.id) textFallbackOrdinal += 1;
if (msgId && msgId.length > 64) msgId = `msg_${shortHash(msgId)}`;
const messageItem = {
type: "message",
role: "assistant",
content: [{
type: "output_text",
text: sanitizeSurrogates(textBlock.text),
annotations: []
}],
status: "completed",
...msgId ? { id: msgId } : {},
phase: parsedSignature?.phase
};
output.push(messageItem);
previousReplayItemWasReasoning = false;
} else if (block.type === "toolCall") {
const toolCall = block;
const [callId, itemIdRaw] = toolCall.id.split("|");
let itemId = shouldReplayResponsesItemIds ? itemIdRaw : void 0;
if (shouldReplayResponsesItemIds && isDifferentModel && itemId?.startsWith("fc_")) itemId = void 0;
output.push({
type: "function_call",
...itemId ? { id: itemId } : {},
call_id: callId,
name: toolCall.name,
arguments: JSON.stringify(toolCall.arguments)
});
previousReplayItemWasReasoning = false;
}
if (output.length === 0) continue;
messages.push(...output);
} else if (msg.role === "toolResult") {
const textResult = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
const hasImages = msg.content.some((c) => c.type === "image");
const hasText = textResult.length > 0;
const [callId] = msg.toolCallId.split("|");
let output;
if (hasImages && model.input.includes("image")) {
const contentParts = [];
if (hasText) contentParts.push({
type: "input_text",
text: sanitizeSurrogates(textResult)
});
for (const block of msg.content) if (block.type === "image") contentParts.push({
type: "input_image",
detail: "auto",
image_url: `data:${block.mimeType};base64,${block.data}`
});
output = contentParts;
} else output = sanitizeSurrogates(hasText ? textResult : "(see attached image)");
messages.push({
type: "function_call_output",
call_id: callId,
output
});
}
msgIndex++;
}
return messages;
}
function createResponsesAssistantOutput(model, api = model.api) {
return {
role: "assistant",
content: [],
api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
},
stopReason: "stop",
timestamp: Date.now()
};
}
function resolveResponsesReasoningEffort(model, reasoning) {
const clampedReasoning = reasoning ? clampThinkingLevel(model, reasoning) : void 0;
if (!clampedReasoning || clampedReasoning === "off") return;
return clampedReasoning === "max" ? "xhigh" : clampedReasoning;
}
function applyCommonResponsesParams(params, model, context, options, config) {
if (options?.maxTokens) params.max_output_tokens = options.maxTokens;
if (options?.temperature !== void 0) params.temperature = options.temperature;
if (context.tools && context.tools.length > 0) params.tools = convertResponsesTools(context.tools, { model });
if (!model.reasoning) return;
if (options?.reasoningEffort || options?.reasoningSummary) {
params.reasoning = {
effort: options?.reasoningEffort ? model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort : "medium",
summary: options?.reasoningSummary || "auto"
};
params.include = ["reasoning.encrypted_content"];
} else if ((config?.setDefaultReasoningOff ?? true) && model.thinkingLevelMap?.off !== null) params.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
}
function buildResponsesRequestOptions(options) {
return {
...options?.signal ? { signal: options.signal } : {},
...options?.timeoutMs !== void 0 ? { timeout: options.timeoutMs } : {},
...options?.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {}
};
}
function cleanStreamingScratchBuffers(output) {
for (const block of output.content) {
delete block.index;
delete block.partialJson;
}
}
async function runResponsesStreamLifecycle(params) {
const { stream, model, output, options } = params;
try {
const client = params.createClient();
let requestParams = params.buildParams();
const nextParams = await options?.onPayload?.(requestParams, model);
if (nextParams !== void 0) requestParams = nextParams;
const { data: openaiStream, response } = await client.responses.create(requestParams, buildResponsesRequestOptions(options)).withResponse();
await options?.onResponse?.({
status: response.status,
headers: headersToRecord(response.headers)
}, model);
stream.push({
type: "start",
partial: output
});
await processResponsesStream(openaiStream, output, stream, model, params.processStreamOptions);
if (options?.signal?.aborted) throw new Error("Request was aborted");
if (output.stopReason === "aborted" || output.stopReason === "error") throw new Error("An unknown error occurred");
stream.push({
type: "done",
reason: output.stopReason,
message: output
});
stream.end();
} catch (error) {
cleanStreamingScratchBuffers(output);
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = params.formatError(error);
stream.push({
type: "error",
reason: output.stopReason,
error: output
});
stream.end();
}
}
async function processResponsesStream(openaiStream, output, stream, model, options) {
let currentItem = null;
let currentBlock = null;
const blocks = output.content;
const blockIndex = () => blocks.length - 1;
for await (const event of openaiStream) if (event.type === "response.created") output.responseId = event.response.id;
else if (event.type === "response.output_item.added") {
const item = event.item;
if (item.type === "reasoning") {
currentItem = item;
currentBlock = {
type: "thinking",
thinking: ""
};
output.content.push(currentBlock);
stream.push({
type: "thinking_start",
contentIndex: blockIndex(),
partial: output
});
} else if (item.type === "message") {
currentItem = item;
currentBlock = {
type: "text",
text: ""
};
output.content.push(currentBlock);
stream.push({
type: "text_start",
contentIndex: blockIndex(),
partial: output
});
} else if (item.type === "function_call") {
currentItem = item;
currentBlock = {
type: "toolCall",
id: `${item.call_id}|${item.id}`,
name: item.name,
arguments: {},
partialJson: item.arguments || ""
};
output.content.push(currentBlock);
stream.push({
type: "toolcall_start",
contentIndex: blockIndex(),
partial: output
});
}
} else if (event.type === "response.reasoning_summary_part.added") {
if (currentItem && currentItem.type === "reasoning") {
currentItem.summary = currentItem.summary || [];
currentItem.summary.push(event.part);
}
} else if (event.type === "response.reasoning_summary_text.delta") {
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
currentItem.summary = currentItem.summary || [];
const lastPart = currentItem.summary[currentItem.summary.length - 1];
if (lastPart) {
currentBlock.thinking += event.delta;
lastPart.text += event.delta;
stream.push({
type: "thinking_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
}
} else if (event.type === "response.reasoning_summary_part.done") {
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
currentItem.summary = currentItem.summary || [];
const lastPart = currentItem.summary[currentItem.summary.length - 1];
if (lastPart) {
currentBlock.thinking += "\n\n";
lastPart.text += "\n\n";
stream.push({
type: "thinking_delta",
contentIndex: blockIndex(),
delta: "\n\n",
partial: output
});
}
}
} else if (event.type === "response.reasoning_text.delta") {
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
currentBlock.thinking += event.delta;
stream.push({
type: "thinking_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
} else if (event.type === "response.content_part.added") {
if (currentItem?.type === "message") {
currentItem.content = currentItem.content || [];
if (event.part.type === "output_text" || event.part.type === "text" || event.part.type === "refusal") currentItem.content.push(event.part);
}
} else if (event.type === "response.output_text.delta") {
if (currentItem?.type === "message" && currentBlock?.type === "text") {
if (!currentItem.content || currentItem.content.length === 0) continue;
const lastPart = currentItem.content[currentItem.content.length - 1];
if (isResponsesTextContentPartType(lastPart?.type)) {
currentBlock.text += event.delta;
lastPart.text += event.delta;
stream.push({
type: "text_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
}
} else if (isAzureResponsesTextDeltaEvent(event)) {
if (currentItem?.type === "message" && currentBlock?.type === "text") {
currentItem.content = currentItem.content || [];
let lastPart = currentItem.content[currentItem.content.length - 1];
if (lastPart?.type !== "text") {
lastPart = {
type: "text",
text: ""
};
currentItem.content.push(lastPart);
}
currentBlock.text += event.delta;
lastPart.text += event.delta;
stream.push({
type: "text_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
} else if (event.type === "response.refusal.delta") {
if (currentItem?.type === "message" && currentBlock?.type === "text") {
if (!currentItem.content || currentItem.content.length === 0) continue;
const lastPart = currentItem.content[currentItem.content.length - 1];
if (lastPart?.type === "refusal") {
currentBlock.text += event.delta;
lastPart.refusal += event.delta;
stream.push({
type: "text_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
}
} else if (event.type === "response.function_call_arguments.delta") {
if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
currentBlock.partialJson += event.delta;
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
stream.push({
type: "toolcall_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output
});
}
} else if (event.type === "response.function_call_arguments.done") {
if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
const previousPartialJson = currentBlock.partialJson;
const doneArguments = typeof event.arguments === "string" ? event.arguments : void 0;
if (doneArguments !== void 0 && (doneArguments.length > 0 || previousPartialJson === "")) {
currentBlock.partialJson = doneArguments;
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
}
if (doneArguments?.startsWith(previousPartialJson)) {
const delta = doneArguments.slice(previousPartialJson.length);
if (delta.length > 0) stream.push({
type: "toolcall_delta",
contentIndex: blockIndex(),
delta,
partial: output
});
}
}
} else if (event.type === "response.output_item.done") {
const item = event.item;
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
currentBlock.thinkingSignature = JSON.stringify(item);
stream.push({
type: "thinking_end",
contentIndex: blockIndex(),
content: currentBlock.thinking,
partial: output
});
currentBlock = null;
} else if (item.type === "message" && currentBlock?.type === "text") {
currentBlock.text = item.content.map((c) => c.type === "output_text" || c.type === "text" ? c.text : c.refusal).join("");
currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? void 0);
stream.push({
type: "text_end",
contentIndex: blockIndex(),
content: currentBlock.text,
partial: output
});
currentBlock = null;
} else if (item.type === "function_call") {
const args = currentBlock?.type === "toolCall" && currentBlock.partialJson ? parseStreamingJson(currentBlock.partialJson) : parseStreamingJson(item.arguments || "{}");
let toolCall;
if (currentBlock?.type === "toolCall") {
currentBlock.arguments = args;
delete currentBlock.partialJson;
toolCall = currentBlock;
} else toolCall = {
type: "toolCall",
id: `${item.call_id}|${item.id}`,
name: item.name,
arguments: args
};
currentBlock = null;
stream.push({
type: "toolcall_end",
contentIndex: blockIndex(),
toolCall,
partial: output
});
}
} else if (event.type === "response.completed") {
const response = event.response;
if (response?.id) output.responseId = response.id;
if (response?.usage) {
const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
output.usage = {
input: (response.usage.input_tokens || 0) - cachedTokens,
output: response.usage.output_tokens || 0,
cacheRead: cachedTokens,
cacheWrite: 0,
totalTokens: response.usage.total_tokens || 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
};
}
calculateCost(model, output.usage);
if (options?.applyServiceTierPricing) {
const serviceTier = options.resolveServiceTier ? options.resolveServiceTier(response?.service_tier, options.serviceTier) : response?.service_tier ?? options.serviceTier;
options.applyServiceTierPricing(output.usage, serviceTier);
}
output.stopReason = mapStopReason(response?.status);
if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") output.stopReason = "toolUse";
} else if (event.type === "error") throw new Error(event.message ? `Error Code ${event.code}: ${event.message}` : "Unknown error");
else if (event.type === "response.failed") {
const error = event.response?.error;
const details = event.response?.incomplete_details;
const msg = error ? `${error.code || "unknown"}: ${error.message || "no message"}` : details?.reason ? `incomplete: ${details.reason}` : "Unknown error (no error details in response)";
throw new Error(msg);
}
}
function mapStopReason(status) {
if (!status) return "stop";
switch (status) {
case "completed": return "stop";
case "incomplete": return "length";
case "failed":
case "cancelled": return "error";
case "in_progress":
case "queued": return "stop";
default: throw new Error(`Unhandled stop reason: ${String(status)}`);
}
}
//#endregion
export { resolveResponsesReasoningEffort as a, processResponsesStream as i, convertResponsesMessages as n, runResponsesStreamLifecycle as o, createResponsesAssistantOutput as r, convertResponsesTools as s, applyCommonResponsesParams as t };